import { spawn } from "node:child_process"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { performance } from "node:perf_hooks"; import process from "node:process"; import { chromium } from "@playwright/test"; import { evaluateLabBudget } from "../src/application/policies/performance-budgets.js"; import { ROUTE_REGISTRY } from "../src/contracts/routes.js"; const server = spawn( "corepack", ["pnpm", "preview", "--host", "127.0.0.1", "--port", "4173"], { stdio: "ignore" }, ); const baseUrl = "http://127.0.0.1:4173"; async function waitForServer() { for (let attempt = 0; attempt < 50; attempt += 1) { try { const response = await fetch(baseUrl); if (response.ok) return; } catch { // The bounded retry loop handles startup races. } await new Promise((resolve) => setTimeout(resolve, 100)); } throw new Error("Preview server did not become ready."); } try { await waitForServer(); const release = JSON.parse( await readFile("dist/release-manifest.json", "utf8"), ); const thresholds = JSON.parse( await readFile("config/performance/budgets.json", "utf8"), ).lab; const browser = await chromium.launch(); try { const context = await browser.newContext({ viewport: { width: 1280, height: 720 }, }); const page = await context.newPage(); const cdp = await context.newCDPSession(page); await cdp.send("Network.enable"); await cdp.send("Network.emulateNetworkConditions", { offline: false, latency: 40, downloadThroughput: 200_000, uploadThroughput: 93_750, connectionType: "cellular4g", }); await cdp.send("Emulation.setCPUThrottlingRate", { rate: 4 }); await page.addInitScript(() => { const evidence = { lcpMs: 0, cls: 0 }; /** @type {any} */ (window).__contractPerformance = evidence; new PerformanceObserver((list) => { for (const entry of list.getEntries()) evidence.lcpMs = entry.startTime; }).observe({ type: "largest-contentful-paint", buffered: true }); new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (!(/** @type {any} */ (entry)).hadRecentInput) { evidence.cls += /** @type {any} */ (entry).value; } } }).observe({ type: "layout-shift", buffered: true }); }); await page.goto(baseUrl, { waitUntil: "networkidle" }); const targetLabel = ROUTE_REGISTRY.SAMPLE_RESOURCE_LIST.navigationLabel; if (!targetLabel) { throw new Error("Performance route must be present in navigation."); } const interactionStarted = performance.now(); await page.getByRole("link", { name: targetLabel }).click(); await page.getByRole("heading", { name: "세션이 필요합니다." }).waitFor(); const namedInteractionMs = performance.now() - interactionStarted; const paint = await page.evaluate( () => /** @type {any} */ (window).__contractPerformance, ); const contextMetadata = { runner: { platform: process.platform, architecture: process.arch, nodeVersion: process.version, }, browser: { name: "chromium", version: await browser.version() }, viewport: { width: 1280, height: 720 }, network: { profile: "contract-fast-4g", latencyMs: 40, downloadBytesPerSecond: 200_000, uploadBytesPerSecond: 93_750, }, cpu: { throttlingRate: 4 }, cache: { state: "cold", isolation: "new-browser-context" }, build: { buildId: release.buildId, releaseId: release.releaseId }, }; const metrics = { lcpMs: Math.round(paint.lcpMs), cls: Number(paint.cls.toFixed(4)), namedInteractionMs: Math.round(namedInteractionMs), }; const result = evaluateLabBudget( { context: contextMetadata, metrics }, thresholds, ); const fixtures = [ { name: "missing-context", passed: !evaluateLabBudget({ metrics }, thresholds).passed, }, { name: "lcp-over-threshold", passed: !evaluateLabBudget( { context: contextMetadata, metrics: { ...metrics, lcpMs: thresholds.lcpMs + 1 }, }, thresholds, ).passed, }, ]; const passed = result.passed && fixtures.every((fixture) => fixture.passed); await mkdir("artifacts/performance", { recursive: true }); await writeFile( "artifacts/performance/lab.json", `${JSON.stringify( { schemaVersion: 1, generatedAt: new Date().toISOString(), context: contextMetadata, metrics, thresholds, fixtures, passed, }, null, 2, )}\n`, ); if (!passed) { throw new Error(`Lab performance failed: ${JSON.stringify(metrics)}`); } process.stdout.write( `Lab performance: PASS (LCP ${metrics.lcpMs}ms, CLS ${metrics.cls}, interaction ${metrics.namedInteractionMs}ms)\n`, ); } finally { await browser.close(); } } finally { server.kill("SIGTERM"); }