diff --git a/packages/studio/vite.browser.test.ts b/packages/studio/vite.browser.test.ts index 411c693c4..b7f6288a1 100644 --- a/packages/studio/vite.browser.test.ts +++ b/packages/studio/vite.browser.test.ts @@ -1,6 +1,7 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const launch = vi.fn(); +const originalBrowserPath = process.env["HYPERFRAMES_BROWSER_PATH"]; vi.mock("puppeteer-core", () => ({ default: { launch }, @@ -8,10 +9,19 @@ vi.mock("puppeteer-core", () => ({ describe("generateThumbnail", () => { beforeEach(() => { + process.env["HYPERFRAMES_BROWSER_PATH"] = process.execPath; launch.mockReset(); launch.mockRejectedValue(new Error("browser launch failed")); }); + afterEach(() => { + if (originalBrowserPath === undefined) { + delete process.env["HYPERFRAMES_BROWSER_PATH"]; + } else { + process.env["HYPERFRAMES_BROWSER_PATH"] = originalBrowserPath; + } + }); + it("contains browser launch failures and retries them on the next request", async () => { const { generateThumbnail } = await import("./vite.browser"); const options = { @@ -47,4 +57,12 @@ describe("findSystemChrome", () => { ).toBe("/custom/browser"); expect(pathExists).toHaveBeenCalledTimes(1); }); + + it("finds a standard Windows Chrome installation", async () => { + const { findSystemChrome } = await import("./vite.browser"); + const chromePath = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"; + const pathExists = vi.fn((path: string) => path === chromePath); + + expect(findSystemChrome({}, pathExists, "win32")).toBe(chromePath); + }); }); diff --git a/packages/studio/vite.browser.ts b/packages/studio/vite.browser.ts index 737a67476..b46b54ea2 100644 --- a/packages/studio/vite.browser.ts +++ b/packages/studio/vite.browser.ts @@ -2,6 +2,7 @@ import { existsSync } from "node:fs"; import { createHash } from "node:crypto"; +import { win32 as pathWin32 } from "node:path"; import { createStudioDevRenderBodyScripts, readStudioDevManualEditManifestContent, @@ -14,20 +15,34 @@ import { seekThumbnailPreview } from "./vite.thumbnail"; let _browser: import("puppeteer-core").Browser | null = null; let _browserLaunchPromise: Promise | null = null; -const CHROME_PATHS = [ - "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - "/usr/bin/google-chrome", - "/usr/bin/chromium-browser", -]; +function systemChromePaths(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): string[] { + if (platform === "win32") { + const programFiles = env["PROGRAMFILES"] ?? "C:\\Program Files"; + const programFilesX86 = env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)"; + const localAppData = env["LOCALAPPDATA"]; + return [ + pathWin32.join(programFiles, "Google", "Chrome", "Application", "chrome.exe"), + pathWin32.join(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"), + ...(localAppData + ? [pathWin32.join(localAppData, "Google", "Chrome", "Application", "chrome.exe")] + : []), + ]; + } + if (platform === "darwin") { + return ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"]; + } + return ["/usr/bin/google-chrome", "/usr/bin/chromium-browser"]; +} /** Resolve the same explicit browser overrides used by the CLI before system paths. */ export function findSystemChrome( env: NodeJS.ProcessEnv = process.env, pathExists: (path: string) => boolean = existsSync, + platform: NodeJS.Platform = process.platform, ): string | undefined { const override = env["HYPERFRAMES_BROWSER_PATH"] ?? env["PRODUCER_HEADLESS_SHELL_PATH"]; if (override && pathExists(override)) return override; - return CHROME_PATHS.find((path) => pathExists(path)); + return systemChromePaths(env, platform).find((path) => pathExists(path)); } async function getSharedBrowser(): Promise {