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)
* [class: Puppeteer](#class-puppeteer)
+ [puppeteer.connect(options)](#puppeteerconnectoptions)
+ [puppeteer.launch([options])](#puppeteerlaunchoptions)
* [class: Browser](#class-browser)
+ [browser.close()](#browserclose)
+ [browser.newPage()](#browsernewpage)
+ [browser.remoteDebuggingURL()](#browserremotedebuggingurl)
+ [browser.version()](#browserversion)
* [class: Page](#class-page)
+ [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])
- `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`.
@ -174,6 +184,10 @@ Closes browser with all the pages (if any were opened). The browser object itsel
#### browser.newPage()
- 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()
- 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();
}
/**
* @return {string}
*/
remoteDebuggingURL() {
return this._connection.url();
}
/**
* @return {!Promise<!Page>}
*/

View File

@ -21,16 +21,30 @@ const WebSocket = require('ws');
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 {number=} delay
*/
constructor(ws, delay) {
constructor(url, ws, delay = 0) {
super();
this._url = url;
this._lastId = 0;
/** @type {!Map<number, {resolve: function, reject: function, method: string}>}*/
this._callbacks = new Map();
this._delay = delay || 0;
this._delay = delay;
this._ws = ws;
this._ws.on('message', this._onMessage.bind(this));
@ -39,6 +53,13 @@ class Connection extends EventEmitter {
this._sessions = new Map();
}
/**
* @return {string}
*/
url() {
return this._url;
}
/**
* @param {string} method
* @param {!Object=} params
@ -114,20 +135,6 @@ class Connection extends EventEmitter {
this._sessions.set(sessionId, 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 {

View File

@ -87,40 +87,49 @@ class Launcher {
removeRecursive(userDataDir);
});
let {port, browserTargetId} = await waitForRemoteDebuggingPort(chromeProcess);
let remoteDebuggingURL = await waitForRemoteDebuggingURL(chromeProcess);
if (terminated)
throw new Error('Failed to launch chrome! ' + stderr);
// Failed to connect to browser.
if (port === -1) {
if (!remoteDebuggingURL) {
chromeProcess.kill();
throw new Error('Failed to connect to chrome!');
}
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());
}
/**
* @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
* @return {!Promise<number>}
* @return {!Promise<string>}
*/
function waitForRemoteDebuggingPort(chromeProcess) {
function waitForRemoteDebuggingURL(chromeProcess) {
return new Promise(fulfill => {
const rl = readline.createInterface({ input: chromeProcess.stderr });
rl.on('line', onLine);
rl.once('close', () => fulfill(-1));
rl.once('close', () => fulfill(''));
/**
* @param {string} line
*/
function onLine(line) {
const match = line.match(/^DevTools listening on .*:(\d+)(\/.*)$/);
const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
if (!match)
return;
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
* @return {!Promise<!Browser>}
*/
static async launch(options) {
static launch(options) {
return Launcher.launch(options);
}
/**
* @param {string} options
* @return {!Promise<!Browser>}
*/
static connect(options) {
return Launcher.connect(options);
}
}
module.exports = Puppeteer;

View File

@ -104,6 +104,15 @@ describe('Browser', function() {
await neverResolves;
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() {

View File

@ -87,6 +87,8 @@ class JSOutline {
args.push(new Documentation.Argument('...' + param.argument.name));
else if (param.type === 'Identifier')
args.push(new Documentation.Argument(param.name));
else if (param.type === 'ObjectPattern')
args.push(new Documentation.Argument('options'));
else
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] Method Foo.bar() fails to describe its parameters:
- Non-existing argument found: options
[MarkDown] Method Foo.constructor() fails to describe its parameters:
- Argument not found: arg3
- Non-existing argument found: arg2