Get rid of page.emulate() / page.emulatedDevices() methods

This patch:
- gets rid of `page.emulate` and `page.emulatedDevices`
  methods. Instead, it is suggested to use `page.setViewport()`
  and `page.setUserAgent()` methods.
- moves DeviceDescriptors to the top level of the puppeteer so that
  it is convenient to require them.
- improves on documentation to describe the suggested emulation
  approach.

References #88.
This commit is contained in:
Andrey Lushnikov 2017-07-20 18:14:43 -07:00
parent 76ac3bded5
commit 139b9e9b6d
5 changed files with 197 additions and 199 deletions

View File

@ -4,124 +4,166 @@
<!-- toc --> <!-- toc -->
- [class: Browser](#class-browser) - [Puppeteer](#puppeteer)
* [new Browser([options])](#new-browseroptions) * [Emulation](#emulation)
* [browser.close()](#browserclose) * [class: Browser](#class-browser)
* [browser.closePage(page)](#browserclosepagepage) + [new Browser([options])](#new-browseroptions)
* [browser.newPage()](#browsernewpage) + [browser.close()](#browserclose)
* [browser.stderr](#browserstderr) + [browser.closePage(page)](#browserclosepagepage)
* [browser.stdout](#browserstdout) + [browser.newPage()](#browsernewpage)
* [browser.version()](#browserversion) + [browser.stderr](#browserstderr)
- [class: Page](#class-page) + [browser.stdout](#browserstdout)
* [event: 'console'](#event-console) + [browser.version()](#browserversion)
* [event: 'dialog'](#event-dialog) * [class: Page](#class-page)
* [event: 'frameattached'](#event-frameattached) + [event: 'console'](#event-console)
* [event: 'framedetached'](#event-framedetached) + [event: 'dialog'](#event-dialog)
* [event: 'framenavigated'](#event-framenavigated) + [event: 'frameattached'](#event-frameattached)
* [event: 'load'](#event-load) + [event: 'framedetached'](#event-framedetached)
* [event: 'pageerror'](#event-pageerror) + [event: 'framenavigated'](#event-framenavigated)
* [event: 'request'](#event-request) + [event: 'load'](#event-load)
* [event: 'requestfailed'](#event-requestfailed) + [event: 'pageerror'](#event-pageerror)
* [event: 'requestfinished'](#event-requestfinished) + [event: 'request'](#event-request)
* [event: 'response'](#event-response) + [event: 'requestfailed'](#event-requestfailed)
* [page.$(selector, pageFunction, ...args)](#pageselector-pagefunction-args) + [event: 'requestfinished'](#event-requestfinished)
* [page.$$(selector, pageFunction, ...args)](#pageselector-pagefunction-args) + [event: 'response'](#event-response)
* [page.addScriptTag(url)](#pageaddscripttagurl) + [page.$(selector, pageFunction, ...args)](#pageselector-pagefunction-args)
* [page.click(selector)](#pageclickselector) + [page.$$(selector, pageFunction, ...args)](#pageselector-pagefunction-args)
* [page.close()](#pageclose) + [page.addScriptTag(url)](#pageaddscripttagurl)
* [page.emulate(name)](#pageemulatename) + [page.click(selector)](#pageclickselector)
* [page.emulatedDevices()](#pageemulateddevices) + [page.close()](#pageclose)
* [page.evaluate(pageFunction, ...args)](#pageevaluatepagefunction-args) + [page.evaluate(pageFunction, ...args)](#pageevaluatepagefunction-args)
* [page.evaluateOnInitialized(pageFunction, ...args)](#pageevaluateoninitializedpagefunction-args) + [page.evaluateOnInitialized(pageFunction, ...args)](#pageevaluateoninitializedpagefunction-args)
* [page.focus(selector)](#pagefocusselector) + [page.focus(selector)](#pagefocusselector)
* [page.frames()](#pageframes) + [page.frames()](#pageframes)
* [page.goBack(options)](#pagegobackoptions) + [page.goBack(options)](#pagegobackoptions)
* [page.goForward(options)](#pagegoforwardoptions) + [page.goForward(options)](#pagegoforwardoptions)
* [page.httpHeaders()](#pagehttpheaders) + [page.httpHeaders()](#pagehttpheaders)
* [page.injectFile(filePath)](#pageinjectfilefilepath) + [page.injectFile(filePath)](#pageinjectfilefilepath)
* [page.keyboard](#pagekeyboard) + [page.keyboard](#pagekeyboard)
* [page.mainFrame()](#pagemainframe) + [page.mainFrame()](#pagemainframe)
* [page.navigate(url, options)](#pagenavigateurl-options) + [page.navigate(url, options)](#pagenavigateurl-options)
* [page.pdf(options)](#pagepdfoptions) + [page.pdf(options)](#pagepdfoptions)
* [page.plainText()](#pageplaintext) + [page.plainText()](#pageplaintext)
* [page.press(key[, options])](#pagepresskey-options) + [page.press(key[, options])](#pagepresskey-options)
* [page.reload(options)](#pagereloadoptions) + [page.reload(options)](#pagereloadoptions)
* [page.screenshot([options])](#pagescreenshotoptions) + [page.screenshot([options])](#pagescreenshotoptions)
* [page.setContent(html)](#pagesetcontenthtml) + [page.setContent(html)](#pagesetcontenthtml)
* [page.setHTTPHeaders(headers)](#pagesethttpheadersheaders) + [page.setHTTPHeaders(headers)](#pagesethttpheadersheaders)
* [page.setInPageCallback(name, callback)](#pagesetinpagecallbackname-callback) + [page.setInPageCallback(name, callback)](#pagesetinpagecallbackname-callback)
* [page.setRequestInterceptor(interceptor)](#pagesetrequestinterceptorinterceptor) + [page.setRequestInterceptor(interceptor)](#pagesetrequestinterceptorinterceptor)
* [page.setUserAgent(userAgent)](#pagesetuseragentuseragent) + [page.setUserAgent(userAgent)](#pagesetuseragentuseragent)
* [page.setViewport(viewport)](#pagesetviewportviewport) + [page.setViewport(viewport)](#pagesetviewportviewport)
* [page.title()](#pagetitle) + [page.title()](#pagetitle)
* [page.type(text)](#pagetypetext) + [page.type(text)](#pagetypetext)
* [page.uploadFile(selector, ...filePaths)](#pageuploadfileselector-filepaths) + [page.uploadFile(selector, ...filePaths)](#pageuploadfileselector-filepaths)
* [page.url()](#pageurl) + [page.url()](#pageurl)
* [page.userAgent()](#pageuseragent) + [page.userAgent()](#pageuseragent)
* [page.viewport()](#pageviewport) + [page.viewport()](#pageviewport)
* [page.waitFor(selector)](#pagewaitforselector) + [page.waitFor(selector)](#pagewaitforselector)
* [page.waitForNavigation(options)](#pagewaitfornavigationoptions) + [page.waitForNavigation(options)](#pagewaitfornavigationoptions)
- [class: Keyboard](#class-keyboard) * [class: Keyboard](#class-keyboard)
* [keyboard.down(key[, options])](#keyboarddownkey-options) + [keyboard.down(key[, options])](#keyboarddownkey-options)
* [keyboard.modifiers()](#keyboardmodifiers) + [keyboard.modifiers()](#keyboardmodifiers)
* [keyboard.press(key[, options])](#keyboardpresskey-options) + [keyboard.press(key[, options])](#keyboardpresskey-options)
* [keyboard.sendCharacter(char)](#keyboardsendcharacterchar) + [keyboard.sendCharacter(char)](#keyboardsendcharacterchar)
* [keyboard.type(text)](#keyboardtypetext) + [keyboard.type(text)](#keyboardtypetext)
* [keyboard.up(key)](#keyboardupkey) + [keyboard.up(key)](#keyboardupkey)
- [class: Dialog](#class-dialog) * [class: Dialog](#class-dialog)
* [dialog.accept([promptText])](#dialogacceptprompttext) + [dialog.accept([promptText])](#dialogacceptprompttext)
* [dialog.dismiss()](#dialogdismiss) + [dialog.dismiss()](#dialogdismiss)
* [dialog.message()](#dialogmessage) + [dialog.message()](#dialogmessage)
* [dialog.type](#dialogtype) + [dialog.type](#dialogtype)
- [class: Frame](#class-frame) * [class: Frame](#class-frame)
* [frame.$(selector, pageFunction, ...args)](#frameselector-pagefunction-args) + [frame.$(selector, pageFunction, ...args)](#frameselector-pagefunction-args)
* [frame.$$(selector, pageFunction, ...args)](#frameselector-pagefunction-args) + [frame.$$(selector, pageFunction, ...args)](#frameselector-pagefunction-args)
* [frame.childFrames()](#framechildframes) + [frame.childFrames()](#framechildframes)
* [frame.evaluate(pageFunction, ...args)](#frameevaluatepagefunction-args) + [frame.evaluate(pageFunction, ...args)](#frameevaluatepagefunction-args)
* [frame.isDetached()](#frameisdetached) + [frame.isDetached()](#frameisdetached)
* [frame.isMainFrame()](#frameismainframe) + [frame.isMainFrame()](#frameismainframe)
* [frame.name()](#framename) + [frame.name()](#framename)
* [frame.parentFrame()](#frameparentframe) + [frame.parentFrame()](#frameparentframe)
* [frame.url()](#frameurl) + [frame.url()](#frameurl)
* [frame.waitFor(selector)](#framewaitforselector) + [frame.waitFor(selector)](#framewaitforselector)
- [class: Request](#class-request) * [class: Request](#class-request)
* [request.headers](#requestheaders) + [request.headers](#requestheaders)
* [request.method](#requestmethod) + [request.method](#requestmethod)
* [request.response()](#requestresponse) + [request.response()](#requestresponse)
* [request.url](#requesturl) + [request.url](#requesturl)
- [class: Response](#class-response) * [class: Response](#class-response)
* [response.headers](#responseheaders) + [response.headers](#responseheaders)
* [response.ok](#responseok) + [response.ok](#responseok)
* [response.request()](#responserequest) + [response.request()](#responserequest)
* [response.status](#responsestatus) + [response.status](#responsestatus)
* [response.statusText](#responsestatustext) + [response.statusText](#responsestatustext)
* [response.url](#responseurl) + [response.url](#responseurl)
- [class: InterceptedRequest](#class-interceptedrequest) * [class: InterceptedRequest](#class-interceptedrequest)
* [interceptedRequest.abort()](#interceptedrequestabort) + [interceptedRequest.abort()](#interceptedrequestabort)
* [interceptedRequest.continue()](#interceptedrequestcontinue) + [interceptedRequest.continue()](#interceptedrequestcontinue)
* [interceptedRequest.headers](#interceptedrequestheaders) + [interceptedRequest.headers](#interceptedrequestheaders)
* [interceptedRequest.isHandled()](#interceptedrequestishandled) + [interceptedRequest.isHandled()](#interceptedrequestishandled)
* [interceptedRequest.method](#interceptedrequestmethod) + [interceptedRequest.method](#interceptedrequestmethod)
* [interceptedRequest.postData](#interceptedrequestpostdata) + [interceptedRequest.postData](#interceptedrequestpostdata)
* [interceptedRequest.url](#interceptedrequesturl) + [interceptedRequest.url](#interceptedrequesturl)
- [class: Headers](#class-headers) * [class: Headers](#class-headers)
* [headers.append(name, value)](#headersappendname-value) + [headers.append(name, value)](#headersappendname-value)
* [headers.delete(name)](#headersdeletename) + [headers.delete(name)](#headersdeletename)
* [headers.entries()](#headersentries) + [headers.entries()](#headersentries)
* [headers.get(name)](#headersgetname) + [headers.get(name)](#headersgetname)
* [headers.has(name)](#headershasname) + [headers.has(name)](#headershasname)
* [headers.keys()](#headerskeys) + [headers.keys()](#headerskeys)
* [headers.set(name, value)](#headerssetname-value) + [headers.set(name, value)](#headerssetname-value)
* [headers.values()](#headersvalues) + [headers.values()](#headersvalues)
- [class: Body](#class-body) * [class: Body](#class-body)
* [body.arrayBuffer()](#bodyarraybuffer) + [body.arrayBuffer()](#bodyarraybuffer)
* [body.bodyUsed](#bodybodyused) + [body.bodyUsed](#bodybodyused)
* [body.buffer()](#bodybuffer) + [body.buffer()](#bodybuffer)
* [body.json()](#bodyjson) + [body.json()](#bodyjson)
* [body.text()](#bodytext) + [body.text()](#bodytext)
<!-- tocstop --> <!-- tocstop -->
## Puppeteer
Puppeteer is a Node library which provides a high-level API to control Chromium over the DevTools Protocol.
Puppeteer provides a top-level require which has a [Browser](#class-browser) class.
The following is a typical example of using a Browser class to drive automation:
```js
const {Browser} = require('puppeteer');
const browser = new Browser();
browser.newPage().then(async page => {
await page.navigate('https://google.com');
// other actions...
browser.close();
});
```
### Emulation
Puppeteer supports device emulation with two primitives:
- [page.setUserAgent(userAgent)](#pagesetuseragentuseragent)
- [page.setViewport(viewport)](#pagesetviewportviewport)
To aid emulation, puppeteer provides a list of device descriptors which could be obtained via the `require('puppeteer/DeviceDescriptors')` command.
Below is an example of emulating iPhone 6 in puppeteer:
```js
const {Browser} = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone 6'];
const browser = new Browser();
browser.newPage().then(async page => {
await Promise.all([
page.setUserAgent(iPhone.userAgent),
page.setViewport(iPhone.viewport)
]);
await page.navigate('https://google.com');
// other actions...
browser.close();
});
```
List of all available devices is available in the source code: [DeviceDescriptors.js](https://github.com/GoogleChrome/puppeteer/blob/master/DeviceDescriptors.js).
### class: Browser ### class: Browser
Browser manages a browser instance, creating it with a predefined Browser manages a browser instance, creating it with a predefined
@ -316,13 +358,6 @@ Adds a `<script></script>` tag to the page with the desired url. Alternatively,
#### page.close() #### page.close()
- returns: <[Promise]> Returns promise which resolves when page gets closed. - returns: <[Promise]> Returns promise which resolves when page gets closed.
#### page.emulate(name)
- `name` <[string]> A name of the device to be emulated. Get the full list of emulated devices via `page.emulatedDevices()`.
- returns: <[Promise]> Returns promise which resolves when device is emulated. Can reload the page if switching between mobile and desktop devices.
#### page.emulatedDevices()
- returns: <[Array]<[String]>> Returns array of device names that can be used with `page.emulate()`.
#### page.evaluate(pageFunction, ...args) #### page.evaluate(pageFunction, ...args)
- `pageFunction` <[function]> Function to be evaluated in browser context - `pageFunction` <[function]> Function to be evaluated in browser context
- `...args` <...[string]> Arguments to pass to `pageFunction` - `...args` <...[string]> Arguments to pass to `pageFunction`
@ -521,8 +556,14 @@ browser.newPage().then(async page =>
- `viewport` <[Object]> An object with two fields: - `viewport` <[Object]> An object with two fields:
- `width` <[number]> Specify page's width in pixels. - `width` <[number]> Specify page's width in pixels.
- `height` <[number]> Specify page's height in pixels. - `height` <[number]> Specify page's height in pixels.
- `deviceScaleFactor` <[number]> Specify device scale factor (could be though of as dpr). Defaults to `1`.
- `isMobile` <[boolean]> Weather the `meta viewport` tag is taken into account. Defaults to `false`.
- `hasTouch`<[boolean]> Specify if viewport supports touch events. Defaults to `false`
- `isLandscape` <[boolean]> Specify if viewport is in the landscape mode. Defaults to `false`.
- returns: <[Promise]> Promise which resolves when the dimensions are updated. - returns: <[Promise]> Promise which resolves when the dimensions are updated.
Note: in certain cases, setting viewport will reload the page so that the `isMobile` or `hasTouch` options will be able to interfere in project loading.
The page's viewport size defines page's dimensions, observable from page via `window.innerWidth / window.innerHeight`. The viewport size defines a size of page The page's viewport size defines page's dimensions, observable from page via `window.innerWidth / window.innerHeight`. The viewport size defines a size of page
screenshot (unless a `fullPage` option is given). screenshot (unless a `fullPage` option is given).
@ -549,9 +590,7 @@ This is a shortcut for [page.mainFrame().url()](#frameurl)
- returns: <[string]> Returns user agent. - returns: <[string]> Returns user agent.
#### page.viewport() #### page.viewport()
- returns: <[Object]> An object with two fields: - returns: <[Object]> An object with the save fields as described in [page.setViewport](#pagesetviewportviewport)
- `width` <[number]> Page's width in pixels.
- `height` <[number]> Page's height in pixels.
#### page.waitFor(selector) #### page.waitFor(selector)

View File

@ -14,38 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
const DeviceDescriptors = require('./DeviceDescriptors');
class EmulationManager { class EmulationManager {
/**
* @return {!Promise<!Array<string>>}
*/
static deviceNames() {
return Promise.resolve(DeviceDescriptors.map(device => device.name));
}
/**
* @param {string} name
* @param {!Object=} options
* @return {!Page.Viewport}
*/
static deviceViewport(name) {
const device = DeviceDescriptors.find(device => device.name === name);
if (!device)
throw new Error(`Unable to emulate ${name}, no such device metrics in the library.`);
return device.viewport;
}
/**
* @param {string} name
*/
static deviceUserAgent(name) {
const device = DeviceDescriptors.find(device => device.name === name);
if (!device)
throw new Error(`Unable to emulate ${name}, no such device metrics in the library.`);
return device.userAgent;
}
/** /**
* @param {!Connection} client * @param {!Connection} client
* @param {!Page.Viewport} viewport * @param {!Page.Viewport} viewport

View File

@ -349,24 +349,6 @@ class Page extends EventEmitter {
return this._viewport; return this._viewport;
} }
/**
* @return {!Promise<!Array<string>>}
*/
static emulatedDevices() {
return EmulationManager.deviceNames(name);
}
/**
* @param {string} name
* @return {!Promise}
*/
emulate(name) {
return Promise.all([
this.setUserAgent(EmulationManager.deviceUserAgent(name)),
this.setViewport(EmulationManager.deviceViewport(name))
]);
}
/** /**
* @param {function()} pageFunction * @param {function()} pageFunction
* @param {!Array<*>} args * @param {!Array<*>} args

View File

@ -30,6 +30,9 @@ let EMPTY_PAGE = PREFIX + '/empty.html';
let HTTPS_PORT = 8908; let HTTPS_PORT = 8908;
let HTTPS_PREFIX = 'https://localhost:' + HTTPS_PORT; let HTTPS_PREFIX = 'https://localhost:' + HTTPS_PORT;
const iPhone = require('../DeviceDescriptors')['iPhone 6'];
const iPhoneLandscape = require('../DeviceDescriptors')['iPhone 6 landscape'];
const headless = (process.env.HEADLESS || 'true').trim().toLowerCase() === 'true'; const headless = (process.env.HEADLESS || 'true').trim().toLowerCase() === 'true';
if (process.env.DEBUG_TEST) if (process.env.DEBUG_TEST)
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000 * 1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000 * 1000;
@ -851,6 +854,12 @@ describe('Puppeteer', function() {
let request = await server.waitForRequest('/empty.html'); let request = await server.waitForRequest('/empty.html');
expect(request.headers['user-agent']).toBe('foobar'); expect(request.headers['user-agent']).toBe('foobar');
})); }));
it('should emulate device user-agent', SX(async function() {
await page.navigate(PREFIX + '/mobile.html');
expect(await page.evaluate(() => navigator.userAgent)).toContain('Chrome');
await page.setUserAgent(iPhone.userAgent);
expect(await page.evaluate(() => navigator.userAgent)).toContain('Safari');
}));
}); });
describe('Page.setHTTPHeaders', function() { describe('Page.setHTTPHeaders', function() {
it('should work', SX(async function() { it('should work', SX(async function() {
@ -987,6 +996,30 @@ describe('Puppeteer', function() {
await page.setViewport({width: 123, height: 456}); await page.setViewport({width: 123, height: 456});
expect(page.viewport()).toEqual({width: 123, height: 456}); expect(page.viewport()).toEqual({width: 123, height: 456});
})); }));
it('should support mobile emulation', SX(async function() {
await page.navigate(PREFIX + '/mobile.html');
expect(await page.evaluate(() => window.innerWidth)).toBe(400);
await page.setViewport(iPhone.viewport);
expect(await page.evaluate(() => window.innerWidth)).toBe(375);
await page.setViewport({width: 400, height: 300});
expect(await page.evaluate(() => window.innerWidth)).toBe(400);
}));
it('should support touch emulation', SX(async function() {
await page.navigate(PREFIX + '/mobile.html');
expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false);
await page.setViewport(iPhone.viewport);
expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(true);
await page.setViewport({width: 100, height: 100});
expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false);
}));
it('should support landscape emulation', SX(async function() {
await page.navigate(PREFIX + '/mobile.html');
expect(await page.evaluate(() => screen.orientation.type)).toBe('portrait-primary');
await page.setViewport(iPhoneLandscape.viewport);
expect(await page.evaluate(() => screen.orientation.type)).toBe('landscape-primary');
await page.setViewport({width: 100, height: 100});
expect(await page.evaluate(() => screen.orientation.type)).toBe('portrait-primary');
}));
}); });
describe('Page.evaluateOnInitialized', function() { describe('Page.evaluateOnInitialized', function() {
@ -1054,31 +1087,6 @@ describe('Puppeteer', function() {
})); }));
}); });
describe('Page.emulate', function() {
it('should respect viewport meta tag', SX(async function() {
await page.navigate(PREFIX + '/mobile.html');
expect(await page.evaluate(() => window.innerWidth)).toBe(400);
await page.emulate('iPhone 6');
expect(await page.evaluate(() => window.innerWidth)).toBe(375);
await page.setViewport({width: 400, height: 300});
expect(await page.evaluate(() => window.innerWidth)).toBe(400);
}));
it('should enable/disable touch', SX(async function() {
await page.navigate(PREFIX + '/mobile.html');
expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false);
await page.emulate('iPhone 6');
expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(true);
await page.setViewport({width: 100, height: 100});
expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false);
}));
it('should emulate UA', SX(async function() {
await page.navigate(PREFIX + '/mobile.html');
expect(await page.evaluate(() => navigator.userAgent)).toContain('Chrome');
await page.emulate('iPhone 6');
expect(await page.evaluate(() => navigator.userAgent)).toContain('Safari');
}));
});
describe('Page.screenshot', function() { describe('Page.screenshot', function() {
it('should work', SX(async function() { it('should work', SX(async function() {
await page.setViewport({width: 500, height: 500}); await page.setViewport({width: 500, height: 500});