diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index e13652b98b7e1..c3f60c232b6f3 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -759,7 +759,9 @@ class FrameSession { if (!frame && event.targetInfo.parentFrameId) { // When connecting to an existing page with an iframe, there is an "iframe" target, // but no local frame is reported in getFrameTree. We can create a remote frame here. - frame = this._page.frameManager.frameAttached(targetId, event.targetInfo.parentFrameId); + // Its parent must already be known, otherwise frameAttached would replace the main frame. + if (this._page.frameManager.frame(event.targetInfo.parentFrameId)) + frame = this._page.frameManager.frameAttached(targetId, event.targetInfo.parentFrameId); } if (!frame) return; // Subtree may be already gone due to renderer/browser race. diff --git a/tests/library/chromium/connect-over-cdp.spec.ts b/tests/library/chromium/connect-over-cdp.spec.ts index 64b4c1be9d543..7122b7806a78b 100644 --- a/tests/library/chromium/connect-over-cdp.spec.ts +++ b/tests/library/chromium/connect-over-cdp.spec.ts @@ -16,10 +16,12 @@ */ import { playwrightTest as test, expect } from '../../config/browserTest'; -import http from 'http'; +import { spawn } from 'child_process'; import fs from 'fs'; +import http from 'http'; +import os from 'os'; import path from 'path'; -import { getUserAgent, server as coreServer } from '../../../packages/playwright-core/lib/coreBundle'; +import { getUserAgent, inprocess, server as coreServer } from '../../../packages/playwright-core/lib/coreBundle'; import { suppressCertificateWarning } from '../../config/utils'; const { WebSocketTransport, nullProgress } = coreServer; @@ -171,6 +173,57 @@ test('should connect to an existing cdp session twice', async ({ browserType, mo } }); +test('should not replace the main frame with a nested iframe when connecting over CDP', async ({ browserType, mode }, testInfo) => { + test.skip(mode !== 'default'); + test.setTimeout(90000); + const server = http.createServer((req, res) => { + const port = (server.address() as { port: number }).port; + const pages: Record = { + '/': `

TOP

`, + '/outer': '', + '/inner': '', + }; + res.setHeader('content-type', 'text/html'); + res.end(pages[req.url || ''] || ''); + }); + await new Promise(resolve => server.listen(0, resolve)); + const url = `http://127.0.0.1:${(server.address() as { port: number }).port}/`; + const port = 9339 + testInfo.workerIndex; + const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pw-cdp-')); + const chrome = spawn(process.env.CRPATH || browserType.executablePath(), [ + '--headless=new', '--no-first-run', '--no-sandbox', '--remote-debugging-port=' + port, + '--user-data-dir=' + userDataDir, url, + ], { stdio: 'ignore' }); + try { + await expect.poll(async () => { + try { + const response = await fetch(`http://127.0.0.1:${port}/json/list`); + return (await response.json()).some((target: { url: string }) => target.url === url); + } catch { + return false; + } + }, { timeout: 20000 }).toBe(true); + // Let the pre-existing page's nested iframe targets finish loading. + await new Promise(resolve => setTimeout(resolve, 2000)); + + const cdpBrowser = await inprocess.playwright.chromium.connectOverCDP(`http://127.0.0.1:${port}/`); + try { + const page = cdpBrowser.contexts().flatMap(context => context.pages()).find(page => page.url() === url); + expect(page).toBeTruthy(); + expect(await page!.evaluate(() => window.top === window)).toBe(true); + expect(await page!.locator('#top').count()).toBe(1); + } finally { + await cdpBrowser.close(); + } + } finally { + chrome.kill(); + if (chrome.exitCode === null) + await new Promise(resolve => chrome.once('exit', () => resolve())); + server.close(); + await fs.promises.rm(userDataDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }); + } +}); + test('should connect to existing page with iframe and navigate', async ({ browserType, mode, server }, testInfo) => { const port = 9339 + testInfo.workerIndex; const browserServer = await browserType.launch({