Bug 1239562 - Use explicit events to fix test races in responsive design. r=ochameau draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Fri, 29 Jan 2016 16:25:04 -0600
changeset 327251 f85e03e0b08dbee8313f3ba24ad8455d12ddac89
parent 327250 4e3797dce4c87fb6aab5b9817868aa1c9aea7662
child 513673 3588a2bbe571ebb51e0f56b6d9a6af26d7c8954c
push id10212
push userjryans@gmail.com
push dateFri, 29 Jan 2016 22:31:02 +0000
reviewersochameau
bugs1239562
milestone47.0a1
Bug 1239562 - Use explicit events to fix test races in responsive design. r=ochameau
devtools/client/responsivedesign/resize-commands.js
devtools/client/responsivedesign/responsivedesign-child.js
devtools/client/responsivedesign/responsivedesign.jsm
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_responsiveuiaddcustompreset.js
devtools/client/responsivedesign/test/head.js
devtools/client/shared/test/browser_telemetry_button_responsive.js
--- a/devtools/client/responsivedesign/resize-commands.js
+++ b/devtools/client/responsivedesign/resize-commands.js
@@ -87,16 +87,16 @@ exports.items = [
         type: 'number',
         description: l10n.lookup("resizePageArgHeightDesc"),
       },
     ],
     exec: gcli_cmd_resize
   }
 ];
 
