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>
101 lines
4.3 KiB
TypeScript
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);
|
|
});
|
|
});
|