


























@@ -1,14 +1,69 @@
11import { chromium } from "playwright-core";
22import { afterEach, describe, expect, it, vi } from "vitest";
33import * as chromeModule from "./chrome.js";
4-import { closePlaywrightBrowserConnection, getPageForTargetId } from "./pw-session.js";
4+import {
5+closePlaywrightBrowserConnection,
6+getPageForTargetId,
7+listPagesViaPlaywright,
8+} from "./pw-session.js";
59610const connectOverCdpSpy = vi.spyOn(chromium, "connectOverCDP");
711const getChromeWebSocketUrlSpy = vi.spyOn(chromeModule, "getChromeWebSocketUrl");
81213+type MockPageSpec = {
14+targetId?: string;
15+url?: string;
16+title?: string;
17+};
18+19+type BrowserMockBundle = {
20+browser: import("playwright-core").Browser;
21+browserClose: ReturnType<typeof vi.fn>;
22+pages: import("playwright-core").Page[];
23+};
24+25+function makeBrowser(pages: MockPageSpec[]): BrowserMockBundle {
26+let context: import("playwright-core").BrowserContext;
27+const browserClose = vi.fn(async () => {});
28+const targetIdByPage = new Map<import("playwright-core").Page, string | undefined>();
29+30+const pageObjects = pages.map((spec, index) => {
31+const page = {
32+on: vi.fn(),
33+context: () => context,
34+title: vi.fn(async () => spec.title ?? spec.targetId ?? `page-${index + 1}`),
35+url: vi.fn(() => spec.url ?? `https://page-${index + 1}.example`),
36+} as unknown as import("playwright-core").Page;
37+targetIdByPage.set(page, spec.targetId);
38+return page;
39+});
40+41+context = {
42+pages: () => pageObjects,
43+on: vi.fn(),
44+newCDPSession: vi.fn(async (page: import("playwright-core").Page) => ({
45+send: vi.fn(async (method: string) =>
46+method === "Target.getTargetInfo"
47+ ? { targetInfo: { targetId: targetIdByPage.get(page) } }
48+ : {},
49+),
50+detach: vi.fn(async () => {}),
51+})),
52+} as unknown as import("playwright-core").BrowserContext;
53+54+const browser = {
55+contexts: () => [context],
56+on: vi.fn(),
57+off: vi.fn(),
58+close: browserClose,
59+} as unknown as import("playwright-core").Browser;
60+61+return { browser, browserClose, pages: pageObjects };
62+}
63+964afterEach(async () => {
10-connectOverCdpSpy.mockClear();
11-getChromeWebSocketUrlSpy.mockClear();
65+connectOverCdpSpy.mockReset();
66+getChromeWebSocketUrlSpy.mockReset();
1267await closePlaywrightBrowserConnection().catch(() => {});
1368});
1469@@ -119,4 +174,73 @@ describe("pw-session getPageForTargetId", () => {
119174fetchSpy.mockRestore();
120175}
121176});
177+178+it("evicts a stale cached page-less browser once and succeeds on a fresh reconnect", async () => {
179+const stale = makeBrowser([]);
180+const fresh = makeBrowser([{ targetId: "TARGET_OK", url: "https://fresh.example" }]);
181+182+connectOverCdpSpy.mockResolvedValueOnce(stale.browser).mockResolvedValueOnce(fresh.browser);
183+getChromeWebSocketUrlSpy.mockResolvedValue(null);
184+185+await listPagesViaPlaywright({ cdpUrl: "http://127.0.0.1:9222" });
186+187+const resolved = await getPageForTargetId({ cdpUrl: "http://127.0.0.1:9222" });
188+189+expect(resolved).toBe(fresh.pages[0]);
190+expect(connectOverCdpSpy).toHaveBeenCalledTimes(2);
191+expect(stale.browserClose).toHaveBeenCalledTimes(1);
192+});
193+194+it("evicts a stale cached tab-selection miss once and succeeds on a fresh reconnect", async () => {
195+const stale = makeBrowser([
196+{ targetId: "TARGET_A", url: "https://alpha.example" },
197+{ targetId: "TARGET_C", url: "https://charlie.example" },
198+]);
199+const fresh = makeBrowser([
200+{ targetId: "TARGET_A", url: "https://alpha.example" },
201+{ targetId: "TARGET_B", url: "https://beta.example" },
202+]);
203+204+connectOverCdpSpy.mockResolvedValueOnce(stale.browser).mockResolvedValueOnce(fresh.browser);
205+getChromeWebSocketUrlSpy.mockResolvedValue(null);
206+207+await getPageForTargetId({ cdpUrl: "http://127.0.0.1:9333" });
208+209+const resolved = await getPageForTargetId({
210+cdpUrl: "http://127.0.0.1:9333",
211+targetId: "TARGET_B",
212+});
213+214+expect(resolved).toBe(fresh.pages[1]);
215+expect(connectOverCdpSpy).toHaveBeenCalledTimes(2);
216+expect(stale.browserClose).toHaveBeenCalledTimes(1);
217+});
218+219+it("fails after a single reconnect when the refreshed browser is still page-less", async () => {
220+const stale = makeBrowser([]);
221+const stillBroken = makeBrowser([]);
222+223+connectOverCdpSpy
224+.mockResolvedValueOnce(stale.browser)
225+.mockResolvedValueOnce(stillBroken.browser);
226+getChromeWebSocketUrlSpy.mockResolvedValue(null);
227+228+await listPagesViaPlaywright({ cdpUrl: "http://127.0.0.1:9444" });
229+230+await expect(getPageForTargetId({ cdpUrl: "http://127.0.0.1:9444" })).rejects.toThrow(
231+"No pages available in the connected browser.",
232+);
233+expect(connectOverCdpSpy).toHaveBeenCalledTimes(2);
234+expect(stale.browserClose).toHaveBeenCalledTimes(1);
235+});
236+237+it("does not add an extra top-level retry for non-recoverable connect failures", async () => {
238+connectOverCdpSpy.mockRejectedValue(new Error("connectOverCDP exploded"));
239+getChromeWebSocketUrlSpy.mockResolvedValue(null);
240+241+await expect(getPageForTargetId({ cdpUrl: "http://127.0.0.1:9555" })).rejects.toThrow(
242+"connectOverCDP exploded",
243+);
244+expect(connectOverCdpSpy).toHaveBeenCalledTimes(3);
245+});
122246});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。