feat: add page.createCDPSession method (#10515)

This commit is contained in:
Alex Rudenko 2023-07-21 14:03:52 +02:00 committed by GitHub
parent c5016cc670
commit d0c5b8e089
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 125 additions and 31 deletions

View File

@ -0,0 +1,19 @@
---
sidebar_label: Page.createCDPSession
---
# Page.createCDPSession() method
Creates a Chrome Devtools Protocol session attached to the page.
#### Signature:
```typescript
class Page {
createCDPSession(): Promise<CDPSession>;
}
```
**Returns:**
Promise&lt;[CDPSession](./puppeteer.cdpsession.md)&gt;

View File

@ -92,6 +92,7 @@ page.off('request', logRequest);
| [close(options)](./puppeteer.page.close.md) | | |
| [content()](./puppeteer.page.content.md) | | The full HTML contents of the page, including the DOCTYPE. |
| [cookies(urls)](./puppeteer.page.cookies.md) | | If no URLs are specified, this method returns cookies for the current page URL. If URLs are specified, only cookies for those URLs are returned. |
| [createCDPSession()](./puppeteer.page.createcdpsession.md) | | Creates a Chrome Devtools Protocol session attached to the page. |
| [createPDFStream(options)](./puppeteer.page.createpdfstream.md) | | Generates a PDF of the page with the <code>print</code> CSS media type. |
| [deleteCookie(cookies)](./puppeteer.page.deletecookie.md) | | |
| [emulate(device)](./puppeteer.page.emulate.md) | | <p>Emulates a given device's metrics and user agent.</p><p>To aid emulation, Puppeteer provides a list of known devices that can be via [KnownDevices](./puppeteer.knowndevices.md).</p> |

View File

@ -21,6 +21,7 @@ import {Protocol} from 'devtools-protocol';
import type {HTTPRequest} from '../api/HTTPRequest.js';
import type {HTTPResponse} from '../api/HTTPResponse.js';
import type {Accessibility} from '../common/Accessibility.js';
import type {CDPSession} from '../common/Connection.js';
import type {ConsoleMessage} from '../common/ConsoleMessage.js';
import type {Coverage} from '../common/Coverage.js';
import {Device} from '../common/Device.js';
@ -622,6 +623,13 @@ export class Page extends EventEmitter {
throw new Error('Not implemented');
}
/**
* Creates a Chrome Devtools Protocol session attached to the page.
*/
createCDPSession(): Promise<CDPSession> {
throw new Error('Not implemented');
}
/**
* {@inheritDoc Keyboard}
*/

View File

@ -862,6 +862,10 @@ export class CDPPage extends Page {
return result[0];
}
override async createCDPSession(): Promise<CDPSession> {
return await this.target().createCDPSession();
}
override async waitForRequest(
urlOrPredicate: string | ((req: HTTPRequest) => boolean | Promise<boolean>),
options: {timeout?: number} = {}

View File

@ -48,6 +48,9 @@ export class Browser extends BrowserBase {
'cdp.Runtime.executionContextsCleared',
// Tracing
'cdp.Tracing.tracingComplete',
// TODO: subscribe to all CDP events in the future.
'cdp.Network.requestWillBeSent',
'cdp.Debugger.scriptParsed',
];
#browserName = '';

View File

@ -5,7 +5,7 @@ import {WaitForOptions} from '../../api/Page.js';
import {assert} from '../../util/assert.js';
import {Deferred} from '../../util/Deferred.js';
import type {CDPSession, Connection as CDPConnection} from '../Connection.js';
import {ProtocolError, TimeoutError} from '../Errors.js';
import {ProtocolError, TargetCloseError, TimeoutError} from '../Errors.js';
import {EventEmitter} from '../EventEmitter.js';
import {PuppeteerLifeCycleEvent} from '../LifecycleWatcher.js';
import {TimeoutSettings} from '../TimeoutSettings.js';
@ -13,6 +13,7 @@ import {getPageContent, setPageContent, waitWithTimeout} from '../util.js';
import {Connection} from './Connection.js';
import {Realm} from './Realm.js';
import {debugError} from './utils.js';
/**
* @internal
@ -36,35 +37,53 @@ const lifeCycleToReadinessState = new Map<
['domcontentloaded', 'interactive'],
]);
/**
* @internal
*/
export const cdpSessions = new Map<string, CDPSessionWrapper>();
/**
* @internal
*/
export class CDPSessionWrapper extends EventEmitter implements CDPSession {
#context: BrowsingContext;
#sessionId = Deferred.create<string>();
#detached = false;
constructor(context: BrowsingContext) {
constructor(context: BrowsingContext, sessionId?: string) {
super();
this.#context = context;
context.connection
.send('cdp.getSession', {
context: context.id,
})
.then(session => {
this.#sessionId.resolve(session.result.session!);
})
.catch(err => {
this.#sessionId.reject(err);
});
if (sessionId) {
this.#sessionId.resolve(sessionId);
cdpSessions.set(sessionId, this);
} else {
context.connection
.send('cdp.getSession', {
context: context.id,
})
.then(session => {
this.#sessionId.resolve(session.result.session!);
cdpSessions.set(session.result.session!, this);
})
.catch(err => {
this.#sessionId.reject(err);
});
}
}
connection(): CDPConnection | undefined {
return undefined;
}
async send<T extends keyof ProtocolMapping.Commands>(
method: T,
...paramArgs: ProtocolMapping.Commands[T]['paramsType']
): Promise<ProtocolMapping.Commands[T]['returnType']> {
if (this.#detached) {
throw new TargetCloseError(
`Protocol error (${method}): Session closed. Most likely the page has been closed.`
);
}
const session = await this.#sessionId.valueOrThrow();
const result = await this.#context.connection.send('cdp.sendCommand', {
method: method,
@ -74,8 +93,12 @@ export class CDPSessionWrapper extends EventEmitter implements CDPSession {
return result.result;
}
detach(): Promise<void> {
throw new Error('Method not implemented.');
async detach(): Promise<void> {
cdpSessions.delete(this.id());
await this.#context.cdpSession.send('Target.detachFromTarget', {
sessionId: this.id(),
});
this.#detached = true;
}
id(): string {
@ -244,6 +267,7 @@ export class BrowsingContext extends Realm {
dispose(): void {
this.removeAllListeners();
this.connection.unregisterBrowsingContexts(this.#id);
void this.#cdpSession.detach().catch(debugError);
}
}

View File

@ -21,7 +21,7 @@ import {ConnectionTransport} from '../ConnectionTransport.js';
import {debug} from '../Debug.js';
import {EventEmitter} from '../EventEmitter.js';
import {BrowsingContext} from './BrowsingContext.js';
import {BrowsingContext, cdpSessions} from './BrowsingContext.js';
const debugProtocolSend = debug('puppeteer:webDriverBiDi:SEND ►');
const debugProtocolReceive = debug('puppeteer:webDriverBiDi:RECV ◀');
@ -235,15 +235,9 @@ export class Connection extends EventEmitter {
} else if ('source' in event.params && event.params.source.context) {
context = this.#browsingContexts.get(event.params.source.context);
} else if (isCDPEvent(event)) {
// TODO: this is not a good solution and we need to find a better one.
// Perhaps we need to have a dedicated CDP event emitter or emulate
// the CDPSession interface with BiDi?.
const cdpSessionId = event.params.session;
for (const context of this.#browsingContexts.values()) {
if (context.cdpSession?.id() === cdpSessionId) {
context.cdpSession!.emit(event.params.event, event.params.params);
}
}
cdpSessions
.get(event.params.session)
?.emit(event.params.event, event.params.params);
}
context?.emit(event.method, event.params);
}

View File

@ -30,6 +30,7 @@ import {
import {assert} from '../../util/assert.js';
import {Deferred} from '../../util/Deferred.js';
import {Accessibility} from '../Accessibility.js';
import {CDPSession} from '../Connection.js';
import {ConsoleMessage, ConsoleMessageLocation} from '../ConsoleMessage.js';
import {Coverage} from '../Coverage.js';
import {EmulationManager} from '../EmulationManager.js';
@ -52,7 +53,7 @@ import {
import {Browser} from './Browser.js';
import {BrowserContext} from './BrowserContext.js';
import {BrowsingContext} from './BrowsingContext.js';
import {BrowsingContext, CDPSessionWrapper} from './BrowsingContext.js';
import {Connection} from './Connection.js';
import {Frame} from './Frame.js';
import {HTTPRequest} from './HTTPRequest.js';
@ -632,6 +633,16 @@ export class Page extends PageBase {
override title(): Promise<string> {
return this.mainFrame().title();
}
override async createCDPSession(): Promise<CDPSession> {
const {sessionId} = await this.mainFrame()
.context()
.cdpSession.send('Target.attachToTarget', {
targetId: this.mainFrame()._id,
flatten: true,
});
return new CDPSessionWrapper(this.mainFrame().context(), sessionId);
}
}
function isConsoleLogEntry(

View File

@ -1667,18 +1667,48 @@
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[CDPSession.spec] Target.createCDPSession should be able to detach session",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[CDPSession.spec] Target.createCDPSession should enable and disable domains independently",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[CDPSession.spec] Target.createCDPSession should enable and disable domains independently",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[CDPSession.spec] Target.createCDPSession should send events",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL", "PASS"]
},
{
"testIdPattern": "[CDPSession.spec] Target.createCDPSession should send events",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[CDPSession.spec] Target.createCDPSession should throw nice errors",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[CDPSession.spec] Target.createCDPSession should work",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["chrome", "webDriverBiDi"],
"expectations": ["PASS"]
},
{
"testIdPattern": "[chromiumonly.spec] Chromium-Specific Launcher tests Puppeteer.launch |browserURL| option should be able to connect using browserUrl, with and without trailing slash",
"platforms": ["darwin", "linux", "win32"],

View File

@ -26,7 +26,7 @@ describe('Target.createCDPSession', function () {
it('should work', async () => {
const {page} = await getTestState();
const client = await page.target().createCDPSession();
const client = await page.createCDPSession();
await Promise.all([
client.send('Runtime.enable'),
@ -56,7 +56,7 @@ describe('Target.createCDPSession', function () {
it('should send events', async () => {
const {page, server} = await getTestState();
const client = await page.target().createCDPSession();
const client = await page.createCDPSession();
await client.send('Network.enable');
const events: unknown[] = [];
client.on('Network.requestWillBeSent', event => {
@ -71,7 +71,7 @@ describe('Target.createCDPSession', function () {
it('should enable and disable domains independently', async () => {
const {page} = await getTestState();
const client = await page.target().createCDPSession();
const client = await page.createCDPSession();
await client.send('Runtime.enable');
await client.send('Debugger.enable');
// JS coverage enables and then disables Debugger domain.
@ -88,7 +88,7 @@ describe('Target.createCDPSession', function () {
it('should be able to detach session', async () => {
const {page} = await getTestState();
const client = await page.target().createCDPSession();
const client = await page.createCDPSession();
await client.send('Runtime.enable');
const evalResponse = await client.send('Runtime.evaluate', {
expression: '1 + 2',
@ -112,7 +112,7 @@ describe('Target.createCDPSession', function () {
it('should throw nice errors', async () => {
const {page} = await getTestState();
const client = await page.target().createCDPSession();
const client = await page.createCDPSession();
const error = await theSourceOfTheProblems().catch(error => {
return error;
});
@ -130,7 +130,7 @@ describe('Target.createCDPSession', function () {
it('should expose the underlying connection', async () => {
const {page} = await getTestState();
const client = await page.target().createCDPSession();
const client = await page.createCDPSession();
expect(client.connection()).toBeTruthy();
});
});