feat: expose raw devtools protocol connection (#1770)

feat: expose raw devtools protocol connection

This patch introduces `target.createCDPSession` method that
allows directly communicating with the target over the
Chrome DevTools Protocol.

Fixes #31.
This commit is contained in:
Andrey Lushnikov 2018-01-10 19:33:22 -08:00 committed by GitHub
parent ec8e40f1cb
commit 5368051610
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 162 additions and 51 deletions

View File

@ -87,6 +87,7 @@
* [page.setUserAgent(userAgent)](#pagesetuseragentuseragent)
* [page.setViewport(viewport)](#pagesetviewportviewport)
* [page.tap(selector)](#pagetapselector)
* [page.target()](#pagetarget)
* [page.title()](#pagetitle)
* [page.touchscreen](#pagetouchscreen)
* [page.tracing](#pagetracing)
@ -198,9 +199,13 @@
* [response.text()](#responsetext)
* [response.url()](#responseurl)
- [class: Target](#class-target)
* [target.createCDPSession()](#targetcreatecdpsession)
* [target.page()](#targetpage)
* [target.type()](#targettype)
* [target.url()](#targeturl)
- [class: CDPSession](#class-cdpsession)
* [cdpSession.detach()](#cdpsessiondetach)
* [cdpSession.send(method[, params])](#cdpsessionsendmethod-params)
- [class: Coverage](#class-coverage)
* [coverage.startCSSCoverage(options)](#coveragestartcsscoverageoptions)
* [coverage.startJSCoverage(options)](#coveragestartjscoverageoptions)
@ -1139,6 +1144,9 @@ In the case of multiple pages in a single browser, each page can have its own vi
This method fetches an element with `selector`, scrolls it into view if needed, and then uses [page.touchscreen](#pagetouchscreen) to tap in the center of the element.
If there's no element matching `selector`, the method throws an error.
#### page.target()
- returns: <[Target]> a target this page was created from.
#### page.title()
- returns: <[Promise]<[string]>> Returns page's title.
@ -2166,6 +2174,11 @@ Contains the URL of the response.
### class: Target
#### target.createCDPSession()
- returns: <[Promise]<[CDPSession]>>
Creates a Chrome Devtools Protocol session attached to the target.
#### target.page()
- returns: <[Promise]<?[Page]>>
@ -2179,6 +2192,40 @@ Identifies what kind of target this is. Can be `"page"`, `"service_worker"`, or
#### target.url()
- returns: <[string]>
### class: CDPSession
* extends: [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter)
The `CDPSession` instances are used to talk raw Chrome Devtools Protocol:
- protocol methods can be called with `session.send` method.
- protocol events can be subscribed to with `session.on` method.
Documentation on DevTools Protocol can be found here: [DevTools Protocol Viewer](https://chromedevtools.github.io/devtools-protocol/).
```js
const client = await page.target().createCDPSession();
await client.send('Animation.enable');
await client.on('Animation.animationCreated', () => console.log('Animation created!'));
const response = await client.send('Animation.getPlaybackRate');
console.log('playback rate is ' + response.playbackRate);
await client.send('Animation.setPlaybackRate', {
playbackRate: response.playbackRate / 2
});
```
#### cdpSession.detach()
- returns: <[Promise]>
Detaches session from target. Once detached, session won't emit any events and can't be used
to send messages.
#### cdpSession.send(method[, params])
- `method` <[string]> protocol method name
- `params` <[Object]> Optional method parameters
- returns: <[Promise]<[Object]>>
### class: Coverage
Coverage gathers information about parts of JavaScript and CSS that were used by the page.
@ -2253,6 +2300,7 @@ reported.
[Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise "Promise"
[string]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type "String"
[stream.Readable]: https://nodejs.org/api/stream.html#stream_class_stream_readable "stream.Readable"
[CDPSession]: #class-cdpsession "CDPSession"
[Error]: https://nodejs.org/api/errors.html#errors_class_error "Error"
[Frame]: #class-frame "Frame"
[ConsoleMessage]: #class-consolemessage "ConsoleMessage"

View File

@ -193,6 +193,7 @@ class Target {
*/
constructor(browser, targetInfo) {
this._browser = browser;
this._targetId = targetInfo.targetId;
this._targetInfo = targetInfo;
/** @type {?Promise<!Page>} */
this._pagePromise = null;
@ -202,13 +203,20 @@ class Target {
this._initializedCallback(true);
}
/**
* @return {!Promise<!Puppeteer.CDPSession>}
*/
createCDPSession() {
return this._browser._connection.createSession(this._targetId);
}
/**
* @return {!Promise<?Page>}
*/
async page() {
if (this._targetInfo.type === 'page' && !this._pagePromise) {
this._pagePromise = this._browser._connection.createSession(this._targetInfo.targetId)
.then(client => Page.create(client, this._browser._ignoreHTTPSErrors, this._browser._appMode, this._browser._screenshotTaskQueue));
this._pagePromise = this._browser._connection.createSession(this._targetId)
.then(client => Page.create(client, this, this._browser._ignoreHTTPSErrors, this._browser._appMode, this._browser._screenshotTaskQueue));
}
return this._pagePromise;
}
@ -258,4 +266,4 @@ helper.tracePublicAPI(Target);
* @property {boolean} attached
*/
module.exports = { Browser, TaskQueue };
module.exports = { Browser, TaskQueue, Target };

View File

@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const {helper} = require('./helper');
const debugProtocol = require('debug')('puppeteer:protocol');
const debugSession = require('debug')('puppeteer:session');
@ -49,7 +50,7 @@ class Connection extends EventEmitter {
this._ws = ws;
this._ws.on('message', this._onMessage.bind(this));
this._ws.on('close', this._onClose.bind(this));
/** @type {!Map<string, !Session>}*/
/** @type {!Map<string, !CDPSession>}*/
this._sessions = new Map();
}
@ -135,17 +136,17 @@ class Connection extends EventEmitter {
/**
* @param {string} targetId
* @return {!Promise<!Session>}
* @return {!Promise<!CDPSession>}
*/
async createSession(targetId) {
const {sessionId} = await this.send('Target.attachToTarget', {targetId});
const session = new Session(this, targetId, sessionId);
const session = new CDPSession(this, targetId, sessionId);
this._sessions.set(sessionId, session);
return session;
}
}
class Session extends EventEmitter {
class CDPSession extends EventEmitter {
/**
* @param {!Connection} connection
* @param {string} targetId
@ -161,13 +162,6 @@ class Session extends EventEmitter {
this._sessionId = sessionId;
}
/**
* @return {string}
*/
targetId() {
return this._targetId;
}
/**
* @param {string} method
* @param {!Object=} params
@ -211,9 +205,8 @@ class Session extends EventEmitter {
}
}
async dispose() {
console.assert(!!this._connection, 'Protocol error: Connection closed. Most likely the page has been closed.');
await this._connection.send('Target.closeTarget', {targetId: this._targetId});
async detach() {
await this._connection.send('Target.detachFromTarget', {sessionId: this._sessionId});
}
_onClosed() {
@ -223,6 +216,7 @@ class Session extends EventEmitter {
this._connection = null;
}
}
helper.tracePublicAPI(CDPSession);
/**
* @param {!Error} error
@ -234,4 +228,4 @@ function rewriteError(error, message) {
return error;
}
module.exports = {Connection, Session};
module.exports = {Connection, CDPSession};

View File

@ -25,7 +25,7 @@ const {helper, debugError} = require('./helper');
class Coverage {
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
*/
constructor(client) {
this._jsCoverage = new JSCoverage(client);
@ -66,7 +66,7 @@ helper.tracePublicAPI(Coverage);
class JSCoverage {
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
*/
constructor(client) {
this._client = client;
@ -154,7 +154,7 @@ class JSCoverage {
class CSSCoverage {
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
*/
constructor(client) {
this._client = client;

View File

@ -18,7 +18,7 @@ const {helper} = require('./helper');
class Dialog {
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
* @param {string} type
* @param {string} message
* @param {(string|undefined)} defaultValue

View File

@ -20,7 +20,7 @@ const {helper, debugError} = require('./helper');
class ElementHandle extends JSHandle {
/**
* @param {!Puppeteer.ExecutionContext} context
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
* @param {!Object} remoteObject
* @param {!Puppeteer.Page} page
*/

View File

@ -16,7 +16,7 @@
class EmulationManager {
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
*/
constructor(client) {
this._client = client;

View File

@ -18,7 +18,7 @@ const {helper} = require('./helper');
class ExecutionContext {
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
* @param {!Object} contextPayload
* @param {function(*):!JSHandle} objectHandleFactory
*/
@ -117,7 +117,7 @@ class ExecutionContext {
class JSHandle {
/**
* @param {!ExecutionContext} context
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
* @param {!Object} remoteObject
*/
constructor(context, client, remoteObject) {

View File

@ -24,7 +24,7 @@ const readFileAsync = helper.promisify(fs.readFile);
class FrameManager extends EventEmitter {
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
* @param {{frame: Object, childFrames: ?Array}} frameTree
* @param {!Puppeteer.Page} page
*/
@ -226,7 +226,7 @@ FrameManager.Events = {
*/
class Frame {
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
* @param {?Frame} parentFrame
* @param {string} frameId
*/

View File

@ -28,7 +28,7 @@ const keyDefinitions = require('./USKeyboardLayout');
class Keyboard {
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
*/
constructor(client) {
this._client = client;
@ -189,7 +189,7 @@ class Keyboard {
class Mouse {
/**
* @param {Puppeteer.Session} client
* @param {Puppeteer.CDPSession} client
* @param {!Keyboard} keyboard
*/
constructor(client, keyboard) {
@ -268,7 +268,7 @@ class Mouse {
class Touchscreen {
/**
* @param {Puppeteer.Session} client
* @param {Puppeteer.CDPSession} client
* @param {Keyboard} keyboard
*/
constructor(client, keyboard) {

View File

@ -19,7 +19,7 @@ const Multimap = require('./Multimap');
class NetworkManager extends EventEmitter {
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
* @param {!Puppeteer.FrameManager} frameManager
*/
constructor(client, frameManager) {
@ -281,7 +281,7 @@ class NetworkManager extends EventEmitter {
class Request {
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
* @param {?string} requestId
* @param {string} interceptionId
* @param {boolean} allowInterception
@ -479,7 +479,7 @@ helper.tracePublicAPI(Request);
class Response {
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
* @param {!Request} request
* @param {number} status
* @param {!Object} headers

View File

@ -31,17 +31,18 @@ const writeFileAsync = helper.promisify(fs.writeFile);
class Page extends EventEmitter {
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
* @param {!Puppeteer.Target} target
* @param {boolean} ignoreHTTPSErrors
* @param {boolean} appMode
* @param {!Puppeteer.TaskQueue} screenshotTaskQueue
* @return {!Promise<!Page>}
*/
static async create(client, ignoreHTTPSErrors, appMode, screenshotTaskQueue) {
static async create(client, target, ignoreHTTPSErrors, appMode, screenshotTaskQueue) {
await client.send('Page.enable');
const {frameTree} = await client.send('Page.getFrameTree');
const page = new Page(client, frameTree, ignoreHTTPSErrors, screenshotTaskQueue);
const page = new Page(client, target, frameTree, ignoreHTTPSErrors, screenshotTaskQueue);
await Promise.all([
client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
@ -60,14 +61,16 @@ class Page extends EventEmitter {
}
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
* @param {!Puppeteer.Target} target
* @param {{frame: Object, childFrames: ?Array}} frameTree
* @param {boolean} ignoreHTTPSErrors
* @param {!Puppeteer.TaskQueue} screenshotTaskQueue
*/
constructor(client, frameTree, ignoreHTTPSErrors, screenshotTaskQueue) {
constructor(client, target, frameTree, ignoreHTTPSErrors, screenshotTaskQueue) {
super();
this._client = client;
this._target = target;
this._keyboard = new Keyboard(client);
this._mouse = new Mouse(client, this._keyboard);
this._touchscreen = new Touchscreen(client, this._keyboard);
@ -101,6 +104,13 @@ class Page extends EventEmitter {
client.on('Performance.metrics', event => this._emitMetrics(event));
}
/**
* @return {!Puppeteer.Target}
*/
target() {
return this._target;
}
_onTargetCrashed() {
this.emit('error', new Error('Page crashed!'));
}
@ -505,7 +515,7 @@ class Page extends EventEmitter {
return request ? request.response() : null;
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
* @param {string} url
* @param {string} referrer
* @return {!Promise<?Error>}
@ -691,7 +701,7 @@ class Page extends EventEmitter {
* @return {!Promise<!Buffer>}
*/
async _screenshotTask(format, options) {
await this._client.send('Target.activateTarget', {targetId: this._client.targetId()});
await this._client.send('Target.activateTarget', {targetId: this._target._targetId});
let clip = options.clip ? Object.assign({}, options['clip']) : undefined;
if (clip)
clip.scale = 1;
@ -785,7 +795,8 @@ class Page extends EventEmitter {
}
async close() {
await this._client.dispose();
console.assert(!!this._client._connection, 'Protocol error: Connection closed. Most likely the page has been closed.');
await this._client._connection.send('Target.closeTarget', {targetId: this._target._targetId});
}
/**

View File

@ -22,7 +22,7 @@ const closeAsync = helper.promisify(fs.close);
class Tracing {
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
*/
constructor(client) {
this._client = client;

7
lib/externs.d.ts vendored
View File

@ -1,5 +1,5 @@
import { Connection as RealConnection, Session as RealSession } from './Connection.js';
import {Browser as RealBrowser, TaskQueue as RealTaskQueue} from './Browser.js';
import { Connection as RealConnection, CDPSession as RealCDPSession } from './Connection.js';
import {Browser as RealBrowser, TaskQueue as RealTaskQueue, Target as RealTarget} from './Browser.js';
import * as RealPage from './Page.js';
import {Mouse as RealMouse, Keyboard as RealKeyboard, Touchscreen as RealTouchscreen} from './Input.js';
import {Frame as RealFrame, FrameManager as RealFrameManager} from './FrameManager.js';
@ -10,12 +10,13 @@ import * as child_process from 'child_process';
export as namespace Puppeteer;
export class Connection extends RealConnection {}
export class Session extends RealSession {}
export class CDPSession extends RealCDPSession {}
export class Mouse extends RealMouse {}
export class Keyboard extends RealKeyboard {}
export class Touchscreen extends RealTouchscreen {}
export class TaskQueue extends RealTaskQueue {}
export class Browser extends RealBrowser {}
export class Target extends RealTarget {}
export class Frame extends RealFrame {}
export class FrameManager extends RealFrameManager {}
export class NetworkManager extends RealNetworkManager {}

View File

@ -83,7 +83,7 @@ class Helper {
}
/**
* @param {!Puppeteer.Session} client
* @param {!Puppeteer.CDPSession} client
* @param {!Object} remoteObject
*/
static async releaseObject(client, remoteObject) {

View File

@ -3474,6 +3474,56 @@ describe('Page', function() {
});
});
describe('Target.createCDPSession', function() {
it('should work', async function({page, server}) {
const client = await page.target().createCDPSession();
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 send events', async function({page, server}) {
const client = await page.target().createCDPSession();
await client.send('Network.enable');
const events = [];
client.on('Network.requestWillBeSent', event => events.push(event));
await page.goto(server.EMPTY_PAGE);
expect(events.length).toBe(1);
});
it('should enable and disable domains independently', async function({page, server}) {
const client = await page.target().createCDPSession();
await client.send('Runtime.enable');
await client.send('Debugger.enable');
// JS coverage enables and then disables Debugger domain.
await page.coverage.startJSCoverage();
await page.coverage.stopJSCoverage();
// generate a script in page and wait for the event.
const [event] = await Promise.all([
waitForEvents(client, 'Debugger.scriptParsed'),
page.evaluate('//# sourceURL=foo.js')
]);
// expect events to be dispatched.
expect(event.url).toBe('foo.js');
});
it('should be able to detach session', async function({page, server}) {
const client = await page.target().createCDPSession();
await client.send('Runtime.enable');
const evalResponse = await client.send('Runtime.evaluate', {expression: '1 + 2', returnByValue: true});
expect(evalResponse.result.value).toBe(3);
await client.detach();
let error = null;
try {
await client.send('Runtime.evaluate', {expression: '3 + 1', returnByValue: true});
} catch (e) {
error = e;
}
expect(error.message).toContain('Session closed.');
});
});
describe('JSCoverage', function() {
it('should work', async function({page, server}) {
await page.coverage.startJSCoverage();
@ -3656,7 +3706,7 @@ runner.run();
* @param {!EventEmitter} emitter
* @param {string} eventName
* @param {number=} eventCount
* @return {!Promise}
* @return {!Promise<!Object>}
*/
function waitForEvents(emitter, eventName, eventCount = 1) {
let fulfill;
@ -3664,12 +3714,12 @@ function waitForEvents(emitter, eventName, eventCount = 1) {
emitter.on(eventName, onEvent);
return promise;
function onEvent() {
function onEvent(event) {
--eventCount;
if (eventCount)
return;
emitter.removeListener(eventName, onEvent);
fulfill();
fulfill(event);
}
}

View File

@ -31,7 +31,6 @@ const EXCLUDE_CLASSES = new Set([
'Multimap',
'NavigatorWatcher',
'NetworkManager',
'Session',
'TaskQueue',
'WaitTask',
]);