Frontend - Feed: DOM-windowing via @tanstack/svelte-virtual (new VirtualFeed). The window virtualizer keeps the sticky header, pull-to-refresh, infinite-scroll sentinel and bottom nav working. List = dynamic measured heights keyed by upload id + anchorTo:start so an SSE prepend doesn't jump a scrolled reader; grid = measured square rows. Drops the content-visibility band-aid and the old FeedGrid. measureElement(null) on row unmount prevents ResizeObserver retention; stable option callbacks + guarded setOptions avoid O(n) re-measure on like/comment patches. - Global: viewport-fit=cover (activates safe-area insets), lang=de, PWA manifest + maskable icon + apple-touch metas, prefers-reduced-motion, safe-area-top headers. - Feed reactivity: like/comment SSE patch the single card in place; upload-processed debounced in-place merge; IntersectionObserver leak fixed; shared 60s clock. - CLS: list cards reserve the skeleton aspect box. - Destructive actions (promote/demote/unban, gallery release) routed through ConfirmSheet (host + admin). - Export OOM: streamed download via single-use ticket instead of res.blob(). - Shared primitives: IconButton (44px hit area), scrollLock action; UploadSheet a11y. - Polish: api timeout + non-JSON guard, focus-trap offsetParent fix, pull-to-refresh passive:false + guards, diashow keyboard a11y, Toaster assertive errors, dark-mode gaps, aria-pressed on like/chip state, CameraCapture mic-on-video + retry, ConfirmSheet spinner, upload beforeunload warning, emoji->SVG icons. Backend - social.rs: like/comment SSE broadcasts now carry the fresh count so feed clients patch one card in place instead of refetching page 1. - admin.rs / main.rs: supporting changes for the above. Verified: svelte-check 0 errors, frontend build clean, /feed SSR 200. Runtime feed scroll-feel validation still owed (documented in FOLLOWUPS.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
64 lines
2.8 KiB
TypeScript
64 lines
2.8 KiB
TypeScript
/**
|
|
* USER_JOURNEYS.md §12 — release the export, see status, download.
|
|
*
|
|
* We don't drive a real export job here (the compression takes too long
|
|
* for E2E timing). Instead we forge the export-job rows via the db helper
|
|
* and assert the API behavior + UI banner state.
|
|
*/
|
|
import { test, expect } from '../../fixtures/test';
|
|
import { ExportPage } from '../../page-objects';
|
|
|
|
const SLUG = 'e2e-test-event';
|
|
|
|
test.describe('Export — release and download', () => {
|
|
test('/export shows the "not yet available" state before release', async ({ page, guest, signIn }) => {
|
|
const g = await guest('PreRelease');
|
|
await signIn(page, g);
|
|
const exportPage = new ExportPage(page);
|
|
await exportPage.goto();
|
|
// The page shouldn't show download buttons before release.
|
|
await expect(page.getByRole('button', { name: /^herunterladen$/i })).not.toBeVisible();
|
|
});
|
|
|
|
test('export status API reflects released flag', async ({ guest, db }) => {
|
|
const g = await guest('ReleaseQuery');
|
|
|
|
let res = await fetch((process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101') + '/api/v1/export/status', {
|
|
headers: { Authorization: `Bearer ${g.jwt}` },
|
|
});
|
|
let body: any = await res.json();
|
|
expect(body.released).toBe(false);
|
|
|
|
await db.setExportReleased(SLUG, true);
|
|
await db.fakeExportJob(SLUG, 'zip', 'done');
|
|
await db.fakeExportJob(SLUG, 'html', 'done');
|
|
|
|
res = await fetch((process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101') + '/api/v1/export/status', {
|
|
headers: { Authorization: `Bearer ${g.jwt}` },
|
|
});
|
|
body = await res.json();
|
|
expect(body.released).toBe(true);
|
|
expect(body.zip.status).toBe('done');
|
|
expect(body.html.status).toBe('done');
|
|
});
|
|
|
|
test('ZIP download returns 404 when no file is on disk (export released but never compressed)', async ({ guest, db }) => {
|
|
const g = await guest('NoFile');
|
|
await db.setExportReleased(SLUG, true);
|
|
await db.fakeExportJob(SLUG, 'zip', 'done');
|
|
const base = process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101';
|
|
// Browser downloads stream straight to disk via a top-level navigation, so the
|
|
// download endpoint authenticates with a single-use ticket (no Bearer header).
|
|
const ticketRes = await fetch(base + '/api/v1/export/ticket', {
|
|
method: 'POST',
|
|
headers: { Authorization: `Bearer ${g.jwt}` },
|
|
});
|
|
const { ticket } = await ticketRes.json();
|
|
// Real backend additionally checks event.export_zip_ready. The faked row is
|
|
// enough for /status; the download path needs the boolean flag too.
|
|
const res = await fetch(base + '/api/v1/export/zip?ticket=' + encodeURIComponent(ticket));
|
|
// Either 404 ("not available" OR "file not found") — both are valid states for this setup.
|
|
expect([404, 200]).toContain(res.status);
|
|
});
|
|
});
|