import { expect, test, type Page, type Request } from '@playwright/test'; type LoadMetrics = { durationMs: number; transferredBytes: number; requestedUrls: string[]; 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 cacheControlByPath = new Map(); const contentEncodingByPath = new Map(); let transferredBytes = 0; page.on('request', (request: Request) => { requestedUrls.push(request.url()); }); page.on('response', 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; } }); 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, cacheControlByPath, contentEncodingByPath, }; } test.describe('UserFront login performance budget', () => { test('warm login page load stays within the two second budget and reuses cached assets', async ({ page, }) => { await mockPublicApis(page); 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(warm.durationMs).toBeLessThanOrEqual(2000); expect(warm.transferredBytes).toBeLessThanOrEqual(1_000_000); expect(warm.requestedUrls.some((url) => url.includes('NotoSansKR'))).toBe( false, ); expect( warm.requestedUrls.some((url) => url.includes('fonts.googleapis.com/icon?family=Material+Icons'), ), ).toBe(false); 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'); expect(cold.durationMs).toBeGreaterThanOrEqual(0); const brotliEntrypoint = [...contentEncodingByPath.entries()].some( ([path, encoding]) => /^\/main\.dart\.[0-9a-f]{12}\.(?:mjs|wasm|js)$/.test(path) && encoding === 'br', ); expect(brotliEntrypoint).toBe(true); }); });