Introduce Puppeteer.connect method (#264)

This patch:
- refactors Connection to use a single remote debugging URL instead of a
  pair of port and browserTargetId
- introduces Puppeteer.connect() method to attach to already running
  browser instance.

Fixes #238.
This commit is contained in:
Andrey Lushnikov 2017-08-15 14:29:42 -07:00 committed by GitHub
parent 96309a207c
commit a424f5613a
8 changed files with 82 additions and 29 deletions

View File

@ -6,10 +6,12 @@
- [Puppeteer](#puppeteer) - [Puppeteer](#puppeteer)
* [class: Puppeteer](#class-puppeteer) * [class: Puppeteer](#class-puppeteer)
+ [puppeteer.connect(options)](#puppeteerconnectoptions)
+ [puppeteer.launch([options])](#puppeteerlaunchoptions) + [puppeteer.launch([options])](#puppeteerlaunchoptions)
* [class: Browser](#class-browser) * [class: Browser](#class-browser)
+ [browser.close()](#browserclose) + [browser.close()](#browserclose)
+ [browser.newPage()](#browsernewpage) + [browser.newPage()](#browsernewpage)
+ [browser.remoteDebuggingURL()](#browserremotedebuggingurl)
+ [browser.version()](#browserversion) + [browser.version()](#browserversion)
* [class: Page](#class-page) * [class: Page](#class-page)
+ [event: 'console'](#event-console) + [event: 'console'](#event-console)
@ -135,6 +137,14 @@ puppeteer.launch().then(async browser => {
}); });
``` ```
#### puppeteer.connect(options)
- `options` <[Object]> Set of options to connect to the browser. Can have the following fields:
- `remoteDebuggingURL` <[string]> a remote debugging URL to connect to.
- `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
- returns: <[Promise]<[Browser]>> Promise which resolves to browser instance.
This method could be used to connect to already running browser instance.
#### puppeteer.launch([options]) #### puppeteer.launch([options])
- `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields: - `options` <[Object]> Set of configurable options to set on the browser. Can have the following fields:
- `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`. - `ignoreHTTPSErrors` <[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
@ -174,6 +184,10 @@ Closes browser with all the pages (if any were opened). The browser object itsel
#### browser.newPage() #### browser.newPage()
- returns: <[Promise]<[Page]>> Promise which resolves to a new [Page] object. - returns: <[Promise]<[Page]>> Promise which resolves to a new [Page] object.
#### browser.remoteDebuggingURL()
- returns: <[string]> A URL that could be used to start debugging this browser instance.
Remote debugging url could be used as an argument to the [puppeteer.connect](#puppeteerconnect).
#### browser.version() #### browser.version()
- returns: <[Promise]<[string]>> String describing browser version. For headless Chromium, this is similar to `HeadlessChrome/61.0.3153.0`. For non-headless, this is `Chrome/61.0.3153.0`. - returns: <[Promise]<[string]>> String describing browser version. For headless Chromium, this is similar to `HeadlessChrome/61.0.3153.0`. For non-headless, this is `Chrome/61.0.3153.0`.

View File

@ -30,6 +30,13 @@ class Browser {
this._closeCallback = closeCallback || new Function(); this._closeCallback = closeCallback || new Function();
} }
/**
* @return {string}
*/
remoteDebuggingURL() {
return this._connection.url();
}
/** /**
* @return {!Promise<!Page>} * @return {!Promise<!Page>}
*/ */

View File

@ -21,16 +21,30 @@ const WebSocket = require('ws');
class Connection extends EventEmitter { class Connection extends EventEmitter {
/** /**
* @param {number} port * @param {string} url
* @param {number=} delay
* @return {!Promise<!Connection>}
*/
static async create(url, delay = 0) {
return new Promise((resolve, reject) => {
let ws = new WebSocket(url, { perMessageDeflate: false });
ws.on('open', () => resolve(new Connection(url, ws, delay)));
ws.on('error', reject);
});
}
/**
* @param {string} url
* @param {!WebSocket} ws * @param {!WebSocket} ws
* @param {number=} delay * @param {number=} delay
*/ */
constructor(ws, delay) { constructor(url, ws, delay = 0) {
super(); super();
this._url = url;
this._lastId = 0; this._lastId = 0;
/** @type {!Map<number, {resolve: function, reject: function, method: string}>}*/ /** @type {!Map<number, {resolve: function, reject: function, method: string}>}*/
this._callbacks = new Map(); this._callbacks = new Map();
this._delay = delay || 0; this._delay = delay;
this._ws = ws; this._ws = ws;
this._ws.on('message', this._onMessage.bind(this)); this._ws.on('message', this._onMessage.bind(this));
@ -39,6 +53,13 @@ class Connection extends EventEmitter {
this._sessions = new Map(); this._sessions = new Map();
} }
/**
* @return {string}
*/
url() {
return this._url;
}
/** /**
* @param {string} method * @param {string} method
* @param {!Object=} params * @param {!Object=} params
@ -114,20 +135,6 @@ class Connection extends EventEmitter {
this._sessions.set(sessionId, session); this._sessions.set(sessionId, session);
return session; return session;
} }
/**
* @param {number} port
* @param {number=} delay
* @return {!Promise<!Connection>}
*/
static async create(port, targetId, delay) {
const url = `ws://localhost:${port}${targetId}`;
return new Promise((resolve, reject) => {
let ws = new WebSocket(url, { perMessageDeflate: false });
ws.on('open', () => resolve(new Connection(ws, delay)));
ws.on('error', reject);
});
}
} }
class Session extends EventEmitter { class Session extends EventEmitter {

View File

@ -87,40 +87,49 @@ class Launcher {
removeRecursive(userDataDir); removeRecursive(userDataDir);
}); });
let {port, browserTargetId} = await waitForRemoteDebuggingPort(chromeProcess); let remoteDebuggingURL = await waitForRemoteDebuggingURL(chromeProcess);
if (terminated) if (terminated)
throw new Error('Failed to launch chrome! ' + stderr); throw new Error('Failed to launch chrome! ' + stderr);
// Failed to connect to browser. // Failed to connect to browser.
if (port === -1) { if (!remoteDebuggingURL) {
chromeProcess.kill(); chromeProcess.kill();
throw new Error('Failed to connect to chrome!'); throw new Error('Failed to connect to chrome!');
} }
let connectionDelay = options.slowMo || 0; let connectionDelay = options.slowMo || 0;
let connection = await Connection.create(port, browserTargetId, connectionDelay); let connection = await Connection.create(remoteDebuggingURL, connectionDelay);
return new Browser(connection, !!options.ignoreHTTPSErrors, () => chromeProcess.kill()); return new Browser(connection, !!options.ignoreHTTPSErrors, () => chromeProcess.kill());
} }
/**
* @param {string} options
* @return {!Promise<!Browser>}
*/
static async connect({remoteDebuggingURL, ignoreHTTPSErrors = false}) {
let connection = await Connection.create(remoteDebuggingURL);
return new Browser(connection, !!ignoreHTTPSErrors);
}
} }
/** /**
* @param {!ChildProcess} chromeProcess * @param {!ChildProcess} chromeProcess
* @return {!Promise<number>} * @return {!Promise<string>}
*/ */
function waitForRemoteDebuggingPort(chromeProcess) { function waitForRemoteDebuggingURL(chromeProcess) {
return new Promise(fulfill => { return new Promise(fulfill => {
const rl = readline.createInterface({ input: chromeProcess.stderr }); const rl = readline.createInterface({ input: chromeProcess.stderr });
rl.on('line', onLine); rl.on('line', onLine);
rl.once('close', () => fulfill(-1)); rl.once('close', () => fulfill(''));
/** /**
* @param {string} line * @param {string} line
*/ */
function onLine(line) { function onLine(line) {
const match = line.match(/^DevTools listening on .*:(\d+)(\/.*)$/); const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
if (!match) if (!match)
return; return;
rl.removeListener('line', onLine); rl.removeListener('line', onLine);
fulfill({port: Number.parseInt(match[1], 10), browserTargetId: match[2]}); fulfill(match[1]);
} }
}); });
} }

View File

@ -21,9 +21,17 @@ class Puppeteer {
* @param {!Object=} options * @param {!Object=} options
* @return {!Promise<!Browser>} * @return {!Promise<!Browser>}
*/ */
static async launch(options) { static launch(options) {
return Launcher.launch(options); return Launcher.launch(options);
} }
/**
* @param {string} options
* @return {!Promise<!Browser>}
*/
static connect(options) {
return Launcher.connect(options);
}
} }
module.exports = Puppeteer; module.exports = Puppeteer;

View File

@ -104,6 +104,15 @@ describe('Browser', function() {
await neverResolves; await neverResolves;
expect(error.message).toContain('Protocol error'); expect(error.message).toContain('Protocol error');
})); }));
it('Puppeteer.connect', SX(async function() {
let originalBrowser = await puppeteer.launch(defaultBrowserOptions);
let browser = await puppeteer.connect({
remoteDebuggingURL: originalBrowser.remoteDebuggingURL()
});
let page = await browser.newPage();
expect(await page.evaluate(() => 7 * 8)).toBe(56);
originalBrowser.close();
}));
}); });
describe('Page', function() { describe('Page', function() {

View File

@ -87,6 +87,8 @@ class JSOutline {
args.push(new Documentation.Argument('...' + param.argument.name)); args.push(new Documentation.Argument('...' + param.argument.name));
else if (param.type === 'Identifier') else if (param.type === 'Identifier')
args.push(new Documentation.Argument(param.name)); args.push(new Documentation.Argument(param.name));
else if (param.type === 'ObjectPattern')
args.push(new Documentation.Argument('options'));
else else
this.errors.push(`JS Parsing issue: unsupported syntax to define parameter in ${this._currentClassName}.${methodName}(): ${this._extractText(param)}`); this.errors.push(`JS Parsing issue: unsupported syntax to define parameter in ${this._currentClassName}.${methodName}(): ${this._extractText(param)}`);
} }

View File

@ -1,7 +1,4 @@
[JavaScript] JS Parsing issue: unsupported syntax to define parameter in Foo.bar(): {visibility}
[MarkDown] Heading arguments for "foo.test(...files)" do not match described ones, i.e. "...files" != "...filePaths" [MarkDown] Heading arguments for "foo.test(...files)" do not match described ones, i.e. "...files" != "...filePaths"
[MarkDown] Method Foo.bar() fails to describe its parameters:
- Non-existing argument found: options
[MarkDown] Method Foo.constructor() fails to describe its parameters: [MarkDown] Method Foo.constructor() fails to describe its parameters:
- Argument not found: arg3 - Argument not found: arg3
- Non-existing argument found: arg2 - Non-existing argument found: arg2