import { expect, test, type Page, type Request } from '@playwright/test'; type LoadMetrics = { durationMs: number; transferredBytes: number; requestedUrls: string[]; requestedPathCounts: Map; cacheControlByPath: Map; contentEncodingByPath: Map; }; async function mockPublicApis(page: Page): Promise { await page.route('**/api/v1/**', async (route) => { const requestUrl = new URL(route.request().url()); if (requestUrl.pathname.endsWith('/api/v1/user/me')) { await route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: 'unauthorized' }), }); return; } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}), }); }); } async function measureSigninLoad(page: Page): Promise { const requestedUrls: string[] = []; const requestedPathCounts = new Map(); const cacheControlByPath = new Map(); const contentEncodingByPath = new Map(); let transferredBytes = 0; const onRequest = (request: Request) => { const requestUrl = new URL(request.url()); requestedUrls.push(request.url()); if (requestUrl.protocol === 'http:' || requestUrl.protocol === 'https:') { const resourceKey = `${requestUrl.origin}${requestUrl.pathname}`; requestedPathCounts.set( resourceKey, (requestedPathCounts.get(resourceKey) ?? 0) + 1, ); } }; const onResponse = async (response) => { const url = new URL(response.url()); const cacheControl = response.headers()['cache-control']; if (cacheControl) { cacheControlByPath.set(url.pathname, cacheControl); } const contentEncoding = response.headers()['content-encoding']; if (contentEncoding) { contentEncodingByPath.set(url.pathname, contentEncoding); } const timing = response.request().timing(); if (timing.responseEnd >= 0) { const sizes = await response.request().sizes().catch(() => null); transferredBytes += sizes?.responseBodySize ?? 0; } }; page.on('request', onRequest); page.on('response', onResponse); try { const start = performance.now(); await page.goto('/ko/signin', { waitUntil: 'networkidle' }); await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/); const durationMs = Math.round(performance.now() - start); return { durationMs, transferredBytes, requestedUrls, requestedPathCounts, cacheControlByPath, contentEncodingByPath, }; } finally { page.off('request', onRequest); page.off('response', onResponse); } } function expectNoDuplicateStaticRequests(metrics: LoadMetrics): void { const duplicates = [...metrics.requestedPathCounts.entries()].filter( ([resourceKey, count]) => { const path = new URL(resourceKey).pathname; return ( count > 1 && !path.startsWith('/api/') && !path.endsWith('/ko/signin') && !path.endsWith('/') && !path.endsWith('/main.dart.wasm') && !path.endsWith('/main.dart.mjs') && !path.endsWith('/skwasm.js') && !path.endsWith('/skwasm.wasm') ); }, ); expect(duplicates).toEqual([]); } function resolvePerformanceBudget(projectName: string): { coldMs: number; warmMs: number; } { if (projectName.includes('mobile')) { return { coldMs: 3000, warmMs: 2300 }; } return { coldMs: 2300, warmMs: 1500 }; } test.describe('UserFront login performance budget', () => { test('warm login page load stays within the platform budget and reuses cached assets', async ({ page, }, testInfo) => { await mockPublicApis(page); const budget = resolvePerformanceBudget(testInfo.project.name); const cold = await measureSigninLoad(page); const warm = await measureSigninLoad(page); console.log( `[userfront-perf] cold=${cold.durationMs}ms/${cold.transferredBytes}B warm=${warm.durationMs}ms/${warm.transferredBytes}B`, ); expect(cold.durationMs).toBeLessThanOrEqual(budget.coldMs); expect(warm.durationMs).toBeLessThanOrEqual(budget.warmMs); expect(warm.transferredBytes).toBeLessThanOrEqual(1_000_000); expectNoDuplicateStaticRequests(cold); expectNoDuplicateStaticRequests(warm); const cacheControlByPath = new Map([ ...cold.cacheControlByPath, ...warm.cacheControlByPath, ]); const contentEncodingByPath = new Map([ ...cold.contentEncodingByPath, ...warm.contentEncodingByPath, ]); const appShellCache = cacheControlByPath.get('/ko/signin') ?? ''; expect(appShellCache).toContain('no-cache'); const serviceWorkerState = await page.evaluate(async () => { if (!('serviceWorker' in navigator)) { return { available: false, secure: window.isSecureContext, scriptUrl: '', }; } const registrations = await navigator.serviceWorker.getRegistrations(); const registration = registrations[0]; return { available: true, secure: window.isSecureContext, count: registrations.length, controller: navigator.serviceWorker.controller?.scriptURL ?? '', scriptUrl: registration?.active?.scriptURL ?? registration?.waiting?.scriptURL ?? registration?.installing?.scriptURL ?? '', }; }); if (testInfo.project.name.includes('mobile')) { expect(new URL(serviceWorkerState.scriptUrl).pathname).toBe( '/flutter_service_worker.js', ); const serviceWorkerResponse = await page.context().request.get( new URL('/flutter_service_worker.js', page.url()).toString(), ); expect(serviceWorkerResponse.headers()['cache-control'] ?? '').toContain( 'no-cache', ); } else { expect(serviceWorkerState.scriptUrl).toBe(''); } expect(cold.durationMs).toBeGreaterThanOrEqual(0); }); test('root redirects to localized signin before Flutter boots', async ({ page, }) => { await mockPublicApis(page); const requestedUrls: string[] = []; page.on('request', (request) => { requestedUrls.push(request.url()); }); const start = performance.now(); await page.goto('/', { waitUntil: 'domcontentloaded' }); await expect(page).toHaveURL(/\/ko\/signin(?:\?.*)?$/); const durationMs = Math.round(performance.now() - start); expect(durationMs).toBeLessThanOrEqual(300); const rootIndex = requestedUrls.findIndex( (url) => new URL(url).pathname === '/', ); const bootstrapIndex = requestedUrls.findIndex((url) => new URL(url).pathname.endsWith('/flutter_bootstrap.js'), ); expect(rootIndex).toBeGreaterThanOrEqual(0); expect(bootstrapIndex).toBeGreaterThan(rootIndex); }); });