Allow emulating devices

This commit is contained in:
Pavel Feldman 2017-06-21 14:19:13 -07:00
parent cf35524285
commit 58e7041a90
3 changed files with 1439 additions and 32 deletions

1149
lib/DeviceDescriptors.js Normal file

File diff suppressed because it is too large Load Diff

199
lib/EmulatedDevice.js Normal file
View File

@ -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.<string>} */
this.capabilities = [EmulatedDevice.Capability.Touch, EmulatedDevice.Capability.Mobile];
/** @type {string} */
this.userAgent = '';
/** @type {!Array.<!EmulatedDevice.Mode>} */
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;

View File

@ -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<!Buffer>}
*/
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',