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