/** * E2E auth helpers. * * Strategy: API-login + localStorage injection. Faster and more reliable * than driving the Login form, and decouples route-coverage tests from * form-rendering flakiness. The Login form itself is exercised by the * existing manual / smoke flows. * * Zustand's `persist` middleware stores auth state under the * `auth-storage` key in localStorage. The shape is: * { state: { token, user, offline }, version: 0 } * * Source of truth for demo credentials: server/src/database/init.js * (kept in sync with client/src/pages/Login.tsx's demoLogin() map). */ import { Page, request as pwRequest } from '@playwright/test'; export type PortalRole = | 'school_admin' | 'systems_admin' | 'principal' | 'hr' | 'bursar' | 'accountant' | 'nurse' | 'dining_staff' | 'teacher' | 'librarian' | 'clubs_head' | 'student' | 'parent' | 'driver' | 'groundsman' | 'matron' | 'boarding_master' | 'security' | 'janitor'; export const DEMO_ACCOUNTS: Record = { systems_admin: { email: 'sysadmin@school.com', password: 'admin123' }, school_admin: { email: 'admin@school.com', password: 'admin123' }, principal: { email: 'principal@school.com', password: 'principal123' }, hr: { email: 'hr@school.com', password: 'hr123' }, bursar: { email: 'bursar@school.com', password: 'bursar123' }, accountant: { email: 'bursar@school.com', password: 'bursar123' }, // no seed; borrow bursar nurse: { email: 'nurse@school.com', password: 'nurse123' }, dining_staff: { email: 'dining@school.com', password: 'dining123' }, teacher: { email: 'teacher@school.com', password: 'teacher123' }, librarian: { email: 'librarian@school.com', password: 'librarian123' }, clubs_head: { email: 'clubs_head@school.com', password: 'clubs123' }, student: { email: 'student@school.com', password: 'student123' }, parent: { email: 'parent@school.com', password: 'parent123' }, driver: { email: 'driver@school.com', password: 'driver123' }, groundsman: { email: 'groundsman@school.com', password: 'grounds123' }, matron: { email: 'matron@school.com', password: 'matron123' }, boarding_master: { email: 'boarding@school.com', password: 'boarding123' }, security: { email: 'security@school.com', password: 'security123' }, janitor: { email: 'janitor@school.com', password: 'janitor123' }, }; const API = 'http://localhost:3001'; export async function apiLogin(role: PortalRole): Promise<{ token: string; user: any }> { const account = DEMO_ACCOUNTS[role]; if (!account) throw new Error(`No demo account for role ${role}`); const ctx = await pwRequest.newContext({ baseURL: API }); try { const res = await ctx.post('/api/auth/login', { data: account }); if (res.status() !== 200) { throw new Error(`Login failed for ${role} (${account.email}): ${res.status()} ${await res.text()}`); } const body = await res.json(); return { token: body.token, user: body.user }; } finally { await ctx.dispose(); } } /** * Probe a demo account to see if it actually exists in the seed. * Used by specs to skip cleanly when the seed is minimal — the * helper itself doesn't fail, only the spec does. */ export async function isRoleAvailable(role: PortalRole): Promise { try { await apiLogin(role); return true; } catch { return false; } } /** * Log `role` in by injecting the auth state into localStorage and * reloading the app. The baseURL is read from the Playwright config * (http://localhost:3000). * * The `?e2e=1` flag on the first navigation is honoured by AppLayout to * suppress notification permission requests and other side effects that * would otherwise interrupt the test run. */ export async function loginAs(page: Page, role: PortalRole): Promise { const { token, user } = await apiLogin(role); await page.addInitScript(({ token, user }) => { const payload = { state: { token, user, offline: false }, version: 0 }; window.localStorage.setItem('auth-storage', JSON.stringify(payload)); // Suppress Notification.requestPermission() in AppLayout's effect. // The shim is a no-op so global notification polling becomes inert. (window as any).__E2E__ = true; }, { token, user }); } export async function logout(page: Page): Promise { await page.evaluate(() => { window.localStorage.removeItem('auth-storage'); }); }