/** * 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((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); }); });