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'; export function authContextForAccess(access: RouteAccess): WebsiteAuthContext { if (access === 'public') return 'public'; if (access === 'auth') return 'auth'; if (access === 'admin') return 'admin'; return 'sponsor'; } export async function setWebsiteAuthContext( context: BrowserContext, auth: WebsiteAuthContext, options: { sessionDrift?: WebsiteSessionDriftMode; faultMode?: WebsiteFaultMode } = {}, ): Promise { const domain = 'localhost'; const base = { domain, path: '/' }; // The website uses `gp_session` cookie for authentication // For smoke tests, we use normal login API with seeded demo user credentials // to get real session cookies if (auth === 'public') { // No authentication needed await context.clearCookies(); return; } // For authenticated contexts, we need to perform a normal login // This ensures we get real session cookies with proper structure // Note: All auth contexts use the same seeded demo driver user for simplicity // Role-based access control is tested separately in integration tests // Call the normal login API with seeded demo user credentials // Use demo.driver@example.com for all auth contexts (driver role) const response = await fetch('http://localhost:3101/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: 'demo.driver@example.com', password: 'Demo1234!', }), credentials: 'include', }); if (!response.ok) { throw new Error(`Normal login failed: ${response.status}`); } // Extract cookies from the response const setCookieHeader = response.headers.get('set-cookie'); if (!setCookieHeader) { throw new Error('No cookies set by normal login'); } // Parse the Set-Cookie headers const cookies = setCookieHeader.split(',').map(cookieStr => { const parts = cookieStr.split(';').map(p => p.trim()); const [nameValue, ...attributes] = parts; const [name, value] = nameValue.split('='); const cookie: any = { name, value: decodeURIComponent(value), domain: 'localhost', path: '/', expires: Math.floor(Date.now() / 1000) + 3600, // 1 hour httpOnly: false, secure: false, sameSite: 'Lax' as const }; for (const attr of attributes) { const [attrName, attrValue] = attr.split('='); const lowerName = attrName.toLowerCase(); if (lowerName === 'path') cookie.path = attrValue; else if (lowerName === 'httponly') cookie.httpOnly = true; else if (lowerName === 'secure') cookie.secure = true; else if (lowerName === 'samesite') cookie.sameSite = attrValue as any; else if (lowerName === 'domain') { // Skip domain from API - we'll use localhost } else if (lowerName === 'max-age') cookie.expires = Math.floor(Date.now() / 1000) + parseInt(attrValue); } // For Docker/local testing, ensure cookies work with localhost // Playwright's context.addCookies requires specific settings for localhost if (cookie.domain === 'localhost') { cookie.secure = false; // Localhost doesn't need HTTPS // Keep sameSite as provided by API, but ensure it's compatible if (cookie.sameSite === 'None') { // For SameSite=None, we need Secure=true, but localhost doesn't support it // So we fall back to Lax for local testing cookie.sameSite = 'Lax'; } } return cookie; }); // Apply session drift or fault modes if specified if (options.sessionDrift || options.faultMode) { const sessionCookie = cookies.find(c => c.name === 'gp_session'); if (sessionCookie) { if (options.sessionDrift) { sessionCookie.value = `drift-${options.sessionDrift}-${sessionCookie.value}`; } if (options.faultMode) { cookies.push({ name: 'gridpilot_fault_mode', value: options.faultMode, domain, path: '/', expires: Math.floor(Date.now() / 1000) + 3600, httpOnly: false, secure: false, sameSite: 'Lax' as const }); } } } // Clear existing cookies and add the new ones await context.clearCookies(); await context.addCookies(cookies); } 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