






















1+import { afterEach, describe, expect, it, vi } from "vitest";
2+import type { ResolvedBrowserProfile } from "./config.js";
3+import {
4+OPEN_TAB_DISCOVERY_POLL_MS,
5+OPEN_TAB_DISCOVERY_WINDOW_MS,
6+} from "./server-context.constants.js";
7+import { createProfileSelectionOps } from "./server-context.selection.js";
8+import type { BrowserTab, ProfileRuntimeState } from "./server-context.types.js";
9+10+const LOCAL_PROFILE: ResolvedBrowserProfile = {
11+name: "openclaw",
12+cdpPort: 18800,
13+cdpUrl: "http://127.0.0.1:18800",
14+cdpHost: "127.0.0.1",
15+cdpIsLoopback: true,
16+color: "#FF4500",
17+driver: "openclaw",
18+headless: true,
19+headlessSource: "config",
20+attachOnly: false,
21+};
22+23+function tab(targetId: string, wsUrl?: string): BrowserTab {
24+return {
25+ targetId,
26+title: targetId,
27+url: `https://${targetId.toLowerCase()}.example`,
28+type: "page",
29+ ...(wsUrl ? { wsUrl } : {}),
30+};
31+}
32+33+function createSelectionHarness(params: {
34+snapshots: Array<BrowserTab[] | Error>;
35+openedTab?: BrowserTab;
36+}) {
37+const snapshots = [...params.snapshots];
38+let lastSnapshot: BrowserTab[] = [];
39+const listTabs = vi.fn(async () => {
40+const next = snapshots.shift();
41+if (next instanceof Error) {
42+throw next;
43+}
44+if (next) {
45+lastSnapshot = next;
46+}
47+return lastSnapshot;
48+});
49+const profileState: ProfileRuntimeState = {
50+profile: LOCAL_PROFILE,
51+running: null,
52+lastTargetId: null,
53+reconcile: null,
54+};
55+const openTab = vi.fn(async () => {
56+const openedTab = params.openedTab ?? tab("OPENED");
57+profileState.lastTargetId = openedTab.targetId;
58+return openedTab;
59+});
60+const selection = createProfileSelectionOps({
61+profile: LOCAL_PROFILE,
62+getProfileState: () => profileState,
63+getCdpControlPolicy: () => undefined,
64+ensureBrowserAvailable: async () => {},
65+ listTabs,
66+ openTab,
67+});
68+return { selection, listTabs, openTab, profileState };
69+}
70+71+async function advancePastDiscoveryWindow(): Promise<void> {
72+await vi.advanceTimersByTimeAsync(OPEN_TAB_DISCOVERY_WINDOW_MS + OPEN_TAB_DISCOVERY_POLL_MS);
73+}
74+75+afterEach(() => {
76+vi.useRealTimers();
77+});
78+79+describe("browser profile tab selection", () => {
80+it("preserves the opened tab when the immediate relist omits it", async () => {
81+const openedTab = tab("OPENED", "ws://127.0.0.1/devtools/page/OPENED");
82+const { selection, listTabs, openTab } = createSelectionHarness({
83+snapshots: [[], []],
84+ openedTab,
85+});
86+87+await expect(selection.ensureTabAvailable()).resolves.toEqual(openedTab);
88+expect(openTab).toHaveBeenCalledOnce();
89+expect(listTabs).toHaveBeenCalledTimes(2);
90+});
91+92+it("preserves a target-id-only opened tab for a Playwright-backed caller", async () => {
93+vi.useFakeTimers();
94+const openedTab = tab("OPENED");
95+const otherWithWs = tab("OTHER", "ws://127.0.0.1/devtools/page/OTHER");
96+const { selection } = createSelectionHarness({
97+snapshots: [[], [otherWithWs]],
98+ openedTab,
99+});
100+101+const selected = selection.ensureTabAvailable(undefined, {
102+allowPlaywrightFallback: true,
103+});
104+await advancePastDiscoveryWindow();
105+106+await expect(selected).resolves.toEqual(openedTab);
107+});
108+109+it("polls until delayed wsUrl discovery makes an existing tab selectable", async () => {
110+vi.useFakeTimers();
111+const withoutWs = tab("LAGGING");
112+const withWs = tab("LAGGING", "ws://127.0.0.1/devtools/page/LAGGING");
113+const { selection, listTabs, openTab } = createSelectionHarness({
114+snapshots: [[withoutWs], [withoutWs], [withWs]],
115+});
116+117+const selected = selection.ensureTabAvailable();
118+await vi.advanceTimersByTimeAsync(OPEN_TAB_DISCOVERY_POLL_MS);
119+120+await expect(selected).resolves.toEqual(withWs);
121+expect(listTabs).toHaveBeenCalledTimes(3);
122+expect(openTab).not.toHaveBeenCalled();
123+});
124+125+it("allows an existing target-id-only tab only for Playwright-backed callers", async () => {
126+vi.useFakeTimers();
127+const withoutWs = tab("PLAYWRIGHT_TARGET");
128+const otherWithWs = tab("OTHER", "ws://127.0.0.1/devtools/page/OTHER");
129+const { selection } = createSelectionHarness({
130+snapshots: [[withoutWs, otherWithWs]],
131+});
132+133+const selected = selection.ensureTabAvailable("PLAYWRIGHT_TARGET", {
134+allowPlaywrightFallback: true,
135+});
136+await advancePastDiscoveryWindow();
137+138+await expect(selected).resolves.toEqual(withoutWs);
139+});
140+141+it("preserves a sticky target-id-only tab instead of switching to another tab", async () => {
142+vi.useFakeTimers();
143+const stickyWithoutWs = tab("STICKY");
144+const otherWithWs = tab("OTHER", "ws://127.0.0.1/devtools/page/OTHER");
145+const { selection, profileState } = createSelectionHarness({
146+snapshots: [[stickyWithoutWs, otherWithWs]],
147+});
148+profileState.lastTargetId = stickyWithoutWs.targetId;
149+150+const selected = selection.ensureTabAvailable(undefined, {
151+allowPlaywrightFallback: true,
152+});
153+await advancePastDiscoveryWindow();
154+155+await expect(selected).resolves.toEqual(stickyWithoutWs);
156+});
157+158+it("keeps polling after a transient tab-list rejection", async () => {
159+vi.useFakeTimers();
160+const withoutWs = tab("RECOVERED");
161+const withWs = tab("RECOVERED", "ws://127.0.0.1/devtools/page/RECOVERED");
162+const { selection, listTabs } = createSelectionHarness({
163+snapshots: [[withoutWs], new Error("transient list failure"), [withWs]],
164+});
165+166+const selected = selection.ensureTabAvailable();
167+await vi.advanceTimersByTimeAsync(OPEN_TAB_DISCOVERY_POLL_MS);
168+169+await expect(selected).resolves.toEqual(withWs);
170+expect(listTabs).toHaveBeenCalledTimes(3);
171+});
172+173+it("falls back to the last nonempty unfiltered snapshot after empty relists", async () => {
174+vi.useFakeTimers();
175+const withoutWs = tab("LAST_NONEMPTY");
176+const { selection, openTab } = createSelectionHarness({
177+snapshots: [[withoutWs], [], new Error("transient list failure")],
178+});
179+180+const selected = selection.ensureTabAvailable(undefined, {
181+allowPlaywrightFallback: true,
182+});
183+await advancePastDiscoveryWindow();
184+185+await expect(selected).resolves.toEqual(withoutWs);
186+expect(openTab).not.toHaveBeenCalled();
187+});
188+189+it("rejects a target-id-only local tab when the caller cannot use Playwright", async () => {
190+vi.useFakeTimers();
191+const { selection } = createSelectionHarness({
192+snapshots: [[tab("NO_PLAYWRIGHT")]],
193+});
194+195+const selected = expect(selection.ensureTabAvailable("NO_PLAYWRIGHT")).rejects.toThrow(
196+/tab not found/i,
197+);
198+await advancePastDiscoveryWindow();
199+200+await selected;
201+});
202+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。