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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var fs = require('fs');
|
const fs = require('fs');
|
||||||
var EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
var mime = require('mime');
|
const mime = require('mime');
|
||||||
var Request = require('./Request');
|
const Request = require('./Request');
|
||||||
var Navigator = require('./Navigator');
|
const Navigator = require('./Navigator');
|
||||||
var Dialog = require('./Dialog');
|
const EmulatedDevice = require('./EmulatedDevice');
|
||||||
var FrameManager = require('./FrameManager');
|
const Dialog = require('./Dialog');
|
||||||
|
const FrameManager = require('./FrameManager');
|
||||||
|
|
||||||
class Page extends EventEmitter {
|
class Page extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
@ -34,6 +35,7 @@ class Page extends EventEmitter {
|
|||||||
client.send('Runtime.enable', {}),
|
client.send('Runtime.enable', {}),
|
||||||
client.send('Security.enable', {}),
|
client.send('Security.enable', {}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
var expression = Page._evaluationString(() => window.devicePixelRatio);
|
var expression = Page._evaluationString(() => window.devicePixelRatio);
|
||||||
var {result:{value: screenDPI}} = await client.send('Runtime.evaluate', { expression, returnByValue: true });
|
var {result:{value: screenDPI}} = await client.send('Runtime.evaluate', { expression, returnByValue: true });
|
||||||
var frameManager = await FrameManager.create(client);
|
var frameManager = await FrameManager.create(client);
|
||||||
@ -206,7 +208,7 @@ class Page extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
async _handleException(exceptionDetails) {
|
async _handleException(exceptionDetails) {
|
||||||
var message = await this._getExceptionMessage(exceptionDetails);
|
var message = await this._getExceptionMessage(exceptionDetails);
|
||||||
this.emit(Page.Events.Error, new Error(message));
|
this.emit(Page.Events.Error, {message});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onConsoleAPI(event) {
|
async _onConsoleAPI(event) {
|
||||||
@ -275,23 +277,7 @@ class Page extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
async setViewportSize(size) {
|
async setViewportSize(size) {
|
||||||
this._viewportSize = size;
|
this._viewportSize = size;
|
||||||
var width = size.width;
|
this.resetDeviceEmulation();
|
||||||
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,
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -301,6 +287,74 @@ class Page extends EventEmitter {
|
|||||||
return this._viewportSize;
|
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 {function()} fun
|
||||||
* @param {!Array<*>} args
|
* @param {!Array<*>} args
|
||||||
@ -410,15 +464,20 @@ class Page extends EventEmitter {
|
|||||||
* @return {!Promise<!Buffer>}
|
* @return {!Promise<!Buffer>}
|
||||||
*/
|
*/
|
||||||
async _screenshotTask(screenshotType, options) {
|
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) {
|
if (options.clip) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this._client.send('Emulation.setVisibleSize', {
|
this._client.send('Emulation.setVisibleSize', {
|
||||||
width: Math.ceil(options.clip.width / this._screenDPI),
|
width: Math.ceil(options.clip.width * dpiFactor),
|
||||||
height: Math.ceil(options.clip.height / this._screenDPI),
|
height: Math.ceil(options.clip.height * dpiFactor),
|
||||||
}),
|
}),
|
||||||
this._client.send('Emulation.forceViewport', {
|
this._client.send('Emulation.forceViewport', {
|
||||||
x: options.clip.x / this._screenDPI,
|
x: options.clip.x * dpiFactorI,
|
||||||
y: options.clip.y / this._screenDPI,
|
y: options.clip.y * dpiFactorI,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
@ -426,8 +485,8 @@ class Page extends EventEmitter {
|
|||||||
var response = await this._client.send('Page.getLayoutMetrics');
|
var response = await this._client.send('Page.getLayoutMetrics');
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this._client.send('Emulation.setVisibleSize', {
|
this._client.send('Emulation.setVisibleSize', {
|
||||||
width: Math.ceil(response.contentSize.width / this._screenDPI),
|
width: Math.ceil(response.contentSize.width * dpiFactor),
|
||||||
height: Math.ceil(response.contentSize.height / this._screenDPI),
|
height: Math.ceil(response.contentSize.height * dpiFactor),
|
||||||
}),
|
}),
|
||||||
this._client.send('Emulation.forceViewport', {
|
this._client.send('Emulation.forceViewport', {
|
||||||
x: 0,
|
x: 0,
|
||||||
@ -580,7 +639,7 @@ function convertPrintParameterToInches(parameter) {
|
|||||||
Page.Events = {
|
Page.Events = {
|
||||||
ConsoleMessage: 'consolemessage',
|
ConsoleMessage: 'consolemessage',
|
||||||
Dialog: 'dialog',
|
Dialog: 'dialog',
|
||||||
Error: 'error',
|
Error: 'jsError',
|
||||||
ResourceLoadingFailed: 'resourceloadingfailed',
|
ResourceLoadingFailed: 'resourceloadingfailed',
|
||||||
ResponseReceived: 'responsereceived',
|
ResponseReceived: 'responsereceived',
|
||||||
FrameAttached: 'frameattached',
|
FrameAttached: 'frameattached',
|
||||||
|
Loading…
Reference in New Issue
Block a user