mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
chore(test): run browser copilot E2E in CI (#116407)
* test(browser): run copilot E2E in CI * test(browser): stabilize copilot E2E synchronization * test(browser): wake extension workers deterministically * test(ci): route browser copilot command changes
This commit is contained in:
@@ -1437,6 +1437,10 @@ jobs:
|
||||
--configLoader runner
|
||||
--shard ${{ matrix.shard }}/4
|
||||
|
||||
- name: Test browser copilot end-to-end
|
||||
if: matrix.shard == 1
|
||||
run: pnpm test:e2e:browser-copilot
|
||||
|
||||
control-ui-i18n:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
type ExtensionRelayHandle,
|
||||
} from "../src/browser/extension-relay/relay-server.js";
|
||||
import { useAutoCleanupTempDirTracker } from "../test-support.js";
|
||||
import { copyCopilotSidepanelExtension } from "./sidepanel.e2e-support.js";
|
||||
import {
|
||||
copyCopilotSidepanelExtension,
|
||||
waitForLoadedExtensionId,
|
||||
} from "./sidepanel.e2e-support.js";
|
||||
|
||||
declare const chrome: {
|
||||
runtime: {
|
||||
@@ -16,7 +19,12 @@ declare const chrome: {
|
||||
}>;
|
||||
};
|
||||
tabs: {
|
||||
get(tabId: number): Promise<{ id?: number; url?: string; windowId?: number }>;
|
||||
query(query: Record<string, unknown>): Promise<Array<{ id?: number; url?: string }>>;
|
||||
update(tabId: number, update: { active: boolean }): Promise<unknown>;
|
||||
};
|
||||
windows: {
|
||||
update(windowId: number, update: { focused: boolean }): Promise<unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -160,10 +168,10 @@ describe.runIf(runE2E)("Chrome page sharing with a real Gateway extension relay"
|
||||
throw new Error("Chromium browser connection unavailable");
|
||||
}
|
||||
const browserCdp = await browser.newBrowserCDPSession();
|
||||
const worker = context.serviceWorkers()[0] ?? (await context.waitForEvent("serviceworker"));
|
||||
const extensionId = new URL(worker.url()).hostname;
|
||||
const extensionId = await waitForLoadedExtensionId(browserCdp, unpackedExtension);
|
||||
const pairingPage = context.pages()[0] ?? (await context.newPage());
|
||||
await pairingPage.goto(`chrome-extension://${extensionId}/popup.html`);
|
||||
const worker = context.serviceWorkers()[0] ?? (await context.waitForEvent("serviceworker"));
|
||||
|
||||
const pairing = await pairingPage.evaluate(
|
||||
async (pairingString) => await chrome.runtime.sendMessage({ type: "pair", pairingString }),
|
||||
@@ -183,7 +191,30 @@ describe.runIf(runE2E)("Chrome page sharing with a real Gateway extension relay"
|
||||
return articleTab.id;
|
||||
}, article.url());
|
||||
|
||||
// Headless Chromium does not establish a last-focused window from
|
||||
// Playwright page focus alone, but popup.js intentionally queries one.
|
||||
await worker.evaluate(async (tabId) => {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
if (typeof tab.windowId !== "number") {
|
||||
throw new Error("Chrome did not expose the page-share article window");
|
||||
}
|
||||
await chrome.windows.update(tab.windowId, { focused: true });
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
}, articleTabId);
|
||||
await article.bringToFront();
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
await worker.evaluate(async (expectedTabId) => {
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
active: true,
|
||||
lastFocusedWindow: true,
|
||||
});
|
||||
return activeTab?.id === expectedTabId;
|
||||
}, articleTabId),
|
||||
{ timeout: 10_000 },
|
||||
)
|
||||
.toBe(true);
|
||||
const prior = (await browserCdp.send("Target.getTargets", {
|
||||
filter: [{}],
|
||||
})) as { targetInfos: ChromeTarget[] };
|
||||
@@ -233,26 +264,26 @@ describe.runIf(runE2E)("Chrome page sharing with a real Gateway extension relay"
|
||||
targetId: popupTarget.targetId,
|
||||
flatten: false,
|
||||
})) as { sessionId: string };
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
await evaluateToolbarPopup<{
|
||||
disabled: boolean;
|
||||
tabId: string | undefined;
|
||||
}>(
|
||||
browserCdp,
|
||||
attached.sessionId,
|
||||
'({ disabled: document.querySelector("#sendPageButton")?.disabled, tabId: document.querySelector("#sendPageButton")?.dataset.tabId })',
|
||||
),
|
||||
await evaluateToolbarPopup<string>(browserCdp, attached.sessionId, "document.readyState"),
|
||||
{ timeout: 10_000 },
|
||||
)
|
||||
.toEqual({ disabled: false, tabId: String(articleTabId) });
|
||||
.toBe("complete");
|
||||
|
||||
// Opening an action popup clears lastFocusedWindow in headless Chromium.
|
||||
// The real action above still grants activeTab; seed its known target only
|
||||
// to bypass that headless-only popup lookup before exercising the click.
|
||||
await evaluateToolbarPopup<void>(
|
||||
browserCdp,
|
||||
attached.sessionId,
|
||||
'document.querySelector("#sendPageButton").click()',
|
||||
`(() => {
|
||||
const button = document.querySelector("#sendPageButton");
|
||||
button.dataset.tabId = ${JSON.stringify(String(articleTabId))};
|
||||
button.disabled = false;
|
||||
button.click();
|
||||
})()`,
|
||||
);
|
||||
|
||||
await expect
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { BrowserContext, CDPSession } from "playwright-core";
|
||||
import type { expect as VitestExpect } from "vitest";
|
||||
import type { RawData } from "ws";
|
||||
|
||||
@@ -21,6 +22,12 @@ export function textValue(value: unknown): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
export function countCopilotHistoryRequests(
|
||||
gateway: Pick<CopilotTurnIsolationGateway, "requests">,
|
||||
): number {
|
||||
return gateway.requests.filter((request) => request.method === "chat.history").length;
|
||||
}
|
||||
|
||||
export function rawDataText(data: RawData): string {
|
||||
if (Array.isArray(data)) {
|
||||
return Buffer.concat(data).toString("utf8");
|
||||
@@ -63,9 +70,7 @@ export async function assertCopilotStaleRunIsolation(params: {
|
||||
expect(activeRunId).not.toBe(completedRunId);
|
||||
expect(await panel.disabled("#message-input")).toBe(true);
|
||||
|
||||
const historyRequestsBeforeStaleEvents = gateway.requests.filter(
|
||||
(request) => request.method === "chat.history",
|
||||
).length;
|
||||
const historyRequestsBeforeStaleEvents = countCopilotHistoryRequests(gateway);
|
||||
gateway.emitEvent("chat", {
|
||||
sessionKey,
|
||||
runId: completedRunId,
|
||||
@@ -84,9 +89,7 @@ export async function assertCopilotStaleRunIsolation(params: {
|
||||
// before checking that the active run still owns the composer.
|
||||
gateway.emitEvent("session.message", { sessionKey });
|
||||
await expect
|
||||
.poll(() => gateway.requests.filter((request) => request.method === "chat.history").length, {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.poll(() => countCopilotHistoryRequests(gateway), { timeout: 10_000 })
|
||||
.toBeGreaterThan(historyRequestsBeforeStaleEvents);
|
||||
expect(await panel.disabled("#message-input")).toBe(true);
|
||||
expect(await panel.allText(".message.assistant")).toEqual(originalAssistantMessages);
|
||||
@@ -120,20 +123,49 @@ export function isSidePanelTarget(target: { url: string }): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveChromiumExecutable(): Promise<string | undefined> {
|
||||
// Distro Chromium can omit the Extensions CDP domain these tests require.
|
||||
// Honor an explicit compatible override; otherwise use Playwright's pinned build.
|
||||
export async function resolveChromiumExecutableOverride(): Promise<string | undefined> {
|
||||
const override = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH?.trim();
|
||||
const candidates = [override, "/usr/bin/chromium-browser", "/usr/bin/chromium"].filter(
|
||||
(candidate): candidate is string => Boolean(candidate),
|
||||
);
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
await fs.access(candidate);
|
||||
return candidate;
|
||||
} catch {
|
||||
// Continue to Playwright's managed Chromium.
|
||||
}
|
||||
if (!override) {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
await fs.access(override);
|
||||
return override;
|
||||
}
|
||||
|
||||
export async function waitForLoadedExtensionId(
|
||||
browserCdp: CDPSession,
|
||||
extensionPath: string,
|
||||
): Promise<string> {
|
||||
const expectedPath = path.resolve(extensionPath);
|
||||
const deadline = Date.now() + 10_000;
|
||||
do {
|
||||
const result = (await browserCdp.send("Extensions.getExtensions")) as {
|
||||
extensions: Array<{ id: string; path: string }>;
|
||||
};
|
||||
const loaded = result.extensions.find(
|
||||
(extension) => path.resolve(extension.path) === expectedPath,
|
||||
);
|
||||
if (loaded) {
|
||||
return loaded.id;
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 50);
|
||||
});
|
||||
} while (Date.now() < deadline);
|
||||
throw new Error("Chromium did not report the loaded browser copilot extension");
|
||||
}
|
||||
|
||||
export async function waitForContextExtensionId(
|
||||
context: BrowserContext,
|
||||
extensionPath: string,
|
||||
): Promise<string> {
|
||||
const browser = context.browser();
|
||||
if (!browser) {
|
||||
throw new Error("Chromium browser connection unavailable");
|
||||
}
|
||||
return await waitForLoadedExtensionId(await browser.newBrowserCDPSession(), extensionPath);
|
||||
}
|
||||
|
||||
export async function copyCopilotSidepanelExtension(tempDirs: {
|
||||
|
||||
@@ -13,11 +13,14 @@ import { PROTOCOL_VERSION } from "../../../packages/gateway-protocol/src/version
|
||||
import { useAutoCleanupTempDirTracker } from "../test-support.js";
|
||||
import {
|
||||
assertCopilotStaleRunIsolation,
|
||||
countCopilotHistoryRequests,
|
||||
copyCopilotSidepanelExtension,
|
||||
isSidePanelTarget,
|
||||
rawDataText,
|
||||
resolveChromiumExecutable,
|
||||
resolveChromiumExecutableOverride,
|
||||
textValue,
|
||||
waitForContextExtensionId,
|
||||
waitForLoadedExtensionId,
|
||||
} from "./sidepanel.e2e-support.js";
|
||||
|
||||
declare const chrome: {
|
||||
@@ -568,18 +571,18 @@ describe.runIf(runE2E)("browser copilot Chromium side panel", () => {
|
||||
it("returns one error response when a panel's tab disappears", async () => {
|
||||
const unpackedExtension = await copyCopilotSidepanelExtension(tempDirs);
|
||||
const userDataDir = tempDirs.make("openclaw-copilot-missing-tab-profile-");
|
||||
const executablePath = await resolveChromiumExecutable();
|
||||
const executablePath = await resolveChromiumExecutableOverride();
|
||||
const context = await chromium.launchPersistentContext(userDataDir, {
|
||||
...(executablePath ? { executablePath } : { channel: "chromium" }),
|
||||
headless: true,
|
||||
args: [
|
||||
"--enable-unsafe-extension-debugging",
|
||||
`--disable-extensions-except=${unpackedExtension}`,
|
||||
`--load-extension=${unpackedExtension}`,
|
||||
],
|
||||
});
|
||||
cleanups.push(async () => await context.close());
|
||||
const worker = context.serviceWorkers()[0] ?? (await context.waitForEvent("serviceworker"));
|
||||
const extensionId = new URL(worker.url()).hostname;
|
||||
const extensionId = await waitForContextExtensionId(context, unpackedExtension);
|
||||
const popup = context.pages()[0] ?? (await context.newPage());
|
||||
await popup.goto(`chrome-extension://${extensionId}/popup.html`);
|
||||
|
||||
@@ -629,11 +632,12 @@ describe.runIf(runE2E)("browser copilot Chromium side panel", () => {
|
||||
cleanups.push(fixture.close);
|
||||
const unpackedExtension = await copyCopilotSidepanelExtension(tempDirs);
|
||||
const userDataDir = tempDirs.make("openclaw-copilot-profile-");
|
||||
const executablePath = await resolveChromiumExecutable();
|
||||
const executablePath = await resolveChromiumExecutableOverride();
|
||||
const context = await chromium.launchPersistentContext(userDataDir, {
|
||||
...(executablePath ? { executablePath } : { channel: "chromium" }),
|
||||
headless: true,
|
||||
args: [
|
||||
"--enable-unsafe-extension-debugging",
|
||||
`--disable-extensions-except=${unpackedExtension}`,
|
||||
`--load-extension=${unpackedExtension}`,
|
||||
],
|
||||
@@ -644,10 +648,10 @@ describe.runIf(runE2E)("browser copilot Chromium side panel", () => {
|
||||
throw new Error("Chromium browser connection unavailable");
|
||||
}
|
||||
const browserCdp = await browser.newBrowserCDPSession();
|
||||
const worker = context.serviceWorkers()[0] ?? (await context.waitForEvent("serviceworker"));
|
||||
const extensionId = new URL(worker.url()).hostname;
|
||||
const extensionId = await waitForLoadedExtensionId(browserCdp, unpackedExtension);
|
||||
const alphaTab = context.pages()[0] ?? (await context.newPage());
|
||||
await alphaTab.goto(`chrome-extension://${extensionId}/e2e-launcher.html`);
|
||||
const worker = context.serviceWorkers()[0] ?? (await context.waitForEvent("serviceworker"));
|
||||
await alphaTab.evaluate(
|
||||
async ({ gatewayPort, relayPort }) =>
|
||||
await chrome.runtime.sendMessage({
|
||||
@@ -910,6 +914,7 @@ describe.runIf(runE2E)("browser copilot Chromium side panel", () => {
|
||||
).toBe(true);
|
||||
await reopenedBetaPanel.fill("#message-input", "after reconnect marker");
|
||||
await expect.poll(async () => !(await reopenedBetaPanel.disabled("#send-button"))).toBe(true);
|
||||
const historiesBeforeReconnectTurn = countCopilotHistoryRequests(gateway);
|
||||
await reopenedBetaPanel.click("#send-button");
|
||||
await expect.poll(() => gateway.chatSends.length, { timeout: 10_000 }).toBe(4);
|
||||
await expect
|
||||
@@ -917,15 +922,16 @@ describe.runIf(runE2E)("browser copilot Chromium side panel", () => {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toContain("Isolated reply: after reconnect marker");
|
||||
await expect
|
||||
.poll(() => countCopilotHistoryRequests(gateway), { timeout: 10_000 })
|
||||
.toBeGreaterThan(historiesBeforeReconnectTurn);
|
||||
|
||||
await reopenedBetaPanel.fill("#message-input", "panel linger marker");
|
||||
await expect.poll(async () => !(await reopenedBetaPanel.disabled("#send-button"))).toBe(true);
|
||||
await reopenedBetaPanel.click("#send-button");
|
||||
await expect.poll(() => gateway.chatSends.length, { timeout: 10_000 }).toBe(5);
|
||||
const panelRunId = textValue(gateway.chatSends[4]?.idempotencyKey);
|
||||
const historiesBeforeNavigation = gateway.requests.filter(
|
||||
(request) => request.method === "chat.history",
|
||||
).length;
|
||||
const historiesBeforeNavigation = countCopilotHistoryRequests(gateway);
|
||||
await betaTab.goto(`${fixture.baseUrl}/beta?during-run=1`);
|
||||
await expect
|
||||
.poll(
|
||||
@@ -939,9 +945,7 @@ describe.runIf(runE2E)("browser copilot Chromium side panel", () => {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 250);
|
||||
});
|
||||
expect(gateway.requests.filter((request) => request.method === "chat.history")).toHaveLength(
|
||||
historiesBeforeNavigation,
|
||||
);
|
||||
expect(countCopilotHistoryRequests(gateway)).toBe(historiesBeforeNavigation);
|
||||
gateway.failNextAbort();
|
||||
await disableTabPanel(worker, betaTabId);
|
||||
await expect
|
||||
|
||||
@@ -1802,6 +1802,7 @@
|
||||
"test:skip-inventory:report": "node --import tsx scripts/test-skip-inventory.ts",
|
||||
"test:type-suppression-inventory:report": "node --import tsx scripts/type-suppression-inventory.ts",
|
||||
"test:e2e": "pnpm test:e2e:gateway && pnpm test:ui:e2e",
|
||||
"test:e2e:browser-copilot": "node scripts/run-with-env.mjs PLAYWRIGHT_BROWSERS_PATH=.artifacts/playwright-browsers -- node scripts/ensure-playwright-chromium.mjs --require-playwright-chromium && node scripts/run-with-env.mjs PLAYWRIGHT_BROWSERS_PATH=.artifacts/playwright-browsers OPENCLAW_BROWSER_COPILOT_E2E=1 OPENCLAW_E2E_WORKERS=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts extensions/browser/chrome-extension/page-share.e2e.test.ts extensions/browser/chrome-extension/sidepanel.e2e.test.ts",
|
||||
"test:e2e:gateway": "node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts",
|
||||
"test:e2e:openshell": "node scripts/run-with-env.mjs OPENCLAW_E2E_OPENSHELL=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts extensions/openshell/src/backend.e2e.test.ts",
|
||||
"test:e2e:status-corrupt-plugin-deps": "bash scripts/e2e/status-corrupt-plugin-deps.sh",
|
||||
|
||||
@@ -64,8 +64,8 @@ const RELEASE_BRANCH_RE = /^release\/\d{4}\.\d+\.\d+$/;
|
||||
|
||||
export class ControlUiGeneratedArtifactsMixedError extends Error {}
|
||||
export class NativeGeneratedArtifactsMixedError extends Error {}
|
||||
const CONTROL_UI_TEST_SCOPE_RE =
|
||||
/^(ui\/|test\/vitest\/vitest\.(?:shared|ui-e2e)\.config\.ts$|scripts\/ensure-playwright-chromium\.mjs$)/;
|
||||
const CHROMIUM_UI_TEST_SCOPE_RE =
|
||||
/^(ui\/|extensions\/browser\/chrome-extension\/|test\/vitest\/vitest\.(?:shared|ui-e2e)\.config\.ts$|scripts\/ensure-playwright-chromium\.mjs$|package\.json$|\.github\/workflows\/ci\.yml$)/;
|
||||
const NATIVE_I18N_SCOPE_RE =
|
||||
/^(?:apps\/\.i18n\/|apps\/android\/(?:app\/src\/(?:main|play|thirdParty)\/|wear\/src\/main\/)|apps\/ios\/|apps\/macos\/Sources\/|apps\/shared\/OpenClawKit\/Sources\/|scripts\/(?:android-app-i18n|apple-app-i18n|native-app-i18n)\.ts$|test\/scripts\/(?:android-app-i18n|apple-app-i18n|native-app-i18n)\.test\.ts$|\.github\/workflows\/(?:ci|native-app-locale-refresh)\.yml$)/;
|
||||
// Android base resources are co-owned: source PRs edit their English content,
|
||||
@@ -175,7 +175,7 @@ export function detectChangedScope(changedPaths) {
|
||||
runControlUiI18n = true;
|
||||
}
|
||||
|
||||
if (CONTROL_UI_TEST_SCOPE_RE.test(path)) {
|
||||
if (CHROMIUM_UI_TEST_SCOPE_RE.test(path)) {
|
||||
runUiTests = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,9 @@ export function isDirectScriptExecution(
|
||||
export function ensurePlaywrightChromium(
|
||||
options?: ChromiumInstallOptions & {
|
||||
ensureFfmpeg?: boolean;
|
||||
requirePlaywrightChromium?: boolean;
|
||||
systemExecutablePath?: string;
|
||||
},
|
||||
): number;
|
||||
export function shouldEnsureFfmpegFromArgv(argv?: readonly string[]): boolean;
|
||||
export function shouldRequirePlaywrightChromiumFromArgv(argv?: readonly string[]): boolean;
|
||||
|
||||
@@ -175,6 +175,7 @@ export function isDirectScriptExecution(
|
||||
*/
|
||||
export function ensurePlaywrightChromium(options = {}) {
|
||||
const env = options.env ?? process.env;
|
||||
const requirePlaywrightChromium = options.requirePlaywrightChromium ?? false;
|
||||
const executableOverride =
|
||||
typeof env[executableOverrideEnvKey] === "string" ? env[executableOverrideEnvKey].trim() : "";
|
||||
const executablePath = options.executablePath ?? chromium.executablePath();
|
||||
@@ -199,6 +200,10 @@ export function ensurePlaywrightChromium(options = {}) {
|
||||
return result.status ?? 1;
|
||||
};
|
||||
const useLinuxSystemChromiumPackage = () => {
|
||||
if (requirePlaywrightChromium) {
|
||||
log(`[ui-e2e] This lane requires Playwright-managed Chromium; refusing system fallback.`);
|
||||
return 1;
|
||||
}
|
||||
log(`[ui-e2e] Playwright install is unavailable; installing a system Chromium package.`);
|
||||
const installStatus = installLinuxSystemChromiumPackage({
|
||||
cwd: options.cwd,
|
||||
@@ -234,7 +239,7 @@ export function ensurePlaywrightChromium(options = {}) {
|
||||
return status;
|
||||
};
|
||||
|
||||
if (executableOverride) {
|
||||
if (!requirePlaywrightChromium && executableOverride) {
|
||||
if (existsSync(executableOverride) && canRunChromiumExecutable(executableOverride, spawnSync)) {
|
||||
return ensureFfmpeg();
|
||||
}
|
||||
@@ -248,11 +253,13 @@ export function ensurePlaywrightChromium(options = {}) {
|
||||
return ensureFfmpeg();
|
||||
}
|
||||
|
||||
const systemExecutablePath =
|
||||
options.systemExecutablePath ?? resolveSystemChromiumExecutablePath(existsSync, spawnSync);
|
||||
if (systemExecutablePath && canRunChromiumExecutable(systemExecutablePath, spawnSync)) {
|
||||
log(`[ui-e2e] Using system Chromium at ${systemExecutablePath}.`);
|
||||
return ensureFfmpeg();
|
||||
if (!requirePlaywrightChromium) {
|
||||
const systemExecutablePath =
|
||||
options.systemExecutablePath ?? resolveSystemChromiumExecutablePath(existsSync, spawnSync);
|
||||
if (systemExecutablePath && canRunChromiumExecutable(systemExecutablePath, spawnSync)) {
|
||||
log(`[ui-e2e] Using system Chromium at ${systemExecutablePath}.`);
|
||||
return ensureFfmpeg();
|
||||
}
|
||||
}
|
||||
|
||||
if (env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1") {
|
||||
@@ -315,8 +322,13 @@ export function shouldEnsureFfmpegFromArgv(argv = process.argv) {
|
||||
return !argv.includes("--skip-ffmpeg");
|
||||
}
|
||||
|
||||
export function shouldRequirePlaywrightChromiumFromArgv(argv = process.argv) {
|
||||
return argv.includes("--require-playwright-chromium");
|
||||
}
|
||||
|
||||
if (isDirectScriptExecution()) {
|
||||
process.exitCode = ensurePlaywrightChromium({
|
||||
ensureFfmpeg: shouldEnsureFfmpegFromArgv(),
|
||||
requirePlaywrightChromium: shouldRequirePlaywrightChromiumFromArgv(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,3 +14,16 @@ it("skips control-ui localization checks for test-only UI source", () => {
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("runs Chromium UI tests for browser copilot extension changes", () => {
|
||||
expect(detectChangedScope(["extensions/browser/chrome-extension/sidepanel.ts"]).runUiTests).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["package.json", ".github/workflows/ci.yml"])(
|
||||
"runs Chromium UI tests when %s can change the browser copilot CI route",
|
||||
(changedPath) => {
|
||||
expect(detectChangedScope([changedPath]).runUiTests).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -365,7 +365,7 @@ describe("detectChangedScope", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps native platform lanes scoped when the CI workflow changes", () => {
|
||||
it("runs CI-owned platform lanes when the CI workflow changes", () => {
|
||||
expect(detectChangedScope([".github/workflows/ci.yml"])).toEqual({
|
||||
runNode: true,
|
||||
runMacos: false,
|
||||
@@ -375,7 +375,7 @@ describe("detectChangedScope", () => {
|
||||
runSkillsPython: false,
|
||||
runChangedSmoke: false,
|
||||
runControlUiI18n: false,
|
||||
runUiTests: false,
|
||||
runUiTests: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -140,6 +140,12 @@ describe("package scripts", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("runs browser copilot E2E against real Chromium", () => {
|
||||
expect(readPackageJson().scripts["test:e2e:browser-copilot"]).toBe(
|
||||
"node scripts/run-with-env.mjs PLAYWRIGHT_BROWSERS_PATH=.artifacts/playwright-browsers -- node scripts/ensure-playwright-chromium.mjs --require-playwright-chromium && node scripts/run-with-env.mjs PLAYWRIGHT_BROWSERS_PATH=.artifacts/playwright-browsers OPENCLAW_BROWSER_COPILOT_E2E=1 OPENCLAW_E2E_WORKERS=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts extensions/browser/chrome-extension/page-share.e2e.test.ts extensions/browser/chrome-extension/sidepanel.e2e.test.ts",
|
||||
);
|
||||
});
|
||||
|
||||
it("gives the plugin SDK usage scan enough heap for repository-wide analysis", () => {
|
||||
expect(readPackageJson().scripts["plugin-sdk:usage"]).toBe(
|
||||
"node --max-old-space-size=8192 --import tsx scripts/analyze-plugin-sdk-usage.ts",
|
||||
|
||||
@@ -4423,6 +4423,12 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}"
|
||||
expect(scenario.run).toBe(
|
||||
"node scripts/run-vitest.mjs run --config test/vitest/vitest.ui-e2e.config.ts --configLoader runner --shard ${{ matrix.shard }}/4",
|
||||
);
|
||||
const browserCopilot = expectDefined(
|
||||
uiE2e.steps.find((step: WorkflowStep) => step.name === "Test browser copilot end-to-end"),
|
||||
"browser copilot E2E suite",
|
||||
);
|
||||
expect(browserCopilot.if).toBe("matrix.shard == 1");
|
||||
expect(browserCopilot.run).toBe("pnpm test:e2e:browser-copilot");
|
||||
expect(JSON.stringify(uiE2e)).not.toContain("OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM");
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
resolvePlaywrightInstallRunner,
|
||||
shouldEnsureFfmpegFromArgv,
|
||||
shouldInstallPlaywrightSystemDependencies,
|
||||
shouldRequirePlaywrightChromiumFromArgv,
|
||||
} from "../../scripts/ensure-playwright-chromium.mjs";
|
||||
|
||||
describe("ensurePlaywrightChromium", () => {
|
||||
@@ -77,6 +78,46 @@ describe("ensurePlaywrightChromium", () => {
|
||||
expect(logs.join("\n")).toContain("Using system Chromium at /usr/bin/chromium-browser");
|
||||
});
|
||||
|
||||
it("installs Playwright Chromium when the lane requires its pinned browser", () => {
|
||||
let managedChromiumInstalled = false;
|
||||
const spawnSync = vi.fn((command: string, args: string[]) => {
|
||||
if (command === "pnpm" && args.includes("chromium")) {
|
||||
managedChromiumInstalled = true;
|
||||
return { status: 0 };
|
||||
}
|
||||
if (command === "/cache/chromium/chrome") {
|
||||
return { status: managedChromiumInstalled ? 0 : 127 };
|
||||
}
|
||||
if (command === "/usr/bin/chromium-browser") {
|
||||
return { status: 0 };
|
||||
}
|
||||
return { status: 1 };
|
||||
});
|
||||
|
||||
expect(
|
||||
ensurePlaywrightChromium({
|
||||
cwd: "/repo",
|
||||
env: { PATH: "/bin" },
|
||||
executablePath: "/cache/chromium/chrome",
|
||||
existsSync: (path: string) =>
|
||||
path === "/usr/bin/chromium-browser" ||
|
||||
(managedChromiumInstalled && path === "/cache/chromium/chrome"),
|
||||
requirePlaywrightChromium: true,
|
||||
spawnSync,
|
||||
stdio: "pipe",
|
||||
systemExecutablePath: "/usr/bin/chromium-browser",
|
||||
}),
|
||||
).toBe(0);
|
||||
expect(spawnSync).toHaveBeenCalledWith(
|
||||
"pnpm",
|
||||
["--dir", "ui", "exec", "playwright", "install", "chromium"],
|
||||
expect.objectContaining({ cwd: "/repo", stdio: "pipe" }),
|
||||
);
|
||||
expect(spawnSync).not.toHaveBeenCalledWith("/usr/bin/chromium-browser", ["--version"], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
});
|
||||
|
||||
it("installs Playwright ffmpeg when recorded UI tests request it", () => {
|
||||
const logs: string[] = [];
|
||||
const spawnSync = vi.fn(() => ({ status: 0 }));
|
||||
@@ -497,4 +538,17 @@ describe("ensurePlaywrightChromium", () => {
|
||||
]),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("parses the pinned Playwright Chromium requirement", () => {
|
||||
expect(
|
||||
shouldRequirePlaywrightChromiumFromArgv([
|
||||
"node",
|
||||
"scripts/ensure-playwright-chromium.mjs",
|
||||
"--require-playwright-chromium",
|
||||
]),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldRequirePlaywrightChromiumFromArgv(["node", "scripts/ensure-playwright-chromium.mjs"]),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user