mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
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
This commit is contained in:
@@ -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<void>) => {
|
||||
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 () => {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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+$/);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user