-function gcli_cmd_resize(args, context) {
+function* gcli_cmd_resize(args, context) {
   let browserWindow = context.environment.chromeWindow;
   let mgr = browserWindow.ResponsiveUI.ResponsiveUIManager;
-  mgr.handleGcliCommand(browserWindow,
-                        browserWindow.gBrowser.selectedTab,
-                        this.name,
-                        args);
+  yield mgr.handleGcliCommand(browserWindow,
+                              browserWindow.gBrowser.selectedTab,
+                              this.name,
+                              args);
 }
--- a/devtools/client/responsivedesign/responsivedesign-child.js
+++ b/devtools/client/responsivedesign/responsivedesign-child.js
@@ -1,132 +1,167 @@
 /* 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/. */
 
-var Ci = Components.interfaces;
-const gDeviceSizeWasPageSize = docShell.deviceSizeIsPageSize;
-const gFloatingScrollbarsStylesheet = Services.io.newURI("chrome://devtools/skin/floating-scrollbars-responsive-design.css", null, null);
-var gRequiresFloatingScrollbars;
-
-var active = false;
-
-addMessageListener("ResponsiveMode:Start", startResponsiveMode);
-addMessageListener("ResponsiveMode:Stop", stopResponsiveMode);
+"use strict";
 
-function startResponsiveMode({data:data}) {
-  if (active) {
-    return;
-  }
-  addMessageListener("ResponsiveMode:RequestScreenshot", screenshot);
-  addMessageListener("ResponsiveMode:NotifyOnResize", notifiyOnResize);
-  let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
-  webProgress.addProgressListener(WebProgressListener, Ci.nsIWebProgress.NOTIFY_ALL);
-  docShell.deviceSizeIsPageSize = true;
-  gRequiresFloatingScrollbars = data.requiresFloatingScrollbars;
+/* global content, docShell, addMessageListener, removeMessageListener,
+   sendAsyncMessage */
 
-  // At this point, a content viewer might not be loaded for this
-  // docshell. makeScrollbarsFloating will be triggered by onLocationChange.
-  if (docShell.contentViewer) {
-    makeScrollbarsFloating();
-  }
-  active = true;
-  sendAsyncMessage("ResponsiveMode:Start:Done");
-}
-
-function notifiyOnResize() {
-  content.addEventListener("resize", () => {
-    sendAsyncMessage("ResponsiveMode:OnContentResize");
-  }, false);
-  sendAsyncMessage("ResponsiveMode:NotifyOnResize:Done");
-}
+var global = this;
 
-function stopResponsiveMode() {
-  if (!active) {
-    return;
-  }
-  active = false;
-  removeMessageListener("ResponsiveMode:RequestScreenshot", screenshot);
-  removeMessageListener("ResponsiveMode:NotifyOnResize", notifiyOnResize);
-  let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
-  webProgress.removeProgressListener(WebProgressListener);
-  docShell.deviceSizeIsPageSize = gDeviceSizeWasPageSize;
-  restoreScrollbars();
-  sendAsyncMessage("ResponsiveMode:Stop:Done");
-}
-
-function makeScrollbarsFloating() {
-  if (!gRequiresFloatingScrollbars) {
+// Guard against loading this frame script mutiple times
+(function() {
+  if (global.responsiveFrameScriptLoaded) {
     return;
   }
 
-  let allDocShells = [docShell];
+  var Ci = Components.interfaces;
+  const gDeviceSizeWasPageSize = docShell.deviceSizeIsPageSize;
+  const gFloatingScrollbarsStylesheet = Services.io.newURI("chrome://devtools/skin/floating-scrollbars-responsive-design.css", null, null);
+  var gRequiresFloatingScrollbars;
 
-  for (let i = 0; i < docShell.childCount; i++) {
-    let child = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell);
-    allDocShells.push(child);
+  var active = false;
+  var resizeNotifications = false;
+
+  addMessageListener("ResponsiveMode:Start", startResponsiveMode);
+  addMessageListener("ResponsiveMode:Stop", stopResponsiveMode);
+  addMessageListener("ResponsiveMode:NotifyOnResize", notifyOnResize);
+
+  function debug(msg) {
+    dump(`RDM CHILD: ${msg}\n`);
   }
 
-  for (let d of allDocShells) {
-    let win = d.contentViewer.DOMDocument.defaultView;
-    let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
-    try {
-      winUtils.loadSheet(gFloatingScrollbarsStylesheet, win.AGENT_SHEET);
-    } catch(e) { }
+  function startResponsiveMode({data:data}) {
+    debug("START");
+    if (active) {
+      return;
+    }
+    addMessageListener("ResponsiveMode:RequestScreenshot", screenshot);
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
+    webProgress.addProgressListener(WebProgressListener, Ci.nsIWebProgress.NOTIFY_ALL);
+    docShell.deviceSizeIsPageSize = true;
+    gRequiresFloatingScrollbars = data.requiresFloatingScrollbars;
+
+    // At this point, a content viewer might not be loaded for this
+    // docshell. makeScrollbarsFloating will be triggered by onLocationChange.
+    if (docShell.contentViewer) {
+      makeScrollbarsFloating();
+    }
+    active = true;
+    sendAsyncMessage("ResponsiveMode:Start:Done");
+  }
+
+  function bindResize() {
+    content.addEventListener("resize", () => {
+      let { width, height } = content.screen;
+      debug(`EMIT RESIZE: ${width} x ${height}`);
+      sendAsyncMessage("ResponsiveMode:OnContentResize", {
+        width,
+        height,
+      });
+    }, false);
+  }
+
+  function notifyOnResize() {
+    debug("GOT START NOTIFY");
+    if (!resizeNotifications) {
+      resizeNotifications = true;
+      bindResize();
+      addEventListener("DOMWindowCreated", bindResize, false);
+    }
+    sendAsyncMessage("ResponsiveMode:NotifyOnResize:Done");
+  }
+
+  function stopResponsiveMode() {
+    debug("STOP");
+    if (!active) {
+      return;
+    }
+    active = false;
+    removeMessageListener("ResponsiveMode:RequestScreenshot", screenshot);
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
+    webProgress.removeProgressListener(WebProgressListener);
+    docShell.deviceSizeIsPageSize = gDeviceSizeWasPageSize;
+    restoreScrollbars();
+    sendAsyncMessage("ResponsiveMode:Stop:Done");
   }
 
-  flushStyle();
-}
-
-function restoreScrollbars() {
-  let allDocShells = [docShell];
-  for (let i = 0; i < docShell.childCount; i++) {
-    allDocShells.push(docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell));
-  }
-  for (let d of allDocShells) {
-    let win = d.contentViewer.DOMDocument.defaultView;
-    let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
-    try {
-      winUtils.removeSheet(gFloatingScrollbarsStylesheet, win.AGENT_SHEET);
-    } catch(e) { }
-  }
-  flushStyle();
-}
-
-function flushStyle() {
-  // Force presContext destruction
-  let isSticky = docShell.contentViewer.sticky;
-  docShell.contentViewer.sticky = false;
-  docShell.contentViewer.hide();
-  docShell.contentViewer.show();
-  docShell.contentViewer.sticky = isSticky;
-}
-
-function screenshot() {
-  let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-  let width = content.innerWidth;
-  let height = content.innerHeight;
-  canvas.mozOpaque = true;
-  canvas.width = width;
-  canvas.height = height;
-  let ctx = canvas.getContext("2d");
-  ctx.drawWindow(content, content.scrollX, content.scrollY, width, height, "#fff");
-  sendAsyncMessage("ResponsiveMode:RequestScreenshot:Done", canvas.toDataURL());
-}
-
-var WebProgressListener = {
-  onLocationChange(webProgress, request, URI, flags) {
-    if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+  function makeScrollbarsFloating() {
+    if (!gRequiresFloatingScrollbars) {
       return;
     }
-    makeScrollbarsFloating();
-  },
-  QueryInterface: function QueryInterface(aIID) {
-    if (aIID.equals(Ci.nsIWebProgressListener) ||
-        aIID.equals(Ci.nsISupportsWeakReference) ||
-        aIID.equals(Ci.nsISupports)) {
-        return this;
+
+    let allDocShells = [docShell];
+
+    for (let i = 0; i < docShell.childCount; i++) {
+      let child = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell);
+      allDocShells.push(child);
+    }
+
+    for (let d of allDocShells) {
+      let win = d.contentViewer.DOMDocument.defaultView;
+      let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+      try {
+        winUtils.loadSheet(gFloatingScrollbarsStylesheet, win.AGENT_SHEET);
+      } catch(e) { }
+    }
+
+    flushStyle();
+  }
+
+  function restoreScrollbars() {
+    let allDocShells = [docShell];
+    for (let i = 0; i < docShell.childCount; i++) {
+      allDocShells.push(docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell));
+    }
+    for (let d of allDocShells) {
+      let win = d.contentViewer.DOMDocument.defaultView;
+      let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+      try {
+        winUtils.removeSheet(gFloatingScrollbarsStylesheet, win.AGENT_SHEET);
+      } catch(e) { }
     }
-    throw Components.results.NS_ERROR_NO_INTERFACE;
+    flushStyle();
+  }
+
+  function flushStyle() {
+    // Force presContext destruction
+    let isSticky = docShell.contentViewer.sticky;
+    docShell.contentViewer.sticky = false;
+    docShell.contentViewer.hide();
+    docShell.contentViewer.show();
+    docShell.contentViewer.sticky = isSticky;
   }
-};
+
+  function screenshot() {
+    let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+    let width = content.innerWidth;
+    let height = content.innerHeight;
+    canvas.mozOpaque = true;
+    canvas.width = width;
+    canvas.height = height;
+    let ctx = canvas.getContext("2d");
+    ctx.drawWindow(content, content.scrollX, content.scrollY, width, height, "#fff");
+    sendAsyncMessage("ResponsiveMode:RequestScreenshot:Done", canvas.toDataURL());
+  }
 
+  var WebProgressListener = {
+    onLocationChange(webProgress, request, URI, flags) {
+      if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+        return;
+      }
+      makeScrollbarsFloating();
+    },
+    QueryInterface: function QueryInterface(aIID) {
+      if (aIID.equals(Ci.nsIWebProgressListener) ||
+          aIID.equals(Ci.nsISupportsWeakReference) ||
+          aIID.equals(Ci.nsISupports)) {
+          return this;
+      }
+      throw Components.results.NS_ERROR_NO_INTERFACE;
+    }
+  };
+})();
+
+global.responsiveFrameScriptLoaded = true;
 sendAsyncMessage("ResponsiveMode:ChildScriptReady");
--- a/devtools/client/responsivedesign/responsivedesign.jsm
+++ b/devtools/client/responsivedesign/responsivedesign.jsm
@@ -16,32 +16,37 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/SystemAppProxy.jsm");
 
 var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 var Telemetry = require("devtools/client/shared/telemetry");
 var { showDoorhanger } = require("devtools/client/shared/doorhanger");
 var { TouchEventSimulator } = require("devtools/shared/touch/simulator");
 var { Task } = require("resource://gre/modules/Task.jsm");
 var promise = require("promise");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
 
 this.EXPORTED_SYMBOLS = ["ResponsiveUIManager"];
 
 const MIN_WIDTH = 50;
 const MIN_HEIGHT = 50;
 
 const MAX_WIDTH = 10000;
 const MAX_HEIGHT = 10000;
 
 const SLOW_RATIO = 6;
 const ROUND_RATIO = 10;
 
 const INPUT_PARSER = /(\d+)[^\d]+(\d+)/;
 
 const SHARED_L10N = new ViewHelpers.L10N("chrome://devtools/locale/shared.properties");
 
+function debug(msg) {
+  dump(`RDM UI: ${msg}\n`);
+}
+
 var ActiveTabs = new Map();
 
 var Manager = {
   /**
    * Check if the a tab is in a responsive mode.
    * Leave the responsive mode if active,
    * active the responsive mode if not active.
    *
@@ -87,51 +92,53 @@ var Manager = {
   /**
    * Handle gcli commands.
    *
    * @param aWindow the browser window.
    * @param aTab the tab targeted.
    * @param aCommand the command name.
    * @param aArgs command arguments.
    */
-  handleGcliCommand: function(aWindow, aTab, aCommand, aArgs) {
+  handleGcliCommand: Task.async(function*(aWindow, aTab, aCommand, aArgs) {
     switch (aCommand) {
       case "resize to":
         this.runIfNeeded(aWindow, aTab);
-        ActiveTabs.get(aTab).setSize(aArgs.width, aArgs.height);
+        let ui = ActiveTabs.get(aTab);
+        yield ui.inited;
+        ui.setSize(aArgs.width, aArgs.height);
         break;
       case "resize on":
         this.runIfNeeded(aWindow, aTab);
         break;
       case "resize off":
         if (this.isActiveForTab(aTab)) {
-          ActiveTabs.get(aTab).close();
+          yield ActiveTabs.get(aTab).close();
         }
         break;
       case "resize toggle":
-          this.toggle(aWindow, aTab);
+        this.toggle(aWindow, aTab);
       default:
     }
-  }
+  })
 }
 
 EventEmitter.decorate(Manager);
 
 // If the experimental HTML UI is enabled, delegate the ResponsiveUIManager API
 // over to that tool instead.  Performing this delegation here allows us to
 // contain the pref check to a single place.
 if (Services.prefs.getBoolPref("devtools.responsive.html.enabled")) {
   let { ResponsiveUIManager } =
     require("devtools/client/responsive.html/manager");
   this.ResponsiveUIManager = ResponsiveUIManager;
 } else {
   this.ResponsiveUIManager = Manager;
 }
 
-var presets = [
+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>
 
@@ -150,141 +157,176 @@ function ResponsiveUI(aWindow, aTab)
   this.mm = this.tab.linkedBrowser.messageManager;
   this.tabContainer = aWindow.gBrowser.tabContainer;
   this.browser = aTab.linkedBrowser;
   this.chromeDoc = aWindow.document;
   this.container = aWindow.gBrowser.getBrowserContainer(this.browser);
   this.stack = this.container.querySelector(".browserStack");
   this._telemetry = new Telemetry();
 
-  let childOn = () => {
-    this.mm.removeMessageListener("ResponsiveMode:Start:Done", childOn);
-    ResponsiveUIManager.emit("on", { tab: this.tab });
-  }
-  this.mm.addMessageListener("ResponsiveMode:Start:Done", childOn);
-
-  let requiresFloatingScrollbars = !this.mainWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
-  this.mm.loadFrameScript("resource://devtools/client/responsivedesign/responsivedesign-child.js", true);
-  this.mm.addMessageListener("ResponsiveMode:ChildScriptReady", () => {
-    this.mm.sendAsyncMessage("ResponsiveMode:Start", {
-      requiresFloatingScrollbars: requiresFloatingScrollbars
-    });
-  });
-
-  // Try to load presets from prefs
-  if (Services.prefs.prefHasUserValue("devtools.responsiveUI.presets")) {
-    try {
-      presets = JSON.parse(Services.prefs.getCharPref("devtools.responsiveUI.presets"));
-    } catch(e) {
-      // User pref is malformated.
-      Cu.reportError("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 {
-    Cu.reportError("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
-  }
-
-  this.container.setAttribute("responsivemode", "true");
-  this.stack.setAttribute("responsivemode", "true");
-
   // Let's bind some callbacks.
   this.bound_presetSelected = this.presetSelected.bind(this);
   this.bound_handleManualInput = this.handleManualInput.bind(this);
   this.bound_addPreset = this.addPreset.bind(this);
   this.bound_removePreset = this.removePreset.bind(this);
   this.bound_rotate = this.rotate.bind(this);
   this.bound_screenshot = () => this.screenshot();
   this.bound_touch = this.toggleTouch.bind(this);
   this.bound_close = this.close.bind(this);
   this.bound_startResizing = this.startResizing.bind(this);
   this.bound_stopResizing = this.stopResizing.bind(this);
   this.bound_onDrag = this.onDrag.bind(this);
 
-  // Events
-  this.tab.addEventListener("TabClose", this);
-  this.tabContainer.addEventListener("TabSelect", this);
-
-  this.buildUI();
-  this.checkMenus();
-
-  try {
-    if (Services.prefs.getBoolPref("devtools.responsiveUI.rotate")) {
-      this.rotate();
-    }
-  } catch(e) {}
+  ActiveTabs.set(this.tab, this);
 
-  ActiveTabs.set(aTab, this);
-
-  this._telemetry.toolOpened("responsive");
-
-  // Touch events support
-  this.touchEnableBefore = false;
-  this.touchEventSimulator = new TouchEventSimulator(this.browser);
-
-  // Hook to display promotional Developer Edition doorhanger. Only displayed once.
-  showDoorhanger({
-    window: this.mainWindow,
-    type: "deveditionpromo",
-    anchor: this.chromeDoc.querySelector("#content")
-  });
+  this.inited = this.init();
 }
 
 ResponsiveUI.prototype = {
   _transitionsEnabled: true,
   get transitionsEnabled() {
     return this._transitionsEnabled;
   },
   set transitionsEnabled(aValue) {
     this._transitionsEnabled = aValue;
     if (aValue && !this._resizing && this.stack.hasAttribute("responsivemode")) {
       this.stack.removeAttribute("notransition");
     } else if (!aValue) {
       this.stack.setAttribute("notransition", "true");
     }
   },
 
+  init: Task.async(function*() {
+    debug("INIT BEGINS");
+
+    let ready = this.waitForMessage("ResponsiveMode:ChildScriptReady");
+    this.mm.loadFrameScript("resource://devtools/client/responsivedesign/responsivedesign-child.js", true);
+    yield ready;
+
+    // Tests expect events on resize to yield on various size changes
+    if (DevToolsUtils.testing) {
+      yield this.notifyOnResize();
+    }
+
+    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
+    });
+    yield started;
+
+    // Load Presets
+    this.loadPresets();
+
+    // Events
+    this.tab.addEventListener("TabClose", this);
+    this.tabContainer.addEventListener("TabSelect", this);
+
+    // Setup the UI
+    this.container.setAttribute("responsivemode", "true");
+    this.stack.setAttribute("responsivemode", "true");
+    this.buildUI();
+    this.checkMenus();
+
+    // Rotate the responsive mode if needed
+    try {
+      if (Services.prefs.getBoolPref("devtools.responsiveUI.rotate")) {
+        this.rotate();
+      }
+    } catch(e) {}
+
+    // Touch events support
+    this.touchEnableBefore = false;
+    this.touchEventSimulator = new TouchEventSimulator(this.browser);
+
+    // Hook to display promotional Developer Edition doorhanger.
+    // Only displayed once.
+    showDoorhanger({
+      window: this.mainWindow,
+      type: "deveditionpromo",
+      anchor: this.chromeDoc.querySelector("#content")
+    });
+
+    // Notify that responsive mode is on.
+    this._telemetry.toolOpened("responsive");
+    ResponsiveUIManager.emit("on", { tab: this.tab });
+  }),
+
+  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.
+        Cu.reportError("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 {
+      Cu.reportError("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: function RUI_close() {
-    if (this.closing)
+  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
+    if (DevToolsUtils.testing) {
+      yield this.waitForMessage("ResponsiveMode:OnContentResize");
+    }
+
     if (this.isResizing)
       this.stopResizing();
 
     // Remove listeners.
     this.menulist.removeEventListener("select", this.bound_presetSelected, true);
     this.menulist.removeEventListener("change", this.bound_handleManualInput, true);
     this.tab.removeEventListener("TabClose", this);
     this.tabContainer.removeEventListener("TabSelect", this);
@@ -311,45 +353,60 @@ ResponsiveUI.prototype = {
     this.container.removeAttribute("responsivemode");
     this.stack.removeAttribute("responsivemode");
 
     ActiveTabs.delete(this.tab);
     if (this.touchEventSimulator) {
       this.touchEventSimulator.stop();
     }
     this._telemetry.toolClosed("responsive");
-    let childOff = () => {
-      this.mm.removeMessageListener("ResponsiveMode:Stop:Done", childOff);
-      ResponsiveUIManager.emit("off", { tab: this.tab });
-    }
-    this.mm.addMessageListener("ResponsiveMode:Stop:Done", childOff);
+    let stopped = this.waitForMessage("ResponsiveMode:Stop:Done");
     this.tab.linkedBrowser.messageManager.sendAsyncMessage("ResponsiveMode:Stop");
+    yield stopped;
+
+    this.inited = null;
+    ResponsiveUIManager.emit("off", { tab: this.tab });
+  }),
+
+  waitForMessage(message) {
+    return new Promise(resolve => {
+      let listener = () => {
+        this.mm.removeMessageListener(message, listener);
+        resolve();
+      };
+      this.mm.addMessageListener(message, listener);
+    });
   },
 
   /**
    * Notify when the content has been resized. Only used in tests.
    */
-  _test_notifyOnResize: function() {
+  notifyOnResize: function() {
     let deferred = promise.defer();
     let mm = this.mm;
 
     this.bound_onContentResize = this.onContentResize.bind(this);
 
     mm.addMessageListener("ResponsiveMode:OnContentResize", this.bound_onContentResize);
 
-    mm.sendAsyncMessage("ResponsiveMode:NotifyOnResize");
     mm.addMessageListener("ResponsiveMode:NotifyOnResize:Done", function onListeningResize() {
       mm.removeMessageListener("ResponsiveMode:NotifyOnResize:Done", onListeningResize);
       deferred.resolve();
     });
+    debug("SEND NOTIFY");
+    mm.sendAsyncMessage("ResponsiveMode:NotifyOnResize");
     return deferred.promise;
   },
 
-  onContentResize: function() {
-    ResponsiveUIManager.emit("contentResize", { tab: this.tab });
+  onContentResize: function(msg) {
+    ResponsiveUIManager.emit("contentResize", {
+      tab: this.tab,
+      width: msg.data.width,
+      height: msg.data.height,
+    });
   },
 
   /**
    * Handle events
    */
   handleEvent: function (aEvent) {
     switch (aEvent.type) {
       case "TabClose":
@@ -859,22 +916,35 @@ ResponsiveUI.prototype = {
            null,
            nbox.PRIORITY_INFO_LOW,
            buttons);
        }
      }
    }),
 
   /**
+   * 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 browser.
    *
    * @param aWidth width of the browser.
    * @param aHeight height of the browser.
    */
   setSize: function RUI_setSize(aWidth, aHeight) {
+    debug(`SET SIZE TO ${aWidth} x ${aHeight}`);
     this.setWidth(aWidth);
     this.setHeight(aHeight);
   },
 
   setWidth: function RUI_setWidth(aWidth) {
     aWidth = Math.min(Math.max(aWidth, MIN_WIDTH), MAX_WIDTH);
     this.stack.style.maxWidth = this.stack.style.minWidth = aWidth + "px";
 
--- a/devtools/client/responsivedesign/test/browser_responsive_cmd.js
+++ b/devtools/client/responsivedesign/test/browser_responsive_cmd.js
@@ -6,112 +6,139 @@
 ///////////////////
 //
 // Whitelisting this test.
 // As part of bug 1077403, the leaking uncaught rejection should be fixed.
 //
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed("destroy");
 
 function test() {
+  let manager = ResponsiveUI.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: "resize toggle",
+        setup() {
+          done = once(manager, "on");
+          return helpers.setInput(options, "resize toggle");
+        },
         check: {
           input:  "resize toggle",
           hints:               "",
           markup: "VVVVVVVVVVVVV",
           status: "VALID"
         },
         exec: {
           output: ""
         },
-        post: function() {
+        post: Task.async(function*() {
+          yield done;
           ok(isOpen(), "responsive mode is open");
-        },
+        }),
       },
       {
-        setup: "resize toggle",
+        setup() {
+          done = once(manager, "off");
+          return helpers.setInput(options, "resize toggle");
+        },
         check: {
           input:  "resize toggle",
           hints:               "",
           markup: "VVVVVVVVVVVVV",
           status: "VALID"
         },
         exec: {
           output: ""
         },
-        post: function() {
+        post: Task.async(function*() {
+          yield done;
           ok(!isOpen(), "responsive mode is closed");
-        },
+        }),
       },
       {
-        setup: "resize on",
+        setup() {
+          done = once(manager, "on");
+          return helpers.setInput(options, "resize on");
+        },
         check: {
           input:  "resize on",
           hints:           "",
           markup: "VVVVVVVVV",
           status: "VALID"
         },
         exec: {
           output: ""
         },
-        post: function() {
+        post: Task.async(function*() {
+          yield done;
           ok(isOpen(), "responsive mode is open");
-        },
+        }),
       },
       {
-        setup: "resize off",
+        setup() {
+          done = once(manager, "off");
+          return helpers.setInput(options, "resize off");
+        },
         check: {
           input:  "resize off",
           hints:            "",
           markup: "VVVVVVVVVV",
           status: "VALID"
         },
         exec: {
           output: ""
         },
-        post: function() {
+        post: Task.async(function*() {
+          yield done;
           ok(!isOpen(), "responsive mode is closed");
-        },
+        }),
       },
       {
-        setup: "resize to 400 400",
+        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: function() {
+        post: Task.async(function*() {
+          yield done;
           ok(isOpen(), "responsive mode is open");
-        },
+        }),
       },
       {
-        setup: "resize off",
+        setup() {
+          done = once(manager, "off");
+          return helpers.setInput(options, "resize off");
+        },
         check: {
           input:  "resize off",
           hints:            "",
           markup: "VVVVVVVVVV",
           status: "VALID"
         },
         exec: {
           output: ""
         },
-        post: function() {
+        post: Task.async(function*() {
+          yield done;
           ok(!isOpen(), "responsive mode is closed");
-        },
+        }),
       },
     ]);
   }).then(finish);
 }
