lib/EmulationManager.js
const util = require('util')
const { helper, assert } = require('./helper')
/**
* @see https://chromedevtools.github.io/devtools-protocol/tot/Emulation
*/
class EmulationManager {
/**
* @param {Chrome|CRIConnection|CDPSession|Object} client
*/
constructor (client) {
/**
* @type {Chrome|CRIConnection|CDPSession|Object}
* @private
*/
this._client = client
/**
* What media type are we emulating
* @type {string}
* @private
*/
this._emulatingMedia = ''
/**
* Are we emulating mobile?
* @type {boolean}
* @private
*/
this._emulatingMobile = false
/**
* Can we interact via touches
* @type {boolean}
* @private
*/
this._hasTouch = false
/**
* Is no script enabled
* @type {boolean}
* @private
*/
this._scriptExecutionDisabled = false
/**
* @type {Set<string>}
* @private
* @since chrome-remote-interface-extra
*/
this._supportedMedia = new Set(['screen', 'print', ''])
}
/**
* @return {boolean}
* @since chrome-remote-interface-extra
*/
isEmulatingMobile () {
return this._emulatingMobile
}
/**
* @return {boolean}
* @since chrome-remote-interface-extra
*/
isEmulatingHasTouch () {
return this._hasTouch
}
/**
* @return {boolean}
*/
isScriptExecutionDisabled () {
return this._scriptExecutionDisabled
}
/**
* Clears the overridden device metrics
* @return {Promise<void>}
*/
async clearDeviceMetricsOverride () {
await this._client.send('Emulation.clearDeviceMetricsOverride', {})
}
/**
* Clears the overridden Geolocation Position and Error
* @return {Promise<void>}
*/
async clearGeolocationOverride () {
await this._client.send('Emulation.clearGeolocationOverride', {})
}
/**
* @param {!Viewport} viewport
* @return {Promise<boolean>}
*/
async emulateViewport (viewport) {
const mobile = viewport.isMobile || false
const width = viewport.width
const height = viewport.height
const deviceScaleFactor = viewport.deviceScaleFactor || 1
/** @type {Object} */
const screenOrientation = viewport.isLandscape
? { angle: 90, type: 'landscapePrimary' }
: { angle: 0, type: 'portraitPrimary' }
const hasTouch = viewport.hasTouch || false
await Promise.all([
this._client.send('Emulation.setDeviceMetricsOverride', {
mobile,
width,
height,
deviceScaleFactor,
screenOrientation
}),
await this._client.send('Emulation.setTouchEmulationEnabled', {
enabled: hasTouch
})
])
const reloadNeeded =
this._emulatingMobile !== mobile || this._hasTouch !== hasTouch
this._emulatingMobile = mobile
return reloadNeeded
}
/**
* Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position unavailable
* @param {!{longitude: number, latitude: number, accuracy: (number|undefined)}} options
* @see https://chromedevtools.github.io/devtools-protocol/tot/Emulation#method-setGeolocationOverride
* @since chrome-remote-interface-extra
*/
async setGeolocation (options) {
const { longitude, latitude, accuracy = 0 } = options
if (longitude < -180 || longitude > 180) {
throw new Error(
`Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.`
)
}
if (latitude < -90 || latitude > 90) {
throw new Error(
`Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.`
)
}
if (accuracy < 0) {
throw new Error(
`Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.`
)
}
await this._client.send('Emulation.setGeolocationOverride', {
longitude,
latitude,
accuracy
})
}
/**
* Sets or clears an override of the default background color of the frame.
* This override is used if the content does not specify one.
* @param {DOMRGBA} [color] - RGBA of the default background color. If not specified, any existing override will be cleared
* @return {Promise<void>}
* @see https://chromedevtools.github.io/devtools-protocol/tot/Emulation#method-setDefaultBackgroundColorOverride
* @since chrome-remote-interface-extra
*/
async setDefaultBackgroundColorOverride (color) {
if (color) {
if (color.r < 0 || color.r > 255) {
throw new Error(
`Invalid r value "${color.r}": precondition 0 <= r <= 255 failed.`
)
}
if (color.g < 0 || color.g > 255) {
throw new Error(
`Invalid g value "${color.g}": precondition 0 <= g <= 255 failed.`
)
}
if (color.b < 0 || color.b > 255) {
throw new Error(
`Invalid b value "${color.b}": precondition 0 <= b <= 255 failed.`
)
}
if (color.a && (color.a < 0 || color.a > 1)) {
throw new Error(
`Invalid a value "${color.a}": precondition 0 <= a <= 1 failed.`
)
}
}
await this._client.send('Emulation.setDefaultBackgroundColorOverride', {
color: color || undefined
})
}
/**
* Overrides the values of device screen dimensions
* - window.screen.width
* - window.screen.height
* - window.innerWidth
* - window.innerHeight
*
* and "device-width"/"device-height"-related CSS media query results).
* @param {Object} options
* @return {Promise<void>}
* @see https://chromedevtools.github.io/devtools-protocol/tot/Emulation#method-setDeviceMetricsOverride
* @since chrome-remote-interface-extra
*/
async setDeviceMetricsOverride (options) {
const MinWH = 0
const MaxWH = 10000000
helper.assertNumberWithin(options.width, MinWH, MaxWH, 'width')
helper.assertNumberWithin(options.height, MinWH, MaxWH, 'height')
if (options.screenWidth) {
helper.assertNumberWithin(
options.screenWidth,
MinWH,
MaxWH,
'screenWidth'
)
}
if (options.screenHeight) {
helper.assertNumberWithin(
options.screenHeight,
MinWH,
MaxWH,
'screenHeight'
)
}
if (options.positionX) {
helper.assertNumberWithin(options.positionX, MinWH, MaxWH, 'positionX')
}
if (options.positionY) {
helper.assertNumberWithin(options.positionY, MinWH, MaxWH, 'positionY')
}
if (options.screenOrientation) {
assert(
helper.isNumber(options.screenOrientation.angle),
'The screenOrientation angle should be a number'
)
assert(
helper.isString(options.screenOrientation.type),
'The screenOrientation type should be a string'
)
assert(
options.screenOrientation.type === 'portraitPrimary' ||
options.screenOrientation.type === 'portraitSecondary' ||
options.screenOrientation.type === 'landscapePrimary' ||
options.screenOrientation.type === 'landscapeSecondary',
'The screenOrientation type should be equal to one of: portraitPrimary, portraitSecondary, landscapePrimary, or landscapeSecondary'
)
}
if (options.mobile) {
assert(
helper.isBoolean(options.mobile),
`The mobile override should be a boolean value received ${typeof options.mobile}`
)
}
await this._client.send('Emulation.setDeviceMetricsOverride', options)
if (options.mobile) {
this._emulatingMobile = options.mobile
}
}
/**
* Enables touch on platforms which do not support them
* @param {boolean} enabled - Whether the touch event emulation should be enabled
* @param {number} [maxTouchPoints = 1] - Maximum touch points supported. Defaults to one
* @return {Promise<void>}
* @see https://chromedevtools.github.io/devtools-protocol/tot/Emulation#method-setTouchEmulationEnabled
* @since chrome-remote-interface-extra
*/
async setTouchEmulationEnabled (enabled, maxTouchPoints = 1) {
assert(
helper.isBoolean(enabled),
`The value of enable for setTouchEmulationEnabled should be a boolean value received ${typeof enabled}`
)
const params = { enabled }
if (maxTouchPoints) {
assert(
helper.isNumber(maxTouchPoints),
`The value of maxTouchPoints for setTouchEmulationEnabled should be a number received ${typeof maxTouchPoints}`
)
params.maxTouchPoints = maxTouchPoints
}
await this._client.send('Emulation.setTouchEmulationEnabled', params)
this._hasTouch = enabled
}
/**
* Emulates the given media for CSS media queries
* @param {string} media - Media type to emulate. Empty string disables the override
* @return {Promise<void>}
* @see https://chromedevtools.github.io/devtools-protocol/tot/Emulation#method-setEmulatedMedia
* @since chrome-remote-interface-extra
*/
async setEmulatedMedia (media) {
assert(
this._supportedMedia.has(media) || media == null,
`Unsupported media type: ${media}`
)
await this._client.send('Emulation.setEmulatedMedia', { media: media || '' })
this._emulatingMedia = media
}
/**
* Switches script execution in the page.
* @param {boolean} disabled - Whether script execution should be disabled in the page
* @return {Promise<void>}
* @see https://chromedevtools.github.io/devtools-protocol/tot/Emulation#method-setScriptExecutionDisabled
*/
async setScriptExecutionDisabled (disabled) {
assert(
helper.isBoolean(disabled),
`The value of disabled for setScriptExecutionDisabled should be a boolean value received ${typeof disabled}`
)
await this._client.send('Emulation.setScriptExecutionDisabled', {
value: disabled
})
this._scriptExecutionDisabled = disabled
}
/** @ignore */
// eslint-disable-next-line space-before-function-paren
[util.inspect.custom](depth, options) {
if (depth < 0) {
return options.stylize('[EmulationManager]', 'special')
}
const newOptions = Object.assign({}, options, {
depth: options.depth == null ? null : options.depth - 1
})
const inner = util.inspect(
{
emulatingMobile: this._emulatingMobile,
hasTouch: this._hasTouch,
emulatingMedia: this._emulatingMedia,
scriptExecutionDisabled: this._scriptExecutionDisabled
},
newOptions
)
return `${options.stylize('EmulationManager', 'special')} ${inner}`
}
}
/**
* @typedef {Object} DOMRGBA
* @property {number} r
* @property {number} g
* @property {number} b
* @property {?number} [a]
*/
/**
* @typedef {Object} Viewport
* @property {number} width
* @property {number} height
* @property {number} [deviceScaleFactor]
* @property {boolean} [isMobile]
* @property {boolean} [isLandscape]
* @property {boolean} [hasTouch]
*/
/**
* @type {EmulationManager}
*/
module.exports = EmulationManager