diff options
| author | Aman Kumar <145339995+amankumarsingh77@users.noreply.github.com> | 2026-07-22 00:59:58 +0530 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-22 00:59:58 +0530 |
| commit | fe00d3361dece3d35f788d3eeb1c750eedbf6294 (patch) | |
| tree | a51b8ffe3cddb1aa8d457784f71cc1b2b0b61d56 /src/cdp | |
| parent | 7de0beaf2cf713afc50c573e941fa279ec6df9d9 (diff) | |
feat: add forms domain and durable window binding (v0.9.0) (#14)
- Add forms domain with a universal field setter that auto-detects element
type and fires framework-compatible input events (React native-setter trick)
- Add window-binding helper to capture and bind the real Chrome windowId as the
session's durable window identity on every new-window code path
- Wire supporting changes across CDP session/ownership, client, navigate,
isolated-tab, prompt, and state
- Add manual tests for forms and window binding
- Bump version 0.8.3 -> 0.9.0
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src/cdp')
| -rw-r--r-- | src/cdp/ownership.ts | 20 | ||||
| -rw-r--r-- | src/cdp/session.ts | 55 | ||||
| -rw-r--r-- | src/cdp/window-binding.ts | 15 |
3 files changed, 89 insertions, 1 deletions
diff --git a/src/cdp/ownership.ts b/src/cdp/ownership.ts index f221d6a..727b3d8 100644 --- a/src/cdp/ownership.ts +++ b/src/cdp/ownership.ts @@ -20,14 +20,24 @@ export type OwnershipRegistry = { replaceAll(ids: ReadonlyArray<string>): void; setHarnessWindow(targetId: string | undefined): void; harnessWindow(): string | undefined; + /** The real Chrome windowId of the dedicated harness window — the durable + * identity of "the window this session initialized", surviving seed-tab + * closure. Undefined until the window is created (or after teardown). */ + setHarnessWindowId(windowId: number | undefined): void; + harnessWindowId(): number | undefined; onChange(cb: () => void): void; }; export const createOwnershipRegistry = ( - initial?: { readonly ownedTargetIds?: ReadonlyArray<string>; readonly harnessWindowTargetId?: string }, + initial?: { + readonly ownedTargetIds?: ReadonlyArray<string>; + readonly harnessWindowTargetId?: string; + readonly harnessWindowId?: number; + }, ): OwnershipRegistry => { const owned = new Set<string>(initial?.ownedTargetIds ?? []); let harnessWindow: string | undefined = initial?.harnessWindowTargetId; + let harnessWindowId: number | undefined = initial?.harnessWindowId; let listener: (() => void) | null = null; const notify = (): void => { @@ -65,6 +75,14 @@ export const createOwnershipRegistry = ( harnessWindow() { return harnessWindow; }, + setHarnessWindowId(windowId) { + if (harnessWindowId === windowId) return; + harnessWindowId = windowId; + notify(); + }, + harnessWindowId() { + return harnessWindowId; + }, onChange(cb) { listener = cb; }, diff --git a/src/cdp/session.ts b/src/cdp/session.ts index e090858..03410ba 100644 --- a/src/cdp/session.ts +++ b/src/cdp/session.ts @@ -92,6 +92,17 @@ export const createCdpSession = ( const tab = targetId ? tabs.get(targetId) : undefined; if (tab) tab.pageInfoDirty = true; } + if (ev.method === "Target.targetCreated" && ownership) { + // Adopt tabs opened BY a tab we already own (window.open, target=_blank, + // popups). Chrome sets openerId to the opener target; if that opener is + // owned, the child belongs to this session's window and must be + // controllable + cleaned up. Targets the user opens themselves have no + // owned opener, so they are never adopted. + const info = (ev.params as { targetInfo?: { targetId?: string; type?: string; openerId?: string } } | undefined)?.targetInfo; + if (info?.type === "page" && info.targetId && info.openerId && ownership.has(info.openerId)) { + ownership.add(info.targetId); + } + } if (ev.method === "Target.targetDestroyed" && ownership) { const params = ev.params as { targetId?: string } | undefined; if (params?.targetId) { @@ -179,6 +190,43 @@ export const createCdpSession = ( if (survivors.length !== ownership.list().length) ownership.replaceAll(survivors); const hw = ownership.harnessWindow(); if (hw && !live.has(hw)) ownership.setHarnessWindow(undefined); + + if (survivors.length === 0) { + // No owned tab survived — the harness window is gone (user closed it, + // or Chrome restarted and reassigned all ids). The persisted windowId + // is now meaningless and could even collide with one of the user's + // windows, so drop it. attachFirstPage will mint a fresh window below. + ownership.setHarnessWindowId(undefined); + } else { + // Self-heal ownership from the window itself. We anchor on a tab we + // KNOW we own (a survivor) and re-adopt only its live window-mates — + // popups/children from last session, tabs reopened in our window. + // Ownership is thus DERIVED from the real window, never from the bare + // persisted integer (which Chrome may reassign to a user window on + // restart — anchoring on a survivor makes that misfire impossible). + const anchorId = survivors[0]!; + const anchorWin = await transport.request("Browser.getWindowForTarget", { targetId: anchorId }, { sessionId: null }); + if (anchorWin.success) { + const anchorWindowId = (anchorWin.data as { windowId?: number }).windowId; + if (typeof anchorWindowId === "number") { + ownership.setHarnessWindowId(anchorWindowId); // refresh to the live id + const windows = await Promise.all( + allPages.map((p) => + p.targetId === anchorId + ? Promise.resolve(anchorWin) + : transport.request("Browser.getWindowForTarget", { targetId: p.targetId }, { sessionId: null }), + ), + ); + allPages.forEach((p, i) => { + const r = windows[i]; + if (r?.success && (r.data as { windowId?: number }).windowId === anchorWindowId) { + ownership.add(p.targetId); + } + }); + } + } + if (!ownership.harnessWindow()) ownership.setHarnessWindow(anchorId); + } } // Prune per-tab state for pages that no longer exist const liveTargetIds = new Set(allPages.map((p) => p.targetId)); @@ -208,6 +256,13 @@ export const createCdpSession = ( if (ownership) { ownership.setHarnessWindow(c.targetId); ownership.add(c.targetId); + // Capture the real Chrome windowId — the durable identity of the + // window this session initialized (survives seed-tab closure). + const win = await transport.request("Browser.getWindowForTarget", { targetId: c.targetId }, { sessionId: null }); + if (win.success) { + const wid = (win.data as { windowId?: number }).windowId; + if (typeof wid === "number") ownership.setHarnessWindowId(wid); + } } } diff --git a/src/cdp/window-binding.ts b/src/cdp/window-binding.ts new file mode 100644 index 0000000..4ae1ac3 --- /dev/null +++ b/src/cdp/window-binding.ts @@ -0,0 +1,15 @@ +import type { BrowserClient } from "../client"; + +/** + * Capture the real Chrome windowId of a freshly-created harness window and bind + * it as the session's durable window identity. Best-effort: a failed CDP call + * leaves the previous binding untouched. Call this on every code path that + * spawns a NEW dedicated window (newWindow:true), so the windowId never points + * at a window that has since been replaced. + */ +export const bindHarnessWindowId = async (client: BrowserClient, targetId: string): Promise<void> => { + const r = await client.session().callBrowser("Browser.getWindowForTarget", { targetId }); + if (!r.success) return; + const wid = (r.data as { windowId?: number }).windowId; + if (typeof wid === "number") client.ownership().setHarnessWindowId(wid); +}; |