--- a/devtools/client/responsivedesign/test/browser_responsive_devicewidth.js
+++ b/devtools/client/responsivedesign/test/browser_responsive_devicewidth.js
@@ -1,24 +1,24 @@
 /* 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} = yield openRDM(tab);
+  let { rdm, manager } = yield openRDM(tab);
   ok(rdm, "An instance of the RDM should be attached to the tab.");
-  rdm.setSize(110, 500);
+  yield setSize(rdm, manager, 110, 500);
 
   info("Checking initial width/height properties.");
   yield doInitialChecks();
 
   info("Changing the RDM size");
-  rdm.setSize(90, 500);
+  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)
--- a/devtools/client/responsivedesign/test/browser_responsivecomputedview.js
+++ b/devtools/client/responsivedesign/test/browser_responsivecomputedview.js
@@ -17,46 +17,46 @@ const TEST_URI = "data:text/html;charset
                  "  }" +
                  "};" +
                  "</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} = yield openRDM();
-  rdm.setSize(500, 500);
+  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);
+  yield testShrink(view, inspector, rdm, manager);
 
   info("Try growing the viewport and checking the applied styles");
-  yield testGrow(view, inspector, rdm);
+  yield testGrow(view, inspector, rdm, manager);
 
   yield closeRDM(rdm);
   yield closeToolbox();
 });
 
-function* testShrink(computedView, inspector, rdm) {
+function* testShrink(computedView, inspector, rdm, manager) {
   is(computedWidth(computedView), "500px", "Should show 500px initially.");
 
   let onRefresh = inspector.once("computed-view-refreshed");
-  rdm.setSize(100, 100);
+  yield setSize(rdm, manager, 100, 100);
   yield onRefresh;
 
   is(computedWidth(computedView), "100px", "Should be 100px after shrinking.");
 }
 
-function* testGrow(computedView, inspector, rdm) {
+function* testGrow(computedView, inspector, rdm, manager) {
   let onRefresh = inspector.once("computed-view-refreshed");
-  rdm.setSize(500, 500);
+  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") {
--- a/devtools/client/responsivedesign/test/browser_responsiveruleview.js
+++ b/devtools/client/responsivedesign/test/browser_responsiveruleview.js
@@ -20,55 +20,55 @@ const TEST_URI = "data:text/html;charset
                  "  }" +
                  "};" +
                  "</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} = yield openRDM();
-  rdm.setSize(500, 500);
+  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);
+  yield testShrink(view, rdm, manager);
 
   info("Try growing the viewport and checking the applied styles");
-  yield testGrow(view, rdm);
+  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) {
+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");
-  rdm.setSize(100, 100);
+  yield setSize(rdm, manager, 100, 100);
   yield onRefresh;
 
   is(numberOfRules(ruleView), 3, "Should have three rules after shrinking.");
 }
 
-function* testGrow(ruleView, rdm) {
+function* testGrow(ruleView, rdm, manager) {
   info("Resize to 500x500 and wait for the rule-view to update");
   let onRefresh = ruleView.once("ruleview-refreshed");
-  rdm.setSize(500, 500);
+  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.");
 
--- a/devtools/client/responsivedesign/test/browser_responsiveui.js
+++ b/devtools/client/responsivedesign/test/browser_responsiveui.js
@@ -1,15 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 add_task(function*() {
-  SimpleTest.requestCompleteLog();
   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("Tools:ResponsiveUI").getAttribute("checked"),
      "true", "Menu item should be checked");
@@ -21,19 +20,16 @@ add_task(function*() {
   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 rdm._test_notifyOnResize();
-  yield waitForTick();
-
   yield testPresets(rdm, manager);
 
   info("Testing mouse resizing");
   yield testManualMouseResize(rdm, manager);
 
   info("Testing mouse resizing with shift key");
   yield testManualMouseResize(rdm, manager, "shift");
 
@@ -48,17 +44,20 @@ add_task(function*() {
 
   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("Tools:ResponsiveUI").getAttribute("checked"),
@@ -75,29 +74,26 @@ add_task(function*() {
 });
 
 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"));
-    let onContentResize = once(manager, "contentResize");
-    rdm.menulist.selectedIndex = c;
-    yield onContentResize;
+    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) {
-  rdm.setSize(100, 100);
-  yield once(manager, "contentResize");
+  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);
 
@@ -166,18 +162,17 @@ function* testInvalidUserInput(rdm) {
   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) {
-  rdm.setSize(100, 200);
-  yield once(manager, "contentResize");
+  yield setSize(rdm, manager, 100, 200);
 
   let {width: initialWidth, height: initialHeight} = yield getSizing();
   rdm.rotate();
 
   yield once(manager, "contentResize");
 
   let newSize = yield getSizing();
   is(newSize.width, initialHeight, "The width should now be the height.");
--- a/devtools/client/responsivedesign/test/browser_responsiveuiaddcustompreset.js
+++ b/devtools/client/responsivedesign/test/browser_responsiveuiaddcustompreset.js
@@ -1,17 +1,17 @@
 /* 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} = yield openRDM(tab);
+  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;
@@ -24,18 +24,16 @@ add_task(function*() {
 
   // 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.");
 
-  yield rdm._test_notifyOnResize();
-
   // 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();
 
@@ -43,87 +41,80 @@ add_task(function*() {
      "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, "contentResize");
   let customHeight = 123, customWidth = 456;
   rdm.startResizing({});
   rdm.setSize(customWidth, customHeight);
   rdm.stopResizing({});
 
   rdm.addbutton.doCommand();
-
-  // Force document reflow to avoid intermittent failures.
-  info("document height " + document.height);
+  yield resized;
 
   yield closeRDM(rdm);
 
-  // We're still in the loop of initializing the responsive mode.
-  // Let's wait next loop to stop it.
-  yield waitForTick();
-
   ({rdm} = yield openRDM(tab));
   is(container.getAttribute("responsivemode"), "true",
      "Should be in responsive mode.");
 
   let presetLabel = "456" + "\u00D7" + "123 (Testing preset)";
-  let customPresetIndex = getPresetIndex(rdm, presetLabel);
-  info(customPresetIndex);
+  let customPresetIndex = yield getPresetIndex(rdm, manager, presetLabel);
   ok(customPresetIndex >= 0, "(idx = " + customPresetIndex + ") should be the" +
                              " previously added preset in the list of items");
 
-  let resizePromise = rdm._test_notifyOnResize();
-  rdm.menulist.selectedIndex = customPresetIndex;
-  yield resizePromise;
+  yield setPresetIndex(rdm, manager, customPresetIndex);
 
   let browser = gBrowser.selectedBrowser;
   let props = yield ContentTask.spawn(browser, {}, function*() {
     let {innerWidth, innerHeight} = content;
     return {innerWidth, innerHeight};
   });
 
   is(props.innerWidth, 456, "Selecting preset should change the width");
   is(props.innerHeight, 123, "Selecting preset should change the height");
 
+  info(`menulist count: ${rdm.menulist.itemCount}`)
+
   rdm.removebutton.doCommand();
 
-  rdm.menulist.selectedIndex = 2;
+  yield setPresetIndex(rdm, manager, 2);
   let deletedPresetA = rdm.menulist.selectedItem.getAttribute("label");
   rdm.removebutton.doCommand();
 
-  rdm.menulist.selectedIndex = 2;
+  yield setPresetIndex(rdm, manager, 2);
   let deletedPresetB = rdm.menulist.selectedItem.getAttribute("label");
   rdm.removebutton.doCommand();
 
   yield closeRDM(rdm);
-  yield waitForTick();
   ({rdm} = yield openRDM(tab));
 
-  customPresetIndex = getPresetIndex(rdm, deletedPresetA);
+  customPresetIndex = yield getPresetIndex(rdm, manager, deletedPresetA);
   is(customPresetIndex, -1,
      "Deleted preset " + deletedPresetA + " should not be in the list anymore");
 
-  customPresetIndex = getPresetIndex(rdm, deletedPresetB);
+  customPresetIndex = yield getPresetIndex(rdm, manager, deletedPresetB);
   is(customPresetIndex, -1,
      "Deleted preset " + deletedPresetB + " should not be in the list anymore");
 
   yield closeRDM(rdm);
 });
 
-function getPresetIndex(rdm, presetLabel) {
-  function testOnePreset(c) {
+var getPresetIndex = Task.async(function*(rdm, manager, presetLabel) {
+  var testOnePreset = Task.async(function*(c) {
     if (c == 0) {
       return -1;
     }
-    rdm.menulist.selectedIndex = c;
+    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);
-}
+});
--- a/devtools/client/responsivedesign/test/head.js
+++ b/devtools/client/responsivedesign/test/head.js
@@ -10,58 +10,71 @@ Services.scriptloader.loadSubScript(shar
 
 // Import the GCLI test helper
 let gcliHelpersURI = testDir + "../../../commandline/test/helpers.js";
 Services.scriptloader.loadSubScript(gcliHelpersURI, this);
 
 DevToolsUtils.testing = true;
 registerCleanupFunction(() => {
   DevToolsUtils.testing = false;
-  while (gBrowser.tabs.length > 1) {
-    gBrowser.removeCurrentTab();
-  }
+  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();
+
 /**
  * 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 = ResponsiveUI.ResponsiveUIManager;
-  let mgrOn = once(manager, "on");
+
+  let opened = once(manager, "on");
+  let resized = once(manager, "contentResize");
   if (method == "menu") {
     document.getElementById("Tools:ResponsiveUI").doCommand();
   } else {
     synthesizeKeyFromKeyTag(document.getElementById("key_responsiveUI"));
   }
-  yield mgrOn;
+  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 mgr = ResponsiveUI.ResponsiveUIManager;
+  let manager = ResponsiveUI.ResponsiveUIManager;
   if (!rdm) {
-    rdm = mgr.getResponsiveUIForTab(gBrowser.selectedTab);
+    rdm = manager.getResponsiveUIForTab(gBrowser.selectedTab);
   }
-  let mgrOff = mgr.once("off");
+  let closed = once(manager, "off");
+  let resized = once(manager, "contentResize");
   rdm.close();
-  yield mgrOff;
+  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");
@@ -247,8 +260,43 @@ function getNodeFront(selector, {walker}
  */
 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("contentResize", onResize);
