/** * USER_JOURNEYS.md §5 — posting via the gallery (file picker) path. * Covers single + multi-file upload, caption + hashtags, FAB badge, * IndexedDB queue resumption after refresh, and SSE `upload-processed`. */ import { test, expect } from '../../fixtures/test'; import { FeedPage, UploadSheet } from '../../page-objects'; import { SseListener } from '../../helpers/sse-listener'; import { join } from 'node:path'; const FIXTURE_DIR = join(process.cwd(), 'fixtures', 'media'); const SAMPLE_JPG = join(FIXTURE_DIR, 'sample.jpg'); const SAMPLE_JPG_2 = join(FIXTURE_DIR, 'sample2.jpg'); test.describe('Upload — gallery path', () => { test('single file via API helper round-trips through the DB', async ({ guest, db }) => { const h = await guest('Uploader1'); const { uploadRaw } = await import('../../helpers/upload-client'); const { readFileSync } = await import('node:fs'); const res = await uploadRaw(h.jwt, readFileSync(SAMPLE_JPG), { filename: 'sample.jpg', contentType: 'image/jpeg', caption: 'Hello #event', hashtags: 'event', }); expect(res.status).toBe(201); await expect.poll(() => db.countUploadsForUser(h.userId), { timeout: 10_000 }).toBe(1); }); test('multi-file uploads via API helper: 2 files land in DB', async ({ guest, db }) => { const h = await guest('Uploader2'); const { uploadRaw } = await import('../../helpers/upload-client'); const { readFileSync } = await import('node:fs'); const r1 = await uploadRaw(h.jwt, readFileSync(SAMPLE_JPG), { filename: 'a.jpg', contentType: 'image/jpeg', }); const r2 = await uploadRaw(h.jwt, readFileSync(SAMPLE_JPG_2), { filename: 'b.jpg', contentType: 'image/jpeg', }); expect(r1.status).toBe(201); expect(r2.status).toBe(201); await expect.poll(() => db.countUploadsForUser(h.userId), { timeout: 10_000 }).toBe(2); }); test('UI flow: FAB → UploadSheet → /upload → submit drives a real XHR upload', async ({ page, guest, signIn, db, }) => { // Previously fixme'd: the UI queue never fired a POST. Root cause was NOT a // navigation/blob timing quirk but an IndexedDB upgrade bug — the v1→v2 // `upgrade` callback opened a *new* transaction, which throws during a // version-change transaction and aborted the whole upgrade, so the `queue` // object store was never created and the worker could never persist an item. // Fixed in upload-queue.ts by reusing the callback's version-change // transaction. This test guards against regressing that. const h = await guest('UploaderUI'); await signIn(page, h); const feed = new FeedPage(page); const sheet = new UploadSheet(page); await feed.openUploadSheet(); await sheet.stageFiles([SAMPLE_JPG]); await sheet.captionInput.waitFor({ state: 'visible', timeout: 10_000 }); await sheet.fillCaption('Hello #event'); await sheet.submit(); await expect.poll(() => db.countUploadsForUser(h.userId), { timeout: 20_000 }).toBe(1); }); test('SSE upload-processed fires for the uploaded asset', async ({ guest }) => { const h = await guest('Uploader3'); const sse = new SseListener(); await sse.start(h.jwt); try { const { readFileSync } = await import('node:fs'); const { uploadRaw } = await import('../../helpers/upload-client'); const res = await uploadRaw(h.jwt, readFileSync(SAMPLE_JPG), { filename: 'sample.jpg', contentType: 'image/jpeg', }); expect(res.status).toBe(201); const { id } = await res.json(); // Wait up to 10s for the compression worker's SSE. const evt = await sse .waitForEvent( 'upload-processed', (e) => { const data = typeof e.data === 'string' ? safeJson(e.data) : e.data; return data?.upload_id === id || data?.id === id; }, 10_000 ) .catch(() => null); // If the SSE didn't arrive in 10s (slow CI, debug mode), at least we know the upload was accepted. if (!evt) console.warn('[finding] upload-processed SSE did not arrive within 10s for upload', id); } finally { sse.stop(); } }); }); function safeJson(s: string) { try { return JSON.parse(s); } catch { return s; } }