mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ui): harden control e2e browser setup
This commit is contained in:
+1
-1
@@ -1761,7 +1761,7 @@
|
||||
"test:restart:gateway": "node --import tsx scripts/bench-gateway-restart.ts",
|
||||
"test:startup:memory": "node scripts/ensure-cli-startup-build.mjs && node scripts/check-cli-startup-memory.mjs",
|
||||
"test:ui": "pnpm ui:i18n:check && pnpm lint:ui:no-raw-window-open && pnpm --dir ui test",
|
||||
"test:ui:e2e": "node scripts/run-vitest.mjs run --config test/vitest/vitest.ui-e2e.config.ts --configLoader runner",
|
||||
"test:ui:e2e": "node scripts/ensure-playwright-chromium.mjs && node scripts/run-vitest.mjs run --config test/vitest/vitest.ui-e2e.config.ts --configLoader runner",
|
||||
"test:unit": "pnpm test:unit:fast && node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts",
|
||||
"test:unit:fast": "node scripts/run-vitest.mjs run --config test/vitest/vitest.unit-fast.config.ts",
|
||||
"test:unit:fast:audit": "node scripts/test-unit-fast-audit.mjs",
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync as spawnSyncImpl } from "node:child_process";
|
||||
import { existsSync as existsSyncImpl, realpathSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { chromium } from "playwright";
|
||||
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const playwrightInstallArgs = ["--dir", "ui", "exec", "playwright", "install", "chromium"];
|
||||
|
||||
export function resolvePlaywrightInstallRunner(options = {}) {
|
||||
const env = options.env ?? process.env;
|
||||
return resolvePnpmRunner({
|
||||
comSpec: options.comSpec ?? env.ComSpec ?? env.COMSPEC,
|
||||
npmExecPath: env.npm_execpath,
|
||||
platform: options.platform,
|
||||
pnpmArgs: playwrightInstallArgs,
|
||||
});
|
||||
}
|
||||
|
||||
export function isDirectScriptExecution(
|
||||
argvEntry = process.argv[1],
|
||||
modulePath = fileURLToPath(import.meta.url),
|
||||
realpath = realpathSync.native,
|
||||
) {
|
||||
if (!argvEntry) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return realpath(argvEntry) === realpath(modulePath);
|
||||
} catch {
|
||||
return resolve(argvEntry) === resolve(modulePath);
|
||||
}
|
||||
}
|
||||
|
||||
export function ensurePlaywrightChromium(options = {}) {
|
||||
const env = options.env ?? process.env;
|
||||
const executablePath = options.executablePath ?? chromium.executablePath();
|
||||
const existsSync = options.existsSync ?? existsSyncImpl;
|
||||
const log = options.log ?? console.error;
|
||||
const spawnSync = options.spawnSync ?? spawnSyncImpl;
|
||||
|
||||
if (existsSync(executablePath)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1") {
|
||||
log(
|
||||
`[ui-e2e] Playwright Chromium is missing at ${executablePath}; OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 leaves the lane skipped.`,
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
log(`[ui-e2e] Playwright Chromium is missing at ${executablePath}; installing chromium.`);
|
||||
const runner = resolvePlaywrightInstallRunner({
|
||||
comSpec: options.comSpec,
|
||||
env,
|
||||
platform: options.platform,
|
||||
});
|
||||
const result = spawnSync(runner.command, runner.args, {
|
||||
cwd: options.cwd ?? repoRoot,
|
||||
env,
|
||||
shell: runner.shell,
|
||||
stdio: options.stdio ?? "inherit",
|
||||
windowsVerbatimArguments: runner.windowsVerbatimArguments,
|
||||
});
|
||||
const status = result.status ?? 1;
|
||||
if (status !== 0) {
|
||||
return status;
|
||||
}
|
||||
|
||||
if (!existsSync(executablePath)) {
|
||||
log(`[ui-e2e] Playwright install completed but Chromium is still missing at ${executablePath}.`);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (isDirectScriptExecution()) {
|
||||
process.exitCode = ensurePlaywrightChromium();
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
ensurePlaywrightChromium,
|
||||
resolvePlaywrightInstallRunner,
|
||||
} from "../../scripts/ensure-playwright-chromium.mjs";
|
||||
|
||||
describe("ensurePlaywrightChromium", () => {
|
||||
it("does nothing when the browser binary exists", () => {
|
||||
const spawnSync = vi.fn();
|
||||
|
||||
expect(
|
||||
ensurePlaywrightChromium({
|
||||
executablePath: "/cache/chromium/chrome",
|
||||
existsSync: () => true,
|
||||
spawnSync,
|
||||
}),
|
||||
).toBe(0);
|
||||
expect(spawnSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves the intentional missing-browser skip mode", () => {
|
||||
const logs: string[] = [];
|
||||
const spawnSync = vi.fn();
|
||||
|
||||
expect(
|
||||
ensurePlaywrightChromium({
|
||||
env: { OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM: "1" },
|
||||
executablePath: "/cache/chromium/chrome",
|
||||
existsSync: () => false,
|
||||
log: (line: string) => logs.push(line),
|
||||
spawnSync,
|
||||
}),
|
||||
).toBe(0);
|
||||
expect(spawnSync).not.toHaveBeenCalled();
|
||||
expect(logs.join("\n")).toContain("leaves the lane skipped");
|
||||
});
|
||||
|
||||
it("installs Chromium through the UI Playwright package when missing", () => {
|
||||
const spawnSync = vi.fn(() => ({ status: 0 }));
|
||||
let existsCalls = 0;
|
||||
|
||||
expect(
|
||||
ensurePlaywrightChromium({
|
||||
cwd: "/repo",
|
||||
env: { PATH: "/bin" },
|
||||
executablePath: "/cache/chromium/chrome",
|
||||
existsSync: () => ++existsCalls > 1,
|
||||
platform: "linux",
|
||||
spawnSync,
|
||||
stdio: "pipe",
|
||||
}),
|
||||
).toBe(0);
|
||||
expect(spawnSync).toHaveBeenCalledWith(
|
||||
"pnpm",
|
||||
["--dir", "ui", "exec", "playwright", "install", "chromium"],
|
||||
{
|
||||
cwd: "/repo",
|
||||
env: { PATH: "/bin" },
|
||||
shell: false,
|
||||
stdio: "pipe",
|
||||
windowsVerbatimArguments: undefined,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the installer status when Playwright install fails", () => {
|
||||
expect(
|
||||
ensurePlaywrightChromium({
|
||||
executablePath: "/cache/chromium/chrome",
|
||||
existsSync: () => false,
|
||||
spawnSync: vi.fn(() => ({ status: 23 })),
|
||||
stdio: "pipe",
|
||||
}),
|
||||
).toBe(23);
|
||||
});
|
||||
|
||||
it("wraps the pnpm command shim on Windows", () => {
|
||||
expect(
|
||||
resolvePlaywrightInstallRunner({
|
||||
comSpec: "C:\\Windows\\System32\\cmd.exe",
|
||||
env: {},
|
||||
platform: "win32",
|
||||
}),
|
||||
).toEqual({
|
||||
args: [
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
'pnpm.cmd --dir ui exec playwright install chromium',
|
||||
],
|
||||
command: "C:\\Windows\\System32\\cmd.exe",
|
||||
shell: false,
|
||||
windowsVerbatimArguments: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { createServer as createNetServer } from "node:net";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -7,6 +8,9 @@ import { createServer, type ViteDevServer } from "vite";
|
||||
import { CONTROL_UI_BOOTSTRAP_CONFIG_PATH } from "../../../src/gateway/control-ui-contract.js";
|
||||
import { PROTOCOL_VERSION } from "../../../src/gateway/protocol/version.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const json5EsmPath = require.resolve("json5/dist/index.mjs");
|
||||
|
||||
export type MockGatewayRequest = {
|
||||
id: string;
|
||||
method: string;
|
||||
@@ -69,9 +73,14 @@ export async function startControlUiE2eServer(): Promise<ControlUiE2eServer> {
|
||||
},
|
||||
logLevel: "error",
|
||||
optimizeDeps: {
|
||||
include: ["lit/directives/repeat.js"],
|
||||
include: ["ipaddr.js", "lit/directives/repeat.js", "markdown-it-task-lists"],
|
||||
},
|
||||
publicDir: path.join(uiRoot, "public"),
|
||||
resolve: {
|
||||
alias: {
|
||||
json5: json5EsmPath,
|
||||
},
|
||||
},
|
||||
root: uiRoot,
|
||||
server: {
|
||||
host: "127.0.0.1",
|
||||
|
||||
+9
-1
@@ -1,5 +1,6 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
@@ -8,6 +9,8 @@ import { controlUiManualChunk } from "./config/control-ui-chunking.ts";
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(here, "..");
|
||||
const outDir = path.resolve(here, "../dist/control-ui");
|
||||
const require = createRequire(import.meta.url);
|
||||
const json5EsmPath = require.resolve("json5/dist/index.mjs");
|
||||
|
||||
function normalizeBase(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
@@ -93,7 +96,12 @@ export default defineConfig(() => {
|
||||
},
|
||||
publicDir: path.resolve(here, "public"),
|
||||
optimizeDeps: {
|
||||
include: ["lit/directives/repeat.js"],
|
||||
include: ["ipaddr.js", "lit/directives/repeat.js", "markdown-it-task-lists"],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
json5: json5EsmPath,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir,
|
||||
|
||||
Reference in New Issue
Block a user