From b48729e4a2a3958544b890896b4a4afb13ca2699 Mon Sep 17 00:00:00 2001 From: liangxu Date: Fri, 24 Apr 2026 09:39:03 +0800 Subject: [PATCH] refactor(desktop): extract hidden Playwright CDP target helper Move CDPTargetDescriptor and the reclaim predicate out of playwright-cdp.ts into a dedicated module so the reclamation rule can be unit-tested in isolation and reused without pulling in the whole hidden browser manager. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/main/playwright-cdp-targets.test.ts | 32 +++++++++++++++++++ .../src/main/playwright-cdp-targets.ts | 29 +++++++++++++++++ .../desktop-client/src/main/playwright-cdp.ts | 23 +++---------- 3 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 apps/desktop-client/src/main/playwright-cdp-targets.test.ts create mode 100644 apps/desktop-client/src/main/playwright-cdp-targets.ts diff --git a/apps/desktop-client/src/main/playwright-cdp-targets.test.ts b/apps/desktop-client/src/main/playwright-cdp-targets.test.ts new file mode 100644 index 0000000..44bc18d --- /dev/null +++ b/apps/desktop-client/src/main/playwright-cdp-targets.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; + +import { isReclaimableHiddenPlaywrightTarget } from "./playwright-cdp-targets"; + +describe("playwright cdp target pruning", () => { + it("does not reclaim blank app-owned page targets", () => { + expect(isReclaimableHiddenPlaywrightTarget({ + id: "main-window-host", + type: "page", + title: "", + url: "", + })).toBe(false); + }); + + it("only reclaims targets marked as hidden Playwright pages", () => { + expect(isReclaimableHiddenPlaywrightTarget({ + id: "hidden-window", + type: "page", + title: "监控任务 · hidden-playwright", + url: "data:text/html;charset=utf-8,%3Cmeta%20name%3D%22geo-hidden-playwright-token%22%3E", + })).toBe(true); + }); + + it("ignores regular user-visible browser targets", () => { + expect(isReclaimableHiddenPlaywrightTarget({ + id: "renderer", + type: "page", + title: "GEO Rankly Desktop", + url: "http://localhost:5173/", + })).toBe(false); + }); +}); diff --git a/apps/desktop-client/src/main/playwright-cdp-targets.ts b/apps/desktop-client/src/main/playwright-cdp-targets.ts new file mode 100644 index 0000000..b18df0a --- /dev/null +++ b/apps/desktop-client/src/main/playwright-cdp-targets.ts @@ -0,0 +1,29 @@ +export interface CDPTargetDescriptor { + id?: string; + type?: string; + title?: string; + url?: string; +} + +const hiddenPlaywrightTargetMarkers = [ + "hidden-playwright", + "geo-hidden-playwright-token", +] as const; + +export function isReclaimableHiddenPlaywrightTarget(target: CDPTargetDescriptor): boolean { + if ((target.type ?? "").trim() !== "page") { + return false; + } + if (!(target.id ?? "").trim()) { + return false; + } + + const title = target.title ?? ""; + const url = target.url ?? ""; + const haystack = `${title}\n${url}`.toLowerCase(); + + // Blank Electron page targets can belong to visible BrowserWindow shells. + // Only reclaim targets that we can prove were created by the hidden + // Playwright bootstrap flow. + return hiddenPlaywrightTargetMarkers.some((marker) => haystack.includes(marker)); +} diff --git a/apps/desktop-client/src/main/playwright-cdp.ts b/apps/desktop-client/src/main/playwright-cdp.ts index 7725fc3..cd0fbfb 100644 --- a/apps/desktop-client/src/main/playwright-cdp.ts +++ b/apps/desktop-client/src/main/playwright-cdp.ts @@ -2,6 +2,10 @@ import { BrowserWindow, nativeTheme } from "electron/main"; import type { BrowserWindow as ElectronBrowserWindow, Session } from "electron/main"; import type { Browser as PlaywrightBrowser, Page as PlaywrightPage } from "playwright-core"; +import { + isReclaimableHiddenPlaywrightTarget, + type CDPTargetDescriptor, +} from "./playwright-cdp-targets"; import { STANDARD_USER_AGENT } from "./user-agent"; const hiddenPlaywrightIdleTTLms = 5 * 60_000; @@ -13,13 +17,6 @@ const cdpPort = Number.parseInt(process.env.GEO_ELECTRON_CDP_PORT ?? "9339", 10) const cdpEndpointURL = `http://127.0.0.1:${cdpPort}`; const cdpTargetsListURL = `${cdpEndpointURL}/json/list`; -interface CDPTargetDescriptor { - id?: string; - type?: string; - title?: string; - url?: string; -} - interface HiddenPlaywrightRecord { key: string; accountId: string; @@ -347,7 +344,7 @@ async function pruneStaleCDPTargets(): Promise { let closedCount = 0; for (const target of targets) { - if (!isStaleCDPTarget(target)) { + if (!isReclaimableHiddenPlaywrightTarget(target)) { continue; } @@ -368,16 +365,6 @@ async function pruneStaleCDPTargets(): Promise { return closedCount; } -function isStaleCDPTarget(target: CDPTargetDescriptor): boolean { - if ((target.type ?? "").trim() !== "page") { - return false; - } - if (!(target.id ?? "").trim()) { - return false; - } - return !(target.title ?? "").trim() && !(target.url ?? "").trim(); -} - function isConnectOverCDPTimeout(error: unknown): boolean { return error instanceof Error && error.name === "TimeoutError"