diff --git a/docs/api.md b/docs/api.md index 9b16bec7209..9ac894563f6 100644 --- a/docs/api.md +++ b/docs/api.md @@ -54,6 +54,7 @@ + [page.setUserAgent(userAgent)](#pagesetuseragentuseragent) + [page.setViewport(viewport)](#pagesetviewportviewport) + [page.title()](#pagetitle) + + [page.tracing](#pagetracing) + [page.type(text, options)](#pagetypetext-options) + [page.uploadFile(selector, ...filePaths)](#pageuploadfileselector-filepaths) + [page.url()](#pageurl) @@ -71,6 +72,9 @@ + [mouse.down([options])](#mousedownoptions) + [mouse.move(x, y)](#mousemovex-y) + [mouse.up([options])](#mouseupoptions) + * [class: Tracing](#class-tracing) + + [tracing.start([options])](#tracingstartoptions) + + [tracing.stop(path)](#tracingstoppath) * [class: Dialog](#class-dialog) + [dialog.accept([promptText])](#dialogacceptprompttext) + [dialog.dismiss()](#dialogdismiss) @@ -638,6 +642,9 @@ In case of multiple pages in one browser, each page can have its own viewport si Shortcut for [page.mainFrame().title()](#frametitle). +#### page.tracing +- returns: <[Tracing]> + #### page.type(text, options) - `text` <[string]> A text to type into a focused element. - `options` <[Object]> @@ -827,6 +834,25 @@ Dispatches a `mousemove` event. Dispatches a `mouseup` event. +### class: Tracing + +You can use [`tracing.start`](#tracingstartoptions) and [`tracing.stop`](#tracingstoppath) to create a trace file which can be opened in Chrome DevTools or [timeline viewer](https://chromedevtools.github.io/timeline-viewer/). + +```js +await page.tracing.start(); +await page.navigate('https://www.google.com'); +await page.tracing.stop('trace.json'); +``` + +#### tracing.start([options]) +- `options` <[Object]> + - `screenshots` <[boolean]> captures screenshots in the trace. +- returns: <[Promise]> + +#### tracing.stop(path) +- `path` <[string]> A path to write the trace file to. +- returns: <[Promise]> + ### class: Dialog [Dialog] objects are dispatched by page via the ['dialog'](#event-dialog) event. @@ -1194,3 +1220,4 @@ If changed, the request url will be modified in a way that's not observable by p [Mouse]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-mouse "Mouse" [Map]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map "Map" [selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors "selector" +[Tracing]: https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-tracing "Tracing" diff --git a/lib/Page.js b/lib/Page.js index 8a98048af12..e344f4ea425 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -23,6 +23,7 @@ let Dialog = require('./Dialog'); let EmulationManager = require('./EmulationManager'); let FrameManager = require('./FrameManager'); let {Keyboard, Mouse} = require('./Input'); +let Tracing = require('./Tracing'); let helper = require('./helper'); class Page extends EventEmitter { @@ -61,6 +62,7 @@ class Page extends EventEmitter { this._frameManager = new FrameManager(client, this._mouse); this._networkManager = new NetworkManager(client); this._emulationManager = new EmulationManager(client); + this._tracing = new Tracing(client); /** @type {!Map} */ this._inPageCallbacks = new Map(); this._ignoreHTTPSErrors = ignoreHTTPSErrors; @@ -98,6 +100,13 @@ class Page extends EventEmitter { return this._keyboard; } + /** + * @return {!Tracing} + */ + get tracing() { + return this._tracing; + } + /** * @return {!Array} */ diff --git a/lib/Tracing.js b/lib/Tracing.js new file mode 100644 index 00000000000..9d55e75207d --- /dev/null +++ b/lib/Tracing.js @@ -0,0 +1,85 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const fs = require('fs'); +const helper = require('./helper'); + +class Tracing { + /** + * @param {!Connection} client + */ + constructor(client) { + this._client = client; + this._recording = false; + } + + /** + * @param {Object=} options + */ + async start(options = {}) { + console.assert(!this._recording, 'Cannot start recording trace while already recording trace.'); + + const categoriesArray = [ + '-*', 'devtools.timeline', 'v8.execute', 'disabled-by-default-devtools.timeline', + 'disabled-by-default-devtools.timeline.frame', 'toplevel', + 'blink.console', 'blink.user_timing', 'latencyInfo', 'disabled-by-default-devtools.timeline.stack', + 'disabled-by-default-v8.cpu_profiler' + ]; + + if (options.screenshots) + categoriesArray.push('disabled-by-default-devtools.screenshot'); + + this._recording = true; + await this._client.send('Tracing.start', { + transferMode: 'ReturnAsStream', + categories: categoriesArray.join(',') + }); + } + + /** + * @param {string} path + */ + async stop(path) { + let fulfill; + let contentPromise = new Promise(x => fulfill = x); + this._client.once('Tracing.tracingComplete', event => { + this._readStream(event.stream, path).then(fulfill); + }); + await this._client.send('Tracing.end'); + this._recording = false; + return contentPromise; + } + + /** + * @param {string} handle + * @param {string=} path + * @return {!Promise} + */ + async _readStream(handle, path) { + let eof = false; + let file = fs.openSync(path, 'w'); + while (!eof) { + let response = await this._client.send('IO.read', {handle}); + eof = response.eof; + if (path) + fs.writeSync(file, response.data); + } + fs.closeSync(file); + await this._client.send('IO.close', {handle}); + } +} +helper.tracePublicAPI(Tracing); + +module.exports = Tracing; diff --git a/test/test.js b/test/test.js index e5d8f64f1bf..17764c00dfb 100644 --- a/test/test.js +++ b/test/test.js @@ -1481,6 +1481,32 @@ describe('Page', function() { await Promise.all(pages.map(page => page.close())); })); }); + + describe('Tracing', function() { + let outputFile = path.join(__dirname, 'assets', 'trace.json'); + afterEach(function() { + fs.unlinkSync(outputFile); + }); + it('should output a trace', SX(async function() { + await page.tracing.start({screenshots: true}); + await page.navigate(PREFIX + '/grid.html'); + await page.tracing.stop(outputFile); + expect(fs.existsSync(outputFile)).toBe(true); + })); + it('should throw if tracing on two pages', SX(async function() { + await page.tracing.start(); + let newPage = await browser.newPage(); + let error = null; + try { + await newPage.tracing.start(); + } catch (e) { + error = e; + } + await newPage.close(); + expect(error).toBeTruthy(); + await page.tracing.stop(outputFile); + })); + }); }); if (process.env.COVERAGE) { diff --git a/utils/doclint/check_public_api/index.js b/utils/doclint/check_public_api/index.js index 3100cec91b3..96e17a35bf3 100644 --- a/utils/doclint/check_public_api/index.js +++ b/utils/doclint/check_public_api/index.js @@ -40,6 +40,7 @@ const EXCLUDE_METHODS = new Set([ 'InterceptedRequest.constructor', 'Keyboard.constructor', 'Mouse.constructor', + 'Tracing.constructor', 'Page.constructor', 'Page.create', 'Request.constructor',