From 58e7041a9009f335f2a31ec97f1dc8b52bdb7c78 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 21 Jun 2017 14:19:13 -0700 Subject: [PATCH] Allow emulating devices --- lib/DeviceDescriptors.js | 1149 ++++++++++++++++++++++++++++++++++++++ lib/EmulatedDevice.js | 199 +++++++ lib/Page.js | 123 ++-- 3 files changed, 1439 insertions(+), 32 deletions(-) create mode 100644 lib/DeviceDescriptors.js create mode 100644 lib/EmulatedDevice.js 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/EmulatedDevice.js b/lib/EmulatedDevice.js new file mode 100644 index 00000000000..e2e24c49b5e --- /dev/null +++ b/lib/EmulatedDevice.js @@ -0,0 +1,199 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +const DeviceDescriptors = require('./DeviceDescriptors'); + +/** + * @unrestricted + */ +EmulatedDevice = class { + constructor() { + /** @type {string} */ + this.title = ''; + /** @type {string} */ + this.type = EmulatedDevice.Type.Unknown; + /** @type {!EmulatedDevice.Orientation} */ + this.vertical = {width: 0, height: 0, outlineInsets: null, outlineImage: null}; + /** @type {!EmulatedDevice.Orientation} */ + this.horizontal = {width: 0, height: 0, outlineInsets: null, outlineImage: null}; + /** @type {number} */ + this.deviceScaleFactor = 1; + /** @type {!Array.} */ + this.capabilities = [EmulatedDevice.Capability.Touch, EmulatedDevice.Capability.Mobile]; + /** @type {string} */ + this.userAgent = ''; + /** @type {!Array.} */ + this.modes = []; + } + + /** + * @param {string} name + * @return {?EmulatedDevice} + */ + static forName(name) { + let 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.`); + return EmulatedDevice.fromJSONV1(descriptor); + } + + /** + * @param {*} json + * @return {?EmulatedDevice} + */ + static fromJSONV1(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 + '\''); + } + var 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) { + var 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 {!EmulatedDevice.Insets} + */ + function parseInsets(json) { + return {left: + parseIntValue(json, 'left'), top: parseIntValue(json, 'top'), right: parseIntValue(json, 'right'), + bottom: parseIntValue(json, 'bottom')}; + } + + /** + * @param {*} json + * @return {!EmulatedDevice.Orientation} + */ + function parseOrientation(json) { + var result = {}; + + result.width = parseIntValue(json, 'width'); + if (result.width < 0 || result.width > EmulatedDevice.MaxDeviceSize || + result.width < EmulatedDevice.MinDeviceSize) + throw new Error('Emulated device has wrong width: ' + result.width); + + result.height = parseIntValue(json, 'height'); + if (result.height < 0 || result.height > EmulatedDevice.MaxDeviceSize || + result.height < EmulatedDevice.MinDeviceSize) + throw new Error('Emulated device has wrong height: ' + result.height); + + var outlineInsets = parseValue(json['outline'], 'insets', 'object', null); + if (outlineInsets) { + result.outlineInsets = parseInsets(outlineInsets); + if (result.outlineInsets.left < 0 || result.outlineInsets.top < 0) + throw new Error('Emulated device has wrong outline insets'); + result.outlineImage = /** @type {string} */ (parseValue(json['outline'], 'image', 'string')); + } + return /** @type {!EmulatedDevice.Orientation} */ (result); + } + + var result = new EmulatedDevice(); + result.title = /** @type {string} */ (parseValue(json, 'title', 'string')); + result.type = /** @type {string} */ (parseValue(json, 'type', 'string')); + result.userAgent = /** @type {string} */ (parseValue(json, 'user-agent', 'string')); + + var capabilities = parseValue(json, 'capabilities', 'object', []); + if (!Array.isArray(capabilities)) + throw new Error('Emulated device capabilities must be an array'); + result.capabilities = []; + for (var 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')); + + var modes = parseValue(json, 'modes', 'object', []); + if (!Array.isArray(modes)) + throw new Error('Emulated device modes must be an array'); + result.modes = []; + for (var i = 0; i < modes.length; ++i) { + var mode = {}; + mode.title = /** @type {string} */ (parseValue(modes[i], 'title', 'string')); + mode.orientation = /** @type {string} */ (parseValue(modes[i], 'orientation', 'string')); + if (mode.orientation !== EmulatedDevice.Vertical && + mode.orientation !== EmulatedDevice.Horizontal) + throw new Error('Emulated device mode has wrong orientation \'' + mode.orientation + '\''); + var orientation = result.orientationByName(mode.orientation); + mode.insets = parseInsets(parseValue(modes[i], 'insets', 'object')); + if (mode.insets.top < 0 || mode.insets.left < 0 || mode.insets.right < 0 || mode.insets.bottom < 0 || + mode.insets.top + mode.insets.bottom > orientation.height || + mode.insets.left + mode.insets.right > orientation.width) + throw new Error('Emulated device mode \'' + mode.title + '\'has wrong mode insets'); + + mode.image = /** @type {string} */ (parseValue(modes[i], 'image', 'string', null)); + result.modes.push(mode); + } + + return result; + } + + /** + * @param {string} name + * @return {!Emulation.EmulatedDevice.Orientation} + */ + orientationByName(name) { + return name === EmulatedDevice.Vertical ? this.vertical : this.horizontal; + } +}; + + +/** @typedef {!{top: number, right: number, bottom: number, left: number}} */ +EmulatedDevice.Insets; + +/** @typedef {!{title: string, orientation: string, insets: !UI.Insets, image: ?string}} */ +EmulatedDevice.Mode; + +/** @typedef {!{width: number, height: number, outlineInsets: ?UI.Insets, outlineImage: ?string}} */ +EmulatedDevice.Orientation; + +EmulatedDevice.Horizontal = 'horizontal'; +EmulatedDevice.Vertical = 'vertical'; + +EmulatedDevice.Type = { + Phone: 'phone', + Tablet: 'tablet', + Notebook: 'notebook', + Desktop: 'desktop', + Unknown: 'unknown' +}; + +EmulatedDevice.Capability = { + Touch: 'touch', + Mobile: 'mobile' +}; + +EmulatedDevice.MinDeviceSize = 50; +EmulatedDevice.MaxDeviceSize = 9999; + +module.exports = EmulatedDevice; diff --git a/lib/Page.js b/lib/Page.js index 83d60cd004f..5d19ac2a26b 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -14,13 +14,14 @@ * limitations under the License. */ -var fs = require('fs'); -var EventEmitter = require('events'); -var mime = require('mime'); -var Request = require('./Request'); -var Navigator = require('./Navigator'); -var Dialog = require('./Dialog'); -var FrameManager = require('./FrameManager'); +const fs = require('fs'); +const EventEmitter = require('events'); +const mime = require('mime'); +const Request = require('./Request'); +const Navigator = require('./Navigator'); +const EmulatedDevice = require('./EmulatedDevice'); +const Dialog = require('./Dialog'); +const FrameManager = require('./FrameManager'); class Page extends EventEmitter { /** @@ -34,6 +35,7 @@ class Page extends EventEmitter { client.send('Runtime.enable', {}), client.send('Security.enable', {}), ]); + var expression = Page._evaluationString(() => window.devicePixelRatio); var {result:{value: screenDPI}} = await client.send('Runtime.evaluate', { expression, returnByValue: true }); var frameManager = await FrameManager.create(client); @@ -206,7 +208,7 @@ class Page extends EventEmitter { */ async _handleException(exceptionDetails) { var message = await this._getExceptionMessage(exceptionDetails); - this.emit(Page.Events.Error, new Error(message)); + this.emit(Page.Events.Error, {message}); } async _onConsoleAPI(event) { @@ -275,23 +277,7 @@ class Page extends EventEmitter { */ async setViewportSize(size) { this._viewportSize = size; - var width = size.width; - var height = size.height; - var zoom = this._screenDPI; - return Promise.all([ - this._client.send('Emulation.setDeviceMetricsOverride', { - width, - height, - deviceScaleFactor: 1, - scale: 1 / zoom, - mobile: false, - fitWindow: false - }), - this._client.send('Emulation.setVisibleSize', { - width: width / zoom, - height: height / zoom, - }) - ]); + this.resetDeviceEmulation(); } /** @@ -301,6 +287,74 @@ class Page extends EventEmitter { return this._viewportSize; } + /** + * @param {string} name + * @param {!Object=} options + * @return {!Promise} + */ + emulateNamedDevice(name, options) { + return this.emulateDevice(EmulatedDevice.forName(name), options); + } + + /** + * @param {!EmulatedDevice} device + * @param {!Object=} options + * @return {!Promise} + */ + emulateDevice(device, options) { + const mobile = device.capabilities.includes(EmulatedDevice.Capability.Mobile); + const landscape = options && options['orientation'] === 'landscape'; + const screen = landscape ? device.horizontal : device.vertical; + const width = screen.width; + const height = screen.height; + const deviceScaleFactor = device.deviceScaleFactor; + const screenOrientation = landscape ? { angle: 90, type: 'landscapePrimary' } : { angle: 0, type: 'portraitPrimary' }; + const fitWindow = false; + const userAgent = device.userAgent; + this._emulatedDevice = device; + return Promise.all([ + this._client.send('Emulation.setDeviceMetricsOverride', { mobile, width, height, deviceScaleFactor, screenOrientation, fitWindow }), + this._client.send('Emulation.setVisibleSize', { width, height }), + this._client.send('Network.setUserAgentOverride', { userAgent }), + this._client.send('Emulation.setTouchEmulationEnabled', { + enabled: device.capabilities.includes(EmulatedDevice.Capability.Touch), + configuration: device.capabilities.includes(EmulatedDevice.Capability.Mobile) ? 'mobile' : 'desktop' + }), + this.evaluate(injectedTouchEventsFunction) + ]); + + 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 + }); + } + } + } + } + } + + resetDeviceEmulation() { + const width = 0; + const height = 0; + const deviceScaleFactor = 1; + const mobile = false; + const screenOrientation = { angle: 0, type: 'portraitPrimary' }; + const fitWindow = false; + const userAgent = ''; + this._emulatedDevice = null; + return Promise.all([ + this._client.send('Emulation.setDeviceMetricsOverride', { mobile, width, height, deviceScaleFactor, screenOrientation, fitWindow }), + this._client.send('Network.setUserAgentOverride', { userAgent }), + this._client.send('Emulation.setTouchEmulationEnabled', { enabled: false, configuration: 'desktop' }), + this._client.send('Emulation.setVisibleSize', this._viewportSize), + ]); + } + /** * @param {function()} fun * @param {!Array<*>} args @@ -410,15 +464,20 @@ class Page extends EventEmitter { * @return {!Promise} */ async _screenshotTask(screenshotType, options) { + var dpiFactor = (this._emulatedDevice ? this._emulatedDevice.deviceScaleFactor : 1) / this._screenDPI; + // if (this._emulatedDevice) { + // this._emulatedDevice.scale = dpiFactor; + // await this.emulateDevice(this._emulatedDevice); + // } if (options.clip) { await Promise.all([ this._client.send('Emulation.setVisibleSize', { - width: Math.ceil(options.clip.width / this._screenDPI), - height: Math.ceil(options.clip.height / this._screenDPI), + width: Math.ceil(options.clip.width * dpiFactor), + height: Math.ceil(options.clip.height * dpiFactor), }), this._client.send('Emulation.forceViewport', { - x: options.clip.x / this._screenDPI, - y: options.clip.y / this._screenDPI, + x: options.clip.x * dpiFactorI, + y: options.clip.y * dpiFactorI, scale: 1, }) ]); @@ -426,8 +485,8 @@ class Page extends EventEmitter { var response = await this._client.send('Page.getLayoutMetrics'); await Promise.all([ this._client.send('Emulation.setVisibleSize', { - width: Math.ceil(response.contentSize.width / this._screenDPI), - height: Math.ceil(response.contentSize.height / this._screenDPI), + width: Math.ceil(response.contentSize.width * dpiFactor), + height: Math.ceil(response.contentSize.height * dpiFactor), }), this._client.send('Emulation.forceViewport', { x: 0, @@ -580,7 +639,7 @@ function convertPrintParameterToInches(parameter) { Page.Events = { ConsoleMessage: 'consolemessage', Dialog: 'dialog', - Error: 'error', + Error: 'jsError', ResourceLoadingFailed: 'resourceloadingfailed', ResponseReceived: 'responsereceived', FrameAttached: 'frameattached',