+      info(`Got contentResize to ${width} x ${height}`);
+      resolve();
+    };
+    info(`Waiting for contentResize to ${width} x ${height}`);
+    manager.on("contentResize", 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, "contentResize");
+    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.setSize(width, height);
+    yield resized;
+  }
+});
--- a/devtools/client/shared/test/browser_telemetry_button_responsive.js
+++ b/devtools/client/shared/test/browser_telemetry_button_responsive.js
@@ -30,38 +30,40 @@ function* testButton(toolbox, Telemetry)
   let button = toolbox.doc.querySelector("#command-button-responsive");
   ok(button, "Captain, we have the button");
 
   yield delayedClicks(button, 4);
 
   checkResults("_RESPONSIVE_", Telemetry);
 }
 
-function delayedClicks(node, clicks) {
+function waitForToggle() {
   return new Promise(resolve => {
-    let clicked = 0;
-
-    // See TOOL_DELAY for why we need setTimeout here
-    setTimeout(function delayedClick() {
-      info("Clicking button " + node.id);
-      if (clicked >= clicks) {
-        node.addEventListener("click", function listener() {
-          node.removeEventListener("click", listener);
-          resolve();
-        });
-      } else {
-        setTimeout(delayedClick, TOOL_DELAY);
-      }
-
-      node.click();
-      clicked++;
-    }, TOOL_DELAY);
+    let handler = () => {
+      manager.off("on", handler);
+      manager.off("off", handler);
+      resolve();
+    };
+    let manager = ResponsiveUI.ResponsiveUIManager;
+    manager.on("on", handler);
+    manager.on("off", handler);
   });
 }
 
+var delayedClicks = Task.async(function*(node, clicks) {
+  for (let i = 0; i < clicks; i++) {
+    info("Clicking button " + node.id);
+    let toggled = waitForToggle();
+    node.click();
+    yield toggled;
+    // See TOOL_DELAY for why we need setTimeout here
+    yield DevToolsUtils.waitForTime(TOOL_DELAY);
+  }
+});
+
 function checkResults(histIdFocus, Telemetry) {
   let result = Telemetry.prototype.telemetryInfo;
 
   for (let [histId, value] of Iterator(result)) {
     if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
         !histId.includes(histIdFocus)) {
       // Inspector stats are tested in
       // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here