mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 00:53:28 +00:00
refactor(browser): dedupe browser and cli command wiring
This commit is contained in:
273
src/browser/server-context.remote-profile-tab-ops.suite.ts
Normal file
273
src/browser/server-context.remote-profile-tab-ops.suite.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import "./server-context.chrome-test-harness.js";
|
||||
import * as chromeModule from "./chrome.js";
|
||||
import * as pwAiModule from "./pw-ai-module.js";
|
||||
import { createBrowserRouteContext } from "./server-context.js";
|
||||
import {
|
||||
createJsonListFetchMock,
|
||||
createRemoteRouteHarness,
|
||||
createSequentialPageLister,
|
||||
makeState,
|
||||
originalFetch,
|
||||
} from "./server-context.remote-tab-ops.harness.js";
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("browser server-context remote profile tab operations", () => {
|
||||
it("uses profile-level attachOnly when global attachOnly is false", async () => {
|
||||
const state = makeState("openclaw");
|
||||
state.resolved.attachOnly = false;
|
||||
state.resolved.profiles.openclaw = {
|
||||
cdpPort: 18800,
|
||||
attachOnly: true,
|
||||
color: "#FF4500",
|
||||
};
|
||||
|
||||
const reachableMock = vi.mocked(chromeModule.isChromeReachable).mockResolvedValueOnce(false);
|
||||
const launchMock = vi.mocked(chromeModule.launchOpenClawChrome);
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
|
||||
await expect(ctx.forProfile("openclaw").ensureBrowserAvailable()).rejects.toThrow(
|
||||
/attachOnly is enabled/i,
|
||||
);
|
||||
expect(reachableMock).toHaveBeenCalled();
|
||||
expect(launchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps attachOnly websocket failures off the loopback ownership error path", async () => {
|
||||
const state = makeState("openclaw");
|
||||
state.resolved.attachOnly = false;
|
||||
state.resolved.profiles.openclaw = {
|
||||
cdpPort: 18800,
|
||||
attachOnly: true,
|
||||
color: "#FF4500",
|
||||
};
|
||||
|
||||
const httpReachableMock = vi.mocked(chromeModule.isChromeReachable).mockResolvedValueOnce(true);
|
||||
const wsReachableMock = vi.mocked(chromeModule.isChromeCdpReady).mockResolvedValueOnce(false);
|
||||
const launchMock = vi.mocked(chromeModule.launchOpenClawChrome);
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
|
||||
await expect(ctx.forProfile("openclaw").ensureBrowserAvailable()).rejects.toThrow(
|
||||
/attachOnly is enabled and CDP websocket/i,
|
||||
);
|
||||
expect(httpReachableMock).toHaveBeenCalled();
|
||||
expect(wsReachableMock).toHaveBeenCalled();
|
||||
expect(launchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses Playwright tab operations when available", async () => {
|
||||
const listPagesViaPlaywright = vi.fn(async () => [
|
||||
{ targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" },
|
||||
]);
|
||||
const createPageViaPlaywright = vi.fn(async () => ({
|
||||
targetId: "T2",
|
||||
title: "Tab 2",
|
||||
url: "http://127.0.0.1:3000",
|
||||
type: "page",
|
||||
}));
|
||||
const closePageByTargetIdViaPlaywright = vi.fn(async () => {});
|
||||
|
||||
vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({
|
||||
listPagesViaPlaywright,
|
||||
createPageViaPlaywright,
|
||||
closePageByTargetIdViaPlaywright,
|
||||
} as unknown as Awaited<ReturnType<typeof pwAiModule.getPwAiModule>>);
|
||||
|
||||
const { state, remote, fetchMock } = createRemoteRouteHarness();
|
||||
|
||||
const tabs = await remote.listTabs();
|
||||
expect(tabs.map((t) => t.targetId)).toEqual(["T1"]);
|
||||
|
||||
const opened = await remote.openTab("http://127.0.0.1:3000");
|
||||
expect(opened.targetId).toBe("T2");
|
||||
expect(state.profiles.get("remote")?.lastTargetId).toBe("T2");
|
||||
expect(createPageViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: "https://browserless.example/chrome?token=abc",
|
||||
url: "http://127.0.0.1:3000",
|
||||
ssrfPolicy: { allowPrivateNetwork: true },
|
||||
});
|
||||
|
||||
await remote.closeTab("T1");
|
||||
expect(closePageByTargetIdViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: "https://browserless.example/chrome?token=abc",
|
||||
targetId: "T1",
|
||||
});
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prefers lastTargetId for remote profiles when targetId is omitted", async () => {
|
||||
const responses = [
|
||||
[
|
||||
{ targetId: "A", title: "A", url: "https://example.com", type: "page" },
|
||||
{ targetId: "B", title: "B", url: "https://www.example.com", type: "page" },
|
||||
],
|
||||
[
|
||||
{ targetId: "A", title: "A", url: "https://example.com", type: "page" },
|
||||
{ targetId: "B", title: "B", url: "https://www.example.com", type: "page" },
|
||||
],
|
||||
[
|
||||
{ targetId: "B", title: "B", url: "https://www.example.com", type: "page" },
|
||||
{ targetId: "A", title: "A", url: "https://example.com", type: "page" },
|
||||
],
|
||||
[
|
||||
{ targetId: "B", title: "B", url: "https://www.example.com", type: "page" },
|
||||
{ targetId: "A", title: "A", url: "https://example.com", type: "page" },
|
||||
],
|
||||
];
|
||||
|
||||
const listPagesViaPlaywright = vi.fn(createSequentialPageLister(responses));
|
||||
|
||||
vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({
|
||||
listPagesViaPlaywright,
|
||||
createPageViaPlaywright: vi.fn(async () => {
|
||||
throw new Error("unexpected create");
|
||||
}),
|
||||
closePageByTargetIdViaPlaywright: vi.fn(async () => {
|
||||
throw new Error("unexpected close");
|
||||
}),
|
||||
} as unknown as Awaited<ReturnType<typeof pwAiModule.getPwAiModule>>);
|
||||
|
||||
const { remote } = createRemoteRouteHarness();
|
||||
|
||||
const first = await remote.ensureTabAvailable();
|
||||
expect(first.targetId).toBe("A");
|
||||
const second = await remote.ensureTabAvailable();
|
||||
expect(second.targetId).toBe("A");
|
||||
});
|
||||
|
||||
it("falls back to the only tab for remote profiles when targetId is stale", async () => {
|
||||
const responses = [
|
||||
[{ targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" }],
|
||||
[{ targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" }],
|
||||
];
|
||||
const listPagesViaPlaywright = vi.fn(createSequentialPageLister(responses));
|
||||
|
||||
vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({
|
||||
listPagesViaPlaywright,
|
||||
} as unknown as Awaited<ReturnType<typeof pwAiModule.getPwAiModule>>);
|
||||
|
||||
const { remote } = createRemoteRouteHarness();
|
||||
const chosen = await remote.ensureTabAvailable("STALE_TARGET");
|
||||
expect(chosen.targetId).toBe("T1");
|
||||
});
|
||||
|
||||
it("keeps rejecting stale targetId for remote profiles when multiple tabs exist", async () => {
|
||||
const responses = [
|
||||
[
|
||||
{ targetId: "A", title: "A", url: "https://a.example", type: "page" },
|
||||
{ targetId: "B", title: "B", url: "https://b.example", type: "page" },
|
||||
],
|
||||
[
|
||||
{ targetId: "A", title: "A", url: "https://a.example", type: "page" },
|
||||
{ targetId: "B", title: "B", url: "https://b.example", type: "page" },
|
||||
],
|
||||
];
|
||||
const listPagesViaPlaywright = vi.fn(createSequentialPageLister(responses));
|
||||
|
||||
vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({
|
||||
listPagesViaPlaywright,
|
||||
} as unknown as Awaited<ReturnType<typeof pwAiModule.getPwAiModule>>);
|
||||
|
||||
const { remote } = createRemoteRouteHarness();
|
||||
await expect(remote.ensureTabAvailable("STALE_TARGET")).rejects.toThrow(/tab not found/i);
|
||||
});
|
||||
|
||||
it("uses Playwright focus for remote profiles when available", async () => {
|
||||
const listPagesViaPlaywright = vi.fn(async () => [
|
||||
{ targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" },
|
||||
]);
|
||||
const focusPageByTargetIdViaPlaywright = vi.fn(async () => {});
|
||||
|
||||
vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({
|
||||
listPagesViaPlaywright,
|
||||
focusPageByTargetIdViaPlaywright,
|
||||
} as unknown as Awaited<ReturnType<typeof pwAiModule.getPwAiModule>>);
|
||||
|
||||
const { state, remote, fetchMock } = createRemoteRouteHarness();
|
||||
|
||||
await remote.focusTab("T1");
|
||||
expect(focusPageByTargetIdViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: "https://browserless.example/chrome?token=abc",
|
||||
targetId: "T1",
|
||||
});
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(state.profiles.get("remote")?.lastTargetId).toBe("T1");
|
||||
});
|
||||
|
||||
it("does not swallow Playwright runtime errors for remote profiles", async () => {
|
||||
vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({
|
||||
listPagesViaPlaywright: vi.fn(async () => {
|
||||
throw new Error("boom");
|
||||
}),
|
||||
} as unknown as Awaited<ReturnType<typeof pwAiModule.getPwAiModule>>);
|
||||
|
||||
const { remote, fetchMock } = createRemoteRouteHarness();
|
||||
|
||||
await expect(remote.listTabs()).rejects.toThrow(/boom/);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to /json/list when Playwright is not available", async () => {
|
||||
vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue(null);
|
||||
const { remote } = createRemoteRouteHarness(
|
||||
vi.fn(
|
||||
createJsonListFetchMock([
|
||||
{
|
||||
id: "T1",
|
||||
title: "Tab 1",
|
||||
url: "https://example.com",
|
||||
webSocketDebuggerUrl: "wss://browserless.example/devtools/page/T1",
|
||||
type: "page",
|
||||
},
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
const tabs = await remote.listTabs();
|
||||
expect(tabs.map((t) => t.targetId)).toEqual(["T1"]);
|
||||
});
|
||||
|
||||
it("does not enforce managed tab cap for remote openclaw profiles", async () => {
|
||||
const listPagesViaPlaywright = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce([
|
||||
{ targetId: "T1", title: "1", url: "https://1.example", type: "page" },
|
||||
])
|
||||
.mockResolvedValueOnce([
|
||||
{ targetId: "T1", title: "1", url: "https://1.example", type: "page" },
|
||||
{ targetId: "T2", title: "2", url: "https://2.example", type: "page" },
|
||||
{ targetId: "T3", title: "3", url: "https://3.example", type: "page" },
|
||||
{ targetId: "T4", title: "4", url: "https://4.example", type: "page" },
|
||||
{ targetId: "T5", title: "5", url: "https://5.example", type: "page" },
|
||||
{ targetId: "T6", title: "6", url: "https://6.example", type: "page" },
|
||||
{ targetId: "T7", title: "7", url: "https://7.example", type: "page" },
|
||||
{ targetId: "T8", title: "8", url: "https://8.example", type: "page" },
|
||||
{ targetId: "T9", title: "9", url: "https://9.example", type: "page" },
|
||||
]);
|
||||
|
||||
const createPageViaPlaywright = vi.fn(async () => ({
|
||||
targetId: "T1",
|
||||
title: "Tab 1",
|
||||
url: "https://1.example",
|
||||
type: "page",
|
||||
}));
|
||||
|
||||
vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({
|
||||
listPagesViaPlaywright,
|
||||
createPageViaPlaywright,
|
||||
} as unknown as Awaited<ReturnType<typeof pwAiModule.getPwAiModule>>);
|
||||
|
||||
const fetchMock = vi.fn(async (url: unknown) => {
|
||||
throw new Error(`unexpected fetch: ${String(url)}`);
|
||||
});
|
||||
|
||||
const { remote } = createRemoteRouteHarness(fetchMock);
|
||||
const opened = await remote.openTab("https://1.example");
|
||||
expect(opened.targetId).toBe("T1");
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user