diff --git a/src/browser/chrome.test.ts b/src/browser/chrome.test.ts index f6e7786af..ddcea3a7d 100644 --- a/src/browser/chrome.test.ts +++ b/src/browser/chrome.test.ts @@ -8,6 +8,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { decorateClawdProfile, findChromeExecutableMac, + findChromeExecutableWindows, isChromeReachable, stopClawdChrome, } from "./chrome.js"; @@ -155,6 +156,34 @@ describe("browser chrome helpers", () => { exists.mockRestore(); }); + it("picks the first existing Chrome candidate on Windows", () => { + const exists = vi + .spyOn(fs, "existsSync") + .mockImplementation((p) => String(p).includes("Chrome SxS")); + const exe = findChromeExecutableWindows(); + expect(exe?.kind).toBe("canary"); + expect(exe?.path).toMatch(/Chrome SxS/); + exists.mockRestore(); + }); + + it("finds Chrome in Program Files on Windows", () => { + // Use path.join to match how the function builds paths (cross-platform) + const marker = path.join("Program Files", "Google", "Chrome"); + const exists = vi + .spyOn(fs, "existsSync") + .mockImplementation((p) => String(p).includes(marker)); + const exe = findChromeExecutableWindows(); + expect(exe?.kind).toBe("chrome"); + expect(exe?.path).toMatch(/chrome\.exe$/); + exists.mockRestore(); + }); + + it("returns null when no Chrome candidate exists on Windows", () => { + const exists = vi.spyOn(fs, "existsSync").mockReturnValue(false); + expect(findChromeExecutableWindows()).toBeNull(); + exists.mockRestore(); + }); + it("reports reachability based on /json/version", async () => { vi.stubGlobal( "fetch", diff --git a/src/browser/chrome.ts b/src/browser/chrome.ts index 96cc7c5b9..9979f803b 100644 --- a/src/browser/chrome.ts +++ b/src/browser/chrome.ts @@ -102,6 +102,72 @@ export function findChromeExecutableLinux(): BrowserExecutable | null { return null; } +export function findChromeExecutableWindows(): BrowserExecutable | null { + const localAppData = process.env.LOCALAPPDATA ?? ""; + const programFiles = process.env.ProgramFiles ?? "C:\\Program Files"; + // Must use bracket notation: variable name contains parentheses + const programFilesX86 = + process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)"; + + const candidates: Array = [ + // Chrome Canary (user install) + { + kind: "canary", + path: path.join( + localAppData, + "Google", + "Chrome SxS", + "Application", + "chrome.exe", + ), + }, + // Chromium (user install) + { + kind: "chromium", + path: path.join(localAppData, "Chromium", "Application", "chrome.exe"), + }, + // Chrome (user install) + { + kind: "chrome", + path: path.join( + localAppData, + "Google", + "Chrome", + "Application", + "chrome.exe", + ), + }, + // Chrome (system install, 64-bit) + { + kind: "chrome", + path: path.join( + programFiles, + "Google", + "Chrome", + "Application", + "chrome.exe", + ), + }, + // Chrome (system install, 32-bit on 64-bit Windows) + { + kind: "chrome", + path: path.join( + programFilesX86, + "Google", + "Chrome", + "Application", + "chrome.exe", + ), + }, + ]; + + for (const candidate of candidates) { + if (exists(candidate.path)) return candidate; + } + + return null; +} + function resolveBrowserExecutable( resolved: ResolvedBrowserConfig, ): BrowserExecutable | null { @@ -116,6 +182,7 @@ function resolveBrowserExecutable( if (process.platform === "darwin") return findChromeExecutableMac(); if (process.platform === "linux") return findChromeExecutableLinux(); + if (process.platform === "win32") return findChromeExecutableWindows(); return null; } @@ -445,7 +512,7 @@ export async function launchClawdChrome( const exe = resolveBrowserExecutable(resolved); if (!exe) { throw new Error( - "No supported browser found (Chrome/Chromium on macOS or Linux).", + "No supported browser found (Chrome/Chromium on macOS, Linux, or Windows).", ); }