`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>
256 lines
8.6 KiB
JavaScript
256 lines
8.6 KiB
JavaScript
// One-off: seed a realistic feed, then capture mobile screenshots (light + dark).
|
|
// Run from the e2e dir so @playwright/test and pg resolve.
|
|
import { chromium, devices } from '@playwright/test';
|
|
import { Client } from 'pg';
|
|
import { readFileSync, mkdirSync } from 'node:fs';
|
|
|
|
const BASE = 'http://localhost:3101';
|
|
const PHOTOS = '/tmp/eventsnap-shots/photos';
|
|
const OUT = '/tmp/eventsnap-shots';
|
|
mkdirSync(OUT, { recursive: true });
|
|
|
|
const api = (path, opts = {}) =>
|
|
fetch(`${BASE}/api/v1${path}`, opts).then(async (r) => ({
|
|
status: r.status,
|
|
body: await r.text().then((t) => {
|
|
try {
|
|
return JSON.parse(t);
|
|
} catch {
|
|
return t;
|
|
}
|
|
}),
|
|
}));
|
|
|
|
const authHeaders = (jwt, json = true) => ({
|
|
Authorization: `Bearer ${jwt}`,
|
|
...(json ? { 'Content-Type': 'application/json' } : {}),
|
|
});
|
|
|
|
async function adminLogin() {
|
|
const r = await api('/admin/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ password: 'admin-test-pw' }),
|
|
});
|
|
return r.body.jwt;
|
|
}
|
|
async function joinGuest(name) {
|
|
const r = await api('/join', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ display_name: name }),
|
|
});
|
|
if (r.status !== 201)
|
|
throw new Error('join failed ' + name + ' ' + r.status + ' ' + JSON.stringify(r.body));
|
|
return r.body; // {jwt,pin,user_id}
|
|
}
|
|
async function upload(jwt, file, caption, hashtags) {
|
|
const form = new FormData();
|
|
form.append('file', new Blob([readFileSync(file)], { type: 'image/jpeg' }), 'photo.jpg');
|
|
if (caption) form.append('caption', caption);
|
|
if (hashtags) form.append('hashtags', hashtags);
|
|
const r = await fetch(`${BASE}/api/v1/upload`, {
|
|
method: 'POST',
|
|
headers: authHeaders(jwt, false),
|
|
body: form,
|
|
});
|
|
if (r.status !== 201) throw new Error('upload failed ' + r.status + ' ' + (await r.text()));
|
|
return (await r.json()).id;
|
|
}
|
|
async function like(jwt, id) {
|
|
await fetch(`${BASE}/api/v1/upload/${id}/like`, { method: 'POST', headers: authHeaders(jwt) });
|
|
}
|
|
async function comment(jwt, id, body) {
|
|
await fetch(`${BASE}/api/v1/upload/${id}/comments`, {
|
|
method: 'POST',
|
|
headers: authHeaders(jwt),
|
|
body: JSON.stringify({ body }),
|
|
});
|
|
}
|
|
|
|
const pg = () =>
|
|
new Client({
|
|
host: 'localhost',
|
|
port: 55432,
|
|
user: 'eventsnap_test',
|
|
password: 'eventsnap_test',
|
|
database: 'eventsnap_test',
|
|
});
|
|
|
|
async function truncate(adminJwt) {
|
|
await fetch(`${BASE}/api/v1/admin/__truncate`, {
|
|
method: 'POST',
|
|
headers: authHeaders(adminJwt),
|
|
});
|
|
}
|
|
async function patchConfig(adminJwt, patch) {
|
|
await fetch(`${BASE}/api/v1/admin/config`, {
|
|
method: 'PATCH',
|
|
headers: authHeaders(adminJwt),
|
|
body: JSON.stringify(patch),
|
|
});
|
|
}
|
|
|
|
// ---- SEED ----
|
|
console.log('[seed] admin login + reset');
|
|
let admin = await adminLogin();
|
|
await truncate(admin);
|
|
admin = await adminLogin();
|
|
await patchConfig(admin, {
|
|
rate_limits_enabled: 'false',
|
|
upload_rate_enabled: 'false',
|
|
feed_rate_enabled: 'false',
|
|
export_rate_enabled: 'false',
|
|
join_rate_enabled: 'false',
|
|
quota_enabled: 'false',
|
|
storage_quota_enabled: 'false',
|
|
upload_count_quota_enabled: 'false',
|
|
});
|
|
|
|
const guests = [
|
|
'Anna Bauer',
|
|
'Lukas Weber',
|
|
'Mia Schulz',
|
|
'Jonas Fischer',
|
|
'Emma Wagner',
|
|
'Ben Hoffmann',
|
|
'Sophie Klein',
|
|
'Paul Richter',
|
|
];
|
|
const accounts = {};
|
|
for (const g of guests) accounts[g] = await joinGuest(g);
|
|
console.log('[seed] joined', guests.length, 'guests');
|
|
|
|
const posts = [
|
|
['Anna Bauer', 'photo_00.jpg', 'Was für ein magischer Tag 💍✨', 'hochzeit,liebe'],
|
|
['Lukas Weber', 'photo_01.jpg', 'Der erste Tanz 🕺💃', 'party,tanzen'],
|
|
['Mia Schulz', 'photo_02.jpg', 'Sonnenuntergang am See 🌅', 'natur,abend'],
|
|
['Jonas Fischer', 'photo_03.jpg', 'Prost auf das Brautpaar! 🥂', 'feier,freunde'],
|
|
['Emma Wagner', 'photo_04.jpg', 'Die Torte war ein Traum 🍰', 'dessert,liebe'],
|
|
['Ben Hoffmann', 'photo_05.jpg', 'Feuerwerk zum Abschluss 🎆', 'party,abend'],
|
|
['Sophie Klein', 'photo_06.jpg', 'Beste Freunde für immer 💕', 'freunde,feier'],
|
|
['Paul Richter', 'photo_07.jpg', 'Was für eine Stimmung! 🎉', 'party'],
|
|
['Anna Bauer', 'photo_08.jpg', 'Details, die zählen 🌸', 'hochzeit'],
|
|
['Mia Schulz', 'photo_09.jpg', 'Tanzfläche brennt 🔥', 'tanzen,party'],
|
|
];
|
|
const ids = [];
|
|
for (const [who, f, cap, tags] of posts) {
|
|
const id = await upload(accounts[who].jwt, `${PHOTOS}/${f}`, cap, tags);
|
|
ids.push(id);
|
|
}
|
|
console.log('[seed] uploaded', ids.length, 'photos');
|
|
|
|
// Likes: distribute varied counts
|
|
const likeMatrix = [7, 3, 12, 5, 9, 4, 6, 2, 8, 5];
|
|
for (let i = 0; i < ids.length; i++) {
|
|
const n = Math.min(likeMatrix[i], guests.length);
|
|
for (let j = 0; j < n; j++) await like(accounts[guests[j]].jwt, ids[i]);
|
|
}
|
|
// Comments on a few
|
|
await comment(accounts['Lukas Weber'].jwt, ids[0], 'Wunderschön! 😍');
|
|
await comment(accounts['Mia Schulz'].jwt, ids[0], 'Der Moment war perfekt.');
|
|
await comment(accounts['Anna Bauer'].jwt, ids[2], 'Traumhaft 🌅');
|
|
await comment(accounts['Ben Hoffmann'].jwt, ids[1], 'Was ein Abend!');
|
|
console.log('[seed] likes + comments done');
|
|
|
|
// Make one guest a host so /host renders populated
|
|
await fetch(`${BASE}/api/v1/host/users/${accounts['Anna Bauer'].user_id}/role`, {
|
|
method: 'PATCH',
|
|
headers: authHeaders(admin),
|
|
body: JSON.stringify({ role: 'host' }),
|
|
});
|
|
|
|
// Wait for compression to finish so previews render
|
|
const c = pg();
|
|
await c.connect();
|
|
for (let t = 0; t < 40; t++) {
|
|
const r = await c.query(
|
|
`SELECT COUNT(*)::int AS n FROM upload WHERE compression_status <> 'done' AND deleted_at IS NULL`
|
|
);
|
|
if (r.rows[0].n === 0) {
|
|
console.log('[seed] compression done');
|
|
break;
|
|
}
|
|
await new Promise((res) => setTimeout(res, 500));
|
|
}
|
|
await c.end();
|
|
|
|
// ---- SCREENSHOTS ----
|
|
const device = devices['Pixel 7'];
|
|
const anna = accounts['Anna Bauer']; // host
|
|
const shot = async (label, theme, who, route, prep) => {
|
|
const ctx = await browser.newContext({ ...device });
|
|
const page = await ctx.newPage();
|
|
// Seed localStorage on the origin
|
|
await page.goto(`${BASE}/join`, { waitUntil: 'domcontentloaded' });
|
|
await page.evaluate(
|
|
({ jwt, pin, uid, name, theme, mode }) => {
|
|
localStorage.setItem('eventsnap_theme', theme);
|
|
localStorage.setItem('eventsnap_data_mode', mode);
|
|
if (jwt) {
|
|
localStorage.setItem('eventsnap_jwt', jwt);
|
|
localStorage.setItem('eventsnap_pin', pin);
|
|
localStorage.setItem('eventsnap_user_id', uid);
|
|
localStorage.setItem('eventsnap_display_name', name);
|
|
}
|
|
localStorage.setItem('eventsnap_guide_seen', '1');
|
|
},
|
|
{ jwt: who?.jwt, pin: who?.pin, uid: who?.user_id, name: who?.name, theme, mode: 'saver' }
|
|
);
|
|
await page.goto(`${BASE}${route}`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(1200);
|
|
if (prep) await prep(page);
|
|
await page.waitForTimeout(600);
|
|
await page.screenshot({ path: `${OUT}/${label}-${theme}.png`, fullPage: false });
|
|
await ctx.close();
|
|
console.log('[shot]', label, theme);
|
|
};
|
|
|
|
const browser = await chromium.launch();
|
|
const hostWho = { ...anna, name: 'Anna Bauer' };
|
|
const adminWho = { jwt: admin, pin: '', user_id: '', name: 'Admin' };
|
|
|
|
for (const theme of ['light', 'dark']) {
|
|
await shot('01-join', theme, null, '/join');
|
|
await shot('02-feed-list', theme, hostWho, '/feed');
|
|
await shot('03-feed-grid', theme, hostWho, '/feed', async (p) => {
|
|
await p
|
|
.getByLabel('Rasteransicht')
|
|
.click()
|
|
.catch(() => {});
|
|
});
|
|
await shot('04-lightbox', theme, hostWho, '/feed', async (p) => {
|
|
await p
|
|
.locator('img')
|
|
.first()
|
|
.click()
|
|
.catch(() => {});
|
|
await p.waitForTimeout(500);
|
|
});
|
|
await shot('05-account', theme, hostWho, '/account');
|
|
await shot('06-host', theme, hostWho, '/host');
|
|
await shot('07-admin', theme, adminWho, '/admin');
|
|
await shot('08-upload', theme, hostWho, '/upload');
|
|
await shot('09-diashow', theme, hostWho, '/diashow');
|
|
}
|
|
// Onboarding: fresh guest, guide not seen
|
|
{
|
|
const ctx = await browser.newContext({ ...device });
|
|
const page = await ctx.newPage();
|
|
await page.goto(`${BASE}/join`, { waitUntil: 'domcontentloaded' });
|
|
await page.evaluate((w) => {
|
|
localStorage.setItem('eventsnap_theme', 'light');
|
|
localStorage.setItem('eventsnap_jwt', w.jwt);
|
|
localStorage.setItem('eventsnap_pin', w.pin);
|
|
localStorage.setItem('eventsnap_user_id', w.user_id);
|
|
localStorage.setItem('eventsnap_display_name', 'Emma Wagner');
|
|
}, accounts['Emma Wagner']);
|
|
await page.goto(`${BASE}/feed`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForTimeout(1500);
|
|
await page.screenshot({ path: `${OUT}/10-onboarding-light.png` });
|
|
await ctx.close();
|
|
console.log('[shot] onboarding');
|
|
}
|
|
await browser.close();
|
|
console.log('DONE');
|