/** * Re-review regression guard — Chain B1 ("offline queue must auto-resume"). * * Bug that was fixed: a file staged while OFFLINE was attempted immediately; * the XHR network error marked the queue item `error`, and every resume path * (the `online` listener, `processQueue`, `loadQueue`) only picks up `pending` * items — so the item was stranded until a manual "Erneut" tap. * * The fix: network failures are a distinct `NetworkError` that keeps the item * `pending`; `processQueue` bails while `navigator.onLine === false` (so an * offline-staged item is never burned to `error`); and `requeueRetriable()` * flips transient `error`→`pending` on the `online` event and on mount. * * This test drives a REAL browser going offline → stage+submit → reconnect and * asserts the upload lands with NO manual interaction. It fails on the old code * (item errors, never resumes) and passes on the fix. */ import { test, expect } from '../../fixtures/test'; import { FeedPage, UploadSheet } from '../../page-objects'; import { join } from 'node:path'; const SAMPLE_JPG = join(process.cwd(), 'fixtures', 'media', 'sample.jpg'); // White-box: read the upload queue straight out of IndexedDB so we can assert // the item's status without depending on the (currently unrendered) queue widget. async function queueStatuses(page: import('@playwright/test').Page): Promise { return page.evaluate(async () => { const statuses = await new Promise((resolve, reject) => { const req = indexedDB.open('eventsnap-uploads', 3); req.onerror = () => reject(req.error); req.onsuccess = () => { const dbh = req.result; const tx = dbh.transaction('queue', 'readonly'); const all = tx.objectStore('queue').getAll(); all.onsuccess = () => resolve(all.result.map((r: any) => r.status)); all.onerror = () => reject(all.error); }; }); return statuses; }); } test.describe('Flow re-review — offline upload auto-resume (B1)', () => { test('offline-staged upload stays pending, then flushes on reconnect with no manual tap', async ({ page, guest, signIn, db, }) => { const g = await guest('OfflineResume'); await signIn(page, g); await page.goto('/feed'); expect(await db.countUploadsForUser(g.userId)).toBe(0); // Warm the code-split `/upload` route chunk while still ONLINE. Without this, the // client-side navigation the UploadSheet triggers would try to fetch the chunk with // no connectivity and hit SvelteKit's error boundary — a test artifact unrelated to // the upload-queue fix under test. (A production PWA service worker would cache it.) await page.goto('/upload'); await page.goto('/feed'); // Go offline BEFORE staging so the first attempt happens with no connectivity. await page.context().setOffline(true); 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('shot with no signal'); await sheet.submit(); // handleSubmit navigates to /feed after enqueuing even while offline. await page.waitForURL('**/feed', { timeout: 15_000 }); // While offline: nothing may have reached the server, and — the crux of the // fix — the item must be parked as `pending`, NOT burned to `error`/`blocked` // (which on the old code would require a manual retry tap). await expect.poll(() => queueStatuses(page), { timeout: 8_000 }).toEqual(['pending']); expect(await db.countUploadsForUser(g.userId)).toBe(0); // Reconnect. The `online` listener must resume the queue automatically — we do // NOT navigate, tap retry, or otherwise touch the page. await page.context().setOffline(false); // The queued upload is now created server-side without any manual interaction. await expect.poll(() => db.countUploadsForUser(g.userId), { timeout: 20_000 }).toBe(1); // And the queue item ends terminal-good (done) or is drained — never stuck in error. await expect .poll( async () => { const s = await queueStatuses(page); return s.every((x) => x === 'done') || s.length === 0; }, { timeout: 10_000 } ) .toBe(true); }); });