test(loadtest): 100-guest / 1000-image stress harness
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>
This commit is contained in:
78
e2e/loadtest/confirm-diashow-fix.mjs
Normal file
78
e2e/loadtest/confirm-diashow-fix.mjs
Normal file
@@ -0,0 +1,78 @@
|
||||
// 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');
|
||||
Reference in New Issue
Block a user