`checks.yml` runs `npm run format:check` for both projects and both were failing. - frontend/src/lib/ui-store.ts is mine, unformatted since the round-1 upload-queue badge fix — the same miss as the rustfmt one: I gated on svelte-check and eslint but never on format:check. - e2e/loadtest/* and e2e/shots.mjs have been unformatted since7758270and are unrelated to the audit work. Fixed here because they block the same gate and the fix is mechanical; no behaviour change in either project. Still red and deliberately NOT fixed here: `npm run lint` in the frontend reports `svelte/prefer-svelte-reactivity` on routes/diashow/+page.svelte:208 (a mutable `Set` where the rule wants `SvelteSet`), pre-existing since5009590. That one is a real reactivity change in code I have no test coverage for, so it belongs in its own change rather than smuggled into a formatting commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
110 lines
4.0 KiB
JavaScript
110 lines
4.0 KiB
JavaScript
// End-to-end confirmation of the diashow SSE fix, reproducing the ORIGINAL failure:
|
|
// kiosk opens /diashow directly BEFORE any photos exist, then a guest uploads.
|
|
// Pre-fix: stream never opens, display stuck on "Noch keine Beiträge" forever.
|
|
// Post-fix: stream opens on mount, the uploaded photo appears live.
|
|
import { chromium } from '@playwright/test';
|
|
import { readFileSync } from 'node:fs';
|
|
|
|
const BASE = 'http://localhost:3101';
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
const j = (r) => r.json();
|
|
|
|
const adminLogin = () =>
|
|
fetch(`${BASE}/api/v1/admin/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ password: 'admin-test-pw' }),
|
|
})
|
|
.then(j)
|
|
.then((b) => b.jwt);
|
|
const join = (name) =>
|
|
fetch(`${BASE}/api/v1/join`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ display_name: name }),
|
|
}).then(j);
|
|
async function truncate(admin) {
|
|
await fetch(`${BASE}/api/v1/admin/__truncate`, {
|
|
method: 'POST',
|
|
headers: { Authorization: `Bearer ${admin}` },
|
|
});
|
|
}
|
|
async function upload(jwt) {
|
|
const form = new FormData();
|
|
form.append(
|
|
'file',
|
|
new Blob([readFileSync('/tmp/eventsnap-loadtest/photos/photo_000.jpg')], {
|
|
type: 'image/jpeg',
|
|
}),
|
|
'live.jpg'
|
|
);
|
|
form.append('caption', 'LIVE-PROBE');
|
|
const r = await fetch(`${BASE}/api/v1/upload`, {
|
|
method: 'POST',
|
|
headers: { Authorization: `Bearer ${jwt}` },
|
|
body: form,
|
|
});
|
|
return (await r.json()).id;
|
|
}
|
|
|
|
// Reproduce the failing scenario: empty event, then open /diashow directly.
|
|
let admin = await adminLogin();
|
|
await truncate(admin);
|
|
admin = await adminLogin();
|
|
const showcase = await join('Showcase Display');
|
|
const guest = await join('Uploading Guest');
|
|
|
|
const browser = await chromium.launch();
|
|
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
|
const page = await ctx.newPage();
|
|
const streamReqs = [];
|
|
page.on('request', (req) => {
|
|
if (req.url().includes('/stream')) streamReqs.push(req.url().replace(BASE, ''));
|
|
});
|
|
|
|
await page.goto(`${BASE}/join`, { waitUntil: 'domcontentloaded' });
|
|
await page.evaluate((g) => {
|
|
localStorage.setItem('eventsnap_jwt', g.jwt);
|
|
localStorage.setItem('eventsnap_pin', g.pin);
|
|
localStorage.setItem('eventsnap_user_id', g.user_id);
|
|
localStorage.setItem('eventsnap_display_name', 'Showcase');
|
|
localStorage.setItem('eventsnap_guide_seen', '1');
|
|
}, showcase);
|
|
|
|
// Open /diashow DIRECTLY (never via /feed) on an EMPTY event.
|
|
await page.goto(`${BASE}/diashow`, { waitUntil: 'domcontentloaded' });
|
|
await sleep(2500);
|
|
const emptyBefore = (await page.locator('text=Noch keine Beiträge').count()) > 0;
|
|
const streamOpened = streamReqs.length > 0;
|
|
console.log(`\n1) direct /diashow on empty event:`);
|
|
console.log(` "Noch keine Beiträge" shown: ${emptyBefore} (expected: true)`);
|
|
console.log(
|
|
` SSE stream opened: ${streamOpened} ${streamOpened ? '(' + streamReqs.join(', ') + ')' : ''} (expected: true — this is the fix)`
|
|
);
|
|
|
|
// Now a guest uploads a photo while the display is open.
|
|
console.log(`\n2) guest uploads a photo (display already open)…`);
|
|
await upload(guest.jwt);
|
|
|
|
// Wait for compression → upload-processed SSE → debounced refresh → slide appears.
|
|
let appeared = false;
|
|
for (let i = 0; i < 15; i++) {
|
|
await sleep(1000);
|
|
const stillEmpty = (await page.locator('text=Noch keine Beiträge').count()) > 0;
|
|
const imgs = await page.locator('img').count();
|
|
if (!stillEmpty && imgs > 0) {
|
|
appeared = true;
|
|
console.log(` photo appeared live after ~${i + 1}s (img rendered, placeholder gone)`);
|
|
break;
|
|
}
|
|
}
|
|
if (!appeared) console.log(` ✗ photo did NOT appear within 15s`);
|
|
|
|
await page.screenshot({ path: 'results/diashow-fix-confirm.png' });
|
|
await browser.close();
|
|
|
|
console.log(`\n════ VERDICT ════`);
|
|
console.log(`SSE opens on direct /diashow: ${streamOpened ? 'YES ✓' : 'NO ✗'}`);
|
|
console.log(`Live photo appears on showcase: ${appeared ? 'YES ✓' : 'NO ✗'}`);
|
|
console.log(streamOpened && appeared ? 'FIX CONFIRMED' : 'FIX NOT confirmed');
|