| import fs from "node:fs"; |
| import fsp from "node:fs/promises"; |
| import os from "node:os"; |
| import path from "node:path"; |
|
|
| import { afterEach, describe, expect, it, vi } from "vitest"; |
|
|
| import { |
| decorateOpenClawProfile, |
| ensureProfileCleanExit, |
| findChromeExecutableMac, |
| findChromeExecutableWindows, |
| isChromeReachable, |
| resolveBrowserExecutableForPlatform, |
| stopOpenClawChrome, |
| } from "./chrome.js"; |
| import { |
| DEFAULT_OPENCLAW_BROWSER_COLOR, |
| DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, |
| } from "./constants.js"; |
|
|
| async function readJson(filePath: string): Promise<Record<string, unknown>> { |
| const raw = await fsp.readFile(filePath, "utf-8"); |
| return JSON.parse(raw) as Record<string, unknown>; |
| } |
|
|
| describe("browser chrome profile decoration", () => { |
| afterEach(() => { |
| vi.unstubAllGlobals(); |
| vi.restoreAllMocks(); |
| }); |
|
|
| it("writes expected name + signed ARGB seed to Chrome prefs", async () => { |
| const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-test-")); |
| try { |
| decorateOpenClawProfile(userDataDir, { color: DEFAULT_OPENCLAW_BROWSER_COLOR }); |
|
|
| const expectedSignedArgb = ((0xff << 24) | 0xff4500) >> 0; |
|
|
| const localState = await readJson(path.join(userDataDir, "Local State")); |
| const profile = localState.profile as Record<string, unknown>; |
| const infoCache = profile.info_cache as Record<string, unknown>; |
| const def = infoCache.Default as Record<string, unknown>; |
|
|
| expect(def.name).toBe(DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME); |
| expect(def.shortcut_name).toBe(DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME); |
| expect(def.profile_color_seed).toBe(expectedSignedArgb); |
| expect(def.profile_highlight_color).toBe(expectedSignedArgb); |
| expect(def.default_avatar_fill_color).toBe(expectedSignedArgb); |
| expect(def.default_avatar_stroke_color).toBe(expectedSignedArgb); |
|
|
| const prefs = await readJson(path.join(userDataDir, "Default", "Preferences")); |
| const browser = prefs.browser as Record<string, unknown>; |
| const theme = browser.theme as Record<string, unknown>; |
| const autogenerated = prefs.autogenerated as Record<string, unknown>; |
| const autogeneratedTheme = autogenerated.theme as Record<string, unknown>; |
|
|
| expect(theme.user_color2).toBe(expectedSignedArgb); |
| expect(autogeneratedTheme.color).toBe(expectedSignedArgb); |
|
|
| const marker = await fsp.readFile( |
| path.join(userDataDir, ".openclaw-profile-decorated"), |
| "utf-8", |
| ); |
| expect(marker.trim()).toMatch(/^\d+$/); |
| } finally { |
| await fsp.rm(userDataDir, { recursive: true, force: true }); |
| } |
| }); |
|
|
| it("best-effort writes name when color is invalid", async () => { |
| const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-test-")); |
| try { |
| decorateOpenClawProfile(userDataDir, { color: "lobster-orange" }); |
| const localState = await readJson(path.join(userDataDir, "Local State")); |
| const profile = localState.profile as Record<string, unknown>; |
| const infoCache = profile.info_cache as Record<string, unknown>; |
| const def = infoCache.Default as Record<string, unknown>; |
|
|
| expect(def.name).toBe(DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME); |
| expect(def.profile_color_seed).toBeUndefined(); |
| } finally { |
| await fsp.rm(userDataDir, { recursive: true, force: true }); |
| } |
| }); |
|
|
| it("recovers from missing/invalid preference files", async () => { |
| const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-test-")); |
| try { |
| await fsp.mkdir(path.join(userDataDir, "Default"), { recursive: true }); |
| await fsp.writeFile(path.join(userDataDir, "Local State"), "{", "utf-8"); |
| await fsp.writeFile( |
| path.join(userDataDir, "Default", "Preferences"), |
| "[]", |
| "utf-8", |
| ); |
|
|
| decorateOpenClawProfile(userDataDir, { color: DEFAULT_OPENCLAW_BROWSER_COLOR }); |
|
|
| const localState = await readJson(path.join(userDataDir, "Local State")); |
| expect(typeof localState.profile).toBe("object"); |
|
|
| const prefs = await readJson(path.join(userDataDir, "Default", "Preferences")); |
| expect(typeof prefs.profile).toBe("object"); |
| } finally { |
| await fsp.rm(userDataDir, { recursive: true, force: true }); |
| } |
| }); |
|
|
| it("writes clean exit prefs to avoid restore prompts", async () => { |
| const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-test-")); |
| try { |
| ensureProfileCleanExit(userDataDir); |
| const prefs = await readJson(path.join(userDataDir, "Default", "Preferences")); |
| expect(prefs.exit_type).toBe("Normal"); |
| expect(prefs.exited_cleanly).toBe(true); |
| } finally { |
| await fsp.rm(userDataDir, { recursive: true, force: true }); |
| } |
| }); |
|
|
| it("is idempotent when rerun on an existing profile", async () => { |
| const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-test-")); |
| try { |
| decorateOpenClawProfile(userDataDir, { color: DEFAULT_OPENCLAW_BROWSER_COLOR }); |
| decorateOpenClawProfile(userDataDir, { color: DEFAULT_OPENCLAW_BROWSER_COLOR }); |
|
|
| const prefs = await readJson(path.join(userDataDir, "Default", "Preferences")); |
| const profile = prefs.profile as Record<string, unknown>; |
| expect(profile.name).toBe(DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME); |
| } finally { |
| await fsp.rm(userDataDir, { recursive: true, force: true }); |
| } |
| }); |
| }); |
|
|
| describe("browser chrome helpers", () => { |
| afterEach(() => { |
| vi.unstubAllEnvs(); |
| vi.unstubAllGlobals(); |
| vi.restoreAllMocks(); |
| }); |
|
|
| it("picks the first existing Chrome candidate on macOS", () => { |
| const exists = vi |
| .spyOn(fs, "existsSync") |
| .mockImplementation((p) => |
| String(p).includes("Google Chrome.app/Contents/MacOS/Google Chrome"), |
| ); |
| const exe = findChromeExecutableMac(); |
| expect(exe?.kind).toBe("chrome"); |
| expect(exe?.path).toMatch(/Google Chrome\.app/); |
| exists.mockRestore(); |
| }); |
|
|
| it("returns null when no Chrome candidate exists", () => { |
| const exists = vi.spyOn(fs, "existsSync").mockReturnValue(false); |
| expect(findChromeExecutableMac()).toBeNull(); |
| exists.mockRestore(); |
| }); |
|
|
| it("picks the first existing Chrome candidate on Windows", () => { |
| vi.stubEnv("LOCALAPPDATA", "C:\\Users\\Test\\AppData\\Local"); |
| const exists = vi.spyOn(fs, "existsSync").mockImplementation((p) => { |
| const pathStr = String(p); |
| return ( |
| pathStr.includes("Google\\Chrome\\Application\\chrome.exe") || |
| pathStr.includes("BraveSoftware\\Brave-Browser\\Application\\brave.exe") || |
| pathStr.includes("Microsoft\\Edge\\Application\\msedge.exe") |
| ); |
| }); |
| const exe = findChromeExecutableWindows(); |
| expect(exe?.kind).toBe("chrome"); |
| expect(exe?.path).toMatch(/chrome\.exe$/); |
| exists.mockRestore(); |
| }); |
|
|
| it("finds Chrome in Program Files on Windows", () => { |
| const marker = path.win32.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("resolves Windows executables without LOCALAPPDATA", () => { |
| vi.stubEnv("LOCALAPPDATA", ""); |
| vi.stubEnv("ProgramFiles", "C:\\Program Files"); |
| vi.stubEnv("ProgramFiles(x86)", "C:\\Program Files (x86)"); |
| const marker = path.win32.join( |
| "Program Files", |
| "Google", |
| "Chrome", |
| "Application", |
| "chrome.exe", |
| ); |
| const exists = vi.spyOn(fs, "existsSync").mockImplementation((p) => String(p).includes(marker)); |
| const exe = resolveBrowserExecutableForPlatform( |
| {} as Parameters<typeof resolveBrowserExecutableForPlatform>[0], |
| "win32", |
| ); |
| expect(exe?.kind).toBe("chrome"); |
| expect(exe?.path).toMatch(/chrome\.exe$/); |
| exists.mockRestore(); |
| }); |
|
|
| it("reports reachability based on /json/version", async () => { |
| vi.stubGlobal( |
| "fetch", |
| vi.fn().mockResolvedValue({ |
| ok: true, |
| json: async () => ({ webSocketDebuggerUrl: "ws://127.0.0.1/devtools" }), |
| } as unknown as Response), |
| ); |
| await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe(true); |
|
|
| vi.stubGlobal( |
| "fetch", |
| vi.fn().mockResolvedValue({ |
| ok: false, |
| json: async () => ({}), |
| } as unknown as Response), |
| ); |
| await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe(false); |
|
|
| vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("boom"))); |
| await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe(false); |
| }); |
|
|
| it("stopOpenClawChrome no-ops when process is already killed", async () => { |
| const proc = { killed: true, exitCode: null, kill: vi.fn() }; |
| await stopOpenClawChrome( |
| { |
| proc, |
| cdpPort: 12345, |
| } as unknown as Parameters<typeof stopOpenClawChrome>[0], |
| 10, |
| ); |
| expect(proc.kill).not.toHaveBeenCalled(); |
| }); |
|
|
| it("stopOpenClawChrome sends SIGTERM and returns once CDP is down", async () => { |
| vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("down"))); |
| const proc = { killed: false, exitCode: null, kill: vi.fn() }; |
| await stopOpenClawChrome( |
| { |
| proc, |
| cdpPort: 12345, |
| } as unknown as Parameters<typeof stopOpenClawChrome>[0], |
| 10, |
| ); |
| expect(proc.kill).toHaveBeenCalledWith("SIGTERM"); |
| }); |
| }); |
|
|