HTTP-level load driver simulating ~100 guests uploading ~1000 images in bursts over a window, plus SSE viewers and one real browser on /diashow. Correlates upload→upload-processed (pipeline latency), waits for the compression backlog to drain against DB ground truth, and emits per-status/latency metrics with pass/fail flags. Includes a realistic-JPEG generator and a diashow-SSE regression check (confirm-diashow-fix.mjs). Run artifacts are gitignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79 lines
3.9 KiB
JavaScript
79 lines
3.9 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');
|