The Playwright suite had no linter and no formatter — only tsc. Add flat-config ESLint
(typescript-eslint, type-aware) and Prettier (2-space, matching the suite's style).
Rules keep the ones that catch real TEST bugs and drop the noise:
- no-floating-promises KEPT — an un-awaited request/assertion can let a test end before it runs,
passing vacuously. It caught one: the SSE reader loop in sse-listener is now explicitly `void`.
- no-unused-vars KEPT — caught three dead bindings (an unused adminToken fixture arg, an unused
`api` arg, an unused JPEG_MAGIC import), all removed.
- no-explicit-any OFF — all test code; `any` is the honest type for an untyped res.json() body or
a page.evaluate() return.
- no-empty-pattern OFF — Playwright's dependency-free fixtures are `async ({}, use) => {}`.
Refactor: `const BASE = process.env.E2E_FRONTEND_URL ?? '...'` was redeclared verbatim in 23
files — extracted to helpers/env.ts and imported, so a port/scheme change is one edit not a sweep.
Then `prettier --write`. Verified: eslint clean, tsc clean, prettier clean, desktop suite 210
passed / 1 skipped. (One mobile spec flaked once under retries:0 — a pre-existing cross-test
reflow-timing vector from the flakiness audit, not this change: the each-key edit is stable across
16 isolated runs and a clean full mobile re-run.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
4.3 KiB
TypeScript
100 lines
4.3 KiB
TypeScript
/**
|
|
* 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<string[]> {
|
|
return page.evaluate(async () => {
|
|
const statuses = await new Promise<string[]>((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);
|
|
});
|
|
});
|