From 2ff546b5ae9b87bc2a1255f92e54ede613b4b629 Mon Sep 17 00:00:00 2001 From: Sakana <15715093608@163.com> Date: Sun, 13 Sep 2026 10:31:42 +0800 Subject: [PATCH] fix(in-page-channel): connect panels nested in popup windows Panel iframes have no opener of their own, so their handshake never reached the inspected page through the popup window's opener. Discover openers across the ancestor chain, deduplicate targets, and cover direct and nested popup handshakes with regression tests. --- .../in-page-channel/in-page-channel.test.ts | 33 +++++++++++++++++++ .../devframe/src/in-page-channel/panel.ts | 2 +- .../devframe/src/in-page-channel/protocol.ts | 20 ++++++----- .../devframe/src/in-page-channel/types.ts | 2 +- 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/packages/devframe/src/in-page-channel/in-page-channel.test.ts b/packages/devframe/src/in-page-channel/in-page-channel.test.ts index 9c0706bb..3d6dd7f4 100644 --- a/packages/devframe/src/in-page-channel/in-page-channel.test.ts +++ b/packages/devframe/src/in-page-channel/in-page-channel.test.ts @@ -675,6 +675,39 @@ function createWindowPair(origin = 'https://app.test'): { hostWin: FakeWindow, p const fastHello = { helloIntervalMs: 5, heartbeat: false as const } describe('in-page channel handshake', () => { + it.each([0, 1, 2])('connects through a popup opener with %i nested panel frames', async (depth) => { + const { hostWin, panelWin } = createWindowPair() + let popupWin = panelWin + for (let i = 0; i < depth; i++) { + const parent = createFakeWindow(hostWin.location.origin) + popupWin.parent = parent + popupWin = parent + } + popupWin.parent = popupWin + popupWin.opener = hostWin + const pageScript = createPageScriptChannel({ + name: 'devframes:test', + window: asWindow(hostWin), + heartbeat: false, + functions: defaultPageScriptFunctions, + }) + const panel = connectPanelChannel({ + name: 'devframes:test', + window: asWindow(panelWin), + ...fastHello, + functions: defaultPanelFunctions, + }) + try { + await panel.whenConnected(200) + expect(panel.pageScript?.instanceId).toBe(pageScript.instanceId) + await expect(panel.call('echo', 'popup')).resolves.toBe('popup') + } + finally { + panel.close() + pageScript.close() + } + }) + it('connects a panel to the page script and survives page-script restarts', async () => { const { hostWin, panelWin } = createWindowPair() const pageScript = createPageScriptChannel({ diff --git a/packages/devframe/src/in-page-channel/panel.ts b/packages/devframe/src/in-page-channel/panel.ts index 63ce2ed6..559b63b3 100644 --- a/packages/devframe/src/in-page-channel/panel.ts +++ b/packages/devframe/src/in-page-channel/panel.ts @@ -38,7 +38,7 @@ const DEFAULT_EVENT_BUFFER_LIMIT = 64 * Connect the panel endpoint of an in-page channel. * * The panel initiates: it posts a versioned hello to every window a - * same-tab page script can live in (its ancestor chain and its `opener`), + * page script can live in (its ancestor chain and those windows' openers), * retrying with backoff until one answers with a dedicated port, so boot * order never matters, and a reload of either side is just a re-handshake * (`WindowProxy` references survive navigations). While `connecting`, diff --git a/packages/devframe/src/in-page-channel/protocol.ts b/packages/devframe/src/in-page-channel/protocol.ts index 2bf6cde0..c5aa03ec 100644 --- a/packages/devframe/src/in-page-channel/protocol.ts +++ b/packages/devframe/src/in-page-channel/protocol.ts @@ -92,8 +92,8 @@ export function resolveAllowedOrigins(allowedOrigins: string[] | undefined, win: } /** - * Default handshake targets of a panel: its ancestor chain plus its - * `opener`, every same-tab window a page script can live in. `WindowProxy` + * Default handshake targets of a panel: its ancestor chain plus the + * `opener` of each window, including a popup containing the panel iframe. `WindowProxy` * references stay valid across navigations, so hellos posted to these reach * a page script even after the host page reloads. */ @@ -111,13 +111,15 @@ export function defaultHandshakeTargets(win: Window): Window[] { catch { // Walking stopped by the browser; keep what we have. } - try { - const opener = win.opener as Window | null - if (opener && opener !== win) - targets.push(opener) - } - catch { - // Inaccessible opener; ignore. + for (const current of [win, ...targets]) { + try { + const opener = current.opener as Window | null + if (opener && opener !== win && !targets.includes(opener)) + targets.push(opener) + } + catch { + // Inaccessible opener; continue with the other ancestors. + } } return targets } diff --git a/packages/devframe/src/in-page-channel/types.ts b/packages/devframe/src/in-page-channel/types.ts index 1da339e0..39f51b89 100644 --- a/packages/devframe/src/in-page-channel/types.ts +++ b/packages/devframe/src/in-page-channel/types.ts @@ -274,7 +274,7 @@ export interface ConnectPanelChannelOptions