import { devices, expect, test, type Page, type Request, type Response, } from '@playwright/test'; type LoadMetrics = { appOrigin: string; 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 appOrigin = new URL( process.env.BASE_URL ?? `http://127.0.0.1:${process.env.PORT ?? '4173'}`, ).origin; 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: 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 { appOrigin, 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 resourceUrl = new URL(resourceKey); const path = resourceUrl.pathname; return ( count > 1 && resourceUrl.origin === metrics.appOrigin && !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('webkit')) { return { coldMs: 4000, warmMs: 4000 }; } if (projectName.includes('firefox')) { return { coldMs: 2600, warmMs: 2800 }; } if (projectName.includes('mobile')) { return { coldMs: 3000, warmMs: 2300 }; } return { coldMs: 2300, warmMs: 1500 }; } function resolveRootRedirectBudget(projectName: string): number { if (projectName.includes('webkit')) { return 700; } if (projectName.includes('firefox')) { return 600; } return 300; } test.describe('UserFront login performance budget', () => { test('mobile Chrome service worker install does not fetch unused CanvasKit variants', async ({ browser, }, testInfo) => { test.skip( testInfo.project.name !== 'chromium-mobile-webapp', 'service worker install race is covered once in the mobile Chromium project', ); const context = await browser.newContext({ ...devices['Pixel 7'], locale: 'ko-KR', serviceWorkers: 'allow', }); const page = await context.newPage(); await mockPublicApis(page); try { const serviceWorkerResponse = await context.request.get( new URL( '/flutter_service_worker.js', process.env.BASE_URL ?? `http://127.0.0.1:${process.env.PORT ?? '4173'}`, ).toString(), ); const serviceWorkerBody = await serviceWorkerResponse.text(); expect(serviceWorkerBody).not.toContain('"/canvaskit/'); expect(serviceWorkerBody).not.toContain('"/main.dart.'); await page.goto('/ko/signin', { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3_000); } finally { await context.close(); } }); 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') && serviceWorkerState.scriptUrl) { 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, }, testInfo) => { 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( resolveRootRedirectBudget(testInfo.project.name), ); 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); }); });