diff --git a/docs/api.md b/docs/api.md index 1ef3459700f..32d2fbd001d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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`. diff --git a/lib/Browser.js b/lib/Browser.js index 5662cdec7cf..b0b8fd7bf64 100644 --- a/lib/Browser.js +++ b/lib/Browser.js @@ -30,6 +30,13 @@ class Browser { this._closeCallback = closeCallback || new Function(); } + /** + * @return {string} + */ + remoteDebuggingURL() { + return this._connection.url(); + } + /** * @return {!Promise} */ diff --git a/lib/Connection.js b/lib/Connection.js index 87dc3ea5aa3..8cd31e369a4 100644 --- a/lib/Connection.js +++ b/lib/Connection.js @@ -21,16 +21,30 @@ const WebSocket = require('ws'); class Connection extends EventEmitter { /** - * @param {number} port + * @param {string} url + * @param {number=} delay + * @return {!Promise} + */ + 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}*/ 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} - */ - 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 { diff --git a/lib/Launcher.js b/lib/Launcher.js index 1d4d29b3267..787a1d38f8b 100644 --- a/lib/Launcher.js +++ b/lib/Launcher.js @@ -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} + */ + static async connect({remoteDebuggingURL, ignoreHTTPSErrors = false}) { + let connection = await Connection.create(remoteDebuggingURL); + return new Browser(connection, !!ignoreHTTPSErrors); + } } /** * @param {!ChildProcess} chromeProcess - * @return {!Promise} + * @return {!Promise} */ -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]); } }); } diff --git a/lib/Puppeteer.js b/lib/Puppeteer.js index dbf8451913b..fab0a980bd6 100644 --- a/lib/Puppeteer.js +++ b/lib/Puppeteer.js @@ -21,9 +21,17 @@ class Puppeteer { * @param {!Object=} options * @return {!Promise} */ - static async launch(options) { + static launch(options) { return Launcher.launch(options); } + + /** + * @param {string} options + * @return {!Promise} + */ + static connect(options) { + return Launcher.connect(options); + } } module.exports = Puppeteer; diff --git a/test/test.js b/test/test.js index 2af7bdd961f..924c42a5fa1 100644 --- a/test/test.js +++ b/test/test.js @@ -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() { diff --git a/utils/doclint/check_public_api/JSBuilder.js b/utils/doclint/check_public_api/JSBuilder.js index c50376ea19a..28b95f7a06c 100644 --- a/utils/doclint/check_public_api/JSBuilder.js +++ b/utils/doclint/check_public_api/JSBuilder.js @@ -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)}`); } diff --git a/utils/doclint/check_public_api/test/golden/04-bad-arguments.txt b/utils/doclint/check_public_api/test/golden/04-bad-arguments.txt index d1ac10147df..07e6e28cfa0 100644 --- a/utils/doclint/check_public_api/test/golden/04-bad-arguments.txt +++ b/utils/doclint/check_public_api/test/golden/04-bad-arguments.txt @@ -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 \ No newline at end of file