mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
Allow emulating devices
This commit is contained in:
parent
cf35524285
commit
58e7041a90
1149
lib/DeviceDescriptors.js
Normal file
1149
lib/DeviceDescriptors.js
Normal file
File diff suppressed because it is too large
Load Diff
199
lib/EmulatedDevice.js
Normal file
199
lib/EmulatedDevice.js
Normal 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;
|
123
lib/Page.js
123
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<!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',
|
||||
|
Loading…
Reference in New Issue
Block a user