summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/emulation/responsive.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/emulation/responsive.js')
-rw-r--r--devtools/server/actors/emulation/responsive.js424
1 files changed, 424 insertions, 0 deletions
diff --git a/devtools/server/actors/emulation/responsive.js b/devtools/server/actors/emulation/responsive.js
new file mode 100644
index 0000000000..8e688ec760
--- /dev/null
+++ b/devtools/server/actors/emulation/responsive.js
@@ -0,0 +1,424 @@
+/* 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 Services = require("Services");
+const protocol = require("devtools/shared/protocol");
+const { responsiveSpec } = require("devtools/shared/specs/responsive");
+
+loader.lazyRequireGetter(
+ this,
+ "ScreenshotActor",
+ "devtools/server/actors/screenshot",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "TouchSimulator",
+ "devtools/server/actors/emulation/touch-simulator",
+ true
+);
+
+const FLOATING_SCROLLBARS_SHEET = Services.io.newURI(
+ "chrome://devtools/skin/floating-scrollbars-responsive-design.css"
+);
+
+/**
+ * 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 ResponsiveActor = protocol.ActorClassWithSpec(responsiveSpec, {
+ initialize(conn, targetActor) {
+ protocol.Actor.prototype.initialize.call(this, conn);
+ this.targetActor = targetActor;
+ this.docShell = targetActor.docShell;
+
+ this.onWindowReady = this.onWindowReady.bind(this);
+
+ this.targetActor.on("window-ready", this.onWindowReady);
+ },
+
+ destroy() {
+ this.clearDPPXOverride();
+ this.clearNetworkThrottling();
+ this.clearTouchEventsOverride();
+ this.clearMetaViewportOverride();
+ this.clearUserAgentOverride();
+
+ 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);
+ },
+
+ async onWindowReady() {
+ await this.setFloatingScrollbars(true);
+ },
+
+ /**
+ * 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.isDestroyed()) {
+ 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;
+ },
+
+ /* 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
+ * DevTools Server instance.
+ * When the Toolbox is running, it uses a different DevToolsServer. 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
+ * @param {String} pickerType
+ */
+ setElementPickerState(state, pickerType) {
+ this.touchSimulator.setElementPickerState(state, pickerType);
+ },
+
+ 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
+ // See BrowsingContext.webidl `TouchEventsOverride` enum for values.
+ if (flag == "enabled") {
+ this.touchSimulator.start();
+ } else {
+ this.touchSimulator.stop();
+ }
+
+ this.docShell.browsingContext.touchEventsOverride = flag;
+ return true;
+ },
+
+ getTouchEventsOverride() {
+ return this.docShell.browsingContext.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();
+ }
+ // Bug 1637494: TODO - customUserAgent should only be set from parent
+ // process.
+ this.docShell.customUserAgent = userAgent;
+ return true;
+ },
+
+ getUserAgentOverride() {
+ return this.docShell.browsingContext.customUserAgent;
+ },
+
+ clearUserAgentOverride() {
+ if (this._previousUserAgentOverride !== undefined) {
+ return this.setUserAgentOverride(this._previousUserAgentOverride);
+ }
+ return false;
+ },
+
+ setScreenOrientation(type, angle) {
+ if (
+ this.win.screen.orientation.angle !== angle ||
+ this.win.screen.orientation.type !== type
+ ) {
+ this.docShell.browsingContext.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({});
+ },
+
+ /**
+ * Applies a mobile scrollbar overlay to the content document.
+ *
+ * @param {Boolean} applyFloatingScrollbars
+ */
+ async setFloatingScrollbars(applyFloatingScrollbars) {
+ const docShell = this.docShell;
+ const allDocShells = [docShell];
+
+ for (let i = 0; i < docShell.childCount; i++) {
+ const child = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell);
+ allDocShells.push(child);
+ }
+
+ for (const d of allDocShells) {
+ const win = d.contentViewer.DOMDocument.defaultView;
+ const winUtils = win.windowUtils;
+ try {
+ if (applyFloatingScrollbars) {
+ winUtils.loadSheet(FLOATING_SCROLLBARS_SHEET, this.win.AGENT_SHEET);
+ } else {
+ winUtils.removeSheet(FLOATING_SCROLLBARS_SHEET, this.win.AGENT_SHEET);
+ }
+ } catch (e) {}
+ }
+
+ this.flushStyle();
+ },
+
+ async setMaxTouchPoints(touchSimulationEnabled) {
+ const maxTouchPoints = touchSimulationEnabled ? 1 : 0;
+ this.docShell.browsingContext.setRDMPaneMaxTouchPoints(maxTouchPoints);
+ },
+
+ flushStyle() {
+ // Force presContext destruction
+ const isSticky = this.docShell.contentViewer.sticky;
+ this.docShell.contentViewer.sticky = false;
+ this.docShell.contentViewer.hide();
+ this.docShell.contentViewer.show();
+ this.docShell.contentViewer.sticky = isSticky;
+ },
+});
+
+exports.ResponsiveActor = ResponsiveActor;