Files
EventSnap/e2e/specs/10-flow-review/offline-resume.spec.ts
MechaCat02 641174717c
Some checks failed
E2E / Playwright E2E (chromium-desktop) (push) Failing after 6m52s
E2E / Cross-UA smoke matrix (push) Failing after 3m50s
fix(user-flow): close offline-queue, export, session, ban & realtime gaps
Comprehensive user-flow review across guest/host/admin roles, then fixes with
e2e regression guards. Highlights:

- Offline upload queue auto-resumes on reconnect: network errors keep items
  pending (not error), 4xx are terminal (no infinite retry), quota exhaustion
  returns a distinct 413; queue cap + dedup.
- Export lifecycle: release atomically locks uploads; reopen invalidates and
  re-release regenerates the keepsake; workers claim their job atomically so a
  reopen->re-release can't corrupt the ZIP; startup re-spawns interrupted
  exports.
- Sessions slide on activity (no 30-day cliff); JWT expiry deferred to the
  revocable session row; sign-out-everywhere + revoke-on-PIN-reset.
- Ban always hides content (v_feed / find_visible_media / export filter
  is_banned) but stays a read-only ban per USER_JOURNEYS §10 — sessions are
  not revoked, read access + keepsake download preserved.
- Realtime: server-clock SSE delta cursor; event-closed/opened drive the UI
  live; feed_delta rate-limited; like returns {liked, like_count} to fix
  multi-device drift; lightbox live comments; diashow delta backfill.
- Forgotten-PIN in-app request flow; simultaneous same-name join returns 409;
  quota increment is transactional; operator floor.

Adds e2e/specs/10-flow-review/ (offline resume, export integrity, deterministic
anti-race guard) and updates existing specs for the new contracts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 23:05:37 +02:00

101 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);
});
});