mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-12 11:50:22 +03:00
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
@ -349,7 +349,7 @@ context.clear_permissions()
|
||||
```csharp
|
||||
var context = await browser.NewContextAsync();
|
||||
await context.GrantPermissionsAsync(new[] { "clipboard-read" });
|
||||
// Alternatively, you can use the helper class ContextPermissions
|
||||
// Alternatively, you can use the helper class ContextPermissions
|
||||
// to specify the permissions...
|
||||
// do stuff ...
|
||||
await context.ClearPermissionsAsync();
|
||||
@ -589,7 +589,7 @@ await page.SetContentAsync("<script>\n" +
|
||||
"<div>Or click me</div>\n");
|
||||
|
||||
await page.ClickAsync("div");
|
||||
// Note: it makes sense to await the result here, because otherwise, the context
|
||||
// Note: it makes sense to await the result here, because otherwise, the context
|
||||
// gets closed and the binding function will throw an exception.
|
||||
Assert.Equal("Click me", await result.Task);
|
||||
```
|
||||
@ -834,9 +834,10 @@ CDP sessions are only supported on Chromium-based browsers.
|
||||
Returns the newly created session.
|
||||
|
||||
### 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
|
||||
- returns: <[Page]>
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
import { Page, BindingCall } from './page';
|
||||
import { Frame } from './frame';
|
||||
import * as network from './network';
|
||||
import * as channels from '../protocol/channels';
|
||||
import fs from 'fs';
|
||||
@ -308,9 +309,12 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
||||
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) => {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
import { BrowserContext } from '../server/browserContext';
|
||||
import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher';
|
||||
import { PageDispatcher, BindingCallDispatcher, WorkerDispatcher } from './pageDispatcher';
|
||||
import { FrameDispatcher } from './frameDispatcher';
|
||||
import * as channels from '../protocol/channels';
|
||||
import { RouteDispatcher, RequestDispatcher, ResponseDispatcher } from './networkDispatchers';
|
||||
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> {
|
||||
if (!this._object._browser.options.isChromium)
|
||||
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;
|
||||
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> {
|
||||
|
@ -845,10 +845,12 @@ export type BrowserContextRecorderSupplementEnableOptions = {
|
||||
};
|
||||
export type BrowserContextRecorderSupplementEnableResult = void;
|
||||
export type BrowserContextNewCDPSessionParams = {
|
||||
page: PageChannel,
|
||||
page?: PageChannel,
|
||||
frame?: FrameChannel,
|
||||
};
|
||||
export type BrowserContextNewCDPSessionOptions = {
|
||||
|
||||
page?: PageChannel,
|
||||
frame?: FrameChannel,
|
||||
};
|
||||
export type BrowserContextNewCDPSessionResult = {
|
||||
session: CDPSessionChannel,
|
||||
|
@ -629,7 +629,8 @@ BrowserContext:
|
||||
|
||||
newCDPSession:
|
||||
parameters:
|
||||
page: Page
|
||||
page: Page?
|
||||
frame: Frame?
|
||||
returns:
|
||||
session: CDPSession
|
||||
|
||||
|
@ -406,7 +406,8 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||
outputFile: tOptional(tString),
|
||||
});
|
||||
scheme.BrowserContextNewCDPSessionParams = tObject({
|
||||
page: tChannel('Page'),
|
||||
page: tOptional(tChannel('Page')),
|
||||
frame: tOptional(tChannel('Frame')),
|
||||
});
|
||||
scheme.BrowserContextTracingStartParams = tObject({
|
||||
name: tOptional(tString),
|
||||
|
@ -20,6 +20,7 @@ import { assertBrowserContextIsNotOwned, BrowserContext, validateBrowserContextO
|
||||
import { assert } from '../../utils/utils';
|
||||
import * as network from '../network';
|
||||
import { Page, PageBinding, PageDelegate, Worker } from '../page';
|
||||
import { Frame } from '../frames';
|
||||
import { ConnectionTransport } from '../transport';
|
||||
import * as types from '../types';
|
||||
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);
|
||||
}
|
||||
|
||||
async newCDPSession(page: Page): Promise<CRSession> {
|
||||
if (!(page instanceof Page))
|
||||
throw new Error('page: expected Page');
|
||||
const targetId = (page._delegate as CRPage)._targetId;
|
||||
async newCDPSession(page: Page | Frame): Promise<CRSession> {
|
||||
let targetId: string | null = null;
|
||||
if (page instanceof Page) {
|
||||
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 { sessionId } = await rootSession.send('Target.attachToTarget', { targetId, flatten: true });
|
||||
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);
|
||||
});
|
||||
|
||||
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) {
|
||||
const browserSession = await browser.newBrowserCDPSession();
|
||||
const oopifs = [];
|
||||
|
@ -37,10 +37,15 @@ it('should send events', async function({page, server}) {
|
||||
expect(events.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should only accept a page', async function({page}) {
|
||||
// @ts-expect-error newCDPSession expects a Page
|
||||
it('should only accept a page or frame', async function({page}) {
|
||||
// @ts-expect-error newCDPSession expects a Page or Frame
|
||||
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}) {
|
||||
@ -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}) {
|
||||
const context = await browser.newContext();
|
||||
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.
|
||||
*
|
||||
* 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.
|
||||
|
Loading…
Reference in New Issue
Block a user