/** * Central `test.extend` for EventSnap E2E tests. Specs import `test` from here * (not `@playwright/test` directly) so they get the shared fixtures: API * client, DB helper, fresh admin token, and convenience factories. * * Every test runs against a freshly truncated database (via the `truncate` * auto-fixture). Because truncate wipes the `session` table, any admin JWT * obtained before truncate becomes invalid afterwards — so the * `truncate` fixture always does its own admin login, and the `adminToken` * fixture re-logs-in per test instead of caching. bcrypt cost 4 → ~10 ms, * negligible against the ~1 s per-test setup overhead. */ import { test as base, expect, type Page } from '@playwright/test'; import { ApiClient } from './api-client'; import { db } from './db'; type GuestHandle = { jwt: string; pin: string; userId: string; displayName: string; }; type Fixtures = { api: ApiClient; db: typeof db; adminToken: string; truncate: void; guest: (displayName?: string) => Promise; host: GuestHandle; /** Apply an existing guest's JWT + PIN to the page's localStorage and reload. */ signIn: (page: Page, handle: GuestHandle) => Promise; }; export const test = base.extend({ api: async ({}, use) => { await use(new ApiClient()); }, db: async ({}, use) => { await use(db); }, // Auto-fixture: runs before every test, truncates the DB so each test starts // clean. Acquires its OWN admin token because the previous test's truncate // wiped the session row that backs any cached token. truncate: [ async ({ api }, use) => { const token = await api.adminLogin(); await api.truncate(token); await use(); }, { auto: true, scope: 'test' }, ], // Fresh admin login per test that asks for it. Comes AFTER the truncate // auto-fixture has run (truncate doesn't depend on adminToken, so the // dependency-free truncate runs first; this fixture then logs in on a // freshly reset DB). adminToken: async ({ api }, use) => { const token = await api.adminLogin(); await use(token); }, guest: async ({ api }, use) => { let counter = 0; const factory = async (displayName?: string): Promise => { const name = displayName ?? `Gast${++counter}_${Math.random().toString(36).slice(2, 6)}`; const res = await api.join(name); return { jwt: res.jwt, pin: res.pin, userId: res.user_id, displayName: name }; }; await use(factory); }, host: async ({ api, guest, adminToken }, use) => { const h = await guest('TestHost'); await api.setRole(adminToken, h.userId, 'host'); // Role is encoded in the JWT — re-recover to get a fresh token with role=host. const { body } = await api.recover(h.displayName, h.pin); await use({ ...h, jwt: body.jwt }); }, signIn: async ({}, use) => { const fn = async (page: Page, handle: GuestHandle) => { // Visit any in-app URL first so localStorage is scoped to the right origin. await page.goto('/'); await page.evaluate( ({ jwt, pin, userId, displayName }) => { localStorage.setItem('eventsnap_jwt', jwt); localStorage.setItem('eventsnap_pin', pin); localStorage.setItem('eventsnap_user_id', userId); localStorage.setItem('eventsnap_display_name', displayName); localStorage.setItem('eventsnap_guide_seen', 'true'); }, handle ); await page.goto('/feed'); }; await use(fn); }, }); export { expect };