зеркало из https://github.com/mozilla/gecko-dev.git
423 строки
13 KiB
JavaScript
423 строки
13 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const { Ci } = require("chrome");
|
|
const protocol = require("devtools/shared/protocol");
|
|
const { emulationSpec } = require("devtools/shared/specs/emulation");
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"ScreenshotActor",
|
|
"devtools/server/actors/screenshot",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"TouchSimulator",
|
|
"devtools/server/actors/emulation/touch-simulator",
|
|
true
|
|
);
|
|
|
|
/**
|
|
* This actor overrides various browser features to simulate different environments to
|
|
* test how pages perform under various conditions.
|
|
*
|
|
* The design below, which saves the previous value of each property before setting, is
|
|
* needed because it's possible to have multiple copies of this actor for a single page.
|
|
* When some instance of this actor changes a property, we want it to be able to restore
|
|
* that property to the way it was found before the change.
|
|
*
|
|
* A subtle aspect of the code below is that all get* methods must return non-undefined
|
|
* values, so that the absence of a previous value can be distinguished from the value for
|
|
* "no override" for each of the properties.
|
|
*/
|
|
const EmulationActor = protocol.ActorClassWithSpec(emulationSpec, {
|
|
initialize(conn, targetActor) {
|
|
protocol.Actor.prototype.initialize.call(this, conn);
|
|
this.targetActor = targetActor;
|
|
this.docShell = targetActor.docShell;
|
|
|
|
this.onWillNavigate = this.onWillNavigate.bind(this);
|
|
this.onWindowReady = this.onWindowReady.bind(this);
|
|
|
|
this.targetActor.on("will-navigate", this.onWillNavigate);
|
|
this.targetActor.on("window-ready", this.onWindowReady);
|
|
},
|
|
|
|
destroy() {
|
|
if (this._printSimulationEnabled) {
|
|
this.stopPrintMediaSimulation();
|
|
}
|
|
|
|
this.clearDPPXOverride();
|
|
this.clearNetworkThrottling();
|
|
this.clearTouchEventsOverride();
|
|
this.clearMetaViewportOverride();
|
|
this.clearUserAgentOverride();
|
|
|
|
this.targetActor.off("will-navigate", this.onWillNavigate);
|
|
this.targetActor.off("window-ready", this.onWindowReady);
|
|
|
|
this.targetActor = null;
|
|
this.docShell = null;
|
|
this._screenshotActor = null;
|
|
this._touchSimulator = null;
|
|
|
|
protocol.Actor.prototype.destroy.call(this);
|
|
},
|
|
|
|
/**
|
|
* Retrieve the console actor for this tab. This allows us to expose network throttling
|
|
* as part of emulation settings, even though it's internally connected to the network
|
|
* monitor, which for historical reasons is part of the console actor.
|
|
*/
|
|
get _consoleActor() {
|
|
if (this.targetActor.exited || !this.targetActor.actorID) {
|
|
return null;
|
|
}
|
|
const form = this.targetActor.form();
|
|
return this.conn._getOrCreateActor(form.consoleActor);
|
|
},
|
|
|
|
get screenshotActor() {
|
|
if (!this._screenshotActor) {
|
|
this._screenshotActor = new ScreenshotActor(this.conn, this.targetActor);
|
|
this.manage(this._screenshotActor);
|
|
}
|
|
|
|
return this._screenshotActor;
|
|
},
|
|
|
|
get touchSimulator() {
|
|
if (!this._touchSimulator) {
|
|
this._touchSimulator = new TouchSimulator(
|
|
this.targetActor.chromeEventHandler
|
|
);
|
|
}
|
|
|
|
return this._touchSimulator;
|
|
},
|
|
|
|
get win() {
|
|
return this.docShell.chromeEventHandler.ownerGlobal;
|
|
},
|
|
|
|
onWillNavigate({ isTopLevel }) {
|
|
// Make sure that print simulation is stopped before navigating to another page. We
|
|
// need to do this since the browser will cache the last state of the page in its
|
|
// session history.
|
|
if (this._printSimulationEnabled && isTopLevel) {
|
|
this.stopPrintMediaSimulation(true);
|
|
}
|
|
},
|
|
|
|
onWindowReady({ isTopLevel }) {
|
|
// Since `emulateMedium` only works for the current page, we need to ensure persistent
|
|
// print simulation for when the user navigates to a new page while its enabled.
|
|
// To do this, we need to tell the page to begin print simulation before the DOM
|
|
// content is available to the user:
|
|
if (this._printSimulationEnabled && isTopLevel) {
|
|
this.startPrintMediaSimulation();
|
|
}
|
|
},
|
|
|
|
/* DPPX override */
|
|
|
|
_previousDPPXOverride: undefined,
|
|
|
|
setDPPXOverride(dppx) {
|
|
if (this.getDPPXOverride() === dppx) {
|
|
return false;
|
|
}
|
|
|
|
if (this._previousDPPXOverride === undefined) {
|
|
this._previousDPPXOverride = this.getDPPXOverride();
|
|
}
|
|
|
|
this.docShell.contentViewer.overrideDPPX = dppx;
|
|
|
|
return true;
|
|
},
|
|
|
|
getDPPXOverride() {
|
|
return this.docShell.contentViewer.overrideDPPX;
|
|
},
|
|
|
|
clearDPPXOverride() {
|
|
if (this._previousDPPXOverride !== undefined) {
|
|
return this.setDPPXOverride(this._previousDPPXOverride);
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/* Network Throttling */
|
|
|
|
_previousNetworkThrottling: undefined,
|
|
|
|
/**
|
|
* Transform the RDP format into the internal format and then set network throttling.
|
|
*/
|
|
setNetworkThrottling({ downloadThroughput, uploadThroughput, latency }) {
|
|
const throttleData = {
|
|
latencyMean: latency,
|
|
latencyMax: latency,
|
|
downloadBPSMean: downloadThroughput,
|
|
downloadBPSMax: downloadThroughput,
|
|
uploadBPSMean: uploadThroughput,
|
|
uploadBPSMax: uploadThroughput,
|
|
};
|
|
return this._setNetworkThrottling(throttleData);
|
|
},
|
|
|
|
_setNetworkThrottling(throttleData) {
|
|
const current = this._getNetworkThrottling();
|
|
// Check if they are both objects or both null
|
|
let match = throttleData == current;
|
|
// If both objects, check all entries
|
|
if (match && current && throttleData) {
|
|
match = Object.entries(current).every(([k, v]) => {
|
|
return throttleData[k] === v;
|
|
});
|
|
}
|
|
if (match) {
|
|
return false;
|
|
}
|
|
|
|
if (this._previousNetworkThrottling === undefined) {
|
|
this._previousNetworkThrottling = current;
|
|
}
|
|
|
|
const consoleActor = this._consoleActor;
|
|
if (!consoleActor) {
|
|
return false;
|
|
}
|
|
consoleActor.startListeners(["NetworkActivity"]);
|
|
consoleActor.setPreferences({
|
|
"NetworkMonitor.throttleData": throttleData,
|
|
});
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Get network throttling and then transform the internal format into the RDP format.
|
|
*/
|
|
getNetworkThrottling() {
|
|
const throttleData = this._getNetworkThrottling();
|
|
if (!throttleData) {
|
|
return null;
|
|
}
|
|
const { downloadBPSMax, uploadBPSMax, latencyMax } = throttleData;
|
|
return {
|
|
downloadThroughput: downloadBPSMax,
|
|
uploadThroughput: uploadBPSMax,
|
|
latency: latencyMax,
|
|
};
|
|
},
|
|
|
|
_getNetworkThrottling() {
|
|
const consoleActor = this._consoleActor;
|
|
if (!consoleActor) {
|
|
return null;
|
|
}
|
|
const prefs = consoleActor.getPreferences(["NetworkMonitor.throttleData"]);
|
|
return prefs.preferences["NetworkMonitor.throttleData"] || null;
|
|
},
|
|
|
|
clearNetworkThrottling() {
|
|
if (this._previousNetworkThrottling !== undefined) {
|
|
return this._setNetworkThrottling(this._previousNetworkThrottling);
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/* Touch events override */
|
|
|
|
_previousTouchEventsOverride: undefined,
|
|
|
|
/**
|
|
* Set the current element picker state.
|
|
*
|
|
* True means the element picker is currently active and we should not be emulating
|
|
* touch events.
|
|
* False means the element picker is not active and it is ok to emulate touch events.
|
|
*
|
|
* This actor method is meant to be called by the DevTools front-end. The reason for
|
|
* this is the following:
|
|
* RDM is the only current consumer of the touch simulator. RDM instantiates this actor
|
|
* on its own, whether or not the Toolbox is opened. That means it does so in its own
|
|
* Debugger Server instance.
|
|
* When the Toolbox is running, it uses a different DebuggerServer. Therefore, it is not
|
|
* possible for the touch simulator to know whether the picker is active or not. This
|
|
* state has to be sent by the client code of the Toolbox to this actor.
|
|
* If a future use case arises where we want to use the touch simulator from the Toolbox
|
|
* too, then we could add code in here to detect the picker mode as described in
|
|
* https://bugzilla.mozilla.org/show_bug.cgi?id=1409085#c3
|
|
* @param {Boolean} state
|
|
*/
|
|
setElementPickerState(state) {
|
|
this.touchSimulator.setElementPickerState(state);
|
|
},
|
|
|
|
setTouchEventsOverride(flag) {
|
|
if (this.getTouchEventsOverride() == flag) {
|
|
return false;
|
|
}
|
|
if (this._previousTouchEventsOverride === undefined) {
|
|
this._previousTouchEventsOverride = this.getTouchEventsOverride();
|
|
}
|
|
|
|
// Start or stop the touch simulator depending on the override flag
|
|
if (flag == Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED) {
|
|
this.touchSimulator.start();
|
|
} else {
|
|
this.touchSimulator.stop();
|
|
}
|
|
|
|
this.docShell.touchEventsOverride = flag;
|
|
return true;
|
|
},
|
|
|
|
getTouchEventsOverride() {
|
|
return this.docShell.touchEventsOverride;
|
|
},
|
|
|
|
clearTouchEventsOverride() {
|
|
if (this._previousTouchEventsOverride !== undefined) {
|
|
return this.setTouchEventsOverride(this._previousTouchEventsOverride);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/* Meta viewport override */
|
|
|
|
_previousMetaViewportOverride: undefined,
|
|
|
|
setMetaViewportOverride(flag) {
|
|
if (this.getMetaViewportOverride() == flag) {
|
|
return false;
|
|
}
|
|
if (this._previousMetaViewportOverride === undefined) {
|
|
this._previousMetaViewportOverride = this.getMetaViewportOverride();
|
|
}
|
|
|
|
this.docShell.metaViewportOverride = flag;
|
|
return true;
|
|
},
|
|
|
|
getMetaViewportOverride() {
|
|
return this.docShell.metaViewportOverride;
|
|
},
|
|
|
|
clearMetaViewportOverride() {
|
|
if (this._previousMetaViewportOverride !== undefined) {
|
|
return this.setMetaViewportOverride(this._previousMetaViewportOverride);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/* User agent override */
|
|
|
|
_previousUserAgentOverride: undefined,
|
|
|
|
setUserAgentOverride(userAgent) {
|
|
if (this.getUserAgentOverride() == userAgent) {
|
|
return false;
|
|
}
|
|
if (this._previousUserAgentOverride === undefined) {
|
|
this._previousUserAgentOverride = this.getUserAgentOverride();
|
|
}
|
|
this.docShell.customUserAgent = userAgent;
|
|
return true;
|
|
},
|
|
|
|
getUserAgentOverride() {
|
|
return this.docShell.customUserAgent;
|
|
},
|
|
|
|
clearUserAgentOverride() {
|
|
if (this._previousUserAgentOverride !== undefined) {
|
|
return this.setUserAgentOverride(this._previousUserAgentOverride);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/* Simulating print media for the page */
|
|
|
|
_printSimulationEnabled: false,
|
|
|
|
getIsPrintSimulationEnabled() {
|
|
return this._printSimulationEnabled;
|
|
},
|
|
|
|
async startPrintMediaSimulation() {
|
|
this._printSimulationEnabled = true;
|
|
this.targetActor.docShell.contentViewer.emulateMedium("print");
|
|
},
|
|
|
|
/**
|
|
* Stop simulating print media for the current page.
|
|
*
|
|
* @param {Boolean} state
|
|
* Whether or not to set _printSimulationEnabled to false. If true, we want to
|
|
* stop simulation print media for the current page but NOT set
|
|
* _printSimulationEnabled to false. We do this specifically for the
|
|
* "will-navigate" event where we still want to continue simulating print when
|
|
* navigating to the next page. Defaults to false, meaning we want to completely
|
|
* stop print simulation.
|
|
*/
|
|
async stopPrintMediaSimulation(state = false) {
|
|
this._printSimulationEnabled = state;
|
|
this.targetActor.docShell.contentViewer.stopEmulatingMedium();
|
|
},
|
|
|
|
setScreenOrientation(type, angle) {
|
|
if (
|
|
this.win.screen.orientation.angle !== angle ||
|
|
this.win.screen.orientation.type !== type
|
|
) {
|
|
this.win.document.setRDMPaneOrientation(type, angle);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Simulates the "orientationchange" event when device screen is rotated.
|
|
*
|
|
* @param {String} type
|
|
* The orientation type of the rotated device.
|
|
* @param {Number} angle
|
|
* The rotated angle of the device.
|
|
* @param {Boolean} isViewportRotated
|
|
* Whether or not screen orientation change is a result of rotating the viewport.
|
|
* If true, then dispatch the "orientationchange" event on the content window.
|
|
*/
|
|
async simulateScreenOrientationChange(
|
|
type,
|
|
angle,
|
|
isViewportRotated = false
|
|
) {
|
|
// Don't dispatch the "orientationchange" event if orientation change is a result
|
|
// of switching to a new device, location change, or opening RDM.
|
|
if (!isViewportRotated) {
|
|
this.setScreenOrientation(type, angle);
|
|
return;
|
|
}
|
|
|
|
const { CustomEvent } = this.win;
|
|
const orientationChangeEvent = new CustomEvent("orientationchange");
|
|
|
|
this.setScreenOrientation(type, angle);
|
|
this.win.dispatchEvent(orientationChangeEvent);
|
|
},
|
|
|
|
async captureScreenshot() {
|
|
return this.screenshotActor.capture({});
|
|
},
|
|
});
|
|
|
|
exports.EmulationActor = EmulationActor;
|