From 895f69d17a1aa495d14c5c847142ce469fcf3cb0 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 17 Jul 2017 18:13:04 -0700 Subject: [PATCH] Add emulation for named devices. (#72) This patch introduces page emulation, making it possible to emulate different devices. --- README.md | 2 +- docs/api.md | 25 +- examples/colorwheel.js | 2 +- lib/DeviceDescriptors.js | 1149 ++++++++++++++++++++++++++++++++++++++ lib/EmulationManager.js | 198 +++++++ lib/Page.js | 70 ++- phantom_shim/WebPage.js | 6 +- test/test.js | 18 +- utils/doclint/lint.js | 1 + 9 files changed, 1430 insertions(+), 41 deletions(-) create mode 100644 lib/DeviceDescriptors.js create mode 100644 lib/EmulationManager.js diff --git a/README.md b/README.md index 25e636c69fa..7acdc1795f0 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ However, if you're using Node 8 or higher, `async/await` make life easier: ```javascript browser.newPage().then(async page => { - await page.setViewportSize({width: 1000, height: 1000}); + await page.setViewport({width: 1000, height: 1000}); await page.pdf({path: 'blank.pdf'}); browser.close(); }); diff --git a/docs/api.md b/docs/api.md index a3ac8a68473..db1f6cc2b20 100644 --- a/docs/api.md +++ b/docs/api.md @@ -27,6 +27,8 @@ * [page.addScriptTag(url)](#pageaddscripttagurl) * [page.click(selector)](#pageclickselector) * [page.close()](#pageclose) + * [page.emulate(name, options)](#pageemulatename-options) + * [page.emulatedDevices()](#pageemulateddevices) * [page.evaluate(pageFunction, ...args)](#pageevaluatepagefunction-args) * [page.evaluateOnInitialized(pageFunction, ...args)](#pageevaluateoninitializedpagefunction-args) * [page.focus(selector)](#pagefocusselector) @@ -37,19 +39,20 @@ * [page.navigate(url, options)](#pagenavigateurl-options) * [page.pdf(options)](#pagepdfoptions) * [page.plainText()](#pageplaintext) + * [page.reload()](#pagereload) * [page.screenshot([options])](#pagescreenshotoptions) * [page.setContent(html)](#pagesetcontenthtml) * [page.setHTTPHeaders(headers)](#pagesethttpheadersheaders) * [page.setInPageCallback(name, callback)](#pagesetinpagecallbackname-callback) * [page.setRequestInterceptor(interceptor)](#pagesetrequestinterceptorinterceptor) * [page.setUserAgent(userAgent)](#pagesetuseragentuseragent) - * [page.setViewportSize(size)](#pagesetviewportsizesize) + * [page.setViewport(viewport)](#pagesetviewportviewport) * [page.title()](#pagetitle) * [page.type(text)](#pagetypetext) * [page.uploadFile(selector, ...filePaths)](#pageuploadfileselector-filepaths) * [page.url()](#pageurl) * [page.userAgent()](#pageuseragent) - * [page.viewportSize()](#pageviewportsize) + * [page.viewport()](#pageviewport) * [page.waitFor(selector)](#pagewaitforselector) - [class: Dialog](#class-dialog) * [dialog.accept([promptText])](#dialogacceptprompttext) @@ -257,6 +260,15 @@ Adds a `` tag to the page with the desired url. Alternatively, #### page.close() - returns: <[Promise]> Returns promise which resolves when page gets closed. +#### page.emulate(name, options) +- `name` <[string]> A name of the device to be emulated. Get the full list of emulated devices via `page.emulatedDevices()`. +- `options` <[Object]> Emulation parameters which might have the following properties: + - `landscape` <[boolean]> Emulates device in the landscape mode, defaults to `false`. +- 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) - `pageFunction` <[function]> Function to be evaluated in browser context - `...args` <...[string]> Arguments to pass to `pageFunction` @@ -354,6 +366,9 @@ The `format` options are: #### page.plainText() - returns: <[Promise]<[string]>> Returns page's inner text. +#### page.reload() +- returns: <[Promise]<[Response]>> Promise which resolves to the main resource response. In case of multiple redirects, the navigation will resolve with the response of the last redirect. + #### page.screenshot([options]) - `options` <[Object]> Options object which might have the following properties: - `path` <[string]> The file path to save the image to. The screenshot type will be inferred from file extension. @@ -423,8 +438,8 @@ browser.newPage().then(async page => - `userAgent` <[string]> Specific user agent to use in this page - returns: <[Promise]> Promise which resolves when the user agent is set. -#### page.setViewportSize(size) -- `size` <[Object]> An object with two fields: +#### page.setViewport(viewport) +- `viewport` <[Object]> An object with two fields: - `width` <[number]> Specify page's width in pixels. - `height` <[number]> Specify page's height in pixels. - returns: <[Promise]> Promise which resolves when the dimensions are updated. @@ -454,7 +469,7 @@ This is a shortcut for [page.mainFrame().url()](#frameurl) #### page.userAgent() - returns: <[string]> Returns user agent. -#### page.viewportSize() +#### page.viewport() - returns: <[Object]> An object with two fields: - `width` <[number]> Page's width in pixels. - `height` <[number]> Page's height in pixels. diff --git a/examples/colorwheel.js b/examples/colorwheel.js index 477f3ae4dce..6c5a006ef22 100644 --- a/examples/colorwheel.js +++ b/examples/colorwheel.js @@ -18,7 +18,7 @@ var Browser = require('../lib/Browser'); var browser = new Browser(); browser.newPage().then(async page => { - await page.setViewportSize({width: 400, height: 400}); + await page.setViewport({width: 400, height: 400}); await page.setContent(''); await page.evaluate(drawColorWheel); await page.screenshot({path: 'colorwheel.png'}); diff --git a/lib/DeviceDescriptors.js b/lib/DeviceDescriptors.js new file mode 100644 index 00000000000..f39da5f6fea --- /dev/null +++ b/lib/DeviceDescriptors.js @@ -0,0 +1,1149 @@ +/** + * 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. + */ + +module.exports = +[ + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'iPhone 4' , + 'screen': { + 'horizontal': { + 'width': 480, + 'height': 320 + }, + 'device-pixel-ratio': 2, + 'vertical': { + 'width': 320, + 'height': 480 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'order': 40, + 'device': { + 'show-by-default': true, + 'title': 'iPhone 5', + 'screen': { + 'horizontal': { + 'outline' : { + 'image': '@url(iPhone5-landscape.svg)', + 'insets' : { 'left': 115, 'top': 25, 'right': 115, 'bottom': 28 } + }, + 'width': 568, + 'height': 320 + }, + 'device-pixel-ratio': 2, + 'vertical': { + 'outline' : { + 'image': '@url(iPhone5-portrait.svg)', + 'insets' : { 'left': 29, 'top': 105, 'right': 25, 'bottom': 111 } + }, + 'width': 320, + 'height': 568 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'order': 50, + 'device': { + 'show-by-default': true, + 'title': 'iPhone 6', + 'screen': { + 'horizontal': { + 'outline' : { + 'image': '@url(iPhone6-landscape.svg)', + 'insets' : { 'left': 106, 'top': 28, 'right': 106, 'bottom': 28 } + }, + 'width': 667, + 'height': 375 + }, + 'device-pixel-ratio': 2, + 'vertical': { + 'outline' : { + 'image': '@url(iPhone6-portrait.svg)', + 'insets' : { 'left': 28, 'top': 105, 'right': 28, 'bottom': 105 } + }, + 'width': 375, + 'height': 667 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'order': 60, + 'device': { + 'show-by-default': true, + 'title': 'iPhone 6 Plus', + 'screen': { + 'horizontal': { + 'outline' : { + 'image': '@url(iPhone6Plus-landscape.svg)', + 'insets' : { 'left': 109, 'top': 29, 'right': 109, 'bottom': 27 } + }, + 'width': 736, + 'height': 414 + }, + 'device-pixel-ratio': 3, + 'vertical': { + 'outline' : { + 'image': '@url(iPhone6Plus-portrait.svg)', + 'insets' : { 'left': 26, 'top': 107, 'right': 30, 'bottom': 111 } + }, + 'width': 414, + 'height': 736 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'BlackBerry Z30', + 'screen': { + 'horizontal': { + 'width': 640, + 'height': 360 + }, + 'device-pixel-ratio': 2, + 'vertical': { + 'width': 360, + 'height': 640 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Nexus 4', + 'screen': { + 'horizontal': { + 'width': 640, + 'height': 384 + }, + 'device-pixel-ratio': 2, + 'vertical': { + 'width': 384, + 'height': 640 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Mobile Safari/537.36', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'title': 'Nexus 5', + 'type': 'phone', + 'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Mobile Safari/537.36', + 'capabilities': [ + 'touch', + 'mobile' + ], + 'show-by-default': false, + 'screen': { + 'device-pixel-ratio': 3, + 'vertical': { + 'width': 360, + 'height': 640 + }, + 'horizontal': { + 'width': 640, + 'height': 360 + } + }, + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 25, 'right': 0, 'bottom': 48 }, + 'image': '@url(google-nexus-5-vertical-default-1x.png) 1x, @url(google-nexus-5-vertical-default-2x.png) 2x' + }, + { + 'title': 'navigation bar', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 80, 'right': 0, 'bottom': 48 }, + 'image': '@url(google-nexus-5-vertical-navigation-1x.png) 1x, @url(google-nexus-5-vertical-navigation-2x.png) 2x' + }, + { + 'title': 'keyboard', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 80, 'right': 0, 'bottom': 312 }, + 'image': '@url(google-nexus-5-vertical-keyboard-1x.png) 1x, @url(google-nexus-5-vertical-keyboard-2x.png) 2x' + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 25, 'right': 42, 'bottom': 0 }, + 'image': '@url(google-nexus-5-horizontal-default-1x.png) 1x, @url(google-nexus-5-horizontal-default-2x.png) 2x' + }, + { + 'title': 'navigation bar', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 80, 'right': 42, 'bottom': 0 }, + 'image': '@url(google-nexus-5-horizontal-navigation-1x.png) 1x, @url(google-nexus-5-horizontal-navigation-2x.png) 2x' + }, + { + 'title': 'keyboard', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 80, 'right': 42, 'bottom': 202 }, + 'image': '@url(google-nexus-5-horizontal-keyboard-1x.png) 1x, @url(google-nexus-5-horizontal-keyboard-2x.png) 2x' + } + ] + } + }, + { + 'type': 'emulated-device', + 'order': 20, + 'device': { + 'title': 'Nexus 5X', + 'type': 'phone', + 'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Mobile Safari/537.36', + 'capabilities': [ + 'touch', + 'mobile' + ], + 'show-by-default': true, + 'screen': { + 'device-pixel-ratio': 2.625, + 'vertical': { + 'outline' : { + 'image': '@url(Nexus5X-portrait.svg)', + 'insets' : { 'left': 18, 'top': 88, 'right': 22, 'bottom': 98 } + }, + 'width': 412, + 'height': 732 + }, + 'horizontal': { + 'outline' : { + 'image': '@url(Nexus5X-landscape.svg)', + 'insets' : { 'left': 88, 'top': 21, 'right': 98, 'bottom': 19 } + }, + 'width': 732, + 'height': 412 + } + }, + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 24, 'right': 0, 'bottom': 48 }, + 'image': '@url(google-nexus-5x-vertical-default-1x.png) 1x, @url(google-nexus-5x-vertical-default-2x.png) 2x' + }, + { + 'title': 'navigation bar', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 80, 'right': 0, 'bottom': 48 }, + 'image': '@url(google-nexus-5x-vertical-navigation-1x.png) 1x, @url(google-nexus-5x-vertical-navigation-2x.png) 2x' + }, + { + 'title': 'keyboard', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 80, 'right': 0, 'bottom': 342 }, + 'image': '@url(google-nexus-5x-vertical-keyboard-1x.png) 1x, @url(google-nexus-5x-vertical-keyboard-2x.png) 2x' + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 24, 'right': 48, 'bottom': 0 }, + 'image': '@url(google-nexus-5x-horizontal-default-1x.png) 1x, @url(google-nexus-5x-horizontal-default-2x.png) 2x' + }, + { + 'title': 'navigation bar', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 80, 'right': 48, 'bottom': 0 }, + 'image': '@url(google-nexus-5x-horizontal-navigation-1x.png) 1x, @url(google-nexus-5x-horizontal-navigation-2x.png) 2x' + }, + { + 'title': 'keyboard', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 80, 'right': 48, 'bottom': 222 }, + 'image': '@url(google-nexus-5x-horizontal-keyboard-1x.png) 1x, @url(google-nexus-5x-horizontal-keyboard-2x.png) 2x' + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Nexus 6', + 'screen': { + 'horizontal': { + 'width': 732, + 'height': 412 + }, + 'device-pixel-ratio': 3.5, + 'vertical': { + 'width': 412, + 'height': 732 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Mobile Safari/537.36', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'order': 30, + 'device': { + 'show-by-default': true, + 'title': 'Nexus 6P', + 'screen': { + 'horizontal': { + 'outline' : { + 'image': '@url(Nexus6P-landscape.svg)', + 'insets' : { 'left': 94, 'top': 17, 'right': 88, 'bottom': 17 } + }, + 'width': 732, + 'height': 412 + }, + 'device-pixel-ratio': 3.5, + 'vertical': { + 'outline' : { + 'image': '@url(Nexus6P-portrait.svg)', + 'insets' : { 'left': 16, 'top': 94, 'right': 16, 'bottom': 88 } + }, + 'width': 412, + 'height': 732 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Mobile Safari/537.36', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'LG Optimus L70', + 'screen': { + 'horizontal': { + 'width': 640, + 'height': 384 + }, + 'device-pixel-ratio': 1.25, + 'vertical': { + 'width': 384, + 'height': 640 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/59.0.3071.104 Mobile Safari/537.36', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Nokia N9', + 'screen': { + 'horizontal': { + 'width': 640, + 'height': 360 + }, + 'device-pixel-ratio': 1, + 'vertical': { + 'width': 360, + 'height': 640 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (MeeGo; NokiaN9) AppleWebKit/534.13 (KHTML, like Gecko) NokiaBrowser/8.5.0 Mobile Safari/534.13', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Nokia Lumia 520', + 'screen': { + 'horizontal': { + 'width': 533, + 'height': 320 + }, + 'device-pixel-ratio': 1.5, + 'vertical': { + 'width': 320, + 'height': 533 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Microsoft Lumia 550', + 'screen': { + 'horizontal': { + 'width': 640, + 'height': 360 + }, + 'device-pixel-ratio': 2, + 'vertical': { + 'width': 640, + 'height': 360 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Microsoft Lumia 950', + 'screen': { + 'horizontal': { + 'width': 640, + 'height': 360 + }, + 'device-pixel-ratio': 4, + 'vertical': { + 'width': 360, + 'height': 640 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/14.14263', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Galaxy S III', + 'screen': { + 'horizontal': { + 'width': 640, + 'height': 360 + }, + 'device-pixel-ratio': 2, + 'vertical': { + 'width': 360, + 'height': 640 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'order': 10, + 'device': { + 'show-by-default': true, + 'title': 'Galaxy S5', + 'screen': { + 'horizontal': { + 'width': 640, + 'height': 360 + }, + 'device-pixel-ratio': 3, + 'vertical': { + 'width': 360, + 'height': 640 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Mobile Safari/537.36', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Kindle Fire HDX', + 'screen': { + 'horizontal': { + 'width': 2560, + 'height': 1600 + }, + 'device-pixel-ratio': 2, + 'vertical': { + 'width': 1600, + 'height': 2560 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true', + 'type': 'tablet', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'iPad Mini', + 'screen': { + 'horizontal': { + 'width': 1024, + 'height': 768 + }, + 'device-pixel-ratio': 2, + 'vertical': { + 'width': 768, + 'height': 1024 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + 'type': 'tablet', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'order': 70, + 'device': { + 'show-by-default': true, + 'title': 'iPad', + 'screen': { + 'horizontal': { + 'outline' : { + 'image': '@url(iPad-landscape.svg)', + 'insets' : { 'left': 112, 'top': 56, 'right': 116, 'bottom': 52 } + }, + 'width': 1024, + 'height': 768 + }, + 'device-pixel-ratio': 2, + 'vertical': { + 'outline' : { + 'image': '@url(iPad-portrait.svg)', + 'insets' : { 'left': 52, 'top': 114, 'right': 55, 'bottom': 114 } + }, + 'width': 768, + 'height': 1024 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + 'type': 'tablet', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'order': 80, + 'device': { + 'show-by-default': true, + 'title': 'iPad Pro', + 'screen': { + 'horizontal': { + 'width': 1366, + 'height': 1024 + }, + 'device-pixel-ratio': 2, + 'vertical': { + 'width': 1024, + 'height': 1366 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + 'type': 'tablet', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Blackberry PlayBook', + 'screen': { + 'horizontal': { + 'width': 1024, + 'height': 600 + }, + 'device-pixel-ratio': 1, + 'vertical': { + 'width': 600, + 'height': 1024 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+', + 'type': 'tablet', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Nexus 10', + 'screen': { + 'horizontal': { + 'width': 1280, + 'height': 800 + }, + 'device-pixel-ratio': 2, + 'vertical': { + 'width': 800, + 'height': 1280 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36', + 'type': 'tablet', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Nexus 7', + 'screen': { + 'horizontal': { + 'width': 960, + 'height': 600 + }, + 'device-pixel-ratio': 2, + 'vertical': { + 'width': 600, + 'height': 960 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36', + 'type': 'tablet', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Galaxy Note 3', + 'screen': { + 'horizontal': { + 'width': 640, + 'height': 360 + }, + 'device-pixel-ratio': 3, + 'vertical': { + 'width': 360, + 'height': 640 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Galaxy Note II', + 'screen': { + 'horizontal': { + 'width': 640, + 'height': 360 + }, + 'device-pixel-ratio': 2, + 'vertical': { + 'width': 360, + 'height': 640 + } + }, + 'capabilities': [ + 'touch', + 'mobile' + ], + 'user-agent': 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', + 'type': 'phone', + 'modes': [ + { + 'title': 'default', + 'orientation': 'vertical', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + }, + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Laptop with touch', + 'screen': { + 'horizontal': { + 'width': 1280, + 'height': 950 + }, + 'device-pixel-ratio': 1, + 'vertical': { + 'width': 950, + 'height': 1280 + } + }, + 'capabilities': [ + 'touch' + ], + 'user-agent': '', + 'type': 'notebook', + 'modes': [ + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Laptop with HiDPI screen', + 'screen': { + 'horizontal': { + 'width': 1440, + 'height': 900 + }, + 'device-pixel-ratio': 2, + 'vertical': { + 'width': 900, + 'height': 1440 + } + }, + 'capabilities': [], + 'user-agent': '', + 'type': 'notebook', + 'modes': [ + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + }, + { + 'type': 'emulated-device', + 'device': { + 'show-by-default': false, + 'title': 'Laptop with MDPI screen', + 'screen': { + 'horizontal': { + 'width': 1280, + 'height': 800 + }, + 'device-pixel-ratio': 1, + 'vertical': { + 'width': 800, + 'height': 1280 + } + }, + 'capabilities': [], + 'user-agent': '', + 'type': 'notebook', + 'modes': [ + { + 'title': 'default', + 'orientation': 'horizontal', + 'insets': { 'left': 0, 'top': 0, 'right': 0, 'bottom': 0 } + } + ] + } + } +]; diff --git a/lib/EmulationManager.js b/lib/EmulationManager.js new file mode 100644 index 00000000000..d73e67d0c91 --- /dev/null +++ b/lib/EmulationManager.js @@ -0,0 +1,198 @@ +/** + * 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 DeviceDescriptors = require('./DeviceDescriptors'); + +class EmulationManager { + /** + * @return {!Promise>} + */ + static deviceNames() { + return Promise.resolve(DeviceDescriptors.map(entry => entry['device'].title)); + } + + /** + * @param {string} name + * @param {!Object=} options + * @return {!Page.Viewport} + */ + static deviceViewport(name, options) { + options = options || {}; + const descriptor = DeviceDescriptors.find(entry => entry['device'].title === name)['device']; + if (!descriptor) + throw new Error(`Unable to emulate ${name}, no such device metrics in the library.`); + const device = EmulationManager.loadFromJSONV1(descriptor); + const viewport = options.landscape ? device.horizontal : device.vertical; + return { + width: viewport.width, + height: viewport.height, + deviceScaleFactor: device.deviceScaleFactor, + isMobile: device.capabilities.includes('mobile'), + hasMobile: device.capabilities.includes('touch'), + isLandscape: options.landscape || false + }; + } + + /** + * @param {string} name + */ + static deviceUserAgent(name, options) { + const descriptor = DeviceDescriptors.find(entry => entry['device'].title === name)['device']; + if (!descriptor) + throw new Error(`Unable to emulate ${name}, no such device metrics in the library.`); + const device = EmulationManager.loadFromJSONV1(descriptor); + return device.userAgent; + } + + /** + * @param {!Connection} client + * @param {!Page.Viewport} viewport + * @return {Promise} + */ + static async emulateViewport(client, viewport) { + const mobile = viewport.isMobile || false; + const landscape = viewport.isLandscape || false; + const width = viewport.width; + const height = viewport.height; + const deviceScaleFactor = viewport.deviceScaleFactor || 1; + const screenOrientation = landscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' }; + + await Promise.all([ + client.send('Emulation.setDeviceMetricsOverride', { mobile, width, height, deviceScaleFactor, screenOrientation }), + client.send('Emulation.setTouchEmulationEnabled', { + enabled: viewport.hasTouch || false, + configuration: viewport.isMobile ? 'mobile' : 'desktop' + }) + ]); + + let reloadNeeded = false; + if (viewport.hasTouch && !client[EmulationManager._touchScriptId]) { + const source = `(${injectedTouchEventsFunction})()`; + client[EmulationManager._touchScriptId] = await client.send('Runtime.addScriptToEvaluateOnNewDocument', { source }); + reloadNeeded = true; + } + + if (!viewport.hasTouch && client[EmulationManager._touchScriptId]) { + client[EmulationManager._touchScriptId] = null; + await client.send('Runtime.removeScriptToEvaluateOnNewDocument', EmulationManager._emulatingTouch); + reloadNeeded = true; + } + + if (client[EmulationManager._emulatingMobile] !== mobile) + reloadNeeded = true; + client[EmulationManager._emulatingMobile] = mobile; + + function injectedTouchEventsFunction() { + const touchEvents = ['ontouchstart', 'ontouchend', 'ontouchmove', 'ontouchcancel']; + const recepients = [window.__proto__, document.__proto__]; + for (let i = 0; i < touchEvents.length; ++i) { + for (let j = 0; j < recepients.length; ++j) { + if (!(touchEvents[i] in recepients[j])) { + Object.defineProperty(recepients[j], touchEvents[i], { + value: null, writable: true, configurable: true, enumerable: true + }); + } + } + } + } + return reloadNeeded; + } + + /** + * @param {*} json + * @return {?Object} + */ + static loadFromJSONV1(json) { + /** + * @param {*} object + * @param {string} key + * @param {string} type + * @param {*=} defaultValue + * @return {*} + */ + function parseValue(object, key, type, defaultValue) { + if (typeof object !== 'object' || object === null || !object.hasOwnProperty(key)) { + if (typeof defaultValue !== 'undefined') + return defaultValue; + throw new Error('Emulated device is missing required property \'' + key + '\''); + } + const value = object[key]; + if (typeof value !== type || value === null) + throw new Error('Emulated device property \'' + key + '\' has wrong type \'' + typeof value + '\''); + return value; + } + + /** + * @param {*} object + * @param {string} key + * @return {number} + */ + function parseIntValue(object, key) { + const value = /** @type {number} */ (parseValue(object, key, 'number')); + if (value !== Math.abs(value)) + throw new Error('Emulated device value \'' + key + '\' must be integer'); + return value; + } + + /** + * @param {*} json + * @return {!{width: number, height: number}} + */ + function parseOrientation(json) { + const result = {}; + const minDeviceSize = 50; + const maxDeviceSize = 9999; + result.width = parseIntValue(json, 'width'); + if (result.width < 0 || result.width > maxDeviceSize || + result.width < minDeviceSize) + throw new Error('Emulated device has wrong width: ' + result.width); + + result.height = parseIntValue(json, 'height'); + if (result.height < 0 || result.height > maxDeviceSize || + result.height < minDeviceSize) + throw new Error('Emulated device has wrong height: ' + result.height); + + return /** @type {!{width: number, height: number}} */ (result); + } + + const result = {}; + result.type = /** @type {string} */ (parseValue(json, 'type', 'string')); + result.userAgent = /** @type {string} */ (parseValue(json, 'user-agent', 'string')); + + const capabilities = parseValue(json, 'capabilities', 'object', []); + if (!Array.isArray(capabilities)) + throw new Error('Emulated device capabilities must be an array'); + result.capabilities = []; + for (let i = 0; i < capabilities.length; ++i) { + if (typeof capabilities[i] !== 'string') + throw new Error('Emulated device capability must be a string'); + result.capabilities.push(capabilities[i]); + } + + result.deviceScaleFactor = /** @type {number} */ (parseValue(json['screen'], 'device-pixel-ratio', 'number')); + if (result.deviceScaleFactor < 0 || result.deviceScaleFactor > 100) + throw new Error('Emulated device has wrong deviceScaleFactor: ' + result.deviceScaleFactor); + + result.vertical = parseOrientation(parseValue(json['screen'], 'vertical', 'object')); + result.horizontal = parseOrientation(parseValue(json['screen'], 'horizontal', 'object')); + return result; + } +} + +EmulationManager._touchScriptId = Symbol('emulatingTouchScriptId'); +EmulationManager._emulatingMobile = Symbol('emulatingMobile'); + +module.exports = EmulationManager; diff --git a/lib/Page.js b/lib/Page.js index b338068e67e..5e82cb95f95 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -20,6 +20,7 @@ let mime = require('mime'); let NetworkManager = require('./NetworkManager'); let Navigator = require('./Navigator'); let Dialog = require('./Dialog'); +let EmulationManager = require('./EmulationManager'); let FrameManager = require('./FrameManager'); let helper = require('./helper'); @@ -41,7 +42,7 @@ class Page extends EventEmitter { let networkManager = new NetworkManager(client, userAgent); let page = new Page(client, frameManager, networkManager); // Initialize default page size. - await page.setViewportSize({width: 400, height: 300}); + await page.setViewport({width: 400, height: 300}); return page; } @@ -257,45 +258,67 @@ class Page extends EventEmitter { */ async navigate(url, options) { const referrer = this._networkManager.httpHeaders()['referer']; + this._navigator = new Navigator(this._client, url, referrer, options); + return this.reload(); + } + + /** + * @return {!Promise} + */ + async reload() { + if (!this._navigator) + return; /** @type {!Map} */ const responses = new Map(); const onResponse = response => responses.set(response.url, response); - const navigator = new Navigator(this._client, url, referrer, options); this._networkManager.on(NetworkManager.Events.Response, onResponse); try { - await navigator.navigate(); + await this._navigator.navigate(); } finally { this._networkManager.removeListener(NetworkManager.Events.Response, onResponse); } const response = responses.get(this.mainFrame().url()); console.assert(response); + + // Await for a single raf rountrip to ensure basic rasterization is complete. + await this.evaluate(() => new Promise(fulfill => requestAnimationFrame(fulfill))); return response; } /** - * @param {!{width: number, height: number}} size + * @param {!Page.Viewport} viewport * @return {!Promise} */ - setViewportSize(size) { - this._viewportSize = size; - return this._resetDeviceEmulation(); + async setViewport(viewport) { + const needsReload = await EmulationManager.emulateViewport(this._client, viewport); + this._viewport = viewport; + if (needsReload) + await this.reload(); } /** - * @return {!{width: number, height: number}} + * @return {!Page.Viewport} */ - viewportSize() { - return this._viewportSize; + viewport() { + return this._viewport; } - _resetDeviceEmulation() { - const width = this._viewportSize.width; - const height = this._viewportSize.height; - const deviceScaleFactor = 1; - const mobile = false; - const screenOrientation = { angle: 0, type: 'portraitPrimary' }; + /** + * @return {!Promise>} + */ + static emulatedDevices() { + return EmulationManager.deviceNames(name); + } + + /** + * @param {string} name + * @param {!Object=} options + * @return {!Promise} + */ + emulate(name, options) { return Promise.all([ - this._client.send('Emulation.setDeviceMetricsOverride', { mobile, width, height, deviceScaleFactor, screenOrientation }), + this.setUserAgent(EmulationManager.deviceUserAgent(name)), + this.setViewport(EmulationManager.deviceViewport(name, options)) ]); } @@ -365,13 +388,13 @@ class Page extends EventEmitter { */ async _screenshotTask(format, options) { if (options.fullPage) { - let metrics = await this._client.send('Page.getLayoutMetrics'); + const metrics = await this._client.send('Page.getLayoutMetrics'); const width = Math.ceil(metrics.contentSize.width); const height = Math.ceil(metrics.contentSize.height); await this._client.send('Emulation.resetPageScaleFactor'); - const mobile = false; - const deviceScaleFactor = 1; - const landscape = false; + const mobile = this._viewport.isMobile || false; + const deviceScaleFactor = this._viewport.deviceScaleFactor || 1; + const landscape = this._viewport.isLandscape || false; const screenOrientation = landscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' }; await this._client.send('Emulation.setDeviceMetricsOverride', { mobile, width, height, deviceScaleFactor, screenOrientation }); } @@ -382,7 +405,7 @@ class Page extends EventEmitter { let result = await this._client.send('Page.captureScreenshot', { format, quality: options.quality, clip }); if (options.fullPage) - await this.setViewportSize(this.viewportSize()); + await this.setViewport(this._viewport); let buffer = new Buffer(result.data, 'base64'); if (options.path) @@ -646,4 +669,7 @@ Page.Events = { Load: 'load', }; +/** @typedef {{width: number, height: number, deviceScaleFactor: number|undefined, isMobile: boolean|undefined, isLandscape: boolean, hasTouch: boolean|undefined}} */ +Page.Viewport; + module.exports = Page; diff --git a/phantom_shim/WebPage.js b/phantom_shim/WebPage.js index c6c44f642be..bda4fd0a961 100644 --- a/phantom_shim/WebPage.js +++ b/phantom_shim/WebPage.js @@ -37,7 +37,7 @@ class WebPage { if (options.settings.userAgent) this.settings.userAgent = options.settings.userAgent; if (options.viewportSize) - await(this._page.setViewportSize(options.viewportSize)); + await(this._page.setViewport(options.viewportSize)); this.loading = false; this.loadingProgress = 0; @@ -214,7 +214,7 @@ class WebPage { * @return {!{width: number, height: number}} */ get viewportSize() { - return this._page.viewportSize(); + return this._page.viewport(); } /** @@ -380,7 +380,7 @@ class WebPage { * @param {!{width: number, height: number}} options */ set viewportSize(options) { - await(this._page.setViewportSize(options)); + await(this._page.setViewport(options)); } /** diff --git a/test/test.js b/test/test.js index 9b4b5b58fa1..46bcad6e841 100644 --- a/test/test.js +++ b/test/test.js @@ -507,13 +507,13 @@ describe('Puppeteer', function() { describe('Page.screenshot', function() { it('should work', SX(async function() { - await page.setViewportSize({width: 500, height: 500}); + await page.setViewport({width: 500, height: 500}); await page.navigate(PREFIX + '/grid.html'); let screenshot = await page.screenshot(); expect(screenshot).toBeGolden('screenshot-sanity.png'); })); it('should clip rect', SX(async function() { - await page.setViewportSize({width: 500, height: 500}); + await page.setViewport({width: 500, height: 500}); await page.navigate(PREFIX + '/grid.html'); let screenshot = await page.screenshot({ clip: { @@ -526,7 +526,7 @@ describe('Puppeteer', function() { expect(screenshot).toBeGolden('screenshot-clip-rect.png'); })); it('should work for offscreen clip', SX(async function() { - await page.setViewportSize({width: 500, height: 500}); + await page.setViewport({width: 500, height: 500}); await page.navigate(PREFIX + '/grid.html'); let screenshot = await page.screenshot({ clip: { @@ -539,7 +539,7 @@ describe('Puppeteer', function() { expect(screenshot).toBeGolden('screenshot-offscreen-clip.png'); })); it('should run in parallel', SX(async function() { - await page.setViewportSize({width: 500, height: 500}); + await page.setViewport({width: 500, height: 500}); await page.navigate(PREFIX + '/grid.html'); let promises = []; for (let i = 0; i < 3; ++i) { @@ -556,7 +556,7 @@ describe('Puppeteer', function() { expect(screenshot).toBeGolden('screenshot-parallel-calls.png'); })); it('should take fullPage screenshots', SX(async function() { - await page.setViewportSize({width: 500, height: 500}); + await page.setViewport({width: 500, height: 500}); await page.navigate(PREFIX + '/grid.html'); let screenshot = await page.screenshot({ fullPage: true @@ -811,11 +811,11 @@ describe('Puppeteer', function() { })); }); - describe('Page.viewportSize', function() { + describe('Page.viewport', function() { it('should get the proper viewport size', SX(async function() { - expect(page.viewportSize()).toEqual({width: 400, height: 300}); - await page.setViewportSize({width: 123, height: 456}); - expect(page.viewportSize()).toEqual({width: 123, height: 456}); + expect(page.viewport()).toEqual({width: 400, height: 300}); + await page.setViewport({width: 123, height: 456}); + expect(page.viewport()).toEqual({width: 123, height: 456}); })); }); diff --git a/utils/doclint/lint.js b/utils/doclint/lint.js index 88bc56c60d1..c75984cb762 100644 --- a/utils/doclint/lint.js +++ b/utils/doclint/lint.js @@ -8,6 +8,7 @@ const PROJECT_DIR = path.join(__dirname, '..', '..'); let EXCLUDE_CLASSES = new Set([ 'Connection', + 'EmulationManager', 'FrameManager', 'Helper', 'Navigator',