Backed out changeset 6e0cb1927bfe (bug 1305777)
authorSebastian Hengst <archaeopteryx@coole-files.de>
Fri, 29 Sep 2017 10:43:36 +0200
changeset 383719 27bf9dc1839e5feb9bb59c1c554819319e39aa49
parent 383718 d9d93f156d3b3afe7a70e6b867e503c653586107
child 383720 4e494ae5b19d9280c2c228f5340fd5f2d405cc0a
push id95615
push userarchaeopteryx@coole-files.de
push dateFri, 29 Sep 2017 11:32:50 +0000
treeherdermozilla-inbound@64949972673f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1305777
milestone58.0a1
backs out6e0cb1927bfe6a686847275d19a787b83bb6c147
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out changeset 6e0cb1927bfe (bug 1305777)
.eslintignore
browser/base/content/browser.css
devtools/client/jar.mn
devtools/client/locales/en-US/responsiveUI.properties
devtools/client/moz.build
devtools/client/preferences/devtools.js
devtools/client/responsivedesign/moz.build
devtools/client/responsivedesign/responsivedesign-old.js
devtools/client/responsivedesign/test/.eslintrc.js
devtools/client/responsivedesign/test/browser.ini
devtools/client/responsivedesign/test/browser_responsive_cmd.js
devtools/client/responsivedesign/test/browser_responsive_devicewidth.js
devtools/client/responsivedesign/test/browser_responsivecomputedview.js
devtools/client/responsivedesign/test/browser_responsiveruleview.js
devtools/client/responsivedesign/test/browser_responsiveui.js
devtools/client/responsivedesign/test/browser_responsiveui_customuseragent.js
devtools/client/responsivedesign/test/browser_responsiveui_touch.js
devtools/client/responsivedesign/test/browser_responsiveui_window_close.js
devtools/client/responsivedesign/test/browser_responsiveuiaddcustompreset.js
devtools/client/responsivedesign/test/head.js
devtools/client/responsivedesign/test/touch.html
devtools/client/themes/devtools-browser.css
devtools/client/themes/images/responsivemode/responsive-horizontal-resizer.png
devtools/client/themes/images/responsivemode/responsive-horizontal-resizer@2x.png
devtools/client/themes/images/responsivemode/responsive-se-resizer.png
devtools/client/themes/images/responsivemode/responsive-se-resizer@2x.png
devtools/client/themes/images/responsivemode/responsive-vertical-resizer.png
devtools/client/themes/images/responsivemode/responsive-vertical-resizer@2x.png
devtools/client/themes/images/responsivemode/responsiveui-home.png
devtools/client/themes/images/responsivemode/responsiveui-rotate.png
devtools/client/themes/images/responsivemode/responsiveui-rotate@2x.png
devtools/client/themes/images/responsivemode/responsiveui-screenshot.png
devtools/client/themes/images/responsivemode/responsiveui-screenshot@2x.png
devtools/client/themes/images/responsivemode/responsiveui-touch.png
devtools/client/themes/images/responsivemode/responsiveui-touch@2x.png
devtools/client/themes/moz.build
devtools/client/themes/responsivedesign.css
--- a/.eslintignore
+++ b/.eslintignore
@@ -91,16 +91,17 @@ devtools/client/framework/**
 !devtools/client/framework/target*
 !devtools/client/framework/toolbox*
 devtools/client/inspector/markup/test/doc_markup_events_*.html
 devtools/client/inspector/rules/test/doc_media_queries.html
 devtools/client/memory/test/chrome/*.html
 devtools/client/performance/components/test/test_jit_optimizations_01.html
 devtools/client/projecteditor/**
 devtools/client/responsive.html/test/browser/touch.html
+devtools/client/responsivedesign/**
 devtools/client/scratchpad/**
 devtools/client/shadereditor/**
 devtools/client/shared/*.jsm
 devtools/client/shared/components/reps/reps.js
 devtools/client/shared/components/reps/test/mochitest/*.html
 !devtools/client/shared/components/reps/test/mochitest/test_reps_infinity.html
 !devtools/client/shared/components/reps/test/mochitest/test_reps_nan.html
 !devtools/client/shared/components/reps/test/mochitest/test_reps_promise.html
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1089,16 +1089,39 @@ html|*#gcli-output-frame,
 }
 
 #developer-toolbar-toolbox-button[error-count]:before {
   content: attr(error-count);
   display: -moz-box;
   -moz-box-pack: center;
 }
 
+/* Responsive Mode */
+
+.browserContainer[responsivemode] {
+  overflow: auto;
+}
+
+.devtools-responsiveui-toolbar:-moz-locale-dir(rtl) {
+  -moz-box-pack: end;
+}
+
+.browserStack[responsivemode] {
+  transition-duration: 200ms;
+  transition-timing-function: linear;
+}
+
+.browserStack[responsivemode] {
+  transition-property: min-width, max-width, min-height, max-height;
+}
+
+.browserStack[responsivemode][notransition] {
+  transition: none;
+}
+
 /* Translation */
 notification[value="translation"] {
   -moz-binding: url("chrome://browser/content/translation-infobar.xml#translationbar");
 }
 
 /** See bug 872317 for why the following rule is necessary. */
 
 #downloads-button {
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -182,16 +182,29 @@ devtools.jar:
     skin/images/play.svg (themes/images/play.svg)
     skin/images/rewind.svg (themes/images/rewind.svg)
     skin/images/debugger-step-in.svg (themes/images/debugger-step-in.svg)
     skin/images/debugger-step-out.svg (themes/images/debugger-step-out.svg)
     skin/images/debugger-step-over.svg (themes/images/debugger-step-over.svg)
     skin/images/debugger-toggleBreakpoints.svg (themes/images/debugger-toggleBreakpoints.svg)
     skin/images/tracer-icon.png (themes/images/tracer-icon.png)
     skin/images/tracer-icon@2x.png (themes/images/tracer-icon@2x.png)
+    skin/images/responsivemode/responsive-se-resizer.png (themes/images/responsivemode/responsive-se-resizer.png)
+    skin/images/responsivemode/responsive-se-resizer@2x.png (themes/images/responsivemode/responsive-se-resizer@2x.png)
+    skin/images/responsivemode/responsive-vertical-resizer.png (themes/images/responsivemode/responsive-vertical-resizer.png)
+    skin/images/responsivemode/responsive-vertical-resizer@2x.png (themes/images/responsivemode/responsive-vertical-resizer@2x.png)
+    skin/images/responsivemode/responsive-horizontal-resizer.png (themes/images/responsivemode/responsive-horizontal-resizer.png)
+    skin/images/responsivemode/responsive-horizontal-resizer@2x.png (themes/images/responsivemode/responsive-horizontal-resizer@2x.png)
+    skin/images/responsivemode/responsiveui-rotate.png (themes/images/responsivemode/responsiveui-rotate.png)
+    skin/images/responsivemode/responsiveui-rotate@2x.png (themes/images/responsivemode/responsiveui-rotate@2x.png)
+    skin/images/responsivemode/responsiveui-touch.png (themes/images/responsivemode/responsiveui-touch.png)
+    skin/images/responsivemode/responsiveui-touch@2x.png (themes/images/responsivemode/responsiveui-touch@2x.png)
+    skin/images/responsivemode/responsiveui-screenshot.png (themes/images/responsivemode/responsiveui-screenshot.png)
+    skin/images/responsivemode/responsiveui-screenshot@2x.png (themes/images/responsivemode/responsiveui-screenshot@2x.png)
+    skin/images/responsivemode/responsiveui-home.png (themes/images/responsivemode/responsiveui-home.png)
     skin/images/toggle-tools.png (themes/images/toggle-tools.png)
     skin/images/toggle-tools@2x.png (themes/images/toggle-tools@2x.png)
     skin/images/dock-bottom.svg (themes/images/dock-bottom.svg)
     skin/images/dock-side.svg (themes/images/dock-side.svg)
     skin/images/dock-undock.svg (themes/images/dock-undock.svg)
     skin/floating-scrollbars-dark-theme.css (themes/floating-scrollbars-dark-theme.css)
     skin/floating-scrollbars-responsive-design.css (themes/floating-scrollbars-responsive-design.css)
     skin/inspector.css (themes/inspector.css)
new file mode 100644
--- /dev/null
+++ b/devtools/client/locales/en-US/responsiveUI.properties
@@ -0,0 +1,78 @@
+# 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/.
+
+# LOCALIZATION NOTE These strings are used inside the Responsive Mode
+# which is available from the Web Developer sub-menu -> 'Responsive Mode'.
+#
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE  (responsiveUI.rotate2): tooltip of the rotate button.
+responsiveUI.rotate2=Rotate
+
+# LOCALIZATION NOTE  (responsiveUI.screenshot): tooltip of the screenshot button.
+responsiveUI.screenshot=Screenshot
+
+# LOCALIZATION NOTE  (responsiveUI.userAgentPlaceholder): placeholder for the user agent input.
+responsiveUI.userAgentPlaceholder=Custom User Agent
+
+# LOCALIZATION NOTE (responsiveUI.screenshotGeneratedFilename): The auto generated filename.
+# The first argument (%1$S) is the date string in yyyy-mm-dd format and the second
+# argument (%2$S) is the time string in HH.MM.SS format.
+responsiveUI.screenshotGeneratedFilename=Screen Shot %1$S at %2$S
+
+# LOCALIZATION NOTE  (responsiveUI.touch): tooltip of the touch button.
+responsiveUI.touch=Simulate touch events (page reload might be needed)
+
+# LOCALIZATION NOTE  (responsiveUI.addPreset): label of the add preset button.
+responsiveUI.addPreset=Add Preset
+
+# LOCALIZATION NOTE  (responsiveUI.removePreset): label of the remove preset button.
+responsiveUI.removePreset=Remove Preset
+
+# LOCALIZATION NOTE  (responsiveUI.customResolution): label of the first item
+# in the menulist at the beginning of the toolbar. For %S is replace with the
+# current size of the page. For example: "400x600".
+responsiveUI.customResolution=%S (custom)
+
+# LOCALIZATION NOTE  (responsiveUI.namedResolution): label of custom items with a name
+# in the menulist of the toolbar.
+# For example: "320x480 (phone)".
+responsiveUI.namedResolution=%S (%S)
+
+# LOCALIZATION NOTE  (responsiveUI.customNamePromptTitle1): prompt title when asking
+# the user to specify a name for a new custom preset.
+responsiveUI.customNamePromptTitle1=Responsive Design Mode
+
+# LOCALIZATION NOTE (responsiveUI.close1): tooltip text of the close button.
+responsiveUI.close1=Leave Responsive Design Mode
+
+# LOCALIZATION NOTE  (responsiveUI.customNamePromptMsg): prompt message when asking
+# the user to specify a name for a new custom preset.
+responsiveUI.customNamePromptMsg=Give a name to the %Sx%S preset
+
+# LOCALIZATION NOTE (responsiveUI.resizer): tooltip showed when
+# overring the resizers.
+responsiveUI.resizerTooltip=Use the Control key for more precision. Use Shift key for rounded sizes.
+
+# LOCALIZATION NOTE (responsiveUI.needReload): notification that appears
+# when touch events are enabled
+responsiveUI.needReload=If touch event listeners have been added earlier, the page needs to be reloaded.
+responsiveUI.notificationReload=Reload
+responsiveUI.notificationReload_accesskey=R
+responsiveUI.dontShowReloadNotification=Never show again
+responsiveUI.dontShowReloadNotification_accesskey=N
+
+# LOCALIZATION NOTE (responsiveUI.newVersionUserDisabled): notification that appears
+# when old RDM is displayed because the user has disabled new RDM.
+responsiveUI.newVersionUserDisabled=A new version of Responsive Design Mode is available, but it appears to be disabled. Please enable it and provide feedback, as this version will be removed.
+# LOCALIZATION NOTE (responsiveUI.newVersionE10sDisabled): notification that appears
+# when old RDM is displayed because e10s is disabled.
+responsiveUI.newVersionE10sDisabled=A new version of Responsive Design Mode is available, but it requires multi-process mode, which is currently disabled. Please enable it and provide feedback, as this version will be removed.
+# LOCALIZATION NOTE (responsiveUI.newVersionEnableAndRestart): button text in notification
+# to enable new RDM itself or e10s as a prerequisite for new RDM.
+responsiveUI.newVersionEnableAndRestart=Enable and Restart
\ No newline at end of file
--- a/devtools/client/moz.build
+++ b/devtools/client/moz.build
@@ -17,16 +17,17 @@ DIRS += [
     'inspector',
     'jsonview',
     'locales',
     'memory',
     'netmonitor',
     'performance',
     'preferences',
     'responsive.html',
+    'responsivedesign',
     'scratchpad',
     'shadereditor',
     'shared',
     'sourceeditor',
     'storage',
     'styleeditor',
     'themes',
     'webaudioeditor',
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -95,16 +95,19 @@ pref("devtools.eyedropper.zoom", 6);
 pref("devtools.markup.collapseAttributes", true);
 
 // Length to collapse attributes
 pref("devtools.markup.collapseAttributeLength", 120);
 
 // DevTools default color unit
 pref("devtools.defaultColorUnit", "authored");
 
+// Enable the Responsive UI tool
+pref("devtools.responsiveUI.no-reload-notification", false);
+
 // Enable the Memory tools
 pref("devtools.memory.enabled", true);
 
 pref("devtools.memory.custom-census-displays", "{}");
 pref("devtools.memory.custom-label-displays", "{}");
 pref("devtools.memory.custom-tree-map-displays", "{}");
 
 pref("devtools.memory.max-individuals", 1000);
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsivedesign/moz.build
@@ -0,0 +1,12 @@
+# 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(
+    'responsivedesign-old.js',
+)
+
+with Files('**'):
+    BUG_COMPONENT = ('Firefox', 'Developer Tools: Responsive Design Mode')
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsivedesign/responsivedesign-old.js
@@ -0,0 +1,1313 @@
+/* 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 { LocalizationHelper } = require("devtools/shared/l10n");
+const { Task } = require("devtools/shared/task");
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/old-event-emitter");
+
+loader.lazyImporter(this, "SystemAppProxy",
+                    "resource://gre/modules/SystemAppProxy.jsm");
+loader.lazyImporter(this, "BrowserUtils",
+                    "resource://gre/modules/BrowserUtils.jsm");
+loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
+loader.lazyRequireGetter(this, "showDoorhanger",
+                         "devtools/client/shared/doorhanger", true);
+loader.lazyRequireGetter(this, "gDevToolsBrowser",
+                         "devtools/client/framework/devtools-browser", true);
+loader.lazyRequireGetter(this, "TouchEventSimulator",
+                         "devtools/shared/touch/simulator", true);
+loader.lazyRequireGetter(this, "flags",
+                         "devtools/shared/flags");
+loader.lazyRequireGetter(this, "EmulationFront",
+                         "devtools/shared/fronts/emulation", true);
+loader.lazyRequireGetter(this, "DebuggerClient",
+                         "devtools/shared/client/main", true);
+loader.lazyRequireGetter(this, "DebuggerServer",
+                         "devtools/server/main", true);
+loader.lazyRequireGetter(this, "system", "devtools/shared/system");
+
+const NEW_RDM_ENABLED = "devtools.responsive.html.enabled";
+
+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 ResponsiveUIManager = {
+  /**
+   * Check if the a tab is in a responsive mode.
+   * Leave the responsive mode if active,
+   * active the responsive mode if not active.
+   *
+   * @param window the main window.
+   * @param tab the tab targeted.
+   */
+  toggle: function (window, tab) {
+    if (this.isActiveForTab(tab)) {
+      ActiveTabs.get(tab).close();
+    } else {
+      this.openIfNeeded(window, tab);
+    }
+  },
+
+  /**
+   * Launches the responsive mode.
+   *
+   * @param window the main window.
+   * @param tab the tab targeted.
+   * @returns {ResponsiveUI} the instance of ResponsiveUI for the current tab.
+   */
+  openIfNeeded: Task.async(function* (window, tab) {
+    let ui;
+    if (!this.isActiveForTab(tab)) {
+      ui = new ResponsiveUI(window, tab);
+      yield ui.inited;
+    } else {
+      ui = this.getResponsiveUIForTab(tab);
+    }
+    return ui;
+  }),
+
+  /**
+   * Returns true if responsive view is active for the provided tab.
+   *
+   * @param tab the tab targeted.
+   */
+  isActiveForTab: function (tab) {
+    return ActiveTabs.has(tab);
+  },
+
+  /**
+   * Return the responsive UI controller for a tab.
+   */
+  getResponsiveUIForTab: function (tab) {
+    return ActiveTabs.get(tab);
+  },
+
+  /**
+   * Handle gcli commands.
+   *
+   * @param window the browser window.
+   * @param tab the tab targeted.
+   * @param command the command name.
+   * @param args command arguments.
+   */
+  handleGcliCommand: Task.async(function* (window, tab, command, args) {
+    switch (command) {
+      case "resize to":
+        let ui = yield this.openIfNeeded(window, tab);
+        ui.setViewportSize(args);
+        break;
+      case "resize on":
+        this.openIfNeeded(window, tab);
+        break;
+      case "resize off":
+        if (this.isActiveForTab(tab)) {
+          yield ActiveTabs.get(tab).close();
+        }
+        break;
+      case "resize toggle":
+        this.toggle(window, tab);
+        break;
+      default:
+    }
+  })
+};
+
+EventEmitter.decorate(ResponsiveUIManager);
+exports.ResponsiveUIManager = ResponsiveUIManager;
+
+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(window, tab) {
+  this.mainWindow = window;
+  this.tab = tab;
+  this.mm = this.tab.linkedBrowser.messageManager;
+  this.tabContainer = window.gBrowser.tabContainer;
+  this.browser = tab.linkedBrowser;
+  this.chromeDoc = window.document;
+  this.container = window.gBrowser.getBrowserContainer(this.browser);
+  this.stack = this.container.querySelector(".browserStack");
+  this._telemetry = new Telemetry();
+
+  // Let's bind some callbacks.
+  this.boundPresetSelected = this.presetSelected.bind(this);
+  this.boundHandleManualInput = this.handleManualInput.bind(this);
+  this.boundAddPreset = this.addPreset.bind(this);
+  this.boundRemovePreset = this.removePreset.bind(this);
+  this.boundRotate = this.rotate.bind(this);
+  this.boundScreenshot = () => this.screenshot();
+  this.boundTouch = this.toggleTouch.bind(this);
+  this.boundClose = this.close.bind(this);
+  this.boundStartResizing = this.startResizing.bind(this);
+  this.boundStopResizing = this.stopResizing.bind(this);
+  this.boundOnDrag = this.onDrag.bind(this);
+  this.boundChangeUA = this.changeUA.bind(this);
+  this.boundOnContentResize = this.onContentResize.bind(this);
+
+  this.mm.addMessageListener("ResponsiveMode:OnContentResize",
+                             this.boundOnContentResize);
+
+  // 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(value) {
+    this._transitionsEnabled = value;
+    if (value && !this._resizing && this.stack.hasAttribute("responsivemode")) {
+      this.stack.removeAttribute("notransition");
+    } else if (!value) {
+      this.stack.setAttribute("notransition", "true");
+    }
+  },
+
+  init: Task.async(function* () {
+    debug("INIT BEGINS");
+    let ready = this.waitForMessage("ResponsiveMode:ChildScriptReady");
+    this.mm.loadFrameScript("resource://devtools/client/responsive.html/browser/content.js", true);
+    yield ready;
+
+    yield gDevToolsBrowser.loadBrowserStyleSheet(this.mainWindow);
+
+    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) {
+      // There is no default value defined, so errors are expected.
+    }
+
+    // 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")
+    });
+
+    this.showNewUINotification();
+
+    // 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) {
+      debug("CLOSE: WAIT ON CONTENT RESIZE");
+      yield this.waitForMessage("ResponsiveMode:OnContentResize");
+      debug("CLOSE: CONTENT RESIZE DONE");
+    }
+
+    if (this.isResizing) {
+      this.stopResizing();
+    }
+
+    // Remove listeners.
+    this.menulist.removeEventListener("select", this.boundPresetSelected, true);
+    this.menulist.removeEventListener("change", this.boundHandleManualInput, true);
+    this.mainWindow.removeEventListener("unload", this);
+    this.tab.removeEventListener("TabClose", this);
+    this.tabContainer.removeEventListener("TabSelect", this);
+    this.rotatebutton.removeEventListener("command", this.boundRotate, true);
+    this.screenshotbutton.removeEventListener("command", this.boundScreenshot, true);
+    this.closebutton.removeEventListener("command", this.boundClose, true);
+    this.addbutton.removeEventListener("command", this.boundAddPreset, true);
+    this.removebutton.removeEventListener("command", this.boundRemovePreset, true);
+    this.touchbutton.removeEventListener("command", this.boundTouch, true);
+    this.userAgentInput.removeEventListener("blur", this.boundChangeUA, 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();
+    }
+
+    debug("CLOSE: WAIT ON CLIENT CLOSE");
+    yield this.client.close();
+    debug("CLOSE: CLIENT CLOSE DONE");
+    this.client = this.emulationFront = null;
+
+    this._telemetry.toolClosed("responsive");
+
+    if (this.tab.linkedBrowser && this.tab.linkedBrowser.messageManager) {
+      let stopped = this.waitForMessage("ResponsiveMode:Stop:Done");
+      this.tab.linkedBrowser.messageManager.sendAsyncMessage("ResponsiveMode:Stop");
+      debug("CLOSE: WAIT ON STOP");
+      yield stopped;
+      debug("CLOSE: STOP DONE");
+    }
+
+    this.hideNewUINotification();
+
+    debug("CLOSE: DONE, EMIT OFF");
+    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 (event) {
+    switch (event.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 () {
+    this.chromeDoc.getElementById("menu_responsiveUI").setAttribute("checked", "true");
+  },
+
+  /**
+   * Uncheck the menu items.
+   */
+  unCheckMenus: function () {
+    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 () {
+    // 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.boundPresetSelected, true);
+    this.menulist.addEventListener("change", this.boundHandleManualInput, 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.boundAddPreset, true);
+
+    this.removebutton = this.chromeDoc.createElement("menuitem");
+    this.removebutton.setAttribute(
+      "label",
+      this.strings.GetStringFromName("responsiveUI.removePreset")
+    );
+    this.removebutton.addEventListener("command", this.boundRemovePreset, 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.boundRotate, 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.boundScreenshot, 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.boundClose, 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.boundTouch, 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.boundChangeUA, 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.boundStartResizing;
+
+    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.boundStartResizing;
+
+    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.boundStartResizing;
+
+    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);
+  },
+
+  showNewUINotification() {
+    let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser);
+
+    // One reason we might be using old RDM is that the user explcitly disabled new RDM.
+    // We should encourage them to use the new one, since the old one will be removed.
+    if (Services.prefs.prefHasUserValue(NEW_RDM_ENABLED) &&
+        !Services.prefs.getBoolPref(NEW_RDM_ENABLED)) {
+      let buttons = [{
+        label: this.strings.GetStringFromName("responsiveUI.newVersionEnableAndRestart"),
+        callback: () => {
+          Services.prefs.setBoolPref(NEW_RDM_ENABLED, true);
+          BrowserUtils.restartApplication();
+        },
+      }];
+      nbox.appendNotification(
+        this.strings.GetStringFromName("responsiveUI.newVersionUserDisabled"),
+        "responsive-ui-new-version-user-disabled",
+        null,
+        nbox.PRIORITY_INFO_LOW,
+        buttons
+      );
+      return;
+    }
+
+    // Only show a notification about the new RDM UI on channels where there is an e10s
+    // switch in the preferences UI (Dev. Ed, Nightly).  On other channels, it is less
+    // clear how a user would proceed here, so don't show a message.
+    if (!system.constants.E10S_TESTING_ONLY) {
+      return;
+    }
+
+    let buttons = [{
+      label: this.strings.GetStringFromName("responsiveUI.newVersionEnableAndRestart"),
+      callback: () => {
+        Services.prefs.setBoolPref("browser.tabs.remote.autostart", true);
+        Services.prefs.setBoolPref("browser.tabs.remote.autostart.2", true);
+        BrowserUtils.restartApplication();
+      },
+    }];
+    nbox.appendNotification(
+      this.strings.GetStringFromName("responsiveUI.newVersionE10sDisabled"),
+      "responsive-ui-new-version-e10s-disabled",
+      null,
+      nbox.PRIORITY_INFO_LOW,
+      buttons
+    );
+  },
+
+  hideNewUINotification() {
+    if (!this.mainWindow.gBrowser || !this.mainWindow.gBrowser.getNotificationBox) {
+      return;
+    }
+    let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser);
+    let n = nbox.getNotificationWithValue("responsive-ui-new-version-user-disabled");
+    if (n) {
+      n.close();
+    }
+    n = nbox.getNotificationWithValue("responsive-ui-new-version-e10s-disabled");
+    if (n) {
+      n.close();
+    }
+  },
+
+  /**
+   * Validate and apply any user input on the editable menulist
+   */
+  handleManualInput: function () {
+    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 parent menupopup.
+   */
+  registerPresets: function (parent) {
+    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);
+    }
+    parent.appendChild(fragment);
+  },
+
+  /**
+   * Set the menuitem label of a preset.
+   *
+   * @param menuitem menuitem to edit.
+   * @param preset associated preset.
+   */
+  setMenuLabel: function (menuitem, preset) {
+    let size = SHARED_L10N.getFormatStr("dimensions",
+      Math.round(preset.width), Math.round(preset.height));
+
+    // .inputField might be not reachable yet (async XBL loading)
+    if (this.menulist.inputField) {
+      this.menulist.inputField.value = size;
+    }
+
+    if (preset.custom) {
+      size = this.strings.formatStringFromName("responsiveUI.customResolution",
+                                               [size], 1);
+    } else if (preset.name != null && preset.name !== "") {
+      size = this.strings.formatStringFromName("responsiveUI.namedResolution",
+                                               [size, preset.name], 2);
+    }
+
+    menuitem.setAttribute("label", size);
+  },
+
+  /**
+   * When a preset is selected, apply it.
+   */
+  presetSelected: function () {
+    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 () {
+    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((presetA, presetB) => {
+      // We keep custom preset at first
+      if (presetA.custom && !presetB.custom) {
+        return 1;
+      }
+      if (!presetA.custom && presetB.custom) {
+        return -1;
+      }
+
+      if (presetA.width === presetB.width) {
+        if (presetA.height === presetB.height) {
+          return 0;
+        }
+        return presetA.height > presetB.height;
+      }
+      return presetA.width > presetB.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 () {
+    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 () {
+    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 filename name of the screenshot file (used for tests).
+   */
+  screenshot: function (filename) {
+    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(message) {
+      mm.removeMessageListener("ResponsiveMode:RequestScreenshot:Done", onScreenshot);
+      chromeWindow.saveURL(message.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 () {
+    this.touchbutton.setAttribute("checked", "true");
+    return this.touchEventSimulator.start();
+  },
+
+  disableTouch: function () {
+    this.touchbutton.removeAttribute("checked");
+    return this.touchEventSimulator.stop();
+  },
+
+  hideTouchNotification: function () {
+    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();
+      return;
+    }
+
+    let isReloadNeeded = yield this.enableTouch();
+    if (!isReloadNeeded) {
+      return;
+    }
+
+    const PREF = "devtools.responsiveUI.no-reload-notification";
+    if (Services.prefs.getBoolPref(PREF)) {
+      return;
+    }
+
+    let nbox = this.mainWindow.gBrowser.getNotificationBox(this.browser);
+
+    let 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: () => Services.prefs.setBoolPref(PREF, 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() {
+    return new Promise(resolve => {
+      let onNavigated = (_, { state }) => {
+        if (state != "stop") {
+          return;
+        }
+        this.client.removeListener("tabNavigated", onNavigated);
+        resolve();
+      };
+      this.client.addListener("tabNavigated", onNavigated);
+    });
+  },
+
+  /**
+   * 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 (this.closing) {
+      debug(`ABORT SET SIZE, CLOSING`);
+      return;
+    }
+    if (width) {
+      this.setWidth(width);
+    }
+    if (height) {
+      this.setHeight(height);
+    }
+  },
+
+  setWidth: function (width) {
+    width = Math.min(Math.max(width, MIN_WIDTH), MAX_WIDTH);
+    this.stack.style.maxWidth = this.stack.style.minWidth = width + "px";
+
+    if (!this.ignoreX) {
+      this.resizeBarH.setAttribute("left", Math.round(width / 2));
+    }
+
+    let selectedPreset = this.menuitems.get(this.selectedItem);
+
+    if (selectedPreset.custom) {
+      selectedPreset.width = width;
+      this.setMenuLabel(this.selectedItem, selectedPreset);
+    }
+  },
+
+  setHeight: function (height) {
+    height = Math.min(Math.max(height, MIN_HEIGHT), MAX_HEIGHT);
+    this.stack.style.maxHeight = this.stack.style.minHeight = height + "px";
+
+    if (!this.ignoreY) {
+      this.resizeBarV.setAttribute("top", Math.round(height / 2));
+    }
+
+    let selectedPreset = this.menuitems.get(this.selectedItem);
+    if (selectedPreset.custom) {
+      selectedPreset.height = height;
+      this.setMenuLabel(this.selectedItem, selectedPreset);
+    }
+  },
+  /**
+   * Start the process of resizing the browser.
+   *
+   * @param event
+   */
+  startResizing: function (event) {
+    let selectedPreset = this.menuitems.get(this.selectedItem);
+
+    if (!selectedPreset.custom) {
+      if (this.rotateValue) {
+        this.customPreset.width = selectedPreset.height;
+        this.customPreset.height = selectedPreset.width;
+      } else {
+        this.customPreset.width = selectedPreset.width;
+        this.customPreset.height = 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.boundStopResizing, true);
+    this.mainWindow.addEventListener("mousemove", this.boundOnDrag, true);
+    this.container.style.pointerEvents = "none";
+
+    this._resizing = true;
+    this.stack.setAttribute("notransition", "true");
+
+    this.lastScreenX = event.screenX;
+    this.lastScreenY = event.screenY;
+
+    this.ignoreY = (event.target === this.resizeBarV);
+    this.ignoreX = (event.target === this.resizeBarH);
+
+    this.isResizing = true;
+  },
+
+  /**
+   * Resizing on mouse move.
+   *
+   * @param event
+   */
+  onDrag: function (event) {
+    let shift = event.shiftKey;
+    let ctrl = !event.shiftKey && event.ctrlKey;
+
+    let screenX = event.screenX;
+    let screenY = event.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 () {
+    this.container.style.pointerEvents = "auto";
+
+    this.mainWindow.removeEventListener("mouseup", this.boundStopResizing, true);
+    this.mainWindow.removeEventListener("mousemove", this.boundOnDrag, 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 () {
+    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 () {
+    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 () {
+    // We exclude the custom one
+    let registeredPresets = this.presets.filter(function (preset) {
+      return !preset.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");
+});
new file mode 100644
--- /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
+  }
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser.ini
@@ -0,0 +1,22 @@
+[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]
+skip-if = true # Bug 1358261 - Intermittent failures, mostly on Windows
+[browser_responsiveuiaddcustompreset.js]
+[browser_responsive_devicewidth.js]
+[browser_responsiveui_customuseragent.js]
+[browser_responsiveui_window_close.js]
+skip-if = (os == 'linux') && e10s && debug # Bug 1277274
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsivedesign/test/browser_responsive_cmd.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+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);
+}
new file mode 100644
--- /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
+    };
+  });
+}
new file mode 100644
--- /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;
+}
new file mode 100644
--- /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;
+}
new file mode 100644
--- /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), {});
+  }
+}
new file mode 100644
--- /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}`);
+}
new file mode 100644
--- /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) {
+  return new Promise(resolve => {
+    SpecialPowers.pushPrefEnv({"set": aPrefs}, resolve);
+  });
+}
new file mode 100644
--- /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;
+});
new file mode 100644
--- /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);
+});
new file mode 100644
--- /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();
+
+loader.lazyRequireGetter(this, "ResponsiveUIManager", "devtools/client/responsive.html/manager");
+
+/**
+ * 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_responsiveDesignMode"));
+  }
+  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");
+  return new Promise(resolve => {
+    waitForFocus(resolve, toolbox.win);
+  });
+}
+
+/**
+ * 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.getPanel(id).view || inspector.getPanel(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 = BrowserTestUtils.addTab(gBrowser, 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) {
+  return new Promise(resolve => {
+    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");
+          resolve();
+        }
+      },
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+                                           Ci.nsISupportsWeakReference])
+    };
+    aBrowser.addProgressListener(progressListener);
+    info("Waiting for browser load");
+  });
+}
+
+/**
+ * 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;
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsivedesign/test/touch.html
@@ -0,0 +1,88 @@
+<!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();
+      // Do to time spent processing events our measurement might
+      // be fractionally short of the actual delay.  Round up any
+      // microsecond changes in case we get something like 299.9.
+      div.dataset.isDelay = ((now - touchendTime) >= 299.5) ? 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>
--- a/devtools/client/themes/devtools-browser.css
+++ b/devtools/client/themes/devtools-browser.css
@@ -1,14 +1,15 @@
 /* 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/. */
 
 @import url("resource://devtools/client/themes/splitters.css");
 @import url("resource://devtools/client/themes/commandline-browser.css");
+@import url("resource://devtools/client/themes/responsivedesign.css");
 
 /* Bottom-docked toolbox minimize transition */
 .devtools-toolbox-bottom-iframe {
   transition: margin-bottom .1s;
 }
 
 .devtools-toolbox-side-iframe {
   min-width: 465px;
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7d113f0df411078d756230cdc62e7a9257febeae
GIT binary patch
literal 102
zc%17D@N?(olHy`uVBq!ia0vp^5<tw(!3HFSzV~heQo5cljv*T7lM@!O|M>Ja{pR)e
z`_qDi>wo{;&6CI2;hf98ujY5`IR|qCDVHA%3{RwtR&Ad4TokC0!PC{xWt~$(69CO8
BA;<s#
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bb3c4bde064c0817c853ce90efb018b335a8ea0e
GIT binary patch
literal 129
zc%17D@N?(olHy`uVBq!ia0vp^20+Zm!3HEVi{?iGDGyH<#}JR>$q5py%`7$l{sce2
z-9JAze*gbjmNN`b^eSvmsz}hVkh>}De2Y1L-`_V9n>;z5RIKsL5b2rbw0P--3{8_L
du3irYhDlndT-S5_S_d?f!PC{xWt~$(69COGEFk~@
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..35b54d62cb4b0b79b9719a25a5176fb58182247a
GIT binary patch
literal 129
zc%17D@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`9-c0aAr_~T6C_xfnfKNFj{Nra
z`Fo>HCiQ>+Wvg-2|NORlPR>~cn+>x(xtL4Ns<bldopq^fRywO-zv%bOB?b(hO1~!x
c%vWMyQ0`i~>Qi#`R-l;-p00i_>zopr00hh|)&Kwi
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..9dbf4fe8e8e42a01ce4436e30f02c7ec506b406e
GIT binary patch
literal 205
zc%17D@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJDV{ElAr-gYPVweyFyL{iR&c#j
z;Z~^s$ny8blWvj%yCx_ty`Ye$5Fjy0oJ(?X(8`ciVM<oN1u`8Yy<>xS6x+0%SZ!b7
zp2%rvaf)@;(g!QfR?8iI&{!GtL;m4;nGCk`Pa6bmAXq|X-=Y1S$7P?l@Bh8y?<yIG
z<g|*7{pClQGAmZbPh@_{eX+-S-N%n+tD?m(3EkwBb<E8Gx{txr)z4*}Q$iB}bOldq
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..3b4e78c6fd55144019fcf9df882cc7d5377852ae
GIT binary patch
literal 105
zc%17D@N?(olHy`uVBq!ia0vp^>_9BR!3HGFE<K+MqzpV=978mMlM@tJ_SO83{Py<w
zd!rIFiJHIvveh^iZ+2I8SM8k2qPjx=e%<uNn?Eu!@ZL3ACA5;U2&k9A)78&qol`;+
E0NwK;1poj5
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..cbae60621ddc6b71a468964e16162b5fdd419c9a
GIT binary patch
literal 141
zc%17D@N?(olHy`uVBq!ia0vp^d_Zi#!VDxYi-p+(DgFST5LY1m`SZ^&-+q4k@$>8V
zUuDl;d;kiumIV0)Gw5`5&FmLr*8>V@c)B=-NQ5URIItgTQ#f)U;SrA$509Rnf{E8;
k@7WfO0tPXnjvTTK41%?~-cCko!azL?p00i_>zopr06pR<L;wH)
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..43379d0e949ccb5b6664f6e230f47d448e723254
GIT binary patch
literal 276
zc%17D@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&kwj^(N7l!{JxM1({$v}~lo-U3d
z8t0ex8|EEy5ODp!eJ4YxMo{2|bt&5|{j*%W=V}E_X)a({m*BmxRqzyVg524>6je3N
zAD1%Rjo+Vt<I&HxqVc=T*ZL1a77X#0d<Q<s?dj{VQ#yBmqerl?uR|_EeBG%J-AfiP
zN%s_2XqZrPa%*y0$^+4g$t_E=>y~d?5;ZCJ{oJ5i3}+2^=2yPbzqu~REBf|cb@QH^
z54Po&Pr6ciw_PPCd|P;Gtl^x7*IOdnZmi3-ua&p`{CsE3cb@vJz)8z8x7p7w-YUIt
Xdi(biYu9-KJ;mVZ>gTe~DWM4f`2KW(
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2bacbd2d559b72468aca1b72e07d8d900359a63a
GIT binary patch
literal 245
zc$@+C01E$!P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0002INkl<Zc-qyH
zJqiLb5QQTiMNrV%N^CuV=n1T>xA6qtz*^A8QuJmfncpHZTkT@Vih|&SDU!$cW+ue_
zuK;+JH9YeeQFnlHj==W-d<sAnQ5rU3^IBj^sUJzUW@L->7Ev}Hcx~sGfIM8T*4Ql*
z<rq=-5#;bzfbDD-_1;mYt|1~C1(ZuMpaw~=L^Q~~cIO0jchaf^cTNyU7X`Q+VBJA=
v=0278JOSXP`$iAc`8T}%#Gd^|_iH@?(^^+@;3c~}00000NkvXXu0mjfzR6`a
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..eeeb823287fddd28de8d3127559a3675f545a25b
GIT binary patch
literal 438
zc$@*Y0ZIOeP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80004fNkl<Zc-rlj
zyGlbr5QgIgW0S@{f)-X<_yoRyg@uBmjoPLNh}fGbhzj}!5^JNx$4aAScP_huks)Y6
zPvTiuSdcR?)z0#NyJzM<TPmW6Q~|)7NyRPjbMQt)TrkLc7X3M(ySL=qnt-ll(Kik7
z4uDkihz9}mn3sY0Qa9yF^rvq`OQXbd1Bl2BkgHkp9o-O^9c$g8h)idRYyKo&G2pJW
zFe;H70KA>GHumHO-1V?HK7#=C+O;4FfHAzH-#i(>xNbMVEd#C&+~2a6Hw?OcAimIs
z`xAf{qQ$I2Ab)GETNROs5&kf5(Kn(2)MUPkyPA$tj**?z#*G3{hx>B!BRzG9y+#43
zy$<Ks-HFI-sCe8B06%^fP=7sCJRX32dm!$gy4`LiP&^($$4nOtxtj>_s3vR<=ADjz
zAs!F#@zYSVN%T^Y>Hxr>lv0I7K2tkR2B0q)ApamD<%GB34tnU<k^zXwltJEA8%_yP
geifN374hHW9o`}do}$9<-~a#s07*qoM6N<$f+ZBg@&Et;
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..084220ed1d903c4450c11583a4e9da9398282454
GIT binary patch
literal 303
zc$@(*0nq-5P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0002?Nkl<Zc-qaA
zO-ckY5QP=RtqLOkAb10};!4GZf(t!__YidN2~<QK#DyCVCYV&cq|)%DdS(QB8pG@+
z@Ig_$`a%&hH+x5w+Y$CAnm_-L%14a0_wO*tMS)%UKrhJGB$p-zquQMZrom@^1(P?~
zeLy+-B3B%*1@_R*^$I*la(T4Uf-|aD1@>4K#FS3F7l30)<=qLuVY_N7r&Yl>xz>h}
z2jiZ?1|;W2WUDF&A*^==UJsvGcdCMvnzLcJ94PErU{9*H-{w+tUKK>mFC;g*Pc6bt
zg}qh<P8-b41;AZLb8|Ufsb7F{<Bh36xt)#A_XF{K_MA(_L%aY0002ovPDHLkV1nhQ
BfWZI&
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..927c5cf0bcf649168a687e60dd4757ede293e63f
GIT binary patch
literal 531
zc$@(f0_^>XP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80005nNkl<Zc-rk&
z&nrYx6n;e!SxhN~6blQ3QnIlj`3vkM3$wHo3oEuZQkspj5amY_{;_H1-gEA`S6#j$
zZ{B#kH}6tvPJQ*d=R4<o^X_@)Ow_S|W#0RqH}wC2zTkyxwOWacJJux6SwL(rwYKxY
z1ZcJuB+do4XCVbtku)!e{7BwA?n>NeiR5Js$=U+Tf!2CBk+bW|;&pP1#P$Y(zX{Yp
ztrh{q_6mf*0BW@e$QrCjE)>g#QZl2IA0na=B{GXM__F57FTl7b=9f+HeQ%Uv@PPwv
zPytBTV7DA)G-kJim~aBLt)%?o0U+hm-3lTA4U-(N4UuUw?&O1mXY@ouTpwZ!3&0di
z6ra*04@n-*8VxBYCus3EKz1R{Bjb+JxbVffh%GFjX3RwFwRV5kT=0SO*^E41Sb#Bp
zGCry`$zvK9zWAs`Z1F2_&0|xu9Hw#Mi*pfMya|onZvMu3Op&zg44lCi=OQ-59Js?)
zY;G<ZWpVr3xLW|vIE!--gV-R0h{)|lISnyEcY$JA&srSZ;L8Xg<96%b52h7{51i23
zFo}CbW=FAG!T8nM&M9S9aRy)e>zM6190gG9537hTYh$a5?e%X$lRZVPsAC;_w^uVM
VXou&f+`s?;002ovPDHLkV1jZN^11*3
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..90587034ce786c0f0d061a3e893e2d4af2492345
GIT binary patch
literal 470
zc$@*&0V)28P)<h;3K|Lk000e1NJLTq001BW000mO1^@s6cL04^0004<Nkl<Zc-qaB
zF-ycS6vvAeCqWMU7LE?ua>K1oPNJXSocs=g)7>K8C<q=Rh3j?koLp|vkKp7WB8}p3
z2)1b(m6-q8m~dcCI{Dy{m)9omm;PVl=r$g6mlhw}31x>#UUU#6=NMW`yz57#?MHFo
z4aq5D<Q!8&L7KC@$%HS#5x>!5$8`O)co^?DFJBvQ#9l~tjRA0S6i#T-rTKdoF>>Yr
zae%_&^w+{~5F=*{Ac}TSG^ND|&N*+`0C*AUmjE2z8j)oHSV^D*Ahwl22S98kfowPl
zfY`PPF-icHvoZxBd%<qC0+78>8=xMD0;mb10BV9LfNBtX0PXK^?E#dp3~m=3X=1av
zG-vH3V<$9YXH$TT9alo+?5DH027KNhk|Qr7_rjmQ0phzV06%)_sne!xRcv~pcfJ4#
zssQj@u3rxyH^io&a`W;+np6Scxue?)=ZloFwKOXZ$}iA21%NfrA8_q%`VqM*0b(lv
zT@H+p#8d*9E(b<PVk*IJATEl`N`X1@e>EYR3`J(8z#Mtmg#W_609(Vpb)!QZy#N3J
M07*qoM6N<$g07*(+W-In
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e4645039cf57e2276838fbb9d4ad7aa27a96a1e9
GIT binary patch
literal 927
zc$@*B17Q4#P)<h;3K|Lk000e1NJLTq002M$001Be1^@s6qMd$(000AKNkl<Zc-rll
zziSjh6vwwnj0sUuY%H|WBBT*gc)LM~b^*1p5ft%Hu+X5DKZ2n6tI(jvU?aieb}s<~
ziXf!15V5fk!6*ijn@EBoaJMu20~zO=WK`C3Z*x2E()i$UGj9goyw84jc5bY!HaRI<
zgoqDTd3@gI(Oo#&h+)hkwmy}^cE26Az4A@lWiA|T#QLc7FD)%?^+kNq7twGn#~9{N
z<?ngQwyR=?lu!J_hcJgEIiBwC;={oxiuQh$aw8mV#54pcWedrZ*TuW+p;Mf-?cz*3
z9BssYK^T75Y8(J0u8UU%wkO9C(-0~`95S6B64%A|Ot6mRIAR(?!7Cpz4gl{tio?@J
z-}j@QDi5E)(MAkom{S$u1=IQQo})NCZS)K7OuyqX21gq)j4cCj*)8J$u-TzFNFM#=
z!g)9WM;kGWVGib+R{(QS93+oE3b#Eb;b<d<G0eeSG61@-i`TB>)Bto}7q4B%$pAps
zSpZPI0+4l90GLa%31$VL=8|lJnE@2bN69{e^#O#Jq?D~xpTP_OhPsO%Hjo{J>H(lW
z2&Dqhb7(Cc0KKIHfUOIF<hh{P0Fvi|rUOWx3z`iec`j%=0D48F1IVEW8URT}&;v+U
z1UUeDMWh4BsR()iNktF?pjQM6$>Z}<b`k?f_u|a9bp;Rv!JhR5&|8`<)J-C#Y~wsU
z=8O2MFQSo(2u75AZ<qj35#m5<a(&6+l?Rl+QnLNxl-Osr_}3-?IHlQBsmU3W9X^ll
z)mpp}06aXNmVe*q_sy#C_q7&p2Eg3qdVhaUVxd$3co3aQm-yTjER+ra8%+hkgZQi;
z#A9>-;(@gMSfJyT10n$M@hp2lYo!u)t?mPZYT?gT0RWJj3jmML)yv=ie5%cM%MSJd
z$73(mqJR7Wpqf(wZUdJhVpo+%x6xL=ck%$hE4J4Bz2026TKt<;0B>{d0oScY3||KP
zLKXQwgK~Ex`Sq1JRFMZjp;Yd*@|aw?0e;j-@whe0eM3~7?^F<`8Uj#aFDw(rIp3)u
zJYG}rL3Dp{adC^0i_$UHr3#u}BY=<lOVc|nQ?{_Mu&FAd(<|5E;dsrW5f{+|A;J?-
zj%Cnrq*X;-Q)>96JgikT;wT)SRw&2%Z~yHt`w46JtHcwcv8@0A002ovPDHLkV1i`T
Brj7sr
--- a/devtools/client/themes/moz.build
+++ b/devtools/client/themes/moz.build
@@ -6,12 +6,13 @@
 
 DIRS += [
     'audio',
 ]
 
 DevToolsModules(
     'commandline-browser.css',
     'common.css',
+    'responsivedesign.css',
     'splitters.css',
     'toolbars.css',
     'variables.css',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/responsivedesign.css
@@ -0,0 +1,355 @@
+/* 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/. */
+
+/* Responsive Mode */
+
+@namespace html url("http://www.w3.org/1999/xhtml");
+
+.browserContainer[responsivemode] {
+  background-color: #222;
+  padding: 0 20px 20px 20px;
+}
+
+.browserStack[responsivemode] {
+  box-shadow: 0 0 7px black;
+}
+
+.devtools-responsiveui-toolbar {
+  -moz-appearance: none;
+  background: transparent;
+  /* text color is textColor from dark theme, since no theme is applied to
+   * the responsive toolbar.
+   */
+  color: hsl(210,30%,85%);
+  margin: 10px 0;
+  padding: 0;
+  box-shadow: none;
+  border-bottom-width: 0;
+}
+
+.devtools-responsiveui-textinput {
+  -moz-appearance: none;
+  background: #333;
+  color: #fff;
+  border: 1px solid #111;
+  border-radius: 2px;
+  padding: 0 5px;
+  width: 200px;
+  margin: 0;
+}
+
+.devtools-responsiveui-textinput[attention] {
+  border-color: #38ace6;
+  background: rgba(56,172,230,0.4);
+}
+
+.devtools-responsiveui-menulist,
+.devtools-responsiveui-toolbarbutton {
+  -moz-appearance: none;
+  -moz-box-align: center;
+  min-width: 32px;
+  min-height: 22px;
+  text-shadow: 0 -1px 0 hsla(210,8%,5%,.45);
+  border: 1px solid hsla(210,8%,5%,.45);
+  border-radius: 0;
+  background: linear-gradient(hsla(212,7%,57%,.35), hsla(212,7%,57%,.1)) padding-box;
+  box-shadow: 0 1px 0 hsla(210,16%,76%,.15) inset, 0 0 0 1px hsla(210,16%,76%,.15) inset, 0 1px 0 hsla(210,16%,76%,.15);
+  margin: 0 3px;
+  color: inherit;
+}
+
+.devtools-responsiveui-menulist .menulist-editable-box {
+  -moz-appearance: none;
+  background-color: transparent;
+}
+
+.devtools-responsiveui-menulist html|*.menulist-editable-input {
+  -moz-appearance: none;
+  color: inherit;
+  text-align: center;
+}
+
+.devtools-responsiveui-menulist html|*.menulist-editable-input::-moz-selection {
+  background: hsla(212,7%,57%,.35);
+}
+
+.devtools-responsiveui-toolbarbutton > .toolbarbutton-icon {
+  width: 16px;
+  height: 16px;
+}
+
+.devtools-responsiveui-toolbarbutton > .toolbarbutton-menubutton-button {
+  -moz-box-orient: horizontal;
+}
+
+.devtools-responsiveui-menulist:-moz-focusring,
+.devtools-responsiveui-toolbarbutton:-moz-focusring {
+  outline: 1px dotted hsla(210,30%,85%,0.7);
+  outline-offset: -4px;
+}
+
+.devtools-responsiveui-toolbarbutton:not([label]) > .toolbarbutton-text {
+  display: none;
+}
+
+.devtools-responsiveui-toolbarbutton:not([checked=true]):hover:active {
+  border-color: hsla(210,8%,5%,.6);
+  background: linear-gradient(hsla(220,6%,10%,.3), hsla(212,7%,57%,.15) 65%, hsla(212,7%,57%,.3));
+  box-shadow: 0 0 3px hsla(210,8%,5%,.25) inset, 0 1px 3px hsla(210,8%,5%,.25) inset, 0 1px 0 hsla(210,16%,76%,.15);
+}
+
+.devtools-responsiveui-menulist[open=true],
+.devtools-responsiveui-toolbarbutton[open=true],
+.devtools-responsiveui-toolbarbutton[checked=true] {
+  border-color: hsla(210,8%,5%,.6) !important;
+  background: linear-gradient(hsla(220,6%,10%,.6), hsla(210,11%,18%,.45) 75%, hsla(210,11%,30%,.4));
+  box-shadow: 0 1px 3px hsla(210,8%,5%,.25) inset, 0 1px 3px hsla(210,8%,5%,.25) inset, 0 1px 0 hsla(210,16%,76%,.15);
+}
+
+.devtools-responsiveui-toolbarbutton[checked=true] {
+  color: hsl(208,100%,60%);
+}
+
+.devtools-responsiveui-toolbarbutton[checked=true]:hover {
+  background-color: transparent !important;
+}
+
+.devtools-responsiveui-toolbarbutton[checked=true]:hover:active {
+  background-color: hsla(210,8%,5%,.2) !important;
+}
+
+.devtools-responsiveui-menulist > .menulist-label-box {
+  text-align: center;
+}
+
+.devtools-responsiveui-menulist > .menulist-dropmarker {
+  -moz-appearance: none;
+  display: -moz-box;
+  background-color: transparent;
+  list-style-image: url("chrome://devtools/skin/images/dropmarker.svg");
+  -moz-box-align: center;
+  border-width: 0;
+  min-width: 16px;
+}
+
+.devtools-responsiveui-toolbarbutton[type=menu-button] > .toolbarbutton-menubutton-button {
+  -moz-appearance: none;
+  color: inherit;
+  border-width: 0;
+  border-inline-end: 1px solid hsla(210,8%,5%,.45);
+  box-shadow: -1px 0 0 hsla(210,16%,76%,.15) inset, 1px 0 0 hsla(210,16%,76%,.15);
+}
+
+.devtools-responsiveui-toolbarbutton[type=menu-button]:-moz-locale-dir(rtl) > .toolbarbutton-menubutton-button {
+  box-shadow: 1px 0 0 hsla(210,16%,76%,.15) inset, -1px 0 0 hsla(210,16%,76%,.15);
+}
+
+.devtools-responsiveui-toolbarbutton[type=menu-button] {
+  padding: 0 1px;
+  -moz-box-align: stretch;
+}
+
+.devtools-responsiveui-toolbarbutton[type=menu] > .toolbarbutton-menu-dropmarker,
+.devtools-responsiveui-toolbarbutton[type=menu-button] > .toolbarbutton-menubutton-dropmarker {
+  -moz-appearance: none !important;
+  list-style-image: url("chrome://devtools/skin/images/dropmarker.svg");
+  -moz-box-align: center;
+  padding: 0 3px;
+}
+
+.devtools-responsiveui-toolbar:-moz-locale-dir(ltr) > *:first-child,
+.devtools-responsiveui-toolbar:-moz-locale-dir(rtl) > *:last-child {
+  margin-left: 0;
+}
+
+.devtools-responsiveui-close {
+  list-style-image: url("chrome://devtools/skin/images/close.svg");
+}
+
+.devtools-responsiveui-close > image {
+  filter: invert(1);
+}
+
+.devtools-responsiveui-rotate {
+  list-style-image: url("chrome://devtools/skin/images/responsivemode/responsiveui-rotate.png");
+}
+
+@media (min-resolution: 1.1dppx) {
+  .devtools-responsiveui-rotate {
+    list-style-image: url("chrome://devtools/skin/images/responsivemode/responsiveui-rotate@2x.png");
+  }
+}
+
+.devtools-responsiveui-touch {
+  list-style-image: url("chrome://devtools/skin/images/responsivemode/responsiveui-touch.png");
+  -moz-image-region: rect(0px,16px,16px,0px);
+}
+
+.devtools-responsiveui-touch[checked] {
+  -moz-image-region: rect(0px,32px,16px,16px);
+}
+
+@media (min-resolution: 1.1dppx) {
+  .devtools-responsiveui-touch {
+    list-style-image: url("chrome://devtools/skin/images/responsivemode/responsiveui-touch@2x.png");
+    -moz-image-region: rect(0px,32px,32px,0px);
+  }
+
+  .devtools-responsiveui-touch[checked] {
+    -moz-image-region: rect(0px,64px,32px,32px);
+  }
+}
+
+.devtools-responsiveui-screenshot {
+  list-style-image: url("chrome://devtools/skin/images/responsivemode/responsiveui-screenshot.png");
+}
+
+@media (min-resolution: 1.1dppx) {
+  .devtools-responsiveui-screenshot {
+    list-style-image: url("chrome://devtools/skin/images/responsivemode/responsiveui-screenshot@2x.png");
+  }
+}
+
+.devtools-responsiveui-resizebarV {
+  width: 7px;
+  height: 24px;
+  cursor: ew-resize;
+  transform: translate(12px, -12px);
+  background-size: cover;
+  background-image: url("chrome://devtools/skin/images/responsivemode/responsive-vertical-resizer.png");
+}
+
+.devtools-responsiveui-resizebarH {
+  width: 24px;
+  height: 7px;
+  cursor: ns-resize;
+  transform: translate(-12px, 12px);
+  background-size: cover;
+  background-image: url("chrome://devtools/skin/images/responsivemode/responsive-horizontal-resizer.png");
+}
+
+.devtools-responsiveui-resizehandle {
+  width: 16px;
+  height: 16px;
+  cursor: se-resize;
+  transform: translate(12px, 12px);
+  background-size: cover;
+  background-image: url("chrome://devtools/skin/images/responsivemode/responsive-se-resizer.png");
+}
+
+/* FxOS custom mode with additional buttons and phone look'n feel */
+
+/* Hide devtools manual resizer */
+.browserStack[responsivemode].fxos-mode .devtools-responsiveui-resizehandle,
+.browserStack[responsivemode].fxos-mode .devtools-responsiveui-resizebarH,
+.browserStack[responsivemode].fxos-mode .devtools-responsiveui-resizebarV {
+  display: none;
+}
+
+/* Gives responsive mode a phone look'n feel */
+.browserStack[responsivemode].fxos-mode {
+  padding: 60px 15px 0;
+
+  border-radius: 25px / 20px;
+  border-bottom-left-radius: 0;
+  border-bottom-right-radius: 0;
+  border: 1px solid #FFFFFF;
+  border-bottom-width: 0;
+
+  background-color: #353535;
+
+  box-shadow: 0 3px 0.7px 1px #777777, 0 5px rgba(0, 0, 0, 0.4) inset;
+
+  background-image: linear-gradient(to right, #111 11%, #333 56%);
+  min-width: 320px;
+}
+
+.devtools-responsiveui-hardware-buttons {
+  -moz-appearance: none;
+  padding: 20px;
+
+  border: 1px solid #FFFFFF;
+  border-bottom-left-radius: 25px;
+  border-bottom-right-radius: 25px;
+  border-top-width: 0;
+
+  box-shadow: 0 3px 0.7px 1px #777777, 0 -7px rgba(0, 0, 0, 0.4) inset;
+
+  background-image: linear-gradient(to right, #111 11%, #333 56%);
+}
+
+.devtools-responsiveui-home-button {
+  -moz-user-focus: ignore;
+  width: 40px;
+  height: 30px;
+  list-style-image: url("chrome://devtools/skin/images/responsivemode/responsiveui-home.png");
+}
+
+.devtools-responsiveui-sleep-button {
+  -moz-user-focus: ignore;
+  -moz-appearance: none;
+  /* compensate browserStack top padding */
+  margin-top: -67px;
+  margin-right: 10px;
+
+  min-width: 10px;
+  width: 50px;
+  height: 5px;
+
+  border: 1px solid #444;
+  border-top-right-radius: 12px;
+  border-top-left-radius: 12px;
+  border-bottom-color: transparent;
+
+  background-image: linear-gradient(to top, #111 11%, #333 56%);
+}
+
+.devtools-responsiveui-sleep-button:hover:active {
+  background-image: linear-gradient(to top, #aaa 11%, #ddd 56%);
+}
+
+.devtools-responsiveui-volume-buttons {
+  margin-left: -29px;
+}
+
+.devtools-responsiveui-volume-up-button,
+.devtools-responsiveui-volume-down-button {
+  -moz-user-focus: ignore;
+  -moz-appearance: none;
+  border: 1px solid red;
+  min-width: 8px;
+  height: 40px;
+
+  border: 1px solid #444;
+  border-right-color: transparent;
+
+  background-image: linear-gradient(to right, #111 11%, #333 56%);
+}
+
+.devtools-responsiveui-volume-up-button:hover:active,
+.devtools-responsiveui-volume-down-button:hover:active {
+  background-image: linear-gradient(to right, #aaa 11%, #ddd 56%);
+}
+
+.devtools-responsiveui-volume-up-button {
+  border-top-left-radius: 12px;
+}
+
+.devtools-responsiveui-volume-down-button {
+  border-bottom-left-radius: 12px;
+}
+
+@media (min-resolution: 1.1dppx) {
+  .devtools-responsiveui-resizebarV {
+    background-image: url("chrome://devtools/skin/images/responsivemode/responsive-vertical-resizer@2x.png");
+  }
+
+  .devtools-responsiveui-resizebarH {
+    background-image: url("chrome://devtools/skin/images/responsivemode/responsive-horizontal-resizer@2x.png");
+  }
+
+  .devtools-responsiveui-resizehandle {
+    background-image: url("chrome://devtools/skin/images/responsivemode/responsive-se-resizer@2x.png");
+  }
+}