api(cdp): newCDPSession accepts frames, too (#8157)
Without this, Playwright's CDP feature leaves unreachable targets (namely OOPIFs). This change allows for more advanced experimentation in user-land without relying on out-of-band CDP connections and clients. Now you can, for example, call `DOM.getDocument` on the page OR main frame, observe there is an iframe node with no `contentDocument` (i.e. OOPIF), make note of the referenced `frameId`, and then iterate of page.frames() calling `Target.getInfo` on each to link the Playwright Frame with the CDP `frameId` and then recurse. Relates #8113
This commit is contained in:
parent
e3060080cc
commit
101662765c
|
|
@ -834,9 +834,10 @@ CDP sessions are only supported on Chromium-based browsers.
|
||||||
Returns the newly created session.
|
Returns the newly created session.
|
||||||
|
|
||||||
### param: BrowserContext.newCDPSession.page
|
### param: BrowserContext.newCDPSession.page
|
||||||
- `page` <[Page]>
|
- `page` <[Page]|[Frame]>
|
||||||
|
|
||||||
Page to create new session for.
|
Target to create new session for. For backwards-compatability, this parameter is
|
||||||
|
named `page`, but it can be a `Page` or `Frame` type.
|
||||||
|
|
||||||
## async method: BrowserContext.newPage
|
## async method: BrowserContext.newPage
|
||||||
- returns: <[Page]>
|
- returns: <[Page]>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Page, BindingCall } from './page';
|
import { Page, BindingCall } from './page';
|
||||||
|
import { Frame } from './frame';
|
||||||
import * as network from './network';
|
import * as network from './network';
|
||||||
import * as channels from '../protocol/channels';
|
import * as channels from '../protocol/channels';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
@ -308,9 +309,12 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
||||||
return [...this._serviceWorkers];
|
return [...this._serviceWorkers];
|
||||||
}
|
}
|
||||||
|
|
||||||
async newCDPSession(page: Page): Promise<api.CDPSession> {
|
async newCDPSession(page: Page | Frame): Promise<api.CDPSession> {
|
||||||
|
// channelOwner.ts's validation messages don't handle the pseudo-union type, so we're explicit here
|
||||||
|
if (!(page instanceof Page) && !(page instanceof Frame))
|
||||||
|
throw new Error('page: expected Page or Frame');
|
||||||
return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
|
return this._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
|
||||||
const result = await channel.newCDPSession({ page: page._channel });
|
const result = await channel.newCDPSession(page instanceof Page ? { page: page._channel } : { frame: page._channel });
|
||||||
return CDPSession.from(result.session);
|
return CDPSession.from(result.session);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
import { BrowserContext } from '../server/browserContext';
|
import { BrowserContext } from '../server/browserContext';
|
||||||
import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher';
|
import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher';
|
||||||
import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher';
|
import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher';
|
||||||
|
import { FrameDispatcher } from './frameDispatcher';
|
||||||
import * as channels from '../protocol/channels';
|
import * as channels from '../protocol/channels';
|
||||||
import { RouteDispatcher, RequestDispatcher, ResponseDispatcher } from './networkDispatchers';
|
import { RouteDispatcher, RequestDispatcher, ResponseDispatcher } from './networkDispatchers';
|
||||||
import { CRBrowserContext } from '../server/chromium/crBrowser';
|
import { CRBrowserContext } from '../server/chromium/crBrowser';
|
||||||
|
|
@ -176,8 +177,10 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||||
async newCDPSession(params: channels.BrowserContextNewCDPSessionParams): Promise<channels.BrowserContextNewCDPSessionResult> {
|
async newCDPSession(params: channels.BrowserContextNewCDPSessionParams): Promise<channels.BrowserContextNewCDPSessionResult> {
|
||||||
if (!this._object._browser.options.isChromium)
|
if (!this._object._browser.options.isChromium)
|
||||||
throw new Error(`CDP session is only available in Chromium`);
|
throw new Error(`CDP session is only available in Chromium`);
|
||||||
|
if (!params.page && !params.frame || params.page && params.frame)
|
||||||
|
throw new Error(`CDP session must be initiated with either Page or Frame, not none or both`);
|
||||||
const crBrowserContext = this._object as CRBrowserContext;
|
const crBrowserContext = this._object as CRBrowserContext;
|
||||||
return { session: new CDPSessionDispatcher(this._scope, await crBrowserContext.newCDPSession((params.page as PageDispatcher)._object)) };
|
return { session: new CDPSessionDispatcher(this._scope, await crBrowserContext.newCDPSession((params.page ? params.page as PageDispatcher : params.frame as FrameDispatcher)._object)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async tracingStart(params: channels.BrowserContextTracingStartParams): Promise<channels.BrowserContextTracingStartResult> {
|
async tracingStart(params: channels.BrowserContextTracingStartParams): Promise<channels.BrowserContextTracingStartResult> {
|
||||||
|
|
|
||||||
|
|
@ -845,10 +845,12 @@ export type BrowserContextRecorderSupplementEnableOptions = {
|
||||||
};
|
};
|
||||||
export type BrowserContextRecorderSupplementEnableResult = void;
|
export type BrowserContextRecorderSupplementEnableResult = void;
|
||||||
export type BrowserContextNewCDPSessionParams = {
|
export type BrowserContextNewCDPSessionParams = {
|
||||||
page: PageChannel,
|
page?: PageChannel,
|
||||||
|
frame?: FrameChannel,
|
||||||
};
|
};
|
||||||
export type BrowserContextNewCDPSessionOptions = {
|
export type BrowserContextNewCDPSessionOptions = {
|
||||||
|
page?: PageChannel,
|
||||||
|
frame?: FrameChannel,
|
||||||
};
|
};
|
||||||
export type BrowserContextNewCDPSessionResult = {
|
export type BrowserContextNewCDPSessionResult = {
|
||||||
session: CDPSessionChannel,
|
session: CDPSessionChannel,
|
||||||
|
|
|
||||||
|
|
@ -629,7 +629,8 @@ BrowserContext:
|
||||||
|
|
||||||
newCDPSession:
|
newCDPSession:
|
||||||
parameters:
|
parameters:
|
||||||
page: Page
|
page: Page?
|
||||||
|
frame: Frame?
|
||||||
returns:
|
returns:
|
||||||
session: CDPSession
|
session: CDPSession
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -406,7 +406,8 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
outputFile: tOptional(tString),
|
outputFile: tOptional(tString),
|
||||||
});
|
});
|
||||||
scheme.BrowserContextNewCDPSessionParams = tObject({
|
scheme.BrowserContextNewCDPSessionParams = tObject({
|
||||||
page: tChannel('Page'),
|
page: tOptional(tChannel('Page')),
|
||||||
|
frame: tOptional(tChannel('Frame')),
|
||||||
});
|
});
|
||||||
scheme.BrowserContextTracingStartParams = tObject({
|
scheme.BrowserContextTracingStartParams = tObject({
|
||||||
name: tOptional(tString),
|
name: tOptional(tString),
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextO
|
||||||
import { assert } from '../../utils/utils';
|
import { assert } from '../../utils/utils';
|
||||||
import * as network from '../network';
|
import * as network from '../network';
|
||||||
import { Page, PageBinding, PageDelegate, Worker } from '../page';
|
import { Page, PageBinding, PageDelegate, Worker } from '../page';
|
||||||
|
import { Frame } from '../frames';
|
||||||
import { ConnectionTransport } from '../transport';
|
import { ConnectionTransport } from '../transport';
|
||||||
import * as types from '../types';
|
import * as types from '../types';
|
||||||
import { ConnectionEvents, CRConnection, CRSession } from './crConnection';
|
import { ConnectionEvents, CRConnection, CRSession } from './crConnection';
|
||||||
|
|
@ -503,10 +504,18 @@ export class CRBrowserContext extends BrowserContext {
|
||||||
return Array.from(this._browser._serviceWorkers.values()).filter(serviceWorker => serviceWorker._browserContext === this);
|
return Array.from(this._browser._serviceWorkers.values()).filter(serviceWorker => serviceWorker._browserContext === this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async newCDPSession(page: Page): Promise<CRSession> {
|
async newCDPSession(page: Page | Frame): Promise<CRSession> {
|
||||||
if (!(page instanceof Page))
|
let targetId: string | null = null;
|
||||||
throw new Error('page: expected Page');
|
if (page instanceof Page) {
|
||||||
const targetId = (page._delegate as CRPage)._targetId;
|
targetId = (page._delegate as CRPage)._targetId;
|
||||||
|
} else if (page instanceof Frame) {
|
||||||
|
const session = (page._page._delegate as CRPage)._sessions.get(page._id);
|
||||||
|
if (!session) throw new Error(`This frame does not have a separate CDP session, it is a part of the parent frame's session`);
|
||||||
|
targetId = session._targetId;
|
||||||
|
} else {
|
||||||
|
throw new Error('page: expected Page or Frame');
|
||||||
|
}
|
||||||
|
|
||||||
const rootSession = await this._browser._clientRootSession();
|
const rootSession = await this._browser._clientRootSession();
|
||||||
const { sessionId } = await rootSession.send('Target.attachToTarget', { targetId, flatten: true });
|
const { sessionId } = await rootSession.send('Target.attachToTarget', { targetId, flatten: true });
|
||||||
return this._browser._connection.session(sessionId)!;
|
return this._browser._connection.session(sessionId)!;
|
||||||
|
|
|
||||||
|
|
@ -292,6 +292,21 @@ it('should click', async function({page, browser, server}) {
|
||||||
expect(await handle1.evaluate(() => window['_clicked'])).toBe(true);
|
expect(await handle1.evaluate(() => window['_clicked'])).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow cdp sessions on oopifs', async function({page, browser, server}) {
|
||||||
|
await page.goto(server.PREFIX + '/dynamic-oopif.html');
|
||||||
|
expect(await countOOPIFs(browser)).toBe(1);
|
||||||
|
expect(page.frames().length).toBe(2);
|
||||||
|
expect(await page.frames()[1].evaluate(() => '' + location.href)).toBe(server.CROSS_PROCESS_PREFIX + '/grid.html');
|
||||||
|
|
||||||
|
const parentCDP = await page.context().newCDPSession(page.frames()[0]);
|
||||||
|
const parent = await parentCDP.send('DOM.getDocument', { pierce: true, depth: -1 });
|
||||||
|
expect(JSON.stringify(parent)).not.toContain('./digits/1.png');
|
||||||
|
|
||||||
|
const oopifCDP = await page.context().newCDPSession(page.frames()[1]);
|
||||||
|
const oopif = await oopifCDP.send('DOM.getDocument', { pierce: true, depth: -1});
|
||||||
|
expect(JSON.stringify(oopif)).toContain('./digits/1.png');
|
||||||
|
});
|
||||||
|
|
||||||
async function countOOPIFs(browser) {
|
async function countOOPIFs(browser) {
|
||||||
const browserSession = await browser.newBrowserCDPSession();
|
const browserSession = await browser.newBrowserCDPSession();
|
||||||
const oopifs = [];
|
const oopifs = [];
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,15 @@ it('should send events', async function({page, server}) {
|
||||||
expect(events.length).toBe(1);
|
expect(events.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should only accept a page', async function({page}) {
|
it('should only accept a page or frame', async function({page}) {
|
||||||
// @ts-expect-error newCDPSession expects a Page
|
// @ts-expect-error newCDPSession expects a Page or Frame
|
||||||
const error = await page.context().newCDPSession(page.context()).catch(e => e);
|
const error = await page.context().newCDPSession(page.context()).catch(e => e);
|
||||||
expect(error.message).toContain('page: expected Page');
|
expect(error.message).toContain('page: expected Page or Frame');
|
||||||
|
|
||||||
|
// non-channelable types hit validation at a different layer
|
||||||
|
// @ts-expect-error newCDPSession expects a Page or Frame
|
||||||
|
const errorAlt = await page.context().newCDPSession({}).catch(e => e);
|
||||||
|
expect(errorAlt.message).toContain('page: expected Page or Frame');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should enable and disable domains independently', async function({page}) {
|
it('should enable and disable domains independently', async function({page}) {
|
||||||
|
|
@ -88,6 +93,26 @@ it('should throw nice errors', async function({page}) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should work with main frame', async function({page}) {
|
||||||
|
const client = await page.context().newCDPSession(page.mainFrame());
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
client.send('Runtime.enable'),
|
||||||
|
client.send('Runtime.evaluate', { expression: 'window.foo = "bar"' })
|
||||||
|
]);
|
||||||
|
const foo = await page.evaluate(() => window['foo']);
|
||||||
|
expect(foo).toBe('bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if target is part of main', async function({server, page}){
|
||||||
|
await page.goto(server.PREFIX + '/frames/one-frame.html');
|
||||||
|
expect(page.frames()[0].url()).toContain('/frames/one-frame.html');
|
||||||
|
expect(page.frames()[1].url()).toContain('/frames/frame.html');
|
||||||
|
|
||||||
|
const error = await page.context().newCDPSession(page.frames()[1]).catch(e => e);
|
||||||
|
expect(error.message).toContain(`This frame does not have a separate CDP session, it is a part of the parent frame's session`);
|
||||||
|
});
|
||||||
|
|
||||||
browserTest('should not break page.close()', async function({browser}) {
|
browserTest('should not break page.close()', async function({browser}) {
|
||||||
const context = await browser.newContext();
|
const context = await browser.newContext();
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
|
||||||
4
types/types.d.ts
vendored
4
types/types.d.ts
vendored
|
|
@ -5482,9 +5482,9 @@ export interface BrowserContext {
|
||||||
* > NOTE: CDP sessions are only supported on Chromium-based browsers.
|
* > NOTE: CDP sessions are only supported on Chromium-based browsers.
|
||||||
*
|
*
|
||||||
* Returns the newly created session.
|
* Returns the newly created session.
|
||||||
* @param page Page to create new session for.
|
* @param page Target to create new session for. For backwards-compatability, this parameter is named `page`, but it can be a `Page` or `Frame` type.
|
||||||
*/
|
*/
|
||||||
newCDPSession(page: Page): Promise<CDPSession>;
|
newCDPSession(page: Page|Frame): Promise<CDPSession>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new page in the browser context.
|
* Creates a new page in the browser context.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue