diff --git a/docs/api.md b/docs/api.md index 52700f0097b1c..46d2793c03f2e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -357,6 +357,7 @@ * [target.url()](#targeturl) * [target.worker()](#targetworker) - [class: CDPSession](#class-cdpsession) + * [cdpSession.connection()](#cdpsessionconnection) * [cdpSession.detach()](#cdpsessiondetach) * [cdpSession.send(method[, ...paramArgs])](#cdpsessionsendmethod-paramargs) - [class: Coverage](#class-coverage) @@ -4557,6 +4558,12 @@ await client.send('Animation.setPlaybackRate', { }); ``` +#### cdpSession.connection() + +- returns: <[Connection]> + +Returns the underlying connection associated with the session. Can be used to obtain other related sessions. + #### cdpSession.detach() - returns: <[Promise]> diff --git a/src/common/Connection.ts b/src/common/Connection.ts index 4f11556a28a05..1a1179d446c96 100644 --- a/src/common/Connection.ts +++ b/src/common/Connection.ts @@ -125,11 +125,13 @@ export class Connection extends EventEmitter { sessionId ); this._sessions.set(sessionId, session); + this.emit('sessionattached', session); } else if (object.method === 'Target.detachedFromTarget') { const session = this._sessions.get(object.params.sessionId); if (session) { session._onClosed(); this._sessions.delete(object.params.sessionId); + this.emit('sessiondetached', session); } } if (object.sessionId) { @@ -253,6 +255,10 @@ export class CDPSession extends EventEmitter { this._sessionId = sessionId; } + connection(): Connection { + return this._connection; + } + send( method: T, ...paramArgs: ProtocolMapping.Commands[T]['paramsType'] diff --git a/src/common/Page.ts b/src/common/Page.ts index 9a1eafc49876a..0e4e5b4dc775c 100644 --- a/src/common/Page.ts +++ b/src/common/Page.ts @@ -488,8 +488,16 @@ export class Page extends EventEmitter { this._viewport = null; client.on('Target.attachedToTarget', (event) => { - if (event.targetInfo.type !== 'worker') { + if ( + event.targetInfo.type !== 'worker' && + event.targetInfo.type !== 'iframe' + ) { // If we don't detach from service workers, they will never die. + // We still want to attach to workers for emitting events. + // We still want to attach to iframes so sessions may interact with them. + // We detach from all other types out of an abundance of caution. + // See https://source.chromium.org/chromium/chromium/src/+/master:content/browser/devtools/devtools_agent_host_impl.cc?q=f:devtools%20-f:out%20%22::kTypePage%5B%5D%22&ss=chromium + // for the complete list of available types. client .send('Target.detachFromTarget', { sessionId: event.sessionId, diff --git a/test/headful.spec.ts b/test/headful.spec.ts index 119823eb50b42..53799418e6c2c 100644 --- a/test/headful.spec.ts +++ b/test/headful.spec.ts @@ -42,9 +42,10 @@ describeChromeOnly('headful tests', function () { let headfulOptions; let headlessOptions; let extensionOptions; + let forcedOopifOptions; beforeEach(() => { - const { defaultBrowserOptions } = getTestState(); + const { server, defaultBrowserOptions } = getTestState(); headfulOptions = Object.assign({}, defaultBrowserOptions, { headless: false, }); @@ -59,6 +60,18 @@ describeChromeOnly('headful tests', function () { `--load-extension=${extensionPath}`, ], }); + + forcedOopifOptions = Object.assign({}, defaultBrowserOptions, { + headless: false, + devtools: true, + args: [ + `--host-rules=MAP oopifdomain 127.0.0.1`, + `--isolate-origins=${server.PREFIX.replace( + 'localhost', + 'oopifdomain' + )}`, + ], + }); }); describe('HEADFUL', function () { @@ -147,6 +160,58 @@ describeChromeOnly('headful tests', function () { expect(urls).toEqual([server.EMPTY_PAGE, 'https://google.com/']); await browser.close(); }); + it('OOPIF: should expose events within OOPIFs', async () => { + const { server, puppeteer } = getTestState(); + + const browser = await puppeteer.launch(forcedOopifOptions); + const page = await browser.newPage(); + + // Setup our session listeners to observe OOPIF activity. + const session = await page.target().createCDPSession(); + const networkEvents = []; + const otherSessions = []; + await session.send('Target.setAutoAttach', { + autoAttach: true, + flatten: true, + waitForDebuggerOnStart: true, + }); + session.connection().on('sessionattached', async (session) => { + otherSessions.push(session); + + session.on('Network.requestWillBeSent', (params) => + networkEvents.push(params) + ); + await session.send('Network.enable'); + }); + + // Navigate to the empty page and add an OOPIF iframe with at least one request. + await page.goto(server.EMPTY_PAGE); + await page.evaluate((frameUrl) => { + const frame = document.createElement('iframe'); + frame.setAttribute('src', frameUrl); + document.body.appendChild(frame); + return new Promise((x, y) => { + frame.onload = x; + frame.onerror = y; + }); + }, server.PREFIX.replace('localhost', 'oopifdomain') + '/one-style.html'); + await page.waitForSelector('iframe'); + + // Ensure we found the iframe session. + expect(otherSessions).toHaveLength(1); + + // Resume the iframe and trigger another request. + const iframeSession = otherSessions[0]; + await iframeSession.send('Runtime.runIfWaitingForDebugger'); + await iframeSession.send('Runtime.evaluate', { + expression: `fetch('/fetch')`, + awaitPromise: true, + }); + await browser.close(); + + const requests = networkEvents.map((event) => event.request.url); + expect(requests).toContain(`http://oopifdomain:${server.PORT}/fetch`); + }); it('should close browser with beforeunload page', async () => { const { server, puppeteer } = getTestState();