Bug 1239437 - Browser mochitests for responsive.html. r=pbrosset draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Fri, 22 Jan 2016 20:36:59 -0600
changeset 324481 03b41e5c93f2dc77e32b210ed8324037a8c3d438
parent 324480 d8986c46df7458c7f55d6335ed86134073aaf443
child 513399 78dda91deb9ab7d4af132903f2a8e20539d634de
push id9925
push userjryans@gmail.com
push dateSat, 23 Jan 2016 02:38:47 +0000
reviewerspbrosset
bugs1239437
milestone46.0a1
Bug 1239437 - Browser mochitests for responsive.html. r=pbrosset
devtools/client/responsive.html/index.js
devtools/client/responsive.html/manager.js
devtools/client/responsive.html/moz.build
devtools/client/responsive.html/test/browser/.eslintrc
devtools/client/responsive.html/test/browser/browser.ini
devtools/client/responsive.html/test/browser/browser_viewport_basics.js
devtools/client/responsive.html/test/browser/head.js
--- a/devtools/client/responsive.html/index.js
+++ b/devtools/client/responsive.html/index.js
@@ -65,16 +65,22 @@ window.addEventListener("load", function
 window.addEventListener("unload", function onUnload() {
   window.removeEventListener("unload", onUnload);
   bootstrap.destroy();
 });
 
 // Allows quick testing of actions from the console
 window.dispatch = action => bootstrap.dispatch(action);
 
+// Expose the store on window for testing
+Object.defineProperty(window, "store", {
+  get: () => bootstrap.store,
+  enumerable: true,
+});
+
 /**
  * Called by manager.js to add the initial viewport based on the original page.
  */
 window.addInitialViewport = contentURI => {
   try {
     bootstrap.dispatch(changeLocation(contentURI));
     bootstrap.dispatch(addViewport());
   } catch (e) {
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -24,41 +24,63 @@ exports.ResponsiveUIManager = {
   /**
    * Toggle the responsive UI for a tab.
    *
    * @param window
    *        The main browser chrome window.
    * @param tab
    *        The browser tab.
    * @return Promise
-   *         Resolved when the toggling has completed.
+   *         Resolved when the toggling has completed.  If the UI has opened,
+   *         it is resolved to the ResponsiveUI instance for this tab.  If the
+   *         the UI has closed, there is no resolution value.
    */
   toggle(window, tab) {
-    if (this.isActiveForTab(tab)) {
-      this.activeTabs.get(tab).destroy();
-      this.activeTabs.delete(tab);
-    } else {
-      this.runIfNeeded(window, tab);
-    }
-    // TODO: Becomes a more interesting value in a later patch
-    return promise.resolve();
+    let action = this.isActiveForTab(tab) ? "close" : "open";
+    return this[action + "IfNeeded"](window, tab);
   },
 
   /**
-   * Launches the responsive UI.
+   * Opens the responsive UI, if not already open.
    *
    * @param window
    *        The main browser chrome window.
    * @param tab
    *        The browser tab.
+   * @return Promise
+   *         Resolved to the ResponsiveUI instance for this tab when opening is
+   *         complete.
    */
-  runIfNeeded(window, tab) {
+  openIfNeeded: Task.async(function*(window, tab) {
     if (!this.isActiveForTab(tab)) {
-      this.activeTabs.set(tab, new ResponsiveUI(window, tab));
+      let ui = new ResponsiveUI(window, tab);
+      this.activeTabs.set(tab, ui);
+      yield ui.inited;
+      this.emit("on", { tab });
     }
+    return this.getResponsiveUIForTab(tab);
+  }),
+
+  /**
+   * Closes the responsive UI, if not already closed.
+   *
+   * @param window
+   *        The main browser chrome window.
+   * @param tab
+   *        The browser tab.
+   * @return Promise
+   *         Resolved (with no value) when closing is complete.
+   */
+  closeIfNeeded(window, tab) {
+    if (this.isActiveForTab(tab)) {
+      this.activeTabs.get(tab).destroy();
+      this.activeTabs.delete(tab);
+      this.emit("off", { tab });
+    }
+    return promise.resolve();
   },
 
   /**
    * Returns true if responsive UI is active for a given tab.
    *
    * @param tab
    *        The browser tab.
    * @return boolean
@@ -87,68 +109,81 @@ exports.ResponsiveUIManager = {
    * @param tab
    *        The browser tab.
    * @param command
    *        The GCLI command name.
    * @param args
    *        The GCLI command arguments.
    */
   handleGcliCommand: function(window, tab, command, args) {
+    let completed;
     switch (command) {
       case "resize to":
-        this.runIfNeeded(window, tab);
+        completed = this.openIfNeeded(window, tab);
         // TODO: Probably the wrong API
         this.activeTabs.get(tab).setSize(args.width, args.height);
         break;
       case "resize on":
-        this.runIfNeeded(window, tab);
+        completed = this.openIfNeeded(window, tab);
         break;
       case "resize off":
-        if (this.isActiveForTab(tab)) {
-          this.activeTabs.get(tab).destroy();
-          this.activeTabs.delete(tab);
-        }
+        completed = this.closeIfNeeded(window, tab);
         break;
       case "resize toggle":
-        this.toggle(window, tab);
+        completed = this.toggle(window, tab);
         break;
       default:
     }
+    completed.catch(e => console.error(e));
   }
 };
 
 // GCLI commands in ../responsivedesign/resize-commands.js listen for events
 // from this object to know when the UI for a tab has opened or closed.
 EventEmitter.decorate(exports.ResponsiveUIManager);
 
 /**
  * ResponsiveUI manages the responsive design tool for a specific tab.  The
  * actual tool itself lives in a separate chrome:// document that is loaded into
  * the tab upon opening responsive design.  This object acts a helper to
  * integrate the tool into the surrounding browser UI as needed.
  */
 function ResponsiveUI(window, tab) {
-  this.window = window;
+  this.browserWindow = window;
   this.tab = tab;
-  this.init();
+  this.inited = this.init();
 }
 
 ResponsiveUI.prototype = {
 
   /**
    * The main browser chrome window (that holds many tabs).
    */
-  window: null,
+  browserWindow: null,
 
   /**
    * The specific browser tab this responsive instance is for.
    */
   tab: null,
 
   /**
+   * Promise resovled when the UI init has completed.
+   */
+  inited: null,
+
+  /**
+   * A window reference for the chrome:// document that displays the responsive
+   * design tool.  It is safe to reference this window directly even with e10s,
+   * as the tool UI is always loaded in the parent process.  The web content
+   * contained *within* the tool UI on the other hand is loaded in the child
+   * process.
+   */
+  toolWindow: null,
+
+  /**
    * For the moment, we open the tool by:
    * 1. Recording the tab's URL
    * 2. Navigating the tab to the tool
    * 3. Passing along the URL to the tool to open in the viewport
    *
    * This approach is simple, but it also discards the user's state on the page.
    * It's just like opening a fresh tab and pasting the URL.
    *
@@ -156,25 +191,27 @@ ResponsiveUI.prototype = {
    * state.  Platform discussions are in progress to make this happen.  See
    * bug 1238160 about <iframe mozbrowser> for more details.
    */
   init: Task.async(function*() {
     let tabBrowser = this.tab.linkedBrowser;
     let contentURI = tabBrowser.documentURI.spec;
     tabBrowser.loadURI(TOOL_URL);
     yield tabLoaded(this.tab);
-    let toolWindow = tabBrowser.contentWindow;
+    let toolWindow = this.toolWindow = tabBrowser.contentWindow;
     toolWindow.addInitialViewport(contentURI);
   }),
 
   destroy() {
     let tabBrowser = this.tab.linkedBrowser;
     tabBrowser.goBack();
     this.window = null;
     this.tab = null;
+    this.inited = null;
+    this.toolWindow = null;
   },
 
 };
 
 function tabLoaded(tab) {
   let deferred = promise.defer();
 
   function handle(event) {
--- a/devtools/client/responsive.html/moz.build
+++ b/devtools/client/responsive.html/moz.build
@@ -15,8 +15,9 @@ DevToolsModules(
     'index.css',
     'manager.js',
     'models.js',
     'reducers.js',
     'store.js',
 )
 
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
copy from devtools/shared/worker/tests/browser/.eslintrc
copy to devtools/client/responsive.html/test/browser/.eslintrc
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+  head.js
+
+[browser_viewport_basics.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_viewport_basics.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* TODO: Find out why the comment below is needed. */
+/* import-globals-from ../../../framework/test/shared-redux-head.js */
+
+// Test viewports basics after opening, like size and location
+
+const TEST_URL = "http://example.org/";
+
+addRDMTask(TEST_URL, function*({ ui }) {
+  let store = ui.toolWindow.store;
+
+  // Wait until the viewport has been added
+  yield waitUntilState(store, state => state.viewports.length == 1);
+
+  // A single viewport of default size appeared
+  let browser = ui.toolWindow.document.querySelector(".browser");
+  is(browser.width, "320", "Viewport has default width");
+  is(browser.height, "480", "Viewport has default height");
+
+  // Browser's location should match original tab
+  // TODO: For the moment, we have parent process <iframe>s and we can just
+  // check the location directly.  Bug 1240896 will change this to <iframe
+  // mozbrowser remote>, which is in the child process, so ContentTask or
+  // similar will be needed.
+  yield waitForFrameLoad(browser, TEST_URL);
+  is(browser.contentWindow.location.href, TEST_URL,
+     "Viewport location matches");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/head.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+/* import-globals-from ../../../framework/test/shared-head.js */
+/* import-globals-from ../../../framework/test/shared-redux-head.js */
+/* global ResponsiveUI */
+
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
+  this);
+Services.scriptloader.loadSubScript(
+  "chrome://mochitests/content/browser/devtools/client/framework/test/shared-redux-head.js",
+  this);
+
+Services.prefs.setBoolPref("devtools.responsive.html.enabled", true);
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("devtools.responsive.html.enabled");
+});
+
+/**
+ * Open responsive design mode for the given tab.
+ */
+var openRDM = Task.async(function*(tab) {
+  info("Opening responsive design mode");
+  let manager = ResponsiveUI.ResponsiveUIManager;
+  let ui = yield manager.openIfNeeded(window, tab);
+  info("Responsive design mode opened");
+  return { ui, manager };
+});
+
+/**
+ * Close responsive design mode for the given tab.
+ */
+var closeRDM = Task.async(function*(tab) {
+  info("Closing responsive design mode");
+  let manager = ResponsiveUI.ResponsiveUIManager;
+  manager.closeIfNeeded(window, tab);
+  info("Responsive design mode closed");
+});
+
+/**
+ * Adds a new test task that adds a tab with the given URL, opens responsive
+ * design mode, runs the given generator, closes responsive design mode, and
+ * removes the tab.
+ *
+ * Example usage:
+ *
+ *   addRDMTask(TEST_URL, function*({ ui, manager }) {
+ *     // Your tests go here...
+ *   });
+ */
+function addRDMTask(url, generator) {
+  add_task(function*() {
+    const tab = yield addTab(url);
+    const results = yield openRDM(tab);
+
+    try {
+      yield* generator(results);
+    } catch (err) {
+      ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err));
+    }
+
+    yield closeRDM(tab);
+    yield removeTab(tab);
+  });
+}
+
+var waitForFrameLoad = Task.async(function*(frame, targetURL) {
+  let window = frame.contentWindow;
+  if ((window.document.readyState == "complete" ||
+       window.document.readyState == "interactive") &&
+      window.location.href == targetURL) {
+    return;
+  }
+  yield once(frame, "load");
+});