From 821003b112cfed528cabb46b8cb66213e85d28fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Fri, 23 Oct 2026 01:55:52 +0000 Subject: [PATCH] test(studio): retain Chrome launch diagnostics (#6077) * test(studio): retain Chrome launch diagnostics * fix(studio): read Chrome's version from the file on Windows instead of running it * fix(studio): shorten the Chrome version probe --- packages/studio/tests/chromeTestUtils.test.ts | 44 ++++++++++- packages/studio/tests/chromeTestUtils.ts | 33 ++++++-- packages/studio/vite.browser.ts | 16 +++- packages/studio/vite.browserVersion.test.ts | 75 +++++++++++++++++++ 4 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 packages/studio/vite.browserVersion.test.ts diff --git a/packages/studio/tests/chromeTestUtils.test.ts b/packages/studio/tests/chromeTestUtils.test.ts index 5127fc0b8d..5a4de9c981 100644 --- a/packages/studio/tests/chromeTestUtils.test.ts +++ b/packages/studio/tests/chromeTestUtils.test.ts @@ -2,7 +2,10 @@ import { TimeoutError, type Browser } from "puppeteer-core"; import { describe, expect, it, vi } from "vitest"; import { closeTestChrome, launchTestChrome } from "./chromeTestUtils"; -vi.mock("../vite.browser", () => ({ findSystemChrome: () => "/test/chrome" })); +vi.mock("../vite.browser", () => ({ + findSystemChrome: () => "/test/chrome", + browserVersion: () => "Chrome 152.0.7977.30", +})); const fakeBrowser = (close: () => Promise) => { const kill = vi.fn(); @@ -27,6 +30,43 @@ describe("closeTestChrome", () => { }); describe("launchTestChrome", () => { + it("names the selected browser and the failed attempt before retrying", async () => { + const info = vi.spyOn(console, "info").mockImplementation(() => {}); + const error = vi.spyOn(console, "error").mockImplementation(() => {}); + vi.useFakeTimers(); + try { + const { browser } = fakeBrowser(async () => undefined); + const failure = new TimeoutError("WS endpoint was not announced"); + const launch = vi + .fn() + .mockImplementationOnce(async () => { + vi.advanceTimersByTime(20_000); + throw failure; + }) + .mockResolvedValueOnce(browser); + await expect(launchTestChrome(launch)).resolves.toBe(browser); + expect(info.mock.calls).toEqual([ + ["[studio-test-chrome] launch 1/2: /test/chrome (Chrome 152.0.7977.30)"], + ["[studio-test-chrome] launch 2/2: /test/chrome (Chrome 152.0.7977.30)"], + ["[studio-test-chrome] launch 2/2 ready after 0 ms"], + ]); + expect(error.mock.calls).toEqual([ + ["[studio-test-chrome] launch 1/2 failed after 20000 ms: WS endpoint was not announced"], + ]); + } finally { + vi.useRealTimers(); + info.mockRestore(); + error.mockRestore(); + } + }); + + it("stops after the second stuck launch and preserves its error", async () => { + const failure = new TimeoutError("still stuck"); + const launch = vi.fn().mockRejectedValue(failure); + await expect(launchTestChrome(launch)).rejects.toBe(failure); + expect(launch).toHaveBeenCalledTimes(2); + }); + it("bounds each launch and tries once more after a stuck one", async () => { const { browser } = fakeBrowser(async () => undefined); const launch = vi @@ -35,7 +75,7 @@ describe("launchTestChrome", () => { .mockResolvedValueOnce(browser); await expect(launchTestChrome(launch)).resolves.toBe(browser); expect(launch).toHaveBeenCalledTimes(2); - expect(launch.mock.calls[0]![0]).toMatchObject({ timeout: 20_000 }); + expect(launch.mock.calls[0]![0]).toMatchObject({ timeout: 20_000, dumpio: true }); }); it("tries once more after a stuck launch that Windows reports as already running", async () => { diff --git a/packages/studio/tests/chromeTestUtils.ts b/packages/studio/tests/chromeTestUtils.ts index 8691bee4d7..42745b3bde 100644 --- a/packages/studio/tests/chromeTestUtils.ts +++ b/packages/studio/tests/chromeTestUtils.ts @@ -1,11 +1,12 @@ import { readFileSync } from "node:fs"; import { createRequire } from "node:module"; import puppeteer, { TimeoutError, type Browser, type Page } from "puppeteer-core"; -import { findSystemChrome } from "../vite.browser"; +import { browserVersion, findSystemChrome } from "../vite.browser"; const require = createRequire(import.meta.url); const LAUNCH_TIMEOUT_MS = 20_000; +const LAUNCH_ATTEMPTS = 2; /** Headless Chrome with the e2e launcher's flags. A launch stuck past 20 s (seen on Windows runners) * is killed and tried once more. On Windows Puppeteer reports it as "already running", not a timeout. */ @@ -17,17 +18,33 @@ export async function launchTestChrome( const options = { executablePath, headless: true, + dumpio: true, args: ["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"], timeout: LAUNCH_TIMEOUT_MS, }; - const startedAt = Date.now(); - try { - return await launch(options); - } catch (error) { - const stuck = error instanceof TimeoutError || Date.now() - startedAt >= LAUNCH_TIMEOUT_MS; - if (!stuck) throw error; - return launch(options); + const version = browserVersion(executablePath) ?? "version unavailable"; + for (let attempt = 1; attempt <= LAUNCH_ATTEMPTS; attempt += 1) { + const startedAt = Date.now(); + console.info( + `[studio-test-chrome] launch ${attempt}/${LAUNCH_ATTEMPTS}: ${executablePath} (${version})`, + ); + try { + const browser = await launch(options); + console.info( + `[studio-test-chrome] launch ${attempt}/${LAUNCH_ATTEMPTS} ready after ${Date.now() - startedAt} ms`, + ); + return browser; + } catch (error) { + const elapsed = Date.now() - startedAt; + const reason = error instanceof Error ? error.message : String(error); + console.error( + `[studio-test-chrome] launch ${attempt}/${LAUNCH_ATTEMPTS} failed after ${elapsed} ms: ${reason}`, + ); + const stuck = error instanceof TimeoutError || elapsed >= LAUNCH_TIMEOUT_MS; + if (!stuck || attempt === LAUNCH_ATTEMPTS) throw error; + } } + throw new Error("Chrome launch attempts exhausted"); } /** Closes a test Chrome, killing it when close hangs (seen on Windows runners) so the test never times out. */ diff --git a/packages/studio/vite.browser.ts b/packages/studio/vite.browser.ts index b484d058bb..b15015d8d5 100644 --- a/packages/studio/vite.browser.ts +++ b/packages/studio/vite.browser.ts @@ -84,12 +84,19 @@ function findPuppeteerCacheChrome( return undefined; } -function browserVersion( +export function browserVersion( executable: string, - run: (file: string) => string = (file) => execFileSync(file, ["--version"], { encoding: "utf8" }), + run: typeof execFileSync = execFileSync, + platform: NodeJS.Platform = process.platform, ): string | undefined { + const options = { encoding: "utf8" as const, timeout: 5_000, windowsHide: true }; + const env = { ...process.env, HYPERFRAMES_CHROME_VERSION_PATH: executable }; + const script = + "(Get-Item -LiteralPath $env:HYPERFRAMES_CHROME_VERSION_PATH).VersionInfo.ProductVersion"; try { - return run(executable).trim() || undefined; + if (platform !== "win32") return run(executable, ["--version"], options).trim() || undefined; + const v = run("powershell.exe", ["-NoProfile", "-Command", script], { ...options, env }).trim(); + return /^\d+(\.\d+){3}$/.test(v) ? `Chrome/${v}` : undefined; } catch { return undefined; } @@ -110,7 +117,8 @@ export function findSystemChrome( env: NodeJS.ProcessEnv = process.env, pathExists: (path: string) => boolean = existsSync, platform: NodeJS.Platform = process.platform, - versionOf: (path: string) => string | undefined = browserVersion, + versionOf: (path: string) => string | undefined = (path) => + browserVersion(path, undefined, platform), readDirectories: (path: string) => string[] = readdirSync, ): string | undefined { const override = [ diff --git a/packages/studio/vite.browserVersion.test.ts b/packages/studio/vite.browserVersion.test.ts new file mode 100644 index 0000000000..fccefafe02 --- /dev/null +++ b/packages/studio/vite.browserVersion.test.ts @@ -0,0 +1,75 @@ +import { expect, it, vi } from "vitest"; +import type { ExecFileSyncOptionsWithStringEncoding } from "node:child_process"; +import { browserVersion, findSystemChrome } from "./vite.browser"; + +it("bounds the Unix version command", () => { + const run = vi.fn(() => "Google Chrome 152.0.7977.42\n"); + expect(browserVersion("/test/chrome", run, "linux")).toBe("Google Chrome 152.0.7977.42"); + expect(run).toHaveBeenCalledWith("/test/chrome", ["--version"], { + encoding: "utf8", + timeout: 5_000, + windowsHide: true, + }); +}); + +it("reads Windows executable metadata without executing the browser or interpolating its path", () => { + const executable = "C:\\Program Files\\Browser's $(value)&[x]\\chrome.exe"; + const run = vi.fn( + (_file: string, _args: string[], _options: ExecFileSyncOptionsWithStringEncoding) => + "152.0.7977.42\r\n", + ); + expect(browserVersion(executable, run, "win32")).toBe("Chrome/152.0.7977.42"); + expect( + run.mock.calls.map(([file, args, options]) => ({ + file, + args, + encoding: options.encoding, + timeout: options.timeout, + windowsHide: options.windowsHide, + versionPath: options.env?.["HYPERFRAMES_CHROME_VERSION_PATH"], + })), + ).toEqual([ + { + file: "powershell.exe", + args: [ + "-NoProfile", + "-Command", + "(Get-Item -LiteralPath $env:HYPERFRAMES_CHROME_VERSION_PATH).VersionInfo.ProductVersion", + ], + encoding: "utf8", + timeout: 5_000, + windowsHide: true, + versionPath: executable, + }, + ]); +}); + +it.each(["Opening in existing browser session.", "", "not a version"])( + "does not report Windows output %j as a version", + (output) => { + expect(browserVersion("chrome.exe", () => output, "win32")).toBeUndefined(); + }, +); + +it("reports an unavailable version when the probe fails", () => { + expect( + browserVersion( + "/test/chrome", + () => { + throw new Error("probe timed out"); + }, + "linux", + ), + ).toBeUndefined(); +}); + +it.runIf(process.platform === "win32")( + "reads the selected real Windows Chrome version from metadata", + () => { + const executable = findSystemChrome(); + expect(executable).toBeTruthy(); + const version = browserVersion(executable!); + console.info(`[studio-version-probe] ${executable} (${version})`); + expect(version).toMatch(/^Chrome\/\d+\.\d+\.\d+\.\d+$/); + }, +);