import type { Page, BrowserContext } from '@playwright/test'; import type { RouteAccess } from './websiteRouteInventory'; export type WebsiteAuthContext = 'public' | 'auth' | 'admin' | 'sponsor'; export type WebsiteSessionDriftMode = 'invalid-cookie' | 'expired' | 'missing-sponsor-id'; export type WebsiteFaultMode = 'null-array' | 'missing-field' | 'invalid-date'; type DemoLoginRole = | 'driver' | 'sponsor' | 'league-owner' | 'league-steward' | 'league-admin' | 'system-owner' | 'super-admin'; const demoSessionCookieCache = new Map(); export function authContextForAccess(access: RouteAccess): WebsiteAuthContext { if (access === 'public') return 'public'; if (access === 'auth') return 'auth'; if (access === 'admin') return 'admin'; return 'sponsor'; } function getWebsiteBaseUrl(): string { const configured = process.env.WEBSITE_BASE_URL ?? process.env.PLAYWRIGHT_BASE_URL; if (configured && configured.trim()) { return configured.trim().replace(/\/$/, ''); } return 'http://localhost:3100'; } function demoLoginRoleForAuthContext(auth: WebsiteAuthContext): DemoLoginRole | null { switch (auth) { case 'public': return null; case 'auth': return 'driver'; case 'sponsor': return 'sponsor'; case 'admin': // Website "admin" pages need an elevated role; use the strongest demo role. return 'super-admin'; default: { const exhaustive: never = auth; return exhaustive; } } } function extractCookieValue(setCookieHeader: string, cookieName: string): string | null { // set-cookie header value: "name=value; Path=/; HttpOnly; ..." // Do not split on comma (Expires contains commas). Just regex out the first cookie value. const match = setCookieHeader.match(new RegExp(`(?:^|\\s)${cookieName}=([^;]+)`)); return match?.[1] ?? null; } async function ensureDemoSessionCookie(role: DemoLoginRole): Promise { const cached = demoSessionCookieCache.get(role); if (cached) return cached; const baseUrl = getWebsiteBaseUrl(); const url = `${baseUrl}/api/auth/demo-login`; const response = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json', }, body: JSON.stringify({ role }), }); if (!response.ok) { const body = await response.text().catch(() => ''); throw new Error(`Smoke demo-login failed for role=${role}. ${response.status} ${response.statusText}. ${body}`); } // In Node (playwright runner) `headers.get('set-cookie')` returns a single comma-separated string. // Parse cookies by splitting on `, ` and taking the first `name=value` segment. const rawSetCookie = response.headers.get('set-cookie') ?? ''; const cookieHeaderPairs = rawSetCookie ? rawSetCookie .split(', ') .map((chunk) => chunk.split(';')[0]?.trim()) .filter(Boolean) : []; const gpSessionPair = cookieHeaderPairs.find((pair) => pair.startsWith('gp_session=')); if (!gpSessionPair) { throw new Error( `Smoke demo-login did not return gp_session cookie for role=${role}. set-cookie header: ${rawSetCookie}`, ); } const gpSessionValue = extractCookieValue(gpSessionPair, 'gp_session'); if (!gpSessionValue) { throw new Error( `Smoke demo-login returned a gp_session cookie, but it could not be parsed for role=${role}. Pair: ${gpSessionPair}`, ); } demoSessionCookieCache.set(role, gpSessionValue); return gpSessionValue; } export async function setWebsiteAuthContext( context: BrowserContext, auth: WebsiteAuthContext, options: { sessionDrift?: WebsiteSessionDriftMode; faultMode?: WebsiteFaultMode } = {}, ): Promise { const domain = 'localhost'; const base = { domain, path: '/' }; const driftCookie = options.sessionDrift != null ? [{ ...base, name: 'gridpilot_session_drift', value: String(options.sessionDrift) }] : []; const faultCookie = options.faultMode != null ? [{ ...base, name: 'gridpilot_fault_mode', value: String(options.faultMode) }] : []; await context.clearCookies(); if (auth === 'public') { // Public access: no session cookie, only drift/fault cookies if specified await context.addCookies([...driftCookie, ...faultCookie]); return; } const demoRole = demoLoginRoleForAuthContext(auth); if (!demoRole) { throw new Error(`Expected a demo role for auth context ${auth}`); } const gpSessionValue = await ensureDemoSessionCookie(demoRole); // Only set gp_session cookie (no demo mode or sponsor cookies) // For Docker/local testing, ensure cookies work with localhost const sessionCookie = [{ ...base, name: 'gp_session', value: gpSessionValue, httpOnly: true, secure: false, // Localhost doesn't need HTTPS sameSite: 'Lax' as const // Ensure compatibility }]; await context.addCookies([...sessionCookie, ...driftCookie, ...faultCookie]); } export type ConsoleCapture = { consoleErrors: string[]; pageErrors: string[]; }; export function attachConsoleErrorCapture(page: Page): ConsoleCapture { const consoleErrors: string[] = []; const pageErrors: string[] = []; page.on('pageerror', (err) => { pageErrors.push(String(err)); }); page.on('console', (msg) => { const type = msg.type(); if (type !== 'error') return; const text = msg.text(); // Filter known benign warnings (keep small + generic). if (text.includes('Download the React DevTools')) return; // Next/Image accessibility warning (not a runtime failure for smoke coverage). if (text.includes('Image is missing required "alt" property')) return; // React controlled instead of setting `selected` on