summaryrefslogtreecommitdiffstats
path: root/devtools/client/responsivedesign
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /devtools/client/responsivedesign
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'devtools/client/responsivedesign')
-rw-r--r--devtools/client/responsivedesign/moz.build11
-rw-r--r--devtools/client/responsivedesign/resize-commands.js96
-rw-r--r--devtools/client/responsivedesign/responsivedesign-child.js195
-rw-r--r--devtools/client/responsivedesign/responsivedesign.jsm1193
-rw-r--r--devtools/client/responsivedesign/test/.eslintrc.js10
-rw-r--r--devtools/client/responsivedesign/test/browser.ini21
-rw-r--r--devtools/client/responsivedesign/test/browser_responsive_cmd.js143
-rw-r--r--devtools/client/responsivedesign/test/browser_responsive_devicewidth.js68
-rw-r--r--devtools/client/responsivedesign/test/browser_responsivecomputedview.js67
-rw-r--r--devtools/client/responsivedesign/test/browser_responsiveruleview.js95
-rw-r--r--devtools/client/responsivedesign/test/browser_responsiveui.js250
-rw-r--r--devtools/client/responsivedesign/test/browser_responsiveui_customuseragent.js56
-rw-r--r--devtools/client/responsivedesign/test/browser_responsiveui_touch.js148
-rw-r--r--devtools/client/responsivedesign/test/browser_responsiveui_window_close.js25
-rw-r--r--devtools/client/responsivedesign/test/browser_responsiveuiaddcustompreset.js121
-rw-r--r--devtools/client/responsivedesign/test/head.js302
-rw-r--r--devtools/client/responsivedesign/test/touch.html85
17 files changed, 2886 insertions, 0 deletions
diff --git a/devtools/client/responsivedesign/moz.build b/devtools/client/responsivedesign/moz.build
new file mode 100644
index 000000000..215b4e8fa
--- /dev/null
+++ b/devtools/client/responsivedesign/moz.build
@@ -0,0 +1,11 @@
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+DevToolsModules(
+ 'resize-commands.js',
+ 'responsivedesign-child.js',
+ 'responsivedesign.jsm',
+)
diff --git a/devtools/client/responsivedesign/resize-commands.js b/devtools/client/responsivedesign/resize-commands.js
new file mode 100644
index 000000000..b2f884df8
--- /dev/null
+++ b/devtools/client/responsivedesign/resize-commands.js
@@ -0,0 +1,96 @@
+/* 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 { Cc, Ci, Cu } = require("chrome");
+
+loader.lazyImporter(this, "ResponsiveUIManager", "resource://devtools/client/responsivedesign/responsivedesign.jsm");
+
+const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService).
+ createBundle("chrome://branding/locale/brand.properties").
+ GetStringFromName("brandShortName");
+
+const l10n = require("gcli/l10n");
+
+exports.items = [
+ {
+ name: "resize",
+ description: l10n.lookup("resizeModeDesc")
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "resize on",
+ description: l10n.lookup("resizeModeOnDesc"),
+ manual: l10n.lookupFormat("resizeModeManual2", [BRAND_SHORT_NAME]),
+ exec: gcli_cmd_resize
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "resize off",
+ description: l10n.lookup("resizeModeOffDesc"),
+ manual: l10n.lookupFormat("resizeModeManual2", [BRAND_SHORT_NAME]),
+ exec: gcli_cmd_resize
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "resize toggle",
+ buttonId: "command-button-responsive",
+ buttonClass: "command-button command-button-invertable",
+ tooltipText: l10n.lookup("resizeModeToggleTooltip"),
+ description: l10n.lookup("resizeModeToggleDesc"),
+ manual: l10n.lookupFormat("resizeModeManual2", [BRAND_SHORT_NAME]),
+ state: {
+ isChecked: function (aTarget) {
+ if (!aTarget.tab) {
+ return false;
+ }
+ return ResponsiveUIManager.isActiveForTab(aTarget.tab);
+ },
+ onChange: function (aTarget, aChangeHandler) {
+ if (aTarget.tab) {
+ ResponsiveUIManager.on("on", aChangeHandler);
+ ResponsiveUIManager.on("off", aChangeHandler);
+ }
+ },
+ offChange: function (aTarget, aChangeHandler) {
+ // Do not check for target.tab as it may already be null during destroy
+ ResponsiveUIManager.off("on", aChangeHandler);
+ ResponsiveUIManager.off("off", aChangeHandler);
+ },
+ },
+ exec: gcli_cmd_resize
+ },
+ {
+ item: "command",
+ runAt: "client",
+ name: "resize to",
+ description: l10n.lookup("resizeModeToDesc"),
+ params: [
+ {
+ name: "width",
+ type: "number",
+ description: l10n.lookup("resizePageArgWidthDesc"),
+ },
+ {
+ name: "height",
+ type: "number",
+ description: l10n.lookup("resizePageArgHeightDesc"),
+ },
+ ],
+ exec: gcli_cmd_resize
+ }
+];
+
+function* gcli_cmd_resize(args, context) {
+ let browserWindow = context.environment.chromeWindow;
+ yield ResponsiveUIManager.handleGcliCommand(browserWindow,
+ browserWindow.gBrowser.selectedTab,
+ this.name,
+ args);
+}
diff --git a/devtools/client/responsivedesign/responsivedesign-child.js b/devtools/client/responsivedesign/responsivedesign-child.js
new file mode 100644
index 000000000..a6ce091e2
--- /dev/null
+++ b/devtools/client/responsivedesign/responsivedesign-child.js
@@ -0,0 +1,195 @@
+/* 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";
+
+/* global content, docShell, addEventListener, addMessageListener,
+ removeEventListener, removeMessageListener, sendAsyncMessage, Services */
+
+var global = this;
+
+// Guard against loading this frame script mutiple times
+(function () {
+ if (global.responsiveFrameScriptLoaded) {
+ return;
+ }
+
+ var Ci = Components.interfaces;
+ const gDeviceSizeWasPageSize = docShell.deviceSizeIsPageSize;
+ const gFloatingScrollbarsStylesheet = Services.io.newURI("chrome://devtools/skin/floating-scrollbars-responsive-design.css", null, null);
+ var gRequiresFloatingScrollbars;
+
+ var active = false;
+ var resizeNotifications = false;
+
+ addMessageListener("ResponsiveMode:Start", startResponsiveMode);
+ addMessageListener("ResponsiveMode:Stop", stopResponsiveMode);
+ addMessageListener("ResponsiveMode:IsActive", isActive);
+
+ function debug(msg) {
+ // dump(`RDM CHILD: ${msg}\n`);
+ }
+
+ /**
+ * Used by tests to verify the state of responsive mode.
+ */
+ function isActive() {
+ sendAsyncMessage("ResponsiveMode:IsActive:Done", { active });
+ }
+
+ function startResponsiveMode({data:data}) {
+ debug("START");
+ if (active) {
+ debug("ALREADY STARTED");
+ sendAsyncMessage("ResponsiveMode:Start:Done");
+ return;
+ }
+ addMessageListener("ResponsiveMode:RequestScreenshot", screenshot);
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(WebProgressListener, Ci.nsIWebProgress.NOTIFY_ALL);
+ docShell.deviceSizeIsPageSize = true;
+ gRequiresFloatingScrollbars = data.requiresFloatingScrollbars;
+ if (data.notifyOnResize) {
+ startOnResize();
+ }
+
+ // At this point, a content viewer might not be loaded for this
+ // docshell. makeScrollbarsFloating will be triggered by onLocationChange.
+ if (docShell.contentViewer) {
+ makeScrollbarsFloating();
+ }
+ active = true;
+ sendAsyncMessage("ResponsiveMode:Start:Done");
+ }
+
+ function onResize() {
+ let { width, height } = content.screen;
+ debug(`EMIT RESIZE: ${width} x ${height}`);
+ sendAsyncMessage("ResponsiveMode:OnContentResize", {
+ width,
+ height,
+ });
+ }
+
+ function bindOnResize() {
+ content.addEventListener("resize", onResize, false);
+ }
+
+ function startOnResize() {
+ debug("START ON RESIZE");
+ if (resizeNotifications) {
+ return;
+ }
+ resizeNotifications = true;
+ bindOnResize();
+ addEventListener("DOMWindowCreated", bindOnResize, false);
+ }
+
+ function stopOnResize() {
+ debug("STOP ON RESIZE");
+ if (!resizeNotifications) {
+ return;
+ }
+ resizeNotifications = false;
+ content.removeEventListener("resize", onResize, false);
+ removeEventListener("DOMWindowCreated", bindOnResize, false);
+ }
+
+ function stopResponsiveMode() {
+ debug("STOP");
+ if (!active) {
+ debug("ALREADY STOPPED, ABORT");
+ return;
+ }
+ active = false;
+ removeMessageListener("ResponsiveMode:RequestScreenshot", screenshot);
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
+ webProgress.removeProgressListener(WebProgressListener);
+ docShell.deviceSizeIsPageSize = gDeviceSizeWasPageSize;
+ restoreScrollbars();
+ stopOnResize();
+ sendAsyncMessage("ResponsiveMode:Stop:Done");
+ }
+
+ function makeScrollbarsFloating() {
+ if (!gRequiresFloatingScrollbars) {
+ return;
+ }
+
+ let allDocShells = [docShell];
+
+ for (let i = 0; i < docShell.childCount; i++) {
+ let child = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell);
+ allDocShells.push(child);
+ }
+
+ for (let d of allDocShells) {
+ let win = d.contentViewer.DOMDocument.defaultView;
+ let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ try {
+ winUtils.loadSheet(gFloatingScrollbarsStylesheet, win.AGENT_SHEET);
+ } catch (e) { }
+ }
+
+ flushStyle();
+ }
+
+ function restoreScrollbars() {
+ let allDocShells = [docShell];
+ for (let i = 0; i < docShell.childCount; i++) {
+ allDocShells.push(docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell));
+ }
+ for (let d of allDocShells) {
+ let win = d.contentViewer.DOMDocument.defaultView;
+ let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ try {
+ winUtils.removeSheet(gFloatingScrollbarsStylesheet, win.AGENT_SHEET);
+ } catch (e) { }
+ }
+ flushStyle();
+ }
+
+ function flushStyle() {
+ // Force presContext destruction
+ let isSticky = docShell.contentViewer.sticky;
+ docShell.contentViewer.sticky = false;
+ docShell.contentViewer.hide();
+ docShell.contentViewer.show();
+ docShell.contentViewer.sticky = isSticky;
+ }
+
+ function screenshot() {
+ let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ let ratio = content.devicePixelRatio;
+ let width = content.innerWidth * ratio;
+ let height = content.innerHeight * ratio;
+ canvas.mozOpaque = true;
+ canvas.width = width;
+ canvas.height = height;
+ let ctx = canvas.getContext("2d");
+ ctx.scale(ratio, ratio);
+ ctx.drawWindow(content, content.scrollX, content.scrollY, width, height, "#fff");
+ sendAsyncMessage("ResponsiveMode:RequestScreenshot:Done", canvas.toDataURL());
+ }
+
+ var WebProgressListener = {
+ onLocationChange(webProgress, request, URI, flags) {
+ if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+ return;
+ }
+ makeScrollbarsFloating();
+ },
+ QueryInterface: function QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsIWebProgressListener) ||
+ aIID.equals(Ci.nsISupportsWeakReference) ||
+ aIID.equals(Ci.nsISupports)) {
+ return this;
+ }
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ };
+})();
+
+global.responsiveFrameScriptLoaded = true;
+sendAsyncMessage("ResponsiveMode:ChildScriptReady");
diff --git a/devtools/client/responsivedesign/responsivedesign.jsm b/devtools/client/responsivedesign/responsivedesign.jsm
new file mode 100644
index 000000000..f67d1912c
--- /dev/null
+++ b/devtools/client/responsivedesign/responsivedesign.jsm
@@ -0,0 +1,1193 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+var {loader, require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var Telemetry = require("devtools/client/shared/telemetry");
+var {showDoorhanger} = require("devtools/client/shared/doorhanger");
+var {TouchEventSimulator} = require("devtools/shared/touch/simulator");
+var {Task} = require("devtools/shared/task");
+var promise = require("promise");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var flags = require("devtools/shared/flags");
+var Services = require("Services");
+var EventEmitter = require("devtools/shared/event-emitter");
+var {ViewHelpers} = require("devtools/client/shared/widgets/view-helpers");
+var { LocalizationHelper } = require("devtools/shared/l10n");
+var { EmulationFront } = require("devtools/shared/fronts/emulation");
+
+loader.lazyImporter(this, "SystemAppProxy",
+ "resource://gre/modules/SystemAppProxy.jsm");
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "DebuggerServer",
+ "devtools/server/main", true);
+
+this.EXPORTED_SYMBOLS = ["ResponsiveUIManager"];
+
+const MIN_WIDTH = 50;
+const MIN_HEIGHT = 50;
+
+const MAX_WIDTH = 10000;
+const MAX_HEIGHT = 10000;
+
+const SLOW_RATIO = 6;
+const ROUND_RATIO = 10;
+
+const INPUT_PARSER = /(\d+)[^\d]+(\d+)/;
+
+const SHARED_L10N = new LocalizationHelper("devtools/client/locales/shared.properties");
+
+function debug(msg) {
+ // dump(`RDM UI: ${msg}\n`);
+}
+
+var ActiveTabs = new Map();
+
+var Manager = {
+ /**
+ * Check if the a tab is in a responsive mode.
+ * Leave the responsive mode if active,
+ * active the responsive mode if not active.
+ *
+ * @param aWindow the main window.
+ * @param aTab the tab targeted.
+ */
+ toggle: function (aWindow, aTab) {
+ if (this.isActiveForTab(aTab)) {
+ ActiveTabs.get(aTab).close();
+ } else {
+ this.openIfNeeded(aWindow, aTab);
+ }
+ },
+
+ /**
+ * Launches the responsive mode.
+ *
+ * @param aWindow the main window.
+ * @param aTab the tab targeted.
+ * @returns {ResponsiveUI} the instance of ResponsiveUI for the current tab.
+ */
+ openIfNeeded: Task.async(function* (aWindow, aTab) {
+ let ui;
+ if (!this.isActiveForTab(aTab)) {
+ ui = new ResponsiveUI(aWindow, aTab);
+ yield ui.inited;
+ } else {
+ ui = this.getResponsiveUIForTab(aTab);
+ }
+ return ui;
+ }),
+
+ /**
+ * Returns true if responsive view is active for the provided tab.
+ *
+ * @param aTab the tab targeted.
+ */
+ isActiveForTab: function (aTab) {
+ return ActiveTabs.has(aTab);
+ },
+
+ /**
+ * Return the responsive UI controller for a tab.
+ */
+ getResponsiveUIForTab: function (aTab) {
+ return ActiveTabs.get(aTab);
+ },
+
+ /**
+ * Handle gcli commands.
+ *
+ * @param aWindow the browser window.
+ * @param aTab the tab targeted.
+ * @param aCommand the command name.
+ * @param aArgs command arguments.
+ */
+ handleGcliCommand: Task.async(function* (aWindow, aTab, aCommand, aArgs) {
+ switch (aCommand) {
+ case "resize to":
+ let ui = yield this.openIfNeeded(aWindow, aTab);
+ ui.setViewportSize(aArgs);
+ break;
+ case "resize on":
+ this.openIfNeeded(aWindow, aTab);
+ break;
+ case "resize off":
+ if (this.isActiveForTab(aTab)) {
+ yield ActiveTabs.get(aTab).close();
+ }
+ break;
+ case "resize toggle":
+ this.toggle(aWindow, aTab);
+ default:
+ }
+ })
+};
+
+EventEmitter.decorate(Manager);
+
+// If the new HTML RDM UI is enabled and e10s is enabled by default (e10s is required for
+// the new HTML RDM UI to function), delegate the ResponsiveUIManager API over to that
+// tool instead. Performing this delegation here allows us to contain the pref check to a
+// single place.
+if (Services.prefs.getBoolPref("devtools.responsive.html.enabled") &&
+ Services.appinfo.browserTabsRemoteAutostart) {
+ let { ResponsiveUIManager } =
+ require("devtools/client/responsive.html/manager");
+ this.ResponsiveUIManager = ResponsiveUIManager;
+} else {
+ this.ResponsiveUIManager = Manager;
+}
+
+var defaultPresets = [
+ // Phones
+ {key: "320x480", width: 320, height: 480}, // iPhone, B2G, with <meta viewport>
+ {key: "360x640", width: 360, height: 640}, // Android 4, phones, with <meta viewport>
+
+ // Tablets
+ {key: "768x1024", width: 768, height: 1024}, // iPad, with <meta viewport>
+ {key: "800x1280", width: 800, height: 1280}, // Android 4, Tablet, with <meta viewport>
+
+ // Default width for mobile browsers, no <meta viewport>
+ {key: "980x1280", width: 980, height: 1280},
+
+ // Computer
+ {key: "1280x600", width: 1280, height: 600},
+ {key: "1920x900", width: 1920, height: 900},
+];
+
+function ResponsiveUI(aWindow, aTab)
+{
+ this.mainWindow = aWindow;
+ this.tab = aTab;
+ this.mm = this.tab.linkedBrowser.messageManager;
+ this.tabContainer = aWindow.gBrowser.tabContainer;
+ this.browser = aTab.linkedBrowser;
+ this.chromeDoc = aWindow.document;
+ this.container = aWindow.gBrowser.getBrowserContainer(this.browser);
+ this.stack = this.container.querySelector(".browserStack");
+ this._telemetry = new Telemetry();
+
+ // Let's bind some callbacks.
+ this.bound_presetSelected = this.presetSelected.bind(this);
+ this.bound_handleManualInput = this.handleManualInput.bind(this);
+ this.bound_addPreset = this.addPreset.bind(this);
+ this.bound_removePreset = this.removePreset.bind(this);
+ this.bound_rotate = this.rotate.bind(this);
+ this.bound_screenshot = () => this.screenshot();
+ this.bound_touch = this.toggleTouch.bind(this);
+ this.bound_close = this.close.bind(this);
+ this.bound_startResizing = this.startResizing.bind(this);
+ this.bound_stopResizing = this.stopResizing.bind(this);
+ this.bound_onDrag = this.onDrag.bind(this);
+ this.bound_changeUA = this.changeUA.bind(this);
+ this.bound_onContentResize = this.onContentResize.bind(this);
+
+ this.mm.addMessageListener("ResponsiveMode:OnContentResize",
+ this.bound_onContentResize);
+
+ // We must be ready to handle window or tab close now that we have saved
+ // ourselves in ActiveTabs. Otherwise we risk leaking the window.
+ this.mainWindow.addEventListener("unload", this);
+ this.tab.addEventListener("TabClose", this);
+ this.tabContainer.addEventListener("TabSelect", this);
+
+ ActiveTabs.set(this.tab, this);
+
+ this.inited = this.init();
+}
+
+ResponsiveUI.prototype = {
+ _transitionsEnabled: true,
+ get transitionsEnabled() {
+ return this._transitionsEnabled;
+ },
+ set transitionsEnabled(aValue) {
+ this._transitionsEnabled = aValue;
+ if (aValue && !this._resizing && this.stack.hasAttribute("responsivemode")) {
+ this.stack.removeAttribute("notransition");
+ } else if (!aValue) {
+ this.stack.setAttribute("notransition", "true");
+ }
+ },
+
+ init: Task.async(function* () {
+ debug("INIT BEGINS");
+ let ready = this.waitForMessage("ResponsiveMode:ChildScriptReady");
+ this.mm.loadFrameScript("resource://devtools/client/responsivedesign/responsivedesign-child.js", true);
+ yield ready;
+
+ let requiresFloatingScrollbars =
+ !this.mainWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
+ let started = this.waitForMessage("ResponsiveMode:Start:Done");
+ debug("SEND START");
+ this.mm.sendAsyncMessage("ResponsiveMode:Start", {
+ requiresFloatingScrollbars,
+ // Tests expect events on resize to yield on various size changes
+ notifyOnResize: flags.testing,
+ });
+ yield started;
+
+ // Load Presets
+ this.loadPresets();
+
+ // Setup the UI
+ this.container.setAttribute("responsivemode", "true");
+ this.stack.setAttribute("responsivemode", "true");
+ this.buildUI();
+ this.checkMenus();
+
+ // Rotate the responsive mode if needed
+ try {
+ if (Services.prefs.getBoolPref("devtools.responsiveUI.rotate")) {
+ this.rotate();
+ }
+ } catch (e) {}
+
+ // Touch events support
+ this.touchEnableBefore = false;
+ this.touchEventSimulator = new TouchEventSimulator(this.browser);
+
+ yield this.connectToServer();
+ this.userAgentInput.hidden = false;
+
+ // Hook to display promotional Developer Edition doorhanger.
+ // Only displayed once.
+ showDoorhanger({
+ window: this.mainWindow,
+ type: "deveditionpromo",
+ anchor: this.chromeDoc.querySelector("#content")
+ });
+
+ // Notify that responsive mode is on.
+ this._telemetry.toolOpened("responsive");
+ ResponsiveUIManager.emit("on", { tab: this.tab });
+ }),
+
+ connectToServer: Task.async(function* () {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ this.client = new DebuggerClient(DebuggerServer.connectPipe());
+ yield this.client.connect();
+ let {tab} = yield this.client.getTab();
+ yield this.client.attachTab(tab.actor);
+ this.emulationFront = EmulationFront(this.client, tab);
+ }),
+
+ loadPresets: function () {
+ // Try to load presets from prefs
+ let presets = defaultPresets;
+ if (Services.prefs.prefHasUserValue("devtools.responsiveUI.presets")) {
+ try {
+ presets = JSON.parse(Services.prefs.getCharPref("devtools.responsiveUI.presets"));
+ } catch (e) {
+ // User pref is malformated.
+ console.error("Could not parse pref `devtools.responsiveUI.presets`: " + e);
+ }
+ }
+
+ this.customPreset = {key: "custom", custom: true};
+
+ if (Array.isArray(presets)) {
+ this.presets = [this.customPreset].concat(presets);
+ } else {
+ console.error("Presets value (devtools.responsiveUI.presets) is malformated.");
+ this.presets = [this.customPreset];
+ }
+
+ try {
+ let width = Services.prefs.getIntPref("devtools.responsiveUI.customWidth");
+ let height = Services.prefs.getIntPref("devtools.responsiveUI.customHeight");
+ this.customPreset.width = Math.min(MAX_WIDTH, width);
+ this.customPreset.height = Math.min(MAX_HEIGHT, height);
+
+ this.currentPresetKey = Services.prefs.getCharPref("devtools.responsiveUI.currentPreset");
+ } catch (e) {
+ // Default size. The first preset (custom) is the one that will be used.
+ let bbox = this.stack.getBoundingClientRect();
+
+ this.customPreset.width = bbox.width - 40; // horizontal padding of the container
+ this.customPreset.height = bbox.height - 80; // vertical padding + toolbar height
+
+ this.currentPresetKey = this.presets[1].key; // most common preset
+ }
+ },
+
+ /**
+ * Destroy the nodes. Remove listeners. Reset the style.
+ */
+ close: Task.async(function* () {
+ debug("CLOSE BEGINS");
+ if (this.closing) {
+ debug("ALREADY CLOSING, ABORT");
+ return;
+ }
+ this.closing = true;
+
+ // If we're closing very fast (in tests), ensure init has finished.
+ debug("CLOSE: WAIT ON INITED");
+ yield this.inited;
+ debug("CLOSE: INITED DONE");
+
+ this.unCheckMenus();
+ // Reset style of the stack.
+ debug(`CURRENT SIZE: ${this.stack.getAttribute("style")}`);
+ let style = "max-width: none;" +
+ "min-width: 0;" +
+ "max-height: none;" +
+ "min-height: 0;";
+ debug("RESET STACK SIZE");
+ this.stack.setAttribute("style", style);
+
+ // Wait for resize message before stopping in the child when testing,
+ // but only if we should expect to still get a message.
+ if (flags.testing && this.tab.linkedBrowser.messageManager) {
+ yield this.waitForMessage("ResponsiveMode:OnContentResize");
+ }
+
+ if (this.isResizing)
+ this.stopResizing();
+
+ // Remove listeners.
+ this.menulist.removeEventListener("select", this.bound_presetSelected, true);
+ this.menulist.removeEventListener("change", this.bound_handleManualInput, true);
+ this.mainWindow.removeEventListener("unload", this);
+ this.tab.removeEventListener("TabClose", this);
+ this.tabContainer.removeEventListener("TabSelect", this);
+ this.rotatebutton.removeEventListener("command", this.bound_rotate, true);
+ this.screenshotbutton.removeEventListener("command", this.bound_screenshot, true);
+ this.closebutton.removeEventListener("command", this.bound_close, true);
+ this.addbutton.removeEventListener("command", this.bound_addPreset, true);
+ this.removebutton.removeEventListener("command", this.bound_removePreset, true);
+ this.touchbutton.removeEventListener("command", this.bound_touch, true);
+ this.userAgentInput.removeEventListener("blur", this.bound_changeUA, true);
+
+ // Removed elements.
+ this.container.removeChild(this.toolbar);
+ if (this.bottomToolbar) {
+ this.bottomToolbar.remove();
+ delete this.bottomToolbar;
+ }
+ this.stack.removeChild(this.resizer);
+ this.stack.removeChild(this.resizeBarV);
+ this.stack.removeChild(this.resizeBarH);
+
+ this.stack.classList.remove("fxos-mode");
+
+ // Unset the responsive mode.
+ this.container.removeAttribute("responsivemode");
+ this.stack.removeAttribute("responsivemode");
+
+ ActiveTabs.delete(this.tab);
+ if (this.touchEventSimulator) {
+ this.touchEventSimulator.stop();
+ }
+
+ yield this.client.close();
+ this.client = this.emulationFront = null;
+
+ this._telemetry.toolClosed("responsive");
+
+ if (this.tab.linkedBrowser.messageManager) {
+ let stopped = this.waitForMessage("ResponsiveMode:Stop:Done");
+ this.tab.linkedBrowser.messageManager.sendAsyncMessage("ResponsiveMode:Stop");
+ yield stopped;
+ }
+
+ this.inited = null;
+ ResponsiveUIManager.emit("off", { tab: this.tab });
+ }),
+
+ waitForMessage(message) {
+ return new Promise(resolve => {
+ let listener = () => {
+ this.mm.removeMessageListener(message, listener);
+ resolve();
+ };
+ this.mm.addMessageListener(message, listener);
+ });
+ },
+
+ /**
+ * Emit an event when the content has been resized. Only used in tests.
+ */
+ onContentResize: function (msg) {
+ ResponsiveUIManager.emit("content-resize", {
+ tab: this.tab,
+ width: msg.data.width,
+ height: msg.data.height,
+ });
+ },
+
+ /**
+ * Handle events
+ */
+ handleEvent: function (aEvent) {
+ switch (aEvent.type) {
+ case "TabClose":
+ case "unload":
+ this.close();
+ break;
+ case "TabSelect":
+ if (this.tab.selected) {
+ this.checkMenus();
+ } else if (!this.mainWindow.gBrowser.selectedTab.responsiveUI) {
+ this.unCheckMenus();
+ }
+ break;
+ }
+ },
+
+ getViewportBrowser() {
+ return this.browser;
+ },
+
+ /**
+ * Check the menu items.
+ */
+ checkMenus: function RUI_checkMenus() {
+ this.chromeDoc.getElementById("menu_responsiveUI").setAttribute("checked", "true");
+ },
+
+ /**
+ * Uncheck the menu items.
+ */
+ unCheckMenus: function RUI_unCheckMenus() {
+ let el = this.chromeDoc.getElementById("menu_responsiveUI");
+ if (el) {
+ el.setAttribute("checked", "false");
+ }
+ },
+
+ /**
+ * Build the toolbar and the resizers.
+ *
+ * <vbox class="browserContainer"> From tabbrowser.xml
+ * <toolbar class="devtools-responsiveui-toolbar">
+ * <menulist class="devtools-responsiveui-menulist"/> // presets
+ * <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="rotate"/> // rotate
+ * <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="screenshot"/> // screenshot
+ * <toolbarbutton tabindex="0" class="devtools-responsiveui-toolbarbutton" tooltiptext="Leave Responsive Design Mode"/> // close
+ * </toolbar>
+ * <stack class="browserStack"> From tabbrowser.xml
+ * <browser/>
+ * <box class="devtools-responsiveui-resizehandle" bottom="0" right="0"/>
+ * <box class="devtools-responsiveui-resizebarV" top="0" right="0"/>
+ * <box class="devtools-responsiveui-resizebarH" bottom="0" left="0"/>
+ * // Additional button in FxOS mode:
+ * <button class="devtools-responsiveui-sleep-button" />
+ * <vbox class="devtools-responsiveui-volume-buttons">
+ * <button class="devtools-responsiveui-volume-up-button" />
+ * <button class="devtools-responsiveui-volume-down-button" />
+ * </vbox>
+ * </stack>
+ * <toolbar class="devtools-responsiveui-hardware-button">
+ * <toolbarbutton class="devtools-responsiveui-home-button" />
+ * </toolbar>
+ * </vbox>
+ */
+ buildUI: function RUI_buildUI() {
+ // Toolbar
+ this.toolbar = this.chromeDoc.createElement("toolbar");
+ this.toolbar.className = "devtools-responsiveui-toolbar";
+ this.toolbar.setAttribute("fullscreentoolbar", "true");
+
+ this.menulist = this.chromeDoc.createElement("menulist");
+ this.menulist.className = "devtools-responsiveui-menulist";
+ this.menulist.setAttribute("editable", "true");
+
+ this.menulist.addEventListener("select", this.bound_presetSelected, true);
+ this.menulist.addEventListener("change", this.bound_handleManualInput, true);
+
+ this.menuitems = new Map();
+
+ let menupopup = this.chromeDoc.createElement("menupopup");
+ this.registerPresets(menupopup);
+ this.menulist.appendChild(menupopup);
+
+ this.addbutton = this.chromeDoc.createElement("menuitem");
+ this.addbutton.setAttribute("label", this.strings.GetStringFromName("responsiveUI.addPreset"));
+ this.addbutton.addEventListener("command", this.bound_addPreset, true);
+
+ this.removebutton = this.chromeDoc.createElement("menuitem");
+ this.removebutton.setAttribute("label", this.strings.GetStringFromName("responsiveUI.removePreset"));
+ this.removebutton.addEventListener("command", this.bound_removePreset, true);
+
+ menupopup.appendChild(this.chromeDoc.createElement("menuseparator"));
+ menupopup.appendChild(this.addbutton);
+ menupopup.appendChild(this.removebutton);
+
+ this.rotatebutton = this.chromeDoc.createElement("toolbarbutton");
+ this.rotatebutton.setAttribute("tabindex", "0");
+ this.rotatebutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.rotate2"));
+ this.rotatebutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-rotate";
+ this.rotatebutton.addEventListener("command", this.bound_rotate, true);
+
+ this.screenshotbutton = this.chromeDoc.createElement("toolbarbutton");
+ this.screenshotbutton.setAttribute("tabindex", "0");
+ this.screenshotbutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.screenshot"));
+ this.screenshotbutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-screenshot";
+ this.screenshotbutton.addEventListener("command", this.bound_screenshot, true);
+
+ this.closebutton = this.chromeDoc.createElement("toolbarbutton");
+ this.closebutton.setAttribute("tabindex", "0");
+ this.closebutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-close";
+ this.closebutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.close1"));
+ this.closebutton.addEventListener("command", this.bound_close, true);
+
+ this.toolbar.appendChild(this.closebutton);
+ this.toolbar.appendChild(this.menulist);
+ this.toolbar.appendChild(this.rotatebutton);
+
+ this.touchbutton = this.chromeDoc.createElement("toolbarbutton");
+ this.touchbutton.setAttribute("tabindex", "0");
+ this.touchbutton.setAttribute("tooltiptext", this.strings.GetStringFromName("responsiveUI.touch"));
+ this.touchbutton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-touch";
+ this.touchbutton.addEventListener("command", this.bound_touch, true);
+ this.toolbar.appendChild(this.touchbutton);
+
+ this.toolbar.appendChild(this.screenshotbutton);
+
+ this.userAgentInput = this.chromeDoc.createElement("textbox");
+ this.userAgentInput.className = "devtools-responsiveui-textinput";
+ this.userAgentInput.setAttribute("placeholder",
+ this.strings.GetStringFromName("responsiveUI.userAgentPlaceholder"));
+ this.userAgentInput.addEventListener("blur", this.bound_changeUA, true);
+ this.userAgentInput.hidden = true;
+ this.toolbar.appendChild(this.userAgentInput);
+
+ // Resizers
+ let resizerTooltip = this.strings.GetStringFromName("responsiveUI.resizerTooltip");
+ this.resizer = this.chromeDoc.createElement("box");
+ this.resizer.className = "devtools-responsiveui-resizehandle";
+ this.resizer.setAttribute("right", "0");
+ this.resizer.setAttribute("bottom", "0");
+ this.resizer.setAttribute("tooltiptext", resizerTooltip);
+ this.resizer.onmousedown = this.bound_startResizing;
+
+ this.resizeBarV = this.chromeDoc.createElement("box");
+ this.resizeBarV.className = "devtools-responsiveui-resizebarV";
+ this.resizeBarV.setAttribute("top", "0");
+ this.resizeBarV.setAttribute("right", "0");
+ this.resizeBarV.setAttribute("tooltiptext", resizerTooltip);
+ this.resizeBarV.onmousedown = this.bound_startResizing;
+
+ this.resizeBarH = this.chromeDoc.createElement("box");
+ this.resizeBarH.className = "devtools-responsiveui-resizebarH";
+ this.resizeBarH.setAttribute("bottom", "0");
+ this.resizeBarH.setAttribute("left", "0");
+ this.resizeBarH.setAttribute("tooltiptext", resizerTooltip);
+ this.resizeBarH.onmousedown = this.bound_startResizing;
+
+ this.container.insertBefore(this.toolbar, this.stack);
+ this.stack.appendChild(this.resizer);
+ this.stack.appendChild(this.resizeBarV);
+ this.stack.appendChild(this.resizeBarH);
+ },
+
+ // FxOS custom controls
+ buildPhoneUI: function () {
+ this.stack.classList.add("fxos-mode");
+
+ let sleepButton = this.chromeDoc.createElement("button");
+ sleepButton.className = "devtools-responsiveui-sleep-button";
+ sleepButton.setAttribute("top", 0);
+ sleepButton.setAttribute("right", 0);
+ sleepButton.addEventListener("mousedown", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "Power"});
+ });
+ sleepButton.addEventListener("mouseup", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "Power"});
+ });
+ this.stack.appendChild(sleepButton);
+
+ let volumeButtons = this.chromeDoc.createElement("vbox");
+ volumeButtons.className = "devtools-responsiveui-volume-buttons";
+ volumeButtons.setAttribute("top", 0);
+ volumeButtons.setAttribute("left", 0);
+
+ let volumeUp = this.chromeDoc.createElement("button");
+ volumeUp.className = "devtools-responsiveui-volume-up-button";
+ volumeUp.addEventListener("mousedown", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "AudioVolumeUp"});
+ });
+ volumeUp.addEventListener("mouseup", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "AudioVolumeUp"});
+ });
+
+ let volumeDown = this.chromeDoc.createElement("button");
+ volumeDown.className = "devtools-responsiveui-volume-down-button";
+ volumeDown.addEventListener("mousedown", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "AudioVolumeDown"});
+ });
+ volumeDown.addEventListener("mouseup", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "AudioVolumeDown"});
+ });
+
+ volumeButtons.appendChild(volumeUp);
+ volumeButtons.appendChild(volumeDown);
+ this.stack.appendChild(volumeButtons);
+
+ let bottomToolbar = this.chromeDoc.createElement("toolbar");
+ bottomToolbar.className = "devtools-responsiveui-hardware-buttons";
+ bottomToolbar.setAttribute("align", "center");
+ bottomToolbar.setAttribute("pack", "center");
+
+ let homeButton = this.chromeDoc.createElement("toolbarbutton");
+ homeButton.className = "devtools-responsiveui-toolbarbutton devtools-responsiveui-home-button";
+ homeButton.addEventListener("mousedown", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keydown", {key: "Home"});
+ });
+ homeButton.addEventListener("mouseup", () => {
+ SystemAppProxy.dispatchKeyboardEvent("keyup", {key: "Home"});
+ });
+ bottomToolbar.appendChild(homeButton);
+ this.bottomToolbar = bottomToolbar;
+ this.container.appendChild(bottomToolbar);
+ },
+
+ /**
+ * Validate and apply any user input on the editable menulist
+ */
+ handleManualInput: function RUI_handleManualInput() {
+ let userInput = this.menulist.inputField.value;
+ let value = INPUT_PARSER.exec(userInput);
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+
+ // In case of an invalide value, we show back the last preset
+ if (!value || value.length < 3) {
+ this.setMenuLabel(this.selectedItem, selectedPreset);
+ return;
+ }
+
+ this.rotateValue = false;
+
+ if (!selectedPreset.custom) {
+ let menuitem = this.customMenuitem;
+ this.currentPresetKey = this.customPreset.key;
+ this.menulist.selectedItem = menuitem;
+ }
+
+ let w = this.customPreset.width = parseInt(value[1], 10);
+ let h = this.customPreset.height = parseInt(value[2], 10);
+
+ this.saveCustomSize();
+ this.setViewportSize({
+ width: w,
+ height: h,
+ });
+ },
+
+ /**
+ * Build the presets list and append it to the menupopup.
+ *
+ * @param aParent menupopup.
+ */
+ registerPresets: function RUI_registerPresets(aParent) {
+ let fragment = this.chromeDoc.createDocumentFragment();
+ let doc = this.chromeDoc;
+
+ for (let preset of this.presets) {
+ let menuitem = doc.createElement("menuitem");
+ menuitem.setAttribute("ispreset", true);
+ this.menuitems.set(menuitem, preset);
+
+ if (preset.key === this.currentPresetKey) {
+ menuitem.setAttribute("selected", "true");
+ this.selectedItem = menuitem;
+ }
+
+ if (preset.custom) {
+ this.customMenuitem = menuitem;
+ }
+
+ this.setMenuLabel(menuitem, preset);
+ fragment.appendChild(menuitem);
+ }
+ aParent.appendChild(fragment);
+ },
+
+ /**
+ * Set the menuitem label of a preset.
+ *
+ * @param aMenuitem menuitem to edit.
+ * @param aPreset associated preset.
+ */
+ setMenuLabel: function RUI_setMenuLabel(aMenuitem, aPreset) {
+ let size = SHARED_L10N.getFormatStr("dimensions",
+ Math.round(aPreset.width), Math.round(aPreset.height));
+
+ // .inputField might be not reachable yet (async XBL loading)
+ if (this.menulist.inputField) {
+ this.menulist.inputField.value = size;
+ }
+
+ if (aPreset.custom) {
+ size = this.strings.formatStringFromName("responsiveUI.customResolution", [size], 1);
+ } else if (aPreset.name != null && aPreset.name !== "") {
+ size = this.strings.formatStringFromName("responsiveUI.namedResolution", [size, aPreset.name], 2);
+ }
+
+ aMenuitem.setAttribute("label", size);
+ },
+
+ /**
+ * When a preset is selected, apply it.
+ */
+ presetSelected: function RUI_presetSelected() {
+ if (this.menulist.selectedItem.getAttribute("ispreset") === "true") {
+ this.selectedItem = this.menulist.selectedItem;
+
+ this.rotateValue = false;
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+ this.loadPreset(selectedPreset);
+ this.currentPresetKey = selectedPreset.key;
+ this.saveCurrentPreset();
+
+ // Update the buttons hidden status according to the new selected preset
+ if (selectedPreset == this.customPreset) {
+ this.addbutton.hidden = false;
+ this.removebutton.hidden = true;
+ } else {
+ this.addbutton.hidden = true;
+ this.removebutton.hidden = false;
+ }
+ }
+ },
+
+ /**
+ * Apply a preset.
+ */
+ loadPreset(preset) {
+ this.setViewportSize(preset);
+ },
+
+ /**
+ * Add a preset to the list and the memory
+ */
+ addPreset: function RUI_addPreset() {
+ let w = this.customPreset.width;
+ let h = this.customPreset.height;
+ let newName = {};
+
+ let title = this.strings.GetStringFromName("responsiveUI.customNamePromptTitle1");
+ let message = this.strings.formatStringFromName("responsiveUI.customNamePromptMsg", [w, h], 2);
+ let promptOk = Services.prompt.prompt(null, title, message, newName, null, {});
+
+ if (!promptOk) {
+ // Prompt has been cancelled
+ this.menulist.selectedItem = this.selectedItem;
+ return;
+ }
+
+ let newPreset = {
+ key: w + "x" + h,
+ name: newName.value,
+ width: w,
+ height: h
+ };
+
+ this.presets.push(newPreset);
+
+ // Sort the presets according to width/height ascending order
+ this.presets.sort(function RUI_sortPresets(aPresetA, aPresetB) {
+ // We keep custom preset at first
+ if (aPresetA.custom && !aPresetB.custom) {
+ return 1;
+ }
+ if (!aPresetA.custom && aPresetB.custom) {
+ return -1;
+ }
+
+ if (aPresetA.width === aPresetB.width) {
+ if (aPresetA.height === aPresetB.height) {
+ return 0;
+ } else {
+ return aPresetA.height > aPresetB.height;
+ }
+ } else {
+ return aPresetA.width > aPresetB.width;
+ }
+ });
+
+ this.savePresets();
+
+ let newMenuitem = this.chromeDoc.createElement("menuitem");
+ newMenuitem.setAttribute("ispreset", true);
+ this.setMenuLabel(newMenuitem, newPreset);
+
+ this.menuitems.set(newMenuitem, newPreset);
+ let idx = this.presets.indexOf(newPreset);
+ let beforeMenuitem = this.menulist.firstChild.childNodes[idx + 1];
+ this.menulist.firstChild.insertBefore(newMenuitem, beforeMenuitem);
+
+ this.menulist.selectedItem = newMenuitem;
+ this.currentPresetKey = newPreset.key;
+ this.saveCurrentPreset();
+ },
+
+ /**
+ * remove a preset from the list and the memory
+ */
+ removePreset: function RUI_removePreset() {
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+ let w = selectedPreset.width;
+ let h = selectedPreset.height;
+
+ this.presets.splice(this.presets.indexOf(selectedPreset), 1);
+ this.menulist.firstChild.removeChild(this.selectedItem);
+ this.menuitems.delete(this.selectedItem);
+
+ this.customPreset.width = w;
+ this.customPreset.height = h;
+ let menuitem = this.customMenuitem;
+ this.setMenuLabel(menuitem, this.customPreset);
+ this.menulist.selectedItem = menuitem;
+ this.currentPresetKey = this.customPreset.key;
+
+ this.setViewportSize({
+ width: w,
+ height: h,
+ });
+
+ this.savePresets();
+ },
+
+ /**
+ * Swap width and height.
+ */
+ rotate: function RUI_rotate() {
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+ let width = this.rotateValue ? selectedPreset.height : selectedPreset.width;
+ let height = this.rotateValue ? selectedPreset.width : selectedPreset.height;
+
+ this.setViewportSize({
+ width: height,
+ height: width,
+ });
+
+ if (selectedPreset.custom) {
+ this.saveCustomSize();
+ } else {
+ this.rotateValue = !this.rotateValue;
+ this.saveCurrentPreset();
+ }
+ },
+
+ /**
+ * Take a screenshot of the page.
+ *
+ * @param aFileName name of the screenshot file (used for tests).
+ */
+ screenshot: function RUI_screenshot(aFileName) {
+ let filename = aFileName;
+ if (!filename) {
+ let date = new Date();
+ let month = ("0" + (date.getMonth() + 1)).substr(-2, 2);
+ let day = ("0" + date.getDate()).substr(-2, 2);
+ let dateString = [date.getFullYear(), month, day].join("-");
+ let timeString = date.toTimeString().replace(/:/g, ".").split(" ")[0];
+ filename = this.strings.formatStringFromName("responsiveUI.screenshotGeneratedFilename", [dateString, timeString], 2);
+ }
+ let mm = this.tab.linkedBrowser.messageManager;
+ let chromeWindow = this.chromeDoc.defaultView;
+ let doc = chromeWindow.document;
+ function onScreenshot(aMessage) {
+ mm.removeMessageListener("ResponsiveMode:RequestScreenshot:Done", onScreenshot);
+ chromeWindow.saveURL(aMessage.data, filename + ".png", null, true, true, doc.documentURIObject, doc);
+ }
+ mm.addMessageListener("ResponsiveMode:RequestScreenshot:Done", onScreenshot);
+ mm.sendAsyncMessage("ResponsiveMode:RequestScreenshot");
+ },
+
+ /**
+ * Enable/Disable mouse -> touch events translation.
+ */
+ enableTouch: function RUI_enableTouch() {
+ this.touchbutton.setAttribute("checked", "true");
+ return this.touchEventSimulator.start();
+ },
+
+ disableTouch: function RUI_disableTouch() {
+ this.touchbutton.removeAttribute("checked");
+ return this.touchEventSimulator.stop();
+ },
+
+ hideTouchNotification: function RUI_hideTouchNotification() {
+ let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser);
+ let n = nbox.getNotificationWithValue("responsive-ui-need-reload");
+ if (n) {
+ n.close();
+ }
+ },
+
+ toggleTouch: Task.async(function* () {
+ this.hideTouchNotification();
+ if (this.touchEventSimulator.enabled) {
+ this.disableTouch();
+ } else {
+ let isReloadNeeded = yield this.enableTouch();
+ if (isReloadNeeded) {
+ if (Services.prefs.getBoolPref("devtools.responsiveUI.no-reload-notification")) {
+ return;
+ }
+
+ let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser);
+
+ var buttons = [{
+ label: this.strings.GetStringFromName("responsiveUI.notificationReload"),
+ callback: () => {
+ this.browser.reload();
+ },
+ accessKey: this.strings.GetStringFromName("responsiveUI.notificationReload_accesskey"),
+ }, {
+ label: this.strings.GetStringFromName("responsiveUI.dontShowReloadNotification"),
+ callback: function () {
+ Services.prefs.setBoolPref("devtools.responsiveUI.no-reload-notification", true);
+ },
+ accessKey: this.strings.GetStringFromName("responsiveUI.dontShowReloadNotification_accesskey"),
+ }];
+
+ nbox.appendNotification(
+ this.strings.GetStringFromName("responsiveUI.needReload"),
+ "responsive-ui-need-reload",
+ null,
+ nbox.PRIORITY_INFO_LOW,
+ buttons);
+ }
+ }
+ }),
+
+ waitForReload() {
+ let navigatedDeferred = promise.defer();
+ let onNavigated = (_, { state }) => {
+ if (state != "stop") {
+ return;
+ }
+ this.client.removeListener("tabNavigated", onNavigated);
+ navigatedDeferred.resolve();
+ };
+ this.client.addListener("tabNavigated", onNavigated);
+ return navigatedDeferred.promise;
+ },
+
+ /**
+ * Change the user agent string
+ */
+ changeUA: Task.async(function* () {
+ let value = this.userAgentInput.value;
+ let changed;
+ if (value) {
+ changed = yield this.emulationFront.setUserAgentOverride(value);
+ this.userAgentInput.setAttribute("attention", "true");
+ } else {
+ changed = yield this.emulationFront.clearUserAgentOverride();
+ this.userAgentInput.removeAttribute("attention");
+ }
+ if (changed) {
+ let reloaded = this.waitForReload();
+ this.tab.linkedBrowser.reload();
+ yield reloaded;
+ }
+ ResponsiveUIManager.emit("userAgentChanged", { tab: this.tab });
+ }),
+
+ /**
+ * Get the current width and height.
+ */
+ getSize() {
+ let width = Number(this.stack.style.minWidth.replace("px", ""));
+ let height = Number(this.stack.style.minHeight.replace("px", ""));
+ return {
+ width,
+ height,
+ };
+ },
+
+ /**
+ * Change the size of the viewport.
+ */
+ setViewportSize({ width, height }) {
+ debug(`SET SIZE TO ${width} x ${height}`);
+ if (width) {
+ this.setWidth(width);
+ }
+ if (height) {
+ this.setHeight(height);
+ }
+ },
+
+ setWidth: function RUI_setWidth(aWidth) {
+ aWidth = Math.min(Math.max(aWidth, MIN_WIDTH), MAX_WIDTH);
+ this.stack.style.maxWidth = this.stack.style.minWidth = aWidth + "px";
+
+ if (!this.ignoreX)
+ this.resizeBarH.setAttribute("left", Math.round(aWidth / 2));
+
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+
+ if (selectedPreset.custom) {
+ selectedPreset.width = aWidth;
+ this.setMenuLabel(this.selectedItem, selectedPreset);
+ }
+ },
+
+ setHeight: function RUI_setHeight(aHeight) {
+ aHeight = Math.min(Math.max(aHeight, MIN_HEIGHT), MAX_HEIGHT);
+ this.stack.style.maxHeight = this.stack.style.minHeight = aHeight + "px";
+
+ if (!this.ignoreY)
+ this.resizeBarV.setAttribute("top", Math.round(aHeight / 2));
+
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+ if (selectedPreset.custom) {
+ selectedPreset.height = aHeight;
+ this.setMenuLabel(this.selectedItem, selectedPreset);
+ }
+ },
+ /**
+ * Start the process of resizing the browser.
+ *
+ * @param aEvent
+ */
+ startResizing: function RUI_startResizing(aEvent) {
+ let selectedPreset = this.menuitems.get(this.selectedItem);
+
+ if (!selectedPreset.custom) {
+ this.customPreset.width = this.rotateValue ? selectedPreset.height : selectedPreset.width;
+ this.customPreset.height = this.rotateValue ? selectedPreset.width : selectedPreset.height;
+
+ let menuitem = this.customMenuitem;
+ this.setMenuLabel(menuitem, this.customPreset);
+
+ this.currentPresetKey = this.customPreset.key;
+ this.menulist.selectedItem = menuitem;
+ }
+ this.mainWindow.addEventListener("mouseup", this.bound_stopResizing, true);
+ this.mainWindow.addEventListener("mousemove", this.bound_onDrag, true);
+ this.container.style.pointerEvents = "none";
+
+ this._resizing = true;
+ this.stack.setAttribute("notransition", "true");
+
+ this.lastScreenX = aEvent.screenX;
+ this.lastScreenY = aEvent.screenY;
+
+ this.ignoreY = (aEvent.target === this.resizeBarV);
+ this.ignoreX = (aEvent.target === this.resizeBarH);
+
+ this.isResizing = true;
+ },
+
+ /**
+ * Resizing on mouse move.
+ *
+ * @param aEvent
+ */
+ onDrag: function RUI_onDrag(aEvent) {
+ let shift = aEvent.shiftKey;
+ let ctrl = !aEvent.shiftKey && aEvent.ctrlKey;
+
+ let screenX = aEvent.screenX;
+ let screenY = aEvent.screenY;
+
+ let deltaX = screenX - this.lastScreenX;
+ let deltaY = screenY - this.lastScreenY;
+
+ if (this.ignoreY)
+ deltaY = 0;
+ if (this.ignoreX)
+ deltaX = 0;
+
+ if (ctrl) {
+ deltaX /= SLOW_RATIO;
+ deltaY /= SLOW_RATIO;
+ }
+
+ let width = this.customPreset.width + deltaX;
+ let height = this.customPreset.height + deltaY;
+
+ if (shift) {
+ let roundedWidth, roundedHeight;
+ roundedWidth = 10 * Math.floor(width / ROUND_RATIO);
+ roundedHeight = 10 * Math.floor(height / ROUND_RATIO);
+ screenX += roundedWidth - width;
+ screenY += roundedHeight - height;
+ width = roundedWidth;
+ height = roundedHeight;
+ }
+
+ if (width < MIN_WIDTH) {
+ width = MIN_WIDTH;
+ } else {
+ this.lastScreenX = screenX;
+ }
+
+ if (height < MIN_HEIGHT) {
+ height = MIN_HEIGHT;
+ } else {
+ this.lastScreenY = screenY;
+ }
+
+ this.setViewportSize({ width, height });
+ },
+
+ /**
+ * Stop End resizing
+ */
+ stopResizing: function RUI_stopResizing() {
+ this.container.style.pointerEvents = "auto";
+
+ this.mainWindow.removeEventListener("mouseup", this.bound_stopResizing, true);
+ this.mainWindow.removeEventListener("mousemove", this.bound_onDrag, true);
+
+ this.saveCustomSize();
+
+ delete this._resizing;
+ if (this.transitionsEnabled) {
+ this.stack.removeAttribute("notransition");
+ }
+ this.ignoreY = false;
+ this.ignoreX = false;
+ this.isResizing = false;
+ },
+
+ /**
+ * Store the custom size as a pref.
+ */
+ saveCustomSize: function RUI_saveCustomSize() {
+ Services.prefs.setIntPref("devtools.responsiveUI.customWidth", this.customPreset.width);
+ Services.prefs.setIntPref("devtools.responsiveUI.customHeight", this.customPreset.height);
+ },
+
+ /**
+ * Store the current preset as a pref.
+ */
+ saveCurrentPreset: function RUI_saveCurrentPreset() {
+ Services.prefs.setCharPref("devtools.responsiveUI.currentPreset", this.currentPresetKey);
+ Services.prefs.setBoolPref("devtools.responsiveUI.rotate", this.rotateValue);
+ },
+
+ /**
+ * Store the list of all registered presets as a pref.
+ */
+ savePresets: function RUI_savePresets() {
+ // We exclude the custom one
+ let registeredPresets = this.presets.filter(function (aPreset) {
+ return !aPreset.custom;
+ });
+
+ Services.prefs.setCharPref("devtools.responsiveUI.presets", JSON.stringify(registeredPresets));
+ },
+};
+
+loader.lazyGetter(ResponsiveUI.prototype, "strings", function () {
+ return Services.strings.createBundle("chrome://devtools/locale/responsiveUI.properties");
+});
diff --git a/devtools/client/responsivedesign/test/.eslintrc.js b/devtools/client/responsivedesign/test/.eslintrc.js
new file mode 100644
index 000000000..ba1263286
--- /dev/null
+++ b/devtools/client/responsivedesign/test/.eslintrc.js
@@ -0,0 +1,10 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js",
+ "globals": {
+ "ResponsiveUI": true,
+ "helpers": true
+ }
+};
diff --git a/devtools/client/responsivedesign/test/browser.ini b/devtools/client/responsivedesign/test/browser.ini
new file mode 100644
index 000000000..6a8f5a8d9
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+ head.js
+ touch.html
+ !/devtools/client/commandline/test/helpers.js
+ !/devtools/client/framework/test/shared-head.js
+
+[browser_responsive_cmd.js]
+[browser_responsivecomputedview.js]
+skip-if = e10s && debug # Bug 1252201 - Docshell leak on debug e10s
+[browser_responsiveruleview.js]
+skip-if = e10s && debug # Bug 1252201 - Docshell leak on debug e10s
+[browser_responsiveui.js]
+[browser_responsiveui_touch.js]
+[browser_responsiveuiaddcustompreset.js]
+[browser_responsive_devicewidth.js]
+[browser_responsiveui_customuseragent.js]
+[browser_responsiveui_window_close.js]
+skip-if = (os == 'linux') && e10s && debug # Bug 1277274
diff --git a/devtools/client/responsivedesign/test/browser_responsive_cmd.js b/devtools/client/responsivedesign/test/browser_responsive_cmd.js
new file mode 100644
index 000000000..8c8e798d0
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsive_cmd.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("destroy");
+
+function test() {
+ let manager = ResponsiveUIManager;
+ let done;
+
+ function isOpen() {
+ return gBrowser.getBrowserContainer(gBrowser.selectedBrowser)
+ .hasAttribute("responsivemode");
+ }
+
+ helpers.addTabWithToolbar("data:text/html;charset=utf-8,hi", (options) => {
+ return helpers.audit(options, [
+ {
+ setup() {
+ done = once(manager, "on");
+ return helpers.setInput(options, "resize toggle");
+ },
+ check: {
+ input: "resize toggle",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(isOpen(), "responsive mode is open");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "off");
+ return helpers.setInput(options, "resize toggle");
+ },
+ check: {
+ input: "resize toggle",
+ hints: "",
+ markup: "VVVVVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(!isOpen(), "responsive mode is closed");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "on");
+ return helpers.setInput(options, "resize on");
+ },
+ check: {
+ input: "resize on",
+ hints: "",
+ markup: "VVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(isOpen(), "responsive mode is open");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "off");
+ return helpers.setInput(options, "resize off");
+ },
+ check: {
+ input: "resize off",
+ hints: "",
+ markup: "VVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(!isOpen(), "responsive mode is closed");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "on");
+ return helpers.setInput(options, "resize to 400 400");
+ },
+ check: {
+ input: "resize to 400 400",
+ hints: "",
+ markup: "VVVVVVVVVVVVVVVVV",
+ status: "VALID",
+ args: {
+ width: { value: 400 },
+ height: { value: 400 },
+ }
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(isOpen(), "responsive mode is open");
+ }),
+ },
+ {
+ setup() {
+ done = once(manager, "off");
+ return helpers.setInput(options, "resize off");
+ },
+ check: {
+ input: "resize off",
+ hints: "",
+ markup: "VVVVVVVVVV",
+ status: "VALID"
+ },
+ exec: {
+ output: ""
+ },
+ post: Task.async(function* () {
+ yield done;
+ ok(!isOpen(), "responsive mode is closed");
+ }),
+ },
+ ]);
+ }).then(finish);
+}
diff --git a/devtools/client/responsivedesign/test/browser_responsive_devicewidth.js b/devtools/client/responsivedesign/test/browser_responsive_devicewidth.js
new file mode 100644
index 000000000..604a20783
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsive_devicewidth.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function* () {
+ let tab = yield addTab("about:logo");
+ let { rdm, manager } = yield openRDM(tab);
+ ok(rdm, "An instance of the RDM should be attached to the tab.");
+ yield setSize(rdm, manager, 110, 500);
+
+ info("Checking initial width/height properties.");
+ yield doInitialChecks();
+
+ info("Changing the RDM size");
+ yield setSize(rdm, manager, 90, 500);
+
+ info("Checking for screen props");
+ yield checkScreenProps();
+
+ info("Setting docShell.deviceSizeIsPageSize to false");
+ yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ let docShell = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ docShell.deviceSizeIsPageSize = false;
+ });
+
+ info("Checking for screen props once again.");
+ yield checkScreenProps2();
+
+ yield closeRDM(rdm);
+});
+
+function* doInitialChecks() {
+ let {innerWidth, matchesMedia} = yield grabContentInfo();
+ is(innerWidth, 110, "initial width should be 110px");
+ ok(!matchesMedia, "media query shouldn't match.");
+}
+
+function* checkScreenProps() {
+ let {matchesMedia, screen} = yield grabContentInfo();
+ ok(matchesMedia, "media query should match");
+ isnot(window.screen.width, screen.width,
+ "screen.width should not be the size of the screen.");
+ is(screen.width, 90, "screen.width should be the page width");
+ is(screen.height, 500, "screen.height should be the page height");
+}
+
+function* checkScreenProps2() {
+ let {matchesMedia, screen} = yield grabContentInfo();
+ ok(!matchesMedia, "media query should be re-evaluated.");
+ is(window.screen.width, screen.width,
+ "screen.width should be the size of the screen.");
+}
+
+function grabContentInfo() {
+ return ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ return {
+ screen: {
+ width: content.screen.width,
+ height: content.screen.height
+ },
+ innerWidth: content.innerWidth,
+ matchesMedia: content.matchMedia("(max-device-width:100px)").matches
+ };
+ });
+}
diff --git a/devtools/client/responsivedesign/test/browser_responsivecomputedview.js b/devtools/client/responsivedesign/test/browser_responsivecomputedview.js
new file mode 100644
index 000000000..eee2dbc03
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsivecomputedview.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that when the viewport is resized, the computed-view refreshes.
+
+const TEST_URI = "data:text/html;charset=utf-8,<html><style>" +
+ "div {" +
+ " width: 500px;" +
+ " height: 10px;" +
+ " background: purple;" +
+ "} " +
+ "@media screen and (max-width: 200px) {" +
+ " div { " +
+ " width: 100px;" +
+ " }" +
+ "};" +
+ "</style><div></div></html>";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+
+ info("Open the responsive design mode and set its size to 500x500 to start");
+ let { rdm, manager } = yield openRDM();
+ yield setSize(rdm, manager, 500, 500);
+
+ info("Open the inspector, computed-view and select the test node");
+ let {inspector, view} = yield openComputedView();
+ yield selectNode("div", inspector);
+
+ info("Try shrinking the viewport and checking the applied styles");
+ yield testShrink(view, inspector, rdm, manager);
+
+ info("Try growing the viewport and checking the applied styles");
+ yield testGrow(view, inspector, rdm, manager);
+
+ yield closeRDM(rdm);
+ yield closeToolbox();
+});
+
+function* testShrink(computedView, inspector, rdm, manager) {
+ is(computedWidth(computedView), "500px", "Should show 500px initially.");
+
+ let onRefresh = inspector.once("computed-view-refreshed");
+ yield setSize(rdm, manager, 100, 100);
+ yield onRefresh;
+
+ is(computedWidth(computedView), "100px", "Should be 100px after shrinking.");
+}
+
+function* testGrow(computedView, inspector, rdm, manager) {
+ let onRefresh = inspector.once("computed-view-refreshed");
+ yield setSize(rdm, manager, 500, 500);
+ yield onRefresh;
+
+ is(computedWidth(computedView), "500px", "Should be 500px after growing.");
+}
+
+function computedWidth(computedView) {
+ for (let prop of computedView.propertyViews) {
+ if (prop.name === "width") {
+ return prop.valueNode.textContent;
+ }
+ }
+ return null;
+}
diff --git a/devtools/client/responsivedesign/test/browser_responsiveruleview.js b/devtools/client/responsivedesign/test/browser_responsiveruleview.js
new file mode 100644
index 000000000..5c3698e78
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsiveruleview.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that when the viewport is resized, the rule-view refreshes.
+// Also test that ESC does open the split-console, and that the RDM menu item
+// gets updated correctly when needed.
+// TODO: split this test.
+
+const TEST_URI = "data:text/html;charset=utf-8,<html><style>" +
+ "div {" +
+ " width: 500px;" +
+ " height: 10px;" +
+ " background: purple;" +
+ "} " +
+ "@media screen and (max-width: 200px) {" +
+ " div { " +
+ " width: 100px;" +
+ " }" +
+ "};" +
+ "</style><div></div></html>";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+
+ info("Open the responsive design mode and set its size to 500x500 to start");
+ let { rdm, manager } = yield openRDM();
+ yield setSize(rdm, manager, 500, 500);
+
+ info("Open the inspector, rule-view and select the test node");
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("div", inspector);
+
+ info("Try shrinking the viewport and checking the applied styles");
+ yield testShrink(view, rdm, manager);
+
+ info("Try growing the viewport and checking the applied styles");
+ yield testGrow(view, rdm, manager);
+
+ info("Check that ESC still opens the split console");
+ yield testEscapeOpensSplitConsole(inspector);
+
+ yield closeToolbox();
+
+ info("Test the state of the RDM menu item");
+ yield testMenuItem(rdm);
+
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+});
+
+function* testShrink(ruleView, rdm, manager) {
+ is(numberOfRules(ruleView), 2, "Should have two rules initially.");
+
+ info("Resize to 100x100 and wait for the rule-view to update");
+ let onRefresh = ruleView.once("ruleview-refreshed");
+ yield setSize(rdm, manager, 100, 100);
+ yield onRefresh;
+
+ is(numberOfRules(ruleView), 3, "Should have three rules after shrinking.");
+}
+
+function* testGrow(ruleView, rdm, manager) {
+ info("Resize to 500x500 and wait for the rule-view to update");
+ let onRefresh = ruleView.once("ruleview-refreshed");
+ yield setSize(rdm, manager, 500, 500);
+ yield onRefresh;
+
+ is(numberOfRules(ruleView), 2, "Should have two rules after growing.");
+}
+
+function* testEscapeOpensSplitConsole(inspector) {
+ ok(!inspector._toolbox._splitConsole, "Console is not split.");
+
+ info("Press escape");
+ let onSplit = inspector._toolbox.once("split-console");
+ EventUtils.synthesizeKey("VK_ESCAPE", {});
+ yield onSplit;
+
+ ok(inspector._toolbox._splitConsole, "Console is split after pressing ESC.");
+}
+
+function* testMenuItem(rdm) {
+ is(document.getElementById("menu_responsiveUI").getAttribute("checked"),
+ "true", "The menu item is checked");
+
+ yield closeRDM(rdm);
+
+ is(document.getElementById("menu_responsiveUI").getAttribute("checked"),
+ "false", "The menu item is unchecked");
+}
+
+function numberOfRules(ruleView) {
+ return ruleView.element.querySelectorAll(".ruleview-code").length;
+}
diff --git a/devtools/client/responsivedesign/test/browser_responsiveui.js b/devtools/client/responsivedesign/test/browser_responsiveui.js
new file mode 100644
index 000000000..283974d0f
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsiveui.js
@@ -0,0 +1,250 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function* () {
+ let tab = yield addTab("data:text/html,mop");
+
+ let {rdm, manager} = yield openRDM(tab, "menu");
+ let container = gBrowser.getBrowserContainer();
+ is(container.getAttribute("responsivemode"), "true",
+ "Should be in responsive mode.");
+ is(document.getElementById("menu_responsiveUI").getAttribute("checked"),
+ "true", "Menu item should be checked");
+
+ ok(rdm, "An instance of the RDM should be attached to the tab.");
+
+ let originalWidth = (yield getSizing()).width;
+
+ let documentLoaded = waitForDocLoadComplete();
+ gBrowser.loadURI("data:text/html;charset=utf-8,mop" +
+ "<div style%3D'height%3A5000px'><%2Fdiv>");
+ yield documentLoaded;
+
+ let newWidth = (yield getSizing()).width;
+ is(originalWidth, newWidth, "Floating scrollbars shouldn't change the width");
+
+ yield testPresets(rdm, manager);
+
+ info("Testing mouse resizing");
+ yield testManualMouseResize(rdm, manager);
+
+ info("Testing mouse resizing with shift key");
+ yield testManualMouseResize(rdm, manager, "shift");
+
+ info("Testing mouse resizing with ctrl key");
+ yield testManualMouseResize(rdm, manager, "ctrl");
+
+ info("Testing resizing with user custom keyboard input");
+ yield testResizeUsingCustomInput(rdm, manager);
+
+ info("Testing invalid keyboard input");
+ yield testInvalidUserInput(rdm);
+
+ info("Testing rotation");
+ yield testRotate(rdm, manager);
+
+ let {width: widthBeforeClose, height: heightBeforeClose} = yield getSizing();
+
+ info("Restarting responsive mode");
+ yield closeRDM(rdm);
+
+ let resized = waitForResizeTo(manager, widthBeforeClose, heightBeforeClose);
+ ({rdm} = yield openRDM(tab, "keyboard"));
+ yield resized;
+
+ let currentSize = yield getSizing();
+ is(currentSize.width, widthBeforeClose, "width should be restored");
+ is(currentSize.height, heightBeforeClose, "height should be restored");
+
+ container = gBrowser.getBrowserContainer();
+ is(container.getAttribute("responsivemode"), "true", "In responsive mode.");
+ is(document.getElementById("menu_responsiveUI").getAttribute("checked"),
+ "true", "menu item should be checked");
+
+ let isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
+ if (!isWinXP) {
+ yield testScreenshot(rdm);
+ }
+
+ yield closeRDM(rdm);
+ is(document.getElementById("menu_responsiveUI").getAttribute("checked"),
+ "false", "menu item should be unchecked");
+});
+
+function* testPresets(rdm, manager) {
+ // Starting from length - 4 because last 3 items are not presets :
+ // the separator, the add button and the remove button
+ for (let c = rdm.menulist.firstChild.childNodes.length - 4; c >= 0; c--) {
+ let item = rdm.menulist.firstChild.childNodes[c];
+ let [width, height] = extractSizeFromString(item.getAttribute("label"));
+ yield setPresetIndex(rdm, manager, c);
+
+ let {width: contentWidth, height: contentHeight} = yield getSizing();
+ is(contentWidth, width, "preset" + c + ": the width should be changed");
+ is(contentHeight, height, "preset" + c + ": the height should be changed");
+ }
+}
+
+function* testManualMouseResize(rdm, manager, pressedKey) {
+ yield setSize(rdm, manager, 100, 100);
+
+ let {width: initialWidth, height: initialHeight} = yield getSizing();
+ is(initialWidth, 100, "Width should be reset to 100");
+ is(initialHeight, 100, "Height should be reset to 100");
+
+ let x = 2, y = 2;
+ EventUtils.synthesizeMouse(rdm.resizer, x, y, {type: "mousedown"}, window);
+
+ let mouseMoveParams = {type: "mousemove"};
+ if (pressedKey == "shift") {
+ x += 23; y += 10;
+ mouseMoveParams.shiftKey = true;
+ } else if (pressedKey == "ctrl") {
+ x += 120; y += 60;
+ mouseMoveParams.ctrlKey = true;
+ } else {
+ x += 20; y += 10;
+ }
+
+ EventUtils.synthesizeMouse(rdm.resizer, x, y, mouseMoveParams, window);
+ EventUtils.synthesizeMouse(rdm.resizer, x, y, {type: "mouseup"}, window);
+
+ yield once(manager, "content-resize");
+
+ let expectedWidth = initialWidth + 20;
+ let expectedHeight = initialHeight + 10;
+ info("initial width: " + initialWidth);
+ info("initial height: " + initialHeight);
+
+ yield verifyResize(rdm, expectedWidth, expectedHeight);
+}
+
+function* testResizeUsingCustomInput(rdm, manager) {
+ let {width: initialWidth, height: initialHeight} = yield getSizing();
+ let expectedWidth = initialWidth - 20, expectedHeight = initialHeight - 10;
+
+ let userInput = expectedWidth + " x " + expectedHeight;
+ rdm.menulist.inputField.value = "";
+ rdm.menulist.focus();
+ processStringAsKey(userInput);
+
+ // While typing, the size should not change
+ let currentSize = yield getSizing();
+ is(currentSize.width, initialWidth, "Typing shouldn't change the width");
+ is(currentSize.height, initialHeight, "Typing shouldn't change the height");
+
+ // Only the `change` event must change the size
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ yield once(manager, "content-resize");
+
+ yield verifyResize(rdm, expectedWidth, expectedHeight);
+}
+
+function* testInvalidUserInput(rdm) {
+ let {width: initialWidth, height: initialHeight} = yield getSizing();
+ let index = rdm.menulist.selectedIndex;
+ let expectedValue = initialWidth + "\u00D7" + initialHeight;
+ let expectedLabel = rdm.menulist.firstChild.firstChild.getAttribute("label");
+
+ let userInput = "I'm wrong";
+
+ rdm.menulist.inputField.value = "";
+ rdm.menulist.focus();
+ processStringAsKey(userInput);
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ let currentSize = yield getSizing();
+ is(currentSize.width, initialWidth, "Width should not change");
+ is(currentSize.height, initialHeight, "Height should not change");
+ is(rdm.menulist.selectedIndex, index, "Selected item should not change.");
+ is(rdm.menulist.value, expectedValue, "Value should be reset");
+
+ let label = rdm.menulist.firstChild.firstChild.getAttribute("label");
+ is(label, expectedLabel, "Custom menuitem's label should not change");
+}
+
+function* testRotate(rdm, manager) {
+ yield setSize(rdm, manager, 100, 200);
+
+ let {width: initialWidth, height: initialHeight} = yield getSizing();
+ rdm.rotate();
+
+ yield once(manager, "content-resize");
+
+ let newSize = yield getSizing();
+ is(newSize.width, initialHeight, "The width should now be the height.");
+ is(newSize.height, initialWidth, "The height should now be the width.");
+
+ let label = rdm.menulist.firstChild.firstChild.getAttribute("label");
+ let [width, height] = extractSizeFromString(label);
+ is(width, initialHeight, "Width in label should be updated");
+ is(height, initialWidth, "Height in label should be updated");
+}
+
+function* verifyResize(rdm, expectedWidth, expectedHeight) {
+ let currentSize = yield getSizing();
+ is(currentSize.width, expectedWidth, "Width should now change");
+ is(currentSize.height, expectedHeight, "Height should now change");
+
+ is(rdm.menulist.selectedIndex, -1, "Custom menuitem cannot be selected");
+
+ let label = rdm.menulist.firstChild.firstChild.getAttribute("label");
+ let value = rdm.menulist.value;
+ isnot(label, value,
+ "The menulist item label should be different than the menulist value");
+
+ let [width, height] = extractSizeFromString(label);
+ is(width, expectedWidth, "Width in label should be updated");
+ is(height, expectedHeight, "Height in label should be updated");
+
+ [width, height] = extractSizeFromString(value);
+ is(width, expectedWidth, "Value should be updated with new width");
+ is(height, expectedHeight, "Value should be updated with new height");
+}
+
+function* testScreenshot(rdm) {
+ info("Testing screenshot");
+ rdm.screenshot("responsiveui");
+ let {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+
+ while (true) {
+ // while(true) until we find the file.
+ // no need for a timeout, the test will get killed anyway.
+ let file = FileUtils.getFile("DfltDwnld", [ "responsiveui.png" ]);
+ if (file.exists()) {
+ ok(true, "Screenshot file exists");
+ file.remove(false);
+ break;
+ }
+ info("checking if file exists in 200ms");
+ yield wait(200);
+ }
+}
+
+function* getSizing() {
+ let browser = gBrowser.selectedBrowser;
+ let sizing = yield ContentTask.spawn(browser, {}, function* () {
+ return {
+ width: content.innerWidth,
+ height: content.innerHeight
+ };
+ });
+ return sizing;
+}
+
+function extractSizeFromString(str) {
+ let numbers = str.match(/(\d+)[^\d]*(\d+)/);
+ if (numbers) {
+ return [numbers[1], numbers[2]];
+ }
+ return [null, null];
+}
+
+function processStringAsKey(str) {
+ for (let i = 0, l = str.length; i < l; i++) {
+ EventUtils.synthesizeKey(str.charAt(i), {});
+ }
+}
diff --git a/devtools/client/responsivedesign/test/browser_responsiveui_customuseragent.js b/devtools/client/responsivedesign/test/browser_responsiveui_customuseragent.js
new file mode 100644
index 000000000..35efc4c14
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsiveui_customuseragent.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "data:text/html, Custom User Agent test";
+const DEFAULT_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
+ .getService(Ci.nsIHttpProtocolHandler)
+ .userAgent;
+const CHROME_UA = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36" +
+ " (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36";
+add_task(function* () {
+ yield addTab(TEST_URI);
+
+ let {rdm, manager} = yield openRDM();
+ yield testUserAgent(DEFAULT_UA);
+
+ info("Setting UA to " + CHROME_UA);
+ yield setUserAgent(CHROME_UA, rdm, manager);
+ yield testUserAgent(CHROME_UA);
+
+ info("Resetting UA");
+ yield setUserAgent("", rdm, manager);
+ yield testUserAgent(DEFAULT_UA);
+
+ info("Setting UA to " + CHROME_UA);
+ yield setUserAgent(CHROME_UA, rdm, manager);
+ yield testUserAgent(CHROME_UA);
+
+ info("Closing responsive mode");
+
+ yield closeRDM(rdm);
+ yield testUserAgent(DEFAULT_UA);
+});
+
+function* setUserAgent(ua, rdm, manager) {
+ let input = rdm.userAgentInput;
+ input.focus();
+ input.value = ua;
+ let onUAChanged = once(manager, "userAgentChanged");
+ input.blur();
+ yield onUAChanged;
+
+ if (ua !== "") {
+ ok(input.hasAttribute("attention"), "UA input should be highlighted");
+ } else {
+ ok(!input.hasAttribute("attention"), "UA input shouldn't be highlighted");
+ }
+}
+
+function* testUserAgent(value) {
+ let ua = yield ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+ return content.navigator.userAgent;
+ });
+ is(ua, value, `UA should be set to ${value}`);
+}
diff --git a/devtools/client/responsivedesign/test/browser_responsiveui_touch.js b/devtools/client/responsivedesign/test/browser_responsiveui_touch.js
new file mode 100644
index 000000000..c23d2dd12
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsiveui_touch.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URI = "http://mochi.test:8888/browser/devtools/client/" +
+ "responsivedesign/test/touch.html";
+const layoutReflowSynthMouseMove = "layout.reflow.synthMouseMove";
+const domViewportEnabled = "dom.meta-viewport.enabled";
+
+add_task(function* () {
+ let tab = yield addTab(TEST_URI);
+ let {rdm} = yield openRDM(tab);
+ yield pushPrefs([layoutReflowSynthMouseMove, false]);
+ yield testWithNoTouch();
+ yield rdm.enableTouch();
+ yield testWithTouch();
+ yield rdm.disableTouch();
+ yield testWithNoTouch();
+ yield closeRDM(rdm);
+});
+
+function* testWithNoTouch() {
+ let div = content.document.querySelector("div");
+ let x = 0, y = 0;
+
+ info("testWithNoTouch: Initial test parameter and mouse mouse outside div element");
+ x = -1, y = -1;
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+ div.style.transform = "none";
+ div.style.backgroundColor = "";
+
+ info("testWithNoTouch: Move mouse into the div element");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousemove", isSynthesized: false },
+ gBrowser.selectedBrowser);
+ is(div.style.backgroundColor, "red", "mouseenter or mouseover should work");
+
+ info("testWithNoTouch: Drag the div element");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousedown", isSynthesized: false },
+ gBrowser.selectedBrowser);
+ x = 100; y = 100;
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+ is(div.style.transform, "none", "touchmove shouldn't work");
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mouseup", isSynthesized: false }, gBrowser.selectedBrowser);
+
+ info("testWithNoTouch: Move mouse out of the div element");
+ x = -1; y = -1;
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+ is(div.style.backgroundColor, "blue", "mouseout or mouseleave should work");
+
+ info("testWithNoTouch: Click the div element");
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work");
+}
+
+function* testWithTouch() {
+ let div = content.document.querySelector("div");
+ let x = 0, y = 0;
+
+ info("testWithTouch: Initial test parameter and mouse mouse outside div element");
+ x = -1, y = -1;
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+ div.style.transform = "none";
+ div.style.backgroundColor = "";
+
+ info("testWithTouch: Move mouse into the div element");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousemove", isSynthesized: false },
+ gBrowser.selectedBrowser);
+ isnot(div.style.backgroundColor, "red", "mouseenter or mouseover should not work");
+
+ info("testWithTouch: Drag the div element");
+ yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousedown", isSynthesized: false },
+ gBrowser.selectedBrowser);
+ x = 100; y = 100;
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+ isnot(div.style.transform, "none", "touchmove should work");
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mouseup", isSynthesized: false }, gBrowser.selectedBrowser);
+
+ info("testWithTouch: Move mouse out of the div element");
+ x = -1; y = -1;
+ yield BrowserTestUtils.synthesizeMouse("div", x, y,
+ { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+ isnot(div.style.backgroundColor, "blue", "mouseout or mouseleave should not work");
+
+ yield testWithMetaViewportEnabled();
+ yield testWithMetaViewportDisabled();
+}
+
+function* testWithMetaViewportEnabled() {
+ yield pushPrefs([domViewportEnabled, true]);
+ let meta = content.document.querySelector("meta[name=viewport]");
+ let div = content.document.querySelector("div");
+ div.dataset.isDelay = "false";
+
+ info("testWithMetaViewportEnabled: click the div element with <meta name='viewport'>");
+ meta.content = "";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "true", "300ms delay between touch events and mouse events should work");
+
+ info("testWithMetaViewportEnabled: click the div element with <meta name='viewport' content='user-scalable=no'>");
+ meta.content = "user-scalable=no";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work");
+
+ info("testWithMetaViewportEnabled: click the div element with <meta name='viewport' content='minimum-scale=maximum-scale'>");
+ meta.content = "minimum-scale=maximum-scale";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work");
+
+ info("testWithMetaViewportEnabled: click the div element with <meta name='viewport' content='width=device-width'>");
+ meta.content = "width=device-width";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work");
+}
+
+function* testWithMetaViewportDisabled() {
+ yield pushPrefs([domViewportEnabled, false]);
+ let meta = content.document.querySelector("meta[name=viewport]");
+ let div = content.document.querySelector("div");
+ div.dataset.isDelay = "false";
+
+ info("testWithMetaViewportDisabled: click the div element with <meta name='viewport'>");
+ meta.content = "";
+ yield synthesizeClick(div);
+ is(div.dataset.isDelay, "true", "300ms delay between touch events and mouse events should work");
+}
+
+function synthesizeClick(element) {
+ let waitForClickEvent = BrowserTestUtils.waitForEvent(element, "click");
+ BrowserTestUtils.synthesizeMouseAtCenter(element, { type: "mousedown", isSynthesized: false },
+ gBrowser.selectedBrowser);
+ BrowserTestUtils.synthesizeMouseAtCenter(element, { type: "mouseup", isSynthesized: false },
+ gBrowser.selectedBrowser);
+ return waitForClickEvent;
+}
+
+function pushPrefs(...aPrefs) {
+ let deferred = promise.defer();
+ SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve);
+ return deferred.promise;
+}
diff --git a/devtools/client/responsivedesign/test/browser_responsiveui_window_close.js b/devtools/client/responsivedesign/test/browser_responsiveui_window_close.js
new file mode 100644
index 000000000..a5f890a86
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsiveui_window_close.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function* () {
+ let newWindowPromise = BrowserTestUtils.waitForNewWindow();
+ window.open("about:blank", "_blank");
+ let newWindow = yield newWindowPromise;
+
+ newWindow.focus();
+ yield once(newWindow.gBrowser, "load", true);
+
+ let tab = newWindow.gBrowser.selectedTab;
+ yield ResponsiveUIManager.openIfNeeded(newWindow, tab);
+
+ // Close the window on a tab with an active responsive design UI and
+ // wait for the UI to gracefully shutdown. This has leaked the window
+ // in the past.
+ ok(ResponsiveUIManager.isActiveForTab(tab),
+ "ResponsiveUI should be active for tab when the window is closed");
+ let offPromise = once(ResponsiveUIManager, "off");
+ yield BrowserTestUtils.closeWindow(newWindow);
+ yield offPromise;
+});
diff --git a/devtools/client/responsivedesign/test/browser_responsiveuiaddcustompreset.js b/devtools/client/responsivedesign/test/browser_responsiveuiaddcustompreset.js
new file mode 100644
index 000000000..3ab54b601
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsiveuiaddcustompreset.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(function* () {
+ let tab = yield addTab("data:text/html;charset=utf8,Test RDM custom presets");
+
+ let { rdm, manager } = yield openRDM(tab);
+
+ let oldPrompt = Services.prompt;
+ Services.prompt = {
+ value: "",
+ returnBool: true,
+ prompt: function (parent, dialogTitle, text, value, checkMsg, checkState) {
+ value.value = this.value;
+ return this.returnBool;
+ }
+ };
+
+ registerCleanupFunction(() => {
+ Services.prompt = oldPrompt;
+ });
+
+ // Is it open?
+ let container = gBrowser.getBrowserContainer();
+ is(container.getAttribute("responsivemode"), "true",
+ "Should be in responsive mode.");
+
+ ok(rdm, "RDM instance should be attached to the tab.");
+
+ // Tries to add a custom preset and cancel the prompt
+ let idx = rdm.menulist.selectedIndex;
+ let presetCount = rdm.presets.length;
+
+ Services.prompt.value = "";
+ Services.prompt.returnBool = false;
+ rdm.addbutton.doCommand();
+
+ is(idx, rdm.menulist.selectedIndex,
+ "selected item shouldn't change after add preset and cancel");
+ is(presetCount, rdm.presets.length,
+ "number of presets shouldn't change after add preset and cancel");
+
+ // Adds the custom preset with "Testing preset"
+ Services.prompt.value = "Testing preset";
+ Services.prompt.returnBool = true;
+
+ let resized = once(manager, "content-resize");
+ let customHeight = 123, customWidth = 456;
+ rdm.startResizing({});
+ rdm.setViewportSize({
+ width: customWidth,
+ height: customHeight,
+ });
+ rdm.stopResizing({});
+
+ rdm.addbutton.doCommand();
+ yield resized;
+
+ yield closeRDM(rdm);
+
+ ({rdm} = yield openRDM(tab));
+ is(container.getAttribute("responsivemode"), "true",
+ "Should be in responsive mode.");
+
+ let presetLabel = "456" + "\u00D7" + "123 (Testing preset)";
+ let customPresetIndex = yield getPresetIndex(rdm, manager, presetLabel);
+ ok(customPresetIndex >= 0, "(idx = " + customPresetIndex + ") should be the" +
+ " previously added preset in the list of items");
+
+ yield setPresetIndex(rdm, manager, customPresetIndex);
+
+ let browser = gBrowser.selectedBrowser;
+ yield ContentTask.spawn(browser, null, function* () {
+ let {innerWidth, innerHeight} = content;
+ Assert.equal(innerWidth, 456, "Selecting preset should change the width");
+ Assert.equal(innerHeight, 123, "Selecting preset should change the height");
+ });
+
+ info(`menulist count: ${rdm.menulist.itemCount}`);
+
+ rdm.removebutton.doCommand();
+
+ yield setPresetIndex(rdm, manager, 2);
+ let deletedPresetA = rdm.menulist.selectedItem.getAttribute("label");
+ rdm.removebutton.doCommand();
+
+ yield setPresetIndex(rdm, manager, 2);
+ let deletedPresetB = rdm.menulist.selectedItem.getAttribute("label");
+ rdm.removebutton.doCommand();
+
+ yield closeRDM(rdm);
+ ({rdm} = yield openRDM(tab));
+
+ customPresetIndex = yield getPresetIndex(rdm, manager, deletedPresetA);
+ is(customPresetIndex, -1,
+ "Deleted preset " + deletedPresetA + " should not be in the list anymore");
+
+ customPresetIndex = yield getPresetIndex(rdm, manager, deletedPresetB);
+ is(customPresetIndex, -1,
+ "Deleted preset " + deletedPresetB + " should not be in the list anymore");
+
+ yield closeRDM(rdm);
+});
+
+var getPresetIndex = Task.async(function* (rdm, manager, presetLabel) {
+ var testOnePreset = Task.async(function* (c) {
+ if (c == 0) {
+ return -1;
+ }
+ yield setPresetIndex(rdm, manager, c);
+
+ let item = rdm.menulist.firstChild.childNodes[c];
+ if (item.getAttribute("label") === presetLabel) {
+ return c;
+ }
+ return testOnePreset(c - 1);
+ });
+ return testOnePreset(rdm.menulist.firstChild.childNodes.length - 4);
+});
diff --git a/devtools/client/responsivedesign/test/head.js b/devtools/client/responsivedesign/test/head.js
new file mode 100644
index 000000000..3228021f6
--- /dev/null
+++ b/devtools/client/responsivedesign/test/head.js
@@ -0,0 +1,302 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+// shared-head.js handles imports, constants, and utility functions
+let sharedHeadURI = testDir + "../../../framework/test/shared-head.js";
+Services.scriptloader.loadSubScript(sharedHeadURI, this);
+
+// Import the GCLI test helper
+let gcliHelpersURI = testDir + "../../../commandline/test/helpers.js";
+Services.scriptloader.loadSubScript(gcliHelpersURI, this);
+
+flags.testing = true;
+Services.prefs.setBoolPref("devtools.responsive.html.enabled", false);
+
+registerCleanupFunction(() => {
+ flags.testing = false;
+ Services.prefs.clearUserPref("devtools.responsive.html.enabled");
+ Services.prefs.clearUserPref("devtools.responsiveUI.currentPreset");
+ Services.prefs.clearUserPref("devtools.responsiveUI.customHeight");
+ Services.prefs.clearUserPref("devtools.responsiveUI.customWidth");
+ Services.prefs.clearUserPref("devtools.responsiveUI.presets");
+ Services.prefs.clearUserPref("devtools.responsiveUI.rotate");
+});
+
+SimpleTest.requestCompleteLog();
+
+const { ResponsiveUIManager } = Cu.import("resource://devtools/client/responsivedesign/responsivedesign.jsm", {});
+
+/**
+ * Open the Responsive Design Mode
+ * @param {Tab} The browser tab to open it into (defaults to the selected tab).
+ * @param {method} The method to use to open the RDM (values: menu, keyboard)
+ * @return {rdm, manager} Returns the RUI instance and the manager
+ */
+var openRDM = Task.async(function* (tab = gBrowser.selectedTab,
+ method = "menu") {
+ let manager = ResponsiveUIManager;
+
+ let opened = once(manager, "on");
+ let resized = once(manager, "content-resize");
+ if (method == "menu") {
+ document.getElementById("menu_responsiveUI").doCommand();
+ } else {
+ synthesizeKeyFromKeyTag(document.getElementById("key_responsiveUI"));
+ }
+ yield opened;
+
+ let rdm = manager.getResponsiveUIForTab(tab);
+ rdm.transitionsEnabled = false;
+ registerCleanupFunction(() => {
+ rdm.transitionsEnabled = true;
+ });
+
+ // Wait for content to resize. This is triggered async by the preset menu
+ // auto-selecting its default entry once it's in the document.
+ yield resized;
+
+ return {rdm, manager};
+});
+
+/**
+ * Close a responsive mode instance
+ * @param {rdm} ResponsiveUI instance for the tab
+ */
+var closeRDM = Task.async(function* (rdm) {
+ let manager = ResponsiveUIManager;
+ if (!rdm) {
+ rdm = manager.getResponsiveUIForTab(gBrowser.selectedTab);
+ }
+ let closed = once(manager, "off");
+ let resized = once(manager, "content-resize");
+ rdm.close();
+ yield resized;
+ yield closed;
+});
+
+/**
+ * Open the toolbox, with the inspector tool visible.
+ * @return a promise that resolves when the inspector is ready
+ */
+var openInspector = Task.async(function* () {
+ info("Opening the inspector");
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+ let inspector, toolbox;
+
+ // Checking if the toolbox and the inspector are already loaded
+ // The inspector-updated event should only be waited for if the inspector
+ // isn't loaded yet
+ toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ inspector = toolbox.getPanel("inspector");
+ if (inspector) {
+ info("Toolbox and inspector already open");
+ return {
+ toolbox: toolbox,
+ inspector: inspector
+ };
+ }
+ }
+
+ info("Opening the toolbox");
+ toolbox = yield gDevTools.showToolbox(target, "inspector");
+ yield waitForToolboxFrameFocus(toolbox);
+ inspector = toolbox.getPanel("inspector");
+
+ info("Waiting for the inspector to update");
+ if (inspector._updateProgress) {
+ yield inspector.once("inspector-updated");
+ }
+
+ return {
+ toolbox: toolbox,
+ inspector: inspector
+ };
+});
+
+var closeToolbox = Task.async(function* () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+});
+
+/**
+ * Wait for the toolbox frame to receive focus after it loads
+ * @param {Toolbox} toolbox
+ * @return a promise that resolves when focus has been received
+ */
+function waitForToolboxFrameFocus(toolbox) {
+ info("Making sure that the toolbox's frame is focused");
+ let def = promise.defer();
+ waitForFocus(def.resolve, toolbox.win);
+ return def.promise;
+}
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the sidebar that
+ * corresponds to the given id selected
+ * @return a promise that resolves when the inspector is ready and the sidebar
+ * view is visible and ready
+ */
+var openInspectorSideBar = Task.async(function* (id) {
+ let {toolbox, inspector} = yield openInspector();
+
+ info("Selecting the " + id + " sidebar");
+ inspector.sidebar.select(id);
+
+ return {
+ toolbox: toolbox,
+ inspector: inspector,
+ view: inspector[id].view || inspector[id].computedView
+ };
+});
+
+/**
+ * Checks whether the inspector's sidebar corresponding to the given id already
+ * exists
+ * @param {InspectorPanel}
+ * @param {String}
+ * @return {Boolean}
+ */
+function hasSideBarTab(inspector, id) {
+ return !!inspector.sidebar.getWindowForTab(id);
+}
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the computed-view
+ * sidebar tab selected.
+ * @return a promise that resolves when the inspector is ready and the computed
+ * view is visible and ready
+ */
+function openComputedView() {
+ return openInspectorSideBar("computedview");
+}
+
+/**
+ * Open the toolbox, with the inspector tool visible, and the rule-view
+ * sidebar tab selected.
+ * @return a promise that resolves when the inspector is ready and the rule
+ * view is visible and ready
+ */
+function openRuleView() {
+ return openInspectorSideBar("ruleview");
+}
+
+/**
+ * Add a new test tab in the browser and load the given url.
+ * @param {String} url The url to be loaded in the new tab
+ * @return a promise that resolves to the tab object when the url is loaded
+ */
+var addTab = Task.async(function* (url) {
+ info("Adding a new tab with URL: '" + url + "'");
+
+ window.focus();
+
+ let tab = gBrowser.selectedTab = gBrowser.addTab(url);
+ let browser = tab.linkedBrowser;
+
+ yield BrowserTestUtils.browserLoaded(browser);
+ info("URL '" + url + "' loading complete");
+
+ return tab;
+});
+
+/**
+ * Waits for the next load to complete in the current browser.
+ *
+ * @return promise
+ */
+function waitForDocLoadComplete(aBrowser = gBrowser) {
+ let deferred = promise.defer();
+ let progressListener = {
+ onStateChange: function (webProgress, req, flags, status) {
+ let docStop = Ci.nsIWebProgressListener.STATE_IS_NETWORK |
+ Ci.nsIWebProgressListener.STATE_STOP;
+ info(`Saw state ${flags.toString(16)} and status ${status.toString(16)}`);
+
+ // When a load needs to be retargetted to a new process it is cancelled
+ // with NS_BINDING_ABORTED so ignore that case
+ if ((flags & docStop) == docStop && status != Cr.NS_BINDING_ABORTED) {
+ aBrowser.removeProgressListener(progressListener);
+ info("Browser loaded");
+ deferred.resolve();
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference])
+ };
+ aBrowser.addProgressListener(progressListener);
+ info("Waiting for browser load");
+ return deferred.promise;
+}
+
+/**
+ * Get the NodeFront for a node that matches a given css selector, via the
+ * protocol.
+ * @param {String|NodeFront} selector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return {Promise} Resolves to the NodeFront instance
+ */
+function getNodeFront(selector, {walker}) {
+ if (selector._form) {
+ return selector;
+ }
+ return walker.querySelector(walker.rootNode, selector);
+}
+
+/**
+ * Set the inspector's current selection to the first match of the given css
+ * selector
+ * @param {String|NodeFront} selector
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @param {String} reason Defaults to "test" which instructs the inspector not
+ * to highlight the node upon selection
+ * @return {Promise} Resolves when the inspector is updated with the new node
+ */
+var selectNode = Task.async(function* (selector, inspector, reason = "test") {
+ info("Selecting the node for '" + selector + "'");
+ let nodeFront = yield getNodeFront(selector, inspector);
+ let updated = inspector.once("inspector-updated");
+ inspector.selection.setNodeFront(nodeFront, reason);
+ yield updated;
+});
+
+function waitForResizeTo(manager, width, height) {
+ return new Promise(resolve => {
+ let onResize = (_, data) => {
+ if (data.width != width || data.height != height) {
+ return;
+ }
+ manager.off("content-resize", onResize);
+ info(`Got content-resize to ${width} x ${height}`);
+ resolve();
+ };
+ info(`Waiting for content-resize to ${width} x ${height}`);
+ manager.on("content-resize", onResize);
+ });
+}
+
+var setPresetIndex = Task.async(function* (rdm, manager, index) {
+ info(`Current preset: ${rdm.menulist.selectedIndex}, change to: ${index}`);
+ if (rdm.menulist.selectedIndex != index) {
+ let resized = once(manager, "content-resize");
+ rdm.menulist.selectedIndex = index;
+ yield resized;
+ }
+});
+
+var setSize = Task.async(function* (rdm, manager, width, height) {
+ let size = rdm.getSize();
+ info(`Current size: ${size.width} x ${size.height}, ` +
+ `set to: ${width} x ${height}`);
+ if (size.width != width || size.height != height) {
+ let resized = waitForResizeTo(manager, width, height);
+ rdm.setViewportSize({ width, height });
+ yield resized;
+ }
+});
diff --git a/devtools/client/responsivedesign/test/touch.html b/devtools/client/responsivedesign/test/touch.html
new file mode 100644
index 000000000..f93d36f6c
--- /dev/null
+++ b/devtools/client/responsivedesign/test/touch.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+
+<meta charset="utf-8" />
+<meta name="viewport" />
+<title>test</title>
+
+
+<style>
+ div {
+ border:1px solid red;
+ width: 100px; height: 100px;
+ }
+</style>
+
+<div data-is-delay="false"></div>
+
+<script>
+ var div = document.querySelector("div");
+ var initX, initY;
+ var previousEvent = "", touchendTime = 0;
+ var updatePreviousEvent = function(e){
+ previousEvent = e.type;
+ };
+
+ div.style.transform = "none";
+ div.style.backgroundColor = "";
+
+ div.addEventListener("touchstart", function(evt) {
+ var touch = evt.changedTouches[0];
+ initX = touch.pageX;
+ initY = touch.pageY;
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("touchmove", function(evt) {
+ var touch = evt.changedTouches[0];
+ var deltaX = touch.pageX - initX;
+ var deltaY = touch.pageY - initY;
+ div.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("touchend", function(evt) {
+ if (!evt.touches.length) {
+ div.style.transform = "none";
+ }
+ touchendTime = performance.now();
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mouseenter", function(evt) {
+ div.style.backgroundColor = "red";
+ updatePreviousEvent(evt);
+ }, true);
+ div.addEventListener("mouseover", function(evt) {
+ div.style.backgroundColor = "red";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mouseout", function(evt) {
+ div.style.backgroundColor = "blue";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mouseleave", function(evt) {
+ div.style.backgroundColor = "blue";
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mousedown", function(evt){
+ if (previousEvent === "touchend" && touchendTime !== 0) {
+ let now = performance.now();
+ div.dataset.isDelay = ((now - touchendTime) >= 300) ? true : false;
+ } else {
+ div.dataset.isDelay = false;
+ }
+ updatePreviousEvent(evt);
+ }, true);
+
+ div.addEventListener("mousemove", updatePreviousEvent, true);
+
+ div.addEventListener("mouseup", updatePreviousEvent, true);
+
+ div.addEventListener("click", updatePreviousEvent, true);
+</script>