summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/emulation
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/emulation')
-rw-r--r--devtools/server/actors/emulation/content-viewer.js128
-rw-r--r--devtools/server/actors/emulation/moz.build11
-rw-r--r--devtools/server/actors/emulation/responsive.js424
-rw-r--r--devtools/server/actors/emulation/touch-simulator.js426
4 files changed, 989 insertions, 0 deletions
diff --git a/devtools/server/actors/emulation/content-viewer.js b/devtools/server/actors/emulation/content-viewer.js
new file mode 100644
index 0000000000..3bbc6eeead
--- /dev/null
+++ b/devtools/server/actors/emulation/content-viewer.js
@@ -0,0 +1,128 @@
+/* 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 { contentViewerSpec } = require("devtools/shared/specs/content-viewer");
+
+/**
+ * This actor emulates various browser content environments by using methods available
+ * on the ContentViewer exposed by the platform.
+ */
+const ContentViewerActor = protocol.ActorClassWithSpec(contentViewerSpec, {
+ 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() {
+ this.stopPrintMediaSimulation();
+ this.setEmulatedColorScheme();
+
+ this.targetActor.off("will-navigate", this.onWillNavigate);
+ this.targetActor.off("window-ready", this.onWindowReady);
+
+ this.targetActor = null;
+ this.docShell = null;
+
+ protocol.Actor.prototype.destroy.call(this);
+ },
+
+ 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();
+ }
+ },
+
+ /* Color scheme simulation */
+
+ /**
+ * Returns the currently emulated color scheme.
+ */
+ getEmulatedColorScheme() {
+ return this._emulatedColorScheme;
+ },
+
+ /**
+ * Sets the currently emulated color scheme or if an invalid value is given,
+ * the override is cleared.
+ */
+ setEmulatedColorScheme(scheme = null) {
+ if (this._emulatedColorScheme === scheme) {
+ return;
+ }
+
+ let internalColorScheme;
+ switch (scheme) {
+ case "light":
+ internalColorScheme = Ci.nsIContentViewer.PREFERS_COLOR_SCHEME_LIGHT;
+ break;
+ case "dark":
+ internalColorScheme = Ci.nsIContentViewer.PREFERS_COLOR_SCHEME_DARK;
+ break;
+ default:
+ internalColorScheme = Ci.nsIContentViewer.PREFERS_COLOR_SCHEME_NONE;
+ }
+
+ this._emulatedColorScheme = scheme;
+ this.docShell.contentViewer.emulatePrefersColorScheme(internalColorScheme);
+ },
+
+ // The current emulated color scheme value. It's possible values are listed in the
+ // COLOR_SCHEMES constant in devtools/client/inspector/rules/constants.
+ _emulatedColorScheme: null,
+
+ /* 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();
+ },
+});
+
+exports.ContentViewerActor = ContentViewerActor;
diff --git a/devtools/server/actors/emulation/moz.build b/devtools/server/actors/emulation/moz.build
new file mode 100644
index 0000000000..2ab4b0f676
--- /dev/null
+++ b/devtools/server/actors/emulation/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "content-viewer.js",
+ "responsive.js",
+ "touch-simulator.js",
+)
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;
diff --git a/devtools/server/actors/emulation/touch-simulator.js b/devtools/server/actors/emulation/touch-simulator.js
new file mode 100644
index 0000000000..dbbc5adb17
--- /dev/null
+++ b/devtools/server/actors/emulation/touch-simulator.js
@@ -0,0 +1,426 @@
+/* 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 { Services } = require("resource://gre/modules/Services.jsm");
+
+loader.lazyRequireGetter(this, "InspectorUtils", "InspectorUtils");
+loader.lazyRequireGetter(
+ this,
+ "PICKER_TYPES",
+ "devtools/shared/picker-constants"
+);
+
+var systemAppOrigin = (function() {
+ let systemOrigin = "_";
+ try {
+ systemOrigin = Services.io.newURI(
+ Services.prefs.getCharPref("b2g.system_manifest_url")
+ ).prePath;
+ } catch (e) {
+ // Fall back to default value
+ }
+ return systemOrigin;
+})();
+
+var isClickHoldEnabled = Services.prefs.getBoolPref(
+ "ui.click_hold_context_menus"
+);
+var clickHoldDelay = Services.prefs.getIntPref(
+ "ui.click_hold_context_menus.delay",
+ 500
+);
+
+// Touch state constants are derived from values defined in: nsIDOMWindowUtils.idl
+const TOUCH_CONTACT = 0x02;
+const TOUCH_REMOVE = 0x04;
+
+const TOUCH_STATES = {
+ touchstart: TOUCH_CONTACT,
+ touchmove: TOUCH_CONTACT,
+ touchend: TOUCH_REMOVE,
+};
+
+const kStateHover = 0x00000004; // NS_EVENT_STATE_HOVER
+
+function TouchSimulator(simulatorTarget) {
+ this.simulatorTarget = simulatorTarget;
+ this._currentPickerMap = new Map();
+}
+
+/**
+ * Simulate touch events for platforms where they aren't generally available.
+ */
+TouchSimulator.prototype = {
+ events: [
+ "mousedown",
+ "mousemove",
+ "mouseup",
+ "touchstart",
+ "touchend",
+ "mouseenter",
+ "mouseover",
+ "mouseout",
+ "mouseleave",
+ ],
+
+ contextMenuTimeout: null,
+
+ simulatorTarget: null,
+
+ enabled: false,
+
+ start() {
+ if (this.enabled) {
+ // Simulator is already started
+ return;
+ }
+
+ this.events.forEach(evt => {
+ // Only listen trusted events to prevent messing with
+ // event dispatched manually within content documents
+ this.simulatorTarget.addEventListener(evt, this, true, false);
+ });
+
+ this.enabled = true;
+ },
+
+ stop() {
+ if (!this.enabled) {
+ // Simulator isn't running
+ return;
+ }
+ this.events.forEach(evt => {
+ this.simulatorTarget.removeEventListener(evt, this, true);
+ });
+ this.enabled = false;
+ },
+
+ _isPicking() {
+ const types = Object.values(PICKER_TYPES);
+ return types.some(type => this._currentPickerMap.get(type));
+ },
+
+ /**
+ * Set the state value for one of DevTools pickers (either eyedropper or
+ * element picker).
+ * If any content picker is currently active, we should not be emulating
+ * touch events. Otherwise it is ok to emulate touch events.
+ * In theory only one picker can ever be active at a time, but tracking the
+ * different pickers independantly avoids race issues in the client code.
+ *
+ * @param {Boolean} state
+ * True if the picker is currently active, false otherwise.
+ * @param {String} pickerType
+ * One of PICKER_TYPES.
+ */
+ setElementPickerState(state, pickerType) {
+ if (!Object.values(PICKER_TYPES).includes(pickerType)) {
+ throw new Error(
+ "Unsupported type in setElementPickerState: " + pickerType
+ );
+ }
+ this._currentPickerMap.set(pickerType, state);
+ },
+
+ // eslint-disable-next-line complexity
+ handleEvent(evt) {
+ // Bail out if devtools is in pick mode in the same tab.
+ if (this._isPicking()) {
+ return;
+ }
+
+ // The gaia system window use an hybrid system even on the device which is
+ // a mix of mouse/touch events. So let's not cancel *all* mouse events
+ // if it is the current target.
+ const content = this.getContent(evt.target);
+ if (!content) {
+ return;
+ }
+ const isSystemWindow = content.location
+ .toString()
+ .startsWith(systemAppOrigin);
+
+ // App touchstart & touchend should also be dispatched on the system app
+ // to match on-device behavior.
+ if (evt.type.startsWith("touch") && !isSystemWindow) {
+ const sysFrame = content.realFrameElement;
+ if (!sysFrame) {
+ return;
+ }
+ const sysDocument = sysFrame.ownerDocument;
+ const sysWindow = sysDocument.defaultView;
+
+ const touchEvent = sysDocument.createEvent("touchevent");
+ const touch = evt.touches[0] || evt.changedTouches[0];
+ const point = sysDocument.createTouch(
+ sysWindow,
+ sysFrame,
+ 0,
+ touch.pageX,
+ touch.pageY,
+ touch.screenX,
+ touch.screenY,
+ touch.clientX,
+ touch.clientY,
+ 1,
+ 1,
+ 0,
+ 0
+ );
+
+ const touches = sysDocument.createTouchList(point);
+ const targetTouches = touches;
+ const changedTouches = touches;
+ touchEvent.initTouchEvent(
+ evt.type,
+ true,
+ true,
+ sysWindow,
+ 0,
+ false,
+ false,
+ false,
+ false,
+ touches,
+ targetTouches,
+ changedTouches
+ );
+ sysFrame.dispatchEvent(touchEvent);
+ return;
+ }
+
+ // Ignore all but real mouse event coming from physical mouse
+ // (especially ignore mouse event being dispatched from a touch event)
+ if (
+ evt.button ||
+ evt.mozInputSource != evt.MOZ_SOURCE_MOUSE ||
+ evt.isSynthesized
+ ) {
+ return;
+ }
+
+ const eventTarget = this.target;
+ let type = "";
+ switch (evt.type) {
+ case "mouseenter":
+ case "mouseover":
+ case "mouseout":
+ case "mouseleave":
+ // Don't propagate events which are not related to touch events
+ evt.stopPropagation();
+ evt.preventDefault();
+
+ // We don't want to trigger any visual changes to elements whose content can
+ // be modified via hover states. We can avoid this by removing the element's
+ // content state.
+ InspectorUtils.removeContentState(evt.target, kStateHover);
+ break;
+
+ case "mousedown":
+ this.target = evt.target;
+
+ // If the click-hold feature is enabled, start a timeout to convert long clicks
+ // into contextmenu events.
+ // Just don't do it if the event occurred on a scrollbar.
+ if (isClickHoldEnabled && !evt.originalTarget.closest("scrollbar")) {
+ this.contextMenuTimeout = this.sendContextMenu(evt);
+ }
+
+ this.startX = evt.pageX;
+ this.startY = evt.pageY;
+
+ // Capture events so if a different window show up the events
+ // won't be dispatched to something else.
+ evt.target.setCapture(false);
+
+ type = "touchstart";
+ break;
+
+ case "mousemove":
+ if (!eventTarget) {
+ // Don't propagate mousemove event when touchstart event isn't fired
+ evt.stopPropagation();
+ return;
+ }
+
+ type = "touchmove";
+ break;
+
+ case "mouseup":
+ if (!eventTarget) {
+ return;
+ }
+ this.target = null;
+
+ content.clearTimeout(this.contextMenuTimeout);
+ type = "touchend";
+
+ // Only register click listener after mouseup to ensure
+ // catching only real user click. (Especially ignore click
+ // being dispatched on form submit)
+ if (evt.detail == 1) {
+ this.simulatorTarget.addEventListener("click", this, {
+ capture: true,
+ once: true,
+ });
+ }
+ break;
+ }
+
+ const target = eventTarget || this.target;
+ if (target && type) {
+ this.synthesizeNativeTouch(
+ this.getContent(evt.target),
+ evt.clientX,
+ evt.clientY,
+ evt.screenX,
+ evt.screenY,
+ type
+ );
+ }
+
+ if (!isSystemWindow) {
+ evt.preventDefault();
+ evt.stopImmediatePropagation();
+ }
+ },
+
+ sendContextMenu({ target, clientX, clientY, screenX, screenY }) {
+ const view = target.ownerGlobal;
+ const { MouseEvent } = view;
+ const evt = new MouseEvent("contextmenu", {
+ bubbles: true,
+ cancelable: true,
+ view,
+ screenX,
+ screenY,
+ clientX,
+ clientY,
+ });
+ const content = this.getContent(target);
+ const timeout = content.setTimeout(() => {
+ target.dispatchEvent(evt);
+ }, clickHoldDelay);
+
+ return timeout;
+ },
+
+ /**
+ * Synthesizes a native touch action on a given target element. The `x` and `y` values
+ * passed to this function should be relative to the layout viewport (what is returned
+ * by `MouseEvent.clientX/clientY`) and are reported in CSS pixels.
+ *
+ * @param {Window} win
+ * The target window.
+ * @param {Number} x
+ * The `x` CSS coordinate relative to the layout viewport.
+ * @param {Number} y
+ * The `y` CSS coordinate relative to the layout viewport.
+ * @param {Number} screenX
+ * The `x` screen coordinate relative to the screen origin.
+ * @param {Number} screenY
+ * The `y` screen coordinate relative to the screen origin.
+ * @param {String} type
+ * A key appearing in the TOUCH_STATES associative array.
+ */
+ synthesizeNativeTouch(win, x, y, screenX, screenY, type) {
+ // Native events work in device pixels, so calculate device coordinates from
+ // the screen coordinates.
+ const utils = win.windowUtils;
+ const deviceScale = utils.screenPixelsPerCSSPixelNoOverride;
+ const pt = { x: screenX * deviceScale, y: screenY * deviceScale };
+
+ utils.sendNativeTouchPoint(0, TOUCH_STATES[type], pt.x, pt.y, 1, 90, null);
+ return true;
+ },
+
+ sendTouchEvent(evt, target, name) {
+ const win = target.ownerGlobal;
+ const content = this.getContent(target);
+ if (!content) {
+ return;
+ }
+
+ // To avoid duplicating logic for creating and dispatching touch events on the JS
+ // side, we should use what's already implemented for WindowUtils.sendTouchEvent.
+ const utils = win.windowUtils;
+ utils.sendTouchEvent(
+ name,
+ [0],
+ [evt.clientX],
+ [evt.clientY],
+ [1],
+ [1],
+ [0],
+ [1],
+ 0,
+ false
+ );
+ },
+
+ getContent(target) {
+ const win = target?.ownerDocument ? target.ownerGlobal : null;
+ return win;
+ },
+
+ getDelayBeforeMouseEvent(evt) {
+ // On mobile platforms, Firefox inserts a 300ms delay between
+ // touch events and accompanying mouse events, except if the
+ // content window is not zoomable and the content window is
+ // auto-zoomed to device-width.
+
+ // If the preference dom.meta-viewport.enabled is set to false,
+ // we couldn't read viewport's information from getViewportInfo().
+ // So we always simulate 300ms delay when the
+ // dom.meta-viewport.enabled is false.
+ const savedMetaViewportEnabled = Services.prefs.getBoolPref(
+ "dom.meta-viewport.enabled"
+ );
+ if (!savedMetaViewportEnabled) {
+ return 300;
+ }
+
+ const content = this.getContent(evt.target);
+ if (!content) {
+ return 0;
+ }
+
+ const utils = content.windowUtils;
+
+ const allowZoom = {};
+ const minZoom = {};
+ const maxZoom = {};
+ const autoSize = {};
+
+ utils.getViewportInfo(
+ content.innerWidth,
+ content.innerHeight,
+ {},
+ allowZoom,
+ minZoom,
+ maxZoom,
+ {},
+ {},
+ autoSize
+ );
+
+ // FIXME: On Safari and Chrome mobile platform, if the css property
+ // touch-action set to none or manipulation would also suppress 300ms
+ // delay. But Firefox didn't support this property now, we can't get
+ // this value from utils.getVisitedDependentComputedStyle() to check
+ // if we should suppress 300ms delay.
+ if (
+ !allowZoom.value || // user-scalable = no
+ minZoom.value === maxZoom.value || // minimum-scale = maximum-scale
+ autoSize.value // width = device-width
+ ) {
+ return 0;
+ }
+ return 300;
+ },
+};
+
+exports.TouchSimulator = TouchSimulator;