Bug 1239437 - Browser mochitests for responsive.html. r=pbrosset
--- 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");
+});