From 3071018593ec1ce577a8cb59b3731af74b8923c6 Mon Sep 17 00:00:00 2001 From: Phil Larson Date: Tue, 4 Aug 2026 22:03:52 -0700 Subject: [PATCH] fix: prepare browser smoke fixture before launch (#3333) ## Summary - prepares emitted dashboard CSS and binds the fixture server before starting Chrome - prevents cold client builds from consuming the supervised browser lifetime - adds a regression that holds browser launch until fixture preparation resolves ## Test plan - `FUSION_DASHBOARD_DEEP=1 pnpm --filter @fusion/dashboard exec vitest run --project dashboard-app-quality-foundation-ui --pool=vmThreads --maxWorkers=1 --silent=passed-only --reporter=dot app/__tests__/browser-layout-smoke-fixture.test.ts` - `pnpm --filter @fusion/dashboard typecheck` - `pnpm --filter @fusion/dashboard build` - `FUSION_BROWSER_SMOKE_REQUIRE=1 pnpm --filter @fusion/dashboard test:browser-smoke` - `pnpm exec eslint packages/dashboard/scripts/browser-layout-smoke.mjs packages/dashboard/app/__tests__/browser-layout-smoke-fixture.test.ts` ## Summary by CodeRabbit * **Bug Fixes** * Improved browser smoke test setup to ensure fixture preparation completes before the browser launches. * Added cleanup handling when browser startup fails, preventing leftover test resources. * Preserved the primary browser launch error when cleanup also fails, while recording the cleanup issue. * **Tests** * Added coverage for fixture startup order, launch failures, and cleanup behavior. * Preserved existing HTML fixture validation. --- .../browser-layout-smoke-fixture.test.ts | 70 ++++++++++++++++++- .../scripts/browser-layout-smoke.mjs | 36 ++++++++-- 2 files changed, 100 insertions(+), 6 deletions(-) diff --git a/packages/dashboard/app/__tests__/browser-layout-smoke-fixture.test.ts b/packages/dashboard/app/__tests__/browser-layout-smoke-fixture.test.ts index c379c64b55..b3022262e9 100644 --- a/packages/dashboard/app/__tests__/browser-layout-smoke-fixture.test.ts +++ b/packages/dashboard/app/__tests__/browser-layout-smoke-fixture.test.ts @@ -1,7 +1,73 @@ -import { describe, expect, it } from "vitest"; -import { createSmokeHtml } from "../../scripts/browser-layout-smoke.mjs"; +import { describe, expect, it, vi } from "vitest"; +import { createSmokeHtml, prepareBrowserSmoke } from "../../scripts/browser-layout-smoke.mjs"; describe("browser layout smoke fixture", () => { + /* + FNXC:DashboardBrowserSmoke 2026-08-04-12:24: + Client CSS preparation may run a multi-minute build, so it must finish before Chrome's supervised lifetime begins. Otherwise the 60-second browser cap can expire before the fixture or any named geometry assertion is reached. + */ + it("prepares the fixture before starting the supervised browser lifetime", async () => { + const events: string[] = []; + const fixture = { server: {}, url: "http://127.0.0.1:1234/" }; + const launched = { browser: {}, userDataDir: "/tmp/browser-smoke", wsUrl: "ws://browser" }; + let resolveFixture!: (value: typeof fixture) => void; + const fixtureReady = new Promise((resolve) => { + resolveFixture = resolve; + }); + + const preparing = prepareBrowserSmoke("/browser", { + startFixture: async () => { + events.push("fixture:start"); + const result = await fixtureReady; + events.push("fixture:ready"); + return result; + }, + launch: async () => { + events.push("browser:launch"); + return launched; + }, + }); + + await Promise.resolve(); + expect(events).toEqual(["fixture:start"]); + + resolveFixture(fixture); + await expect(preparing).resolves.toEqual({ fixture, launched }); + expect(events).toEqual(["fixture:start", "fixture:ready", "browser:launch"]); + }); + + /* + FNXC:DashboardBrowserSmoke 2026-08-04-13:29: + A browser launch failure remains the primary diagnostic even when fixture cleanup also fails. Cleanup must still receive the prepared fixture, and its secondary failure must remain observable without replacing the launch error. + */ + it("preserves a browser launch failure when fixture cleanup also fails", async () => { + const fixture = { server: null as never, url: "http://127.0.0.1:1234/" }; + const launchError = new Error("browser launch failed"); + const cleanupError = new Error("fixture cleanup failed"); + const closeFixture = vi.fn(async () => { + throw cleanupError; + }); + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + try { + await expect(prepareBrowserSmoke("/browser", { + startFixture: async () => fixture, + launch: async () => { + throw launchError; + }, + closeFixture, + })).rejects.toBe(launchError); + expect(closeFixture).toHaveBeenCalledOnce(); + expect(closeFixture).toHaveBeenCalledWith(fixture); + expect(warn).toHaveBeenCalledWith( + "[dashboard-browser-smoke] fixture cleanup after browser launch failure also failed:", + cleanupError, + ); + } finally { + warn.mockRestore(); + } + }); + it("includes standalone and embedded Git Manager shell fixtures", () => { const html = createSmokeHtml(); for (const hook of [ diff --git a/packages/dashboard/scripts/browser-layout-smoke.mjs b/packages/dashboard/scripts/browser-layout-smoke.mjs index 271ec3459d..5c2b104a2f 100644 --- a/packages/dashboard/scripts/browser-layout-smoke.mjs +++ b/packages/dashboard/scripts/browser-layout-smoke.mjs @@ -797,6 +797,32 @@ async function startFixtureServer() { }; } +/* +FNXC:DashboardBrowserSmoke 2026-08-04-12:24: +Prepare emitted client CSS and bind the fixture server before starting Chrome's supervised 60-second lifetime. A cold client build can take several minutes on supported development hosts; that build time must not consume the browser's geometry-check budget or kill Chrome before the first named assertion. +*/ +export async function prepareBrowserSmoke(executable, { + startFixture = startFixtureServer, + launch = launchBrowser, + closeFixture = (fixture) => closeServer(fixture.server), +} = {}) { + const fixture = await startFixture(); + try { + const launched = await launch(executable); + return { fixture, launched }; + } catch (error) { + try { + await closeFixture(fixture); + } catch (cleanupError) { + console.warn( + "[dashboard-browser-smoke] fixture cleanup after browser launch failure also failed:", + cleanupError, + ); + } + throw error; + } +} + async function findBrowserExecutable() { const envCandidates = [ process.env.FUSION_BROWSER_SMOKE_BROWSER, @@ -2019,11 +2045,11 @@ async function main() { } log("using local browser; this fixture smoke checks real CSS layout but does not replace full dashboard E2E coverage."); - const launched = await launchBrowser(executable); let fixture; + let launched; let page; try { - fixture = await startFixtureServer(); + ({ fixture, launched } = await prepareBrowserSmoke(executable)); page = await createPage(launched.wsUrl); await runSmokeChecks(page, fixture.url); } finally { @@ -2031,8 +2057,10 @@ async function main() { if (fixture) { await closeServer(fixture.server); } - await stopBrowser(launched.browser); - await rm(launched.userDataDir, { recursive: true, force: true }); + if (launched) { + await stopBrowser(launched.browser); + await rm(launched.userDataDir, { recursive: true, force: true }); + } } }