Three 02-upload tests have been failing on `webkit-iphone` on main — `02-upload` was already in that project's testMatch, so this is pre-existing red, not something the audit work introduced. Root cause is the harness, not the app. Playwright's Linux WebKit build cannot store Blobs in IndexedDB at all: `put()` fails with "UnknownError: Error preparing Blob/File data to be stored in object store". Confirmed it is not about how Playwright delivers files — a Blob constructed in-page with `new Blob([bytes])` fails identically, while Chromium stores both that and a `setInputFiles` File without complaint. That breaks every test driving the composer (FAB → UploadSheet → /upload → submit), because `handleSubmit` awaits `addToQueue`, which persists the file before it can navigate. The symptom is a submit button stuck on "Wird hochgeladen…" and a timeout waiting for /feed — which reads like an app hang and cost real time to run down. Skip those four (the three above plus the new rejection-visible) on WebKit only, behind a named helper carrying the full explanation, so the next person gets the answer instead of the investigation. Deliberately narrow: WebKit still runs every API-driven upload test, all of 01-auth, 03-feed and 06-export — including the keepsake download, which only WebKit can meaningfully verify. Chromium continues to run all 14. Worth being explicit, since these tests exist to protect iOS: real Safari supports Blobs in IndexedDB, so this is NOT evidence that the offline upload queue is broken on the platform. It does mean that guarantee is currently unverifiable in CI and rests on Chromium coverage plus manual device testing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
118 lines
4.4 KiB
TypeScript
118 lines
4.4 KiB
TypeScript
/**
|
|
* 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 { skipIfNoIdbBlobs } from '../../helpers/webkit';
|
|
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,
|
|
browserName,
|
|
}) => {
|
|
skipIfNoIdbBlobs(browserName);
|
|
// 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;
|
|
}
|
|
}
|