/** * AI Software Factory — E2E: Performance (Phase 15) * * Browser-side performance budgets, asserted against a real dev stack. These * are the E2E complements to the k6 suite in tests/performance (which * measures server-side latency under load). Budgets are soft here (logged + * failed only on gross violations) because a shared dev machine is noisy; * the CI k6 suite owns the hard numbers. * * Budgets (PRODUCT-SPEC §Performance): * - First contentful paint of a workspace page < 3s (dev), < 2s (prod target) * - Time to interactive (editor ready) < 4s * - Block keystroke → paint < 100ms (editor interaction < 50ms target) * - Search keystroke → first result < 1s */ import { test, expect } from "../fixtures"; import { createPage } from "../utils/api"; const DEV_FCP_BUDGET_MS = 3_000; const DEV_TTI_BUDGET_MS = 4_000; const KEYSROKE_PAINT_BUDGET_MS = 100; const SEARCH_RESULT_BUDGET_MS = 1_000; test.describe("Performance — Phase 15 (E2E budgets)", () => { test("workspace page first contentful paint under budget", async ({ authPage, freshWorkspace, }) => { const ws = await freshWorkspace("Perf Space"); const metrics = await authPage.goto(`/w/${ws.id}`, { waitUntil: "networkidle" }); void metrics; // Rely on PerformanceNavigationTiming captured via CDP for the FCP proxy. const fcp = await authPage.evaluate(async () => { // FCP via PerformanceObserver (falls back to load-event timing). return new Promise((resolve) => { const observer = new PerformanceObserver((list) => { const entry = list.getEntries().find((e) => e.name === "first-contentful-paint"); if (entry) { observer.disconnect(); resolve(entry.startTime); } }); observer.observe({ type: "paint", buffered: true }); // Fallback: resolve with load timing if no paint event arrives. setTimeout(() => { const nav = performance.getEntriesByType("navigation")[0] as PerformanceNavigationTiming; observer.disconnect(); resolve(nav ? nav.loadEventEnd : performance.now()); }, 5_000); }); }); console.log(`[perf] FCP = ${Math.round(fcp)}ms (budget ${DEV_FCP_BUDGET_MS}ms)`); expect(fcp, `FCP ${fcp}ms exceeded budget ${DEV_FCP_BUDGET_MS}ms`).toBeLessThan( DEV_FCP_BUDGET_MS * 1.5, ); // 50% slack on dev; hard budget enforced in CI k6 suite. }); test("editor becomes interactive under budget", async ({ authPage, api, freshWorkspace, }) => { const ws = await freshWorkspace("Perf TTI Space"); const page = await createPage(api, ws.id, { title: "TTI Page" }); const started = Date.now(); await authPage.goto(`/w/${ws.id}/page/${page.id}`, { waitUntil: "networkidle" }); // Interactive = the first block is focusable/clickable. const firstBlock = authPage.locator('[data-testid^="block-"]').first(); await expect(firstBlock).toBeVisible({ timeout: DEV_TTI_BUDGET_MS }); const tti = Date.now() - started; console.log(`[perf] TTI (editor ready) = ${tti}ms (budget ${DEV_TTI_BUDGET_MS}ms)`); expect(tti).toBeLessThan(DEV_TTI_BUDGET_MS * 1.5); }); test("keystroke-to-paint under budget in the editor", async ({ authPage, api, freshWorkspace, }) => { const ws = await freshWorkspace("Perf Typing Space"); const page = await createPage(api, ws.id, { title: "Typing Page" }); await authPage.goto(`/w/${ws.id}/page/${page.id}`, { waitUntil: "networkidle" }); const block = authPage.locator('[data-testid^="block-"]').first(); await block.click(); // Time a single keystroke → text visible in the DOM. const t0 = Date.now(); await authPage.keyboard.type("x"); await expect(block).toContainText("x"); const dt = Date.now() - t0; console.log(`[perf] keystroke→paint = ${dt}ms (budget ${KEYSROKE_PAINT_BUDGET_MS}ms)`); // Dev machine slack: fail only on gross regression (3× budget). expect(dt, `keystroke→paint ${dt}ms`).toBeLessThan(KEYSROKE_PAINT_BUDGET_MS * 3); }); test("search first result under budget", async ({ authPage, api, freshWorkspace, }) => { const ws = await freshWorkspace("Perf Search Space"); const token = `PERF-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; await createPage(api, ws.id, { title: `${token} fast lookup` }); await authPage.goto(`/w/${ws.id}`, { waitUntil: "networkidle" }); await authPage.getByTestId("search-button").click(); const input = authPage.getByTestId("search-input"); const t0 = Date.now(); await input.fill(token); await expect(authPage.getByTestId("search-result").first()).toBeVisible({ timeout: 5_000, }); const dt = Date.now() - t0; console.log(`[perf] search→result = ${dt}ms (budget ${SEARCH_RESULT_BUDGET_MS}ms)`); // Indexing may lag the write on a fresh dev stack; allow 2× on first pass. expect(dt, `search→result ${dt}ms`).toBeLessThan(SEARCH_RESULT_BUDGET_MS * 2); }); });