mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-14 07:18:35 +00:00
refactor(browser): dedupe browser and cli command wiring
This commit is contained in:
245
src/browser/server-context.tab-selection-state.suite.ts
Normal file
245
src/browser/server-context.tab-selection-state.suite.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { withFetchPreconnect } from "../test-utils/fetch-mock.js";
|
||||
import "./server-context.chrome-test-harness.js";
|
||||
import * as cdpModule from "./cdp.js";
|
||||
import { InvalidBrowserNavigationUrlError } from "./navigation-guard.js";
|
||||
import { createBrowserRouteContext } from "./server-context.js";
|
||||
import {
|
||||
makeManagedTabsWithNew,
|
||||
makeState,
|
||||
originalFetch,
|
||||
} from "./server-context.remote-tab-ops.harness.js";
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function seedRunningProfileState(
|
||||
state: ReturnType<typeof makeState>,
|
||||
profileName = "openclaw",
|
||||
): void {
|
||||
(state.profiles as Map<string, unknown>).set(profileName, {
|
||||
profile: { name: profileName },
|
||||
running: { pid: 1234, proc: { on: vi.fn() } },
|
||||
lastTargetId: null,
|
||||
});
|
||||
}
|
||||
|
||||
async function expectOldManagedTabClose(fetchMock: ReturnType<typeof vi.fn>): Promise<void> {
|
||||
await vi.waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
expect.stringContaining("/json/close/OLD1"),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function createOldTabCleanupFetchMock(
|
||||
existingTabs: ReturnType<typeof makeManagedTabsWithNew>,
|
||||
params?: { rejectNewTabClose?: boolean },
|
||||
): ReturnType<typeof vi.fn> {
|
||||
return vi.fn(async (url: unknown) => {
|
||||
const value = String(url);
|
||||
if (value.includes("/json/list")) {
|
||||
return { ok: true, json: async () => existingTabs } as unknown as Response;
|
||||
}
|
||||
if (value.includes("/json/close/OLD1")) {
|
||||
return { ok: true, json: async () => ({}) } as unknown as Response;
|
||||
}
|
||||
if (params?.rejectNewTabClose && value.includes("/json/close/NEW")) {
|
||||
throw new Error("cleanup must not close NEW");
|
||||
}
|
||||
throw new Error(`unexpected fetch: ${value}`);
|
||||
});
|
||||
}
|
||||
|
||||
describe("browser server-context tab selection state", () => {
|
||||
it("updates lastTargetId when openTab is created via CDP", async () => {
|
||||
const createTargetViaCdp = vi
|
||||
.spyOn(cdpModule, "createTargetViaCdp")
|
||||
.mockResolvedValue({ targetId: "CREATED" });
|
||||
|
||||
const fetchMock = vi.fn(async (url: unknown) => {
|
||||
const u = String(url);
|
||||
if (!u.includes("/json/list")) {
|
||||
throw new Error(`unexpected fetch: ${u}`);
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => [
|
||||
{
|
||||
id: "CREATED",
|
||||
title: "New Tab",
|
||||
url: "http://127.0.0.1:8080",
|
||||
webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/CREATED",
|
||||
type: "page",
|
||||
},
|
||||
],
|
||||
} as unknown as Response;
|
||||
});
|
||||
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
const state = makeState("openclaw");
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
const openclaw = ctx.forProfile("openclaw");
|
||||
|
||||
const opened = await openclaw.openTab("http://127.0.0.1:8080");
|
||||
expect(opened.targetId).toBe("CREATED");
|
||||
expect(state.profiles.get("openclaw")?.lastTargetId).toBe("CREATED");
|
||||
expect(createTargetViaCdp).toHaveBeenCalledWith({
|
||||
cdpUrl: "http://127.0.0.1:18800",
|
||||
url: "http://127.0.0.1:8080",
|
||||
ssrfPolicy: { allowPrivateNetwork: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("closes excess managed tabs after opening a new tab", async () => {
|
||||
vi.spyOn(cdpModule, "createTargetViaCdp").mockResolvedValue({ targetId: "NEW" });
|
||||
const existingTabs = makeManagedTabsWithNew();
|
||||
const fetchMock = createOldTabCleanupFetchMock(existingTabs);
|
||||
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
const state = makeState("openclaw");
|
||||
seedRunningProfileState(state);
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
const openclaw = ctx.forProfile("openclaw");
|
||||
|
||||
const opened = await openclaw.openTab("http://127.0.0.1:3009");
|
||||
expect(opened.targetId).toBe("NEW");
|
||||
await expectOldManagedTabClose(fetchMock);
|
||||
});
|
||||
|
||||
it("never closes the just-opened managed tab during cap cleanup", async () => {
|
||||
vi.spyOn(cdpModule, "createTargetViaCdp").mockResolvedValue({ targetId: "NEW" });
|
||||
const existingTabs = makeManagedTabsWithNew({ newFirst: true });
|
||||
const fetchMock = createOldTabCleanupFetchMock(existingTabs, { rejectNewTabClose: true });
|
||||
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
const state = makeState("openclaw");
|
||||
seedRunningProfileState(state);
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
const openclaw = ctx.forProfile("openclaw");
|
||||
|
||||
const opened = await openclaw.openTab("http://127.0.0.1:3009");
|
||||
expect(opened.targetId).toBe("NEW");
|
||||
await expectOldManagedTabClose(fetchMock);
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("/json/close/NEW"),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not fail tab open when managed-tab cleanup list fails", async () => {
|
||||
vi.spyOn(cdpModule, "createTargetViaCdp").mockResolvedValue({ targetId: "NEW" });
|
||||
|
||||
let listCount = 0;
|
||||
const fetchMock = vi.fn(async (url: unknown) => {
|
||||
const value = String(url);
|
||||
if (value.includes("/json/list")) {
|
||||
listCount += 1;
|
||||
if (listCount === 1) {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => [
|
||||
{
|
||||
id: "NEW",
|
||||
title: "New Tab",
|
||||
url: "http://127.0.0.1:3009",
|
||||
webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/NEW",
|
||||
type: "page",
|
||||
},
|
||||
],
|
||||
} as unknown as Response;
|
||||
}
|
||||
throw new Error("/json/list timeout");
|
||||
}
|
||||
throw new Error(`unexpected fetch: ${value}`);
|
||||
});
|
||||
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
const state = makeState("openclaw");
|
||||
seedRunningProfileState(state);
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
const openclaw = ctx.forProfile("openclaw");
|
||||
|
||||
const opened = await openclaw.openTab("http://127.0.0.1:3009");
|
||||
expect(opened.targetId).toBe("NEW");
|
||||
});
|
||||
|
||||
it("does not run managed tab cleanup in attachOnly mode", async () => {
|
||||
vi.spyOn(cdpModule, "createTargetViaCdp").mockResolvedValue({ targetId: "NEW" });
|
||||
const existingTabs = makeManagedTabsWithNew();
|
||||
|
||||
const fetchMock = vi.fn(async (url: unknown) => {
|
||||
const value = String(url);
|
||||
if (value.includes("/json/list")) {
|
||||
return { ok: true, json: async () => existingTabs } as unknown as Response;
|
||||
}
|
||||
if (value.includes("/json/close/")) {
|
||||
throw new Error("should not close tabs in attachOnly mode");
|
||||
}
|
||||
throw new Error(`unexpected fetch: ${value}`);
|
||||
});
|
||||
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
const state = makeState("openclaw");
|
||||
state.resolved.attachOnly = true;
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
const openclaw = ctx.forProfile("openclaw");
|
||||
|
||||
const opened = await openclaw.openTab("http://127.0.0.1:3009");
|
||||
expect(opened.targetId).toBe("NEW");
|
||||
expect(fetchMock).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("/json/close/"),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not block openTab on slow best-effort cleanup closes", async () => {
|
||||
vi.spyOn(cdpModule, "createTargetViaCdp").mockResolvedValue({ targetId: "NEW" });
|
||||
const existingTabs = makeManagedTabsWithNew();
|
||||
|
||||
const fetchMock = vi.fn(async (url: unknown) => {
|
||||
const value = String(url);
|
||||
if (value.includes("/json/list")) {
|
||||
return { ok: true, json: async () => existingTabs } as unknown as Response;
|
||||
}
|
||||
if (value.includes("/json/close/OLD1")) {
|
||||
return new Promise<Response>(() => {});
|
||||
}
|
||||
throw new Error(`unexpected fetch: ${value}`);
|
||||
});
|
||||
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
const state = makeState("openclaw");
|
||||
seedRunningProfileState(state);
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
const openclaw = ctx.forProfile("openclaw");
|
||||
|
||||
const opened = await Promise.race([
|
||||
openclaw.openTab("http://127.0.0.1:3009"),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("openTab timed out waiting for cleanup")), 300),
|
||||
),
|
||||
]);
|
||||
|
||||
expect(opened.targetId).toBe("NEW");
|
||||
});
|
||||
|
||||
it("blocks unsupported non-network URLs before any HTTP tab-open fallback", async () => {
|
||||
const fetchMock = vi.fn(async () => {
|
||||
throw new Error("unexpected fetch");
|
||||
});
|
||||
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
const state = makeState("openclaw");
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
const openclaw = ctx.forProfile("openclaw");
|
||||
|
||||
await expect(openclaw.openTab("file:///etc/passwd")).rejects.toBeInstanceOf(
|
||||
InvalidBrowserNavigationUrlError,
|
||||
);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user