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>
88 lines
3.7 KiB
TypeScript
88 lines
3.7 KiB
TypeScript
/**
|
|
* Regression guard — a rejected upload must tell the user something.
|
|
*
|
|
* `UploadQueue.svelte` was 162 lines of complete, working UI — the only renderer of an
|
|
* item's error text, the only "Erneut" retry button, the only rate-limit countdown — and it
|
|
* was never imported anywhere, so `retryItem`, `removeItem` and `clearCompleted` were all
|
|
* unreachable at runtime. On a terminal rejection the store dropped the blob, wrote a clear
|
|
* German reason into `entry.error` with the comment "so the UI shows a clear reason", and
|
|
* there was no such UI.
|
|
*
|
|
* Meanwhile the FAB badge counted only pending/uploading, so a rejected photo decremented it
|
|
* exactly as if it had succeeded. Net effect: the photo silently vanished — no toast, no
|
|
* queue row, no error text, and it never appeared in the feed.
|
|
*/
|
|
import { test, expect } from '../../fixtures/test';
|
|
import { skipIfNoIdbBlobs } from '../../helpers/webkit';
|
|
import { FeedPage, UploadSheet } from '../../page-objects';
|
|
import { join } from 'node:path';
|
|
|
|
const SAMPLE_JPG = join(process.cwd(), 'fixtures', 'media', 'sample.jpg');
|
|
|
|
test.describe('Upload — a rejected upload is surfaced', () => {
|
|
test('a terminally rejected upload toasts, and stays visible in the queue', async ({
|
|
page,
|
|
api,
|
|
host,
|
|
guest,
|
|
signIn,
|
|
browserName,
|
|
}) => {
|
|
skipIfNoIdbBlobs(browserName);
|
|
const g = await guest('RejectedUploader');
|
|
await signIn(page, g);
|
|
|
|
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 });
|
|
|
|
// Ban the uploader between staging and sending, so the POST comes back 403 — a
|
|
// terminal 4xx the server will keep rejecting, which is the path that purges the blob.
|
|
await api.banUser(host.jwt, g.userId);
|
|
|
|
await sheet.submit();
|
|
|
|
// 1. The user is told, wherever they are (the flow lands them on /feed).
|
|
const toast = page.getByRole('region', { name: 'Benachrichtigungen' });
|
|
await expect(toast).toContainText(/sample\.jpg/i, { timeout: 15_000 });
|
|
|
|
// 2. The queue row survives with its reason and is reachable on /upload — this is what
|
|
// the orphaned component made impossible.
|
|
await page.goto('/upload');
|
|
const queue = page.getByText('Upload-Warteschlange');
|
|
await expect(queue, 'the upload queue must be rendered somewhere').toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
// Both the status chip ("Gesperrt") and the server's reason ("Du bist gesperrt.") must
|
|
// render — the reason is the part that had no UI at all before.
|
|
await expect(page.getByText('Gesperrt', { exact: true })).toBeVisible();
|
|
await expect(page.getByText('Du bist gesperrt.')).toBeVisible();
|
|
|
|
// 3. The badge must not read as success. It counted only pending/uploading before, so a
|
|
// rejected item dropped it to 0 — indistinguishable from a completed upload.
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
page.evaluate(async () => {
|
|
return new Promise<number>((resolve, reject) => {
|
|
const req = indexedDB.open('eventsnap-uploads', 3);
|
|
req.onerror = () => reject(req.error);
|
|
req.onsuccess = () => {
|
|
const tx = req.result.transaction('queue', 'readonly');
|
|
const all = tx.objectStore('queue').getAll();
|
|
all.onsuccess = () =>
|
|
resolve(
|
|
all.result.filter((r: { status: string }) => r.status === 'blocked').length
|
|
);
|
|
all.onerror = () => reject(all.error);
|
|
};
|
|
});
|
|
}),
|
|
{ timeout: 10_000 }
|
|
)
|
|
.toBe(1);
|
|
});
|
|
});
|