Bug 1241582 - Port touch, window_close tests from legacy tool; r=ntim draft
authorMatteo Ferretti <mferretti@mozilla.com>
Fri, 09 Sep 2016 10:45:07 +0200
changeset 415097 2e6b29cc8e02d3757cda33bce475e6f3481c0ea1
parent 415096 6fef7e47113c21ad3447db194f921bf2b6f97421
child 531544 d67f8ea7eb5928175994491c5e14dca5c83e4053
push id29794
push userbmo:zer0@mozilla.com
push dateMon, 19 Sep 2016 15:17:01 +0000
reviewersntim
bugs1241582
milestone51.0a1
Bug 1241582 - Port touch, window_close tests from legacy tool; r=ntim MozReview-Commit-ID: 8Npzkbfekur
devtools/client/responsive.html/manager.js
devtools/client/responsive.html/test/browser/browser.ini
devtools/client/responsive.html/test/browser/browser_touch_simulation.js
devtools/client/responsive.html/test/browser/browser_window_close.js
devtools/client/responsive.html/test/browser/touch.html
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -300,18 +300,19 @@ ResponsiveUI.prototype = {
    * to ensure all in-page state is preserved, just like when you move a tab to
    * a new window.
    *
    * For more details, see /devtools/docs/responsive-design-mode.md.
    */
   init: Task.async(function* () {
     let ui = this;
 
-    // Watch for tab close so we can clean up RDM synchronously
+    // Watch for tab close and window close so we can clean up RDM synchronously
     this.tab.addEventListener("TabClose", this);
+    this.browserWindow.addEventListener("unload", this);
 
     // Swap page content from the current tab into a viewport within RDM
     this.swap = swapToInnerBrowser({
       tab: this.tab,
       containerURL: TOOL_URL,
       getInnerBrowser: Task.async(function* (containerBrowser) {
         let toolWindow = ui.toolWindow = containerBrowser.contentWindow;
         toolWindow.addEventListener("message", ui);
@@ -343,24 +344,26 @@ ResponsiveUI.prototype = {
     if (this.destroying) {
       return false;
     }
     this.destroying = true;
 
     // If our tab is about to be closed, there's not enough time to exit
     // gracefully, but that shouldn't be a problem since the tab will go away.
     // So, skip any yielding when we're about to close the tab.
-    let isTabClosing = options && options.reason == "TabClose";
+    let isWindowClosing = options && options.reason === "unload";
+    let isTabClosing = (options && options.reason === "TabClose") || isWindowClosing;
 
     // Ensure init has finished before starting destroy
     if (!isTabClosing) {
       yield this.inited;
     }
 
     this.tab.removeEventListener("TabClose", this);
+    this.browserWindow.removeEventListener("unload", this);
     this.toolWindow.removeEventListener("message", this);
 
     if (!isTabClosing) {
       // Stop the touch event simulator if it was running
       yield this.emulationFront.clearTouchEventsOverride();
 
       // Notify the inner browser to stop the frame script
       yield message.request(this.toolWindow, "stop-frame-script");
@@ -376,18 +379,20 @@ ResponsiveUI.prototype = {
 
     // Close the debugger client used to speak with emulation actor
     let clientClosed = this.client.close();
     if (!isTabClosing) {
       yield clientClosed;
     }
     this.client = this.emulationFront = null;
 
-    // Undo the swap and return the content back to a normal tab
-    swap.stop();
+    if (!isWindowClosing) {
+      // Undo the swap and return the content back to a normal tab
+      swap.stop();
+    }
 
     this.destroyed = true;
 
     return true;
   }),
 
   connectToServer: Task.async(function* () {
     if (!DebuggerServer.initialized) {
@@ -402,16 +407,17 @@ ResponsiveUI.prototype = {
 
   handleEvent(event) {
     let { browserWindow, tab } = this;
 
     switch (event.type) {
       case "message":
         this.handleMessage(event);
         break;
+      case "unload":
       case "TabClose":
         ResponsiveUIManager.closeIfNeeded(browserWindow, tab, {
           reason: event.type,
         });
         break;
     }
   },
 
--- a/devtools/client/responsive.html/test/browser/browser.ini
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -3,16 +3,17 @@ tags = devtools
 subsuite = devtools
 # !e10s: RDM only works for remote tabs
 skip-if = !e10s
 support-files =
   devices.json
   doc_page_state.html
   geolocation.html
   head.js
+  touch.html
   !/devtools/client/commandline/test/helpers.js
   !/devtools/client/framework/test/shared-head.js
   !/devtools/client/framework/test/shared-redux-head.js
   !/devtools/client/inspector/test/shared-head.js
   !/devtools/client/shared/test/test-actor.js
   !/devtools/client/shared/test/test-actor-registry.js
 
 [browser_device_change.js]
@@ -31,8 +32,9 @@ support-files =
 [browser_resize_cmd.js]
 [browser_screenshot_button.js]
 [browser_shutdown_close_sync.js]
 [browser_toolbox_computed_view.js]
 [browser_toolbox_rule_view.js]
 [browser_toolbox_swap_browsers.js]
 [browser_touch_simulation.js]
 [browser_viewport_basics.js]
+[browser_window_close.js]
--- a/devtools/client/responsive.html/test/browser/browser_touch_simulation.js
+++ b/devtools/client/responsive.html/test/browser/browser_touch_simulation.js
@@ -1,25 +1,240 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test global touch simulation button
 
-const TEST_URL = "data:text/html;charset=utf-8,";
+const TEST_URL = `${URL_ROOT}touch.html`;
+const PREF_DOM_META_VIEWPORT_ENABLED = "dom.meta-viewport.enabled";
 
 addRDMTask(TEST_URL, function* ({ ui }) {
-  let { store, document } = ui.toolWindow;
-  let touchButton = document.querySelector("#global-touch-simulation-button");
+  yield waitBootstrap(ui);
+  yield testWithNoTouch(ui);
+  yield enableTouchSimulation(ui);
+  yield testWithTouch(ui);
+  yield testWithMetaViewportEnabled(ui);
+  yield testWithMetaViewportDisabled(ui);
+  testTouchButton(ui);
+});
+
+function* testWithNoTouch(ui) {
+  yield injectEventUtils(ui);
+  yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+    let { EventUtils } = content;
+
+    let div = content.document.querySelector("div");
+    let x = 0, y = 0;
+
+    info("testWithNoTouch: Initial test parameter and mouse mouse outside div");
+    x = -1; y = -1;
+    yield EventUtils.synthesizeMouse(div, x, y,
+          { type: "mousemove", isSynthesized: false }, content);
+    div.style.transform = "none";
+    div.style.backgroundColor = "";
+
+    info("testWithNoTouch: Move mouse into the div element");
+    yield EventUtils.synthesizeMouseAtCenter(div,
+          { type: "mousemove", isSynthesized: false }, content);
+    is(div.style.backgroundColor, "red", "mouseenter or mouseover should work");
+
+    info("testWithNoTouch: Drag the div element");
+    yield EventUtils.synthesizeMouseAtCenter(div,
+          { type: "mousedown", isSynthesized: false }, content);
+    x = 100; y = 100;
+    yield EventUtils.synthesizeMouse(div, x, y,
+          { type: "mousemove", isSynthesized: false }, content);
+    is(div.style.transform, "none", "touchmove shouldn't work");
+    yield EventUtils.synthesizeMouse(div, x, y,
+          { type: "mouseup", isSynthesized: false }, content);
+
+    info("testWithNoTouch: Move mouse out of the div element");
+    x = -1; y = -1;
+    yield EventUtils.synthesizeMouse(div, x, y,
+          { type: "mousemove", isSynthesized: false }, content);
+    is(div.style.backgroundColor, "blue", "mouseout or mouseleave should work");
+
+    info("testWithNoTouch: Click the div element");
+    yield EventUtils.synthesizeClick(div);
+    is(div.dataset.isDelay, "false",
+      "300ms delay between touch events and mouse events should not work");
+  });
+}
+
+function* testWithTouch(ui) {
+  yield injectEventUtils(ui);
+
+  yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+    let { EventUtils } = content;
+
+    let div = content.document.querySelector("div");
+    let x = 0, y = 0;
+
+    info("testWithTouch: Initial test parameter and mouse mouse outside div");
+    x = -1; y = -1;
+    yield EventUtils.synthesizeMouse(div, x, y,
+          { type: "mousemove", isSynthesized: false }, content);
+    div.style.transform = "none";
+    div.style.backgroundColor = "";
+
+    info("testWithTouch: Move mouse into the div element");
+    yield EventUtils.synthesizeMouseAtCenter(div,
+          { type: "mousemove", isSynthesized: false }, content);
+    isnot(div.style.backgroundColor, "red",
+      "mouseenter or mouseover should not work");
 
-  // Wait until the viewport has been added
-  yield waitUntilState(store, state => state.viewports.length == 1);
-  yield waitForFrameLoad(ui, TEST_URL);
+    info("testWithTouch: Drag the div element");
+    yield EventUtils.synthesizeMouseAtCenter(div,
+          { type: "mousedown", isSynthesized: false }, content);
+    x = 100; y = 100;
+    yield EventUtils.synthesizeMouse(div, x, y,
+          { type: "mousemove", isSynthesized: false }, content);
+    isnot(div.style.transform, "none", "touchmove should work");
+    yield EventUtils.synthesizeMouse(div, x, y,
+          { type: "mouseup", isSynthesized: false }, content);
+
+    info("testWithTouch: Move mouse out of the div element");
+    x = -1; y = -1;
+    yield EventUtils.synthesizeMouse(div, x, y,
+          { type: "mousemove", isSynthesized: false }, content);
+    isnot(div.style.backgroundColor, "blue",
+      "mouseout or mouseleave should not work");
+  });
+}
+
+function* testWithMetaViewportEnabled(ui) {
+  yield SpecialPowers.pushPrefEnv({set: [[PREF_DOM_META_VIEWPORT_ENABLED, true]]});
+
+  yield injectEventUtils(ui);
+
+  yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+    let { synthesizeClick } = content.EventUtils;
+
+    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(ui) {
+  yield SpecialPowers.pushPrefEnv({set: [[PREF_DOM_META_VIEWPORT_ENABLED, false]]});
+
+  yield injectEventUtils(ui);
+
+  yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+    let { synthesizeClick } = content.EventUtils;
+
+    let meta = content.document.querySelector("meta[name=viewport]");
+    let div = content.document.querySelector("div");
+    div.dataset.isDelay = "false";
+
+    info("testWithMetaViewportDisabled: click the div 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 testTouchButton(ui) {
+  let { document } = ui.toolWindow;
+  let touchButton = document.querySelector("#global-touch-simulation-button");
 
   ok(!touchButton.classList.contains("active"),
     "Touch simulation is not active by default.");
 
   touchButton.click();
 
   ok(touchButton.classList.contains("active"),
     "Touch simulation is started on click.");
-});
+
+  touchButton.click();
+
+  ok(!touchButton.classList.contains("active"),
+    "Touch simulation is stopped on click.");
+}
+
+function* waitBootstrap(ui) {
+  let { store } = ui.toolWindow;
+
+  yield waitUntilState(store, state => state.viewports.length == 1);
+  yield waitForFrameLoad(ui, TEST_URL);
+}
+
+function* injectEventUtils(ui) {
+  yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+    if ("EventUtils" in content) {
+      return;
+    }
+
+    let EventUtils = content.EventUtils = {};
+
+    EventUtils.window = {};
+    EventUtils.parent = EventUtils.window;
+    /* eslint-disable camelcase */
+    EventUtils._EU_Ci = Components.interfaces;
+    EventUtils._EU_Cc = Components.classes;
+    /* eslint-enable camelcase */
+    // EventUtils' `sendChar` function relies on the navigator to synthetize events.
+    EventUtils.navigator = content.navigator;
+    EventUtils.KeyboardEvent = content.KeyboardEvent;
+
+    EventUtils.synthesizeClick = element => new Promise(resolve => {
+      element.addEventListener("click", function onClick() {
+        element.removeEventListener("click", onClick);
+        resolve();
+      });
+
+      EventUtils.synthesizeMouseAtCenter(element,
+        { type: "mousedown", isSynthesized: false }, content);
+      EventUtils.synthesizeMouseAtCenter(element,
+        { type: "mouseup", isSynthesized: false }, content);
+    });
+
+    Services.scriptloader.loadSubScript(
+      "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+  });
+}
+
+const enableTouchSimulation = ui => new Promise(
+  Task.async(function* (resolve) {
+    let browser = ui.getViewportBrowser();
+
+    browser.addEventListener("mozbrowserloadend", function onLoad() {
+      browser.removeEventListener("mozbrowserloadend", onLoad);
+      resolve();
+    });
+
+    yield ui.updateTouchSimulation(true);
+  }));
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_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("data:text/html;charset=utf-8,", "_blank");
+  let newWindow = yield newWindowPromise;
+
+  newWindow.focus();
+  yield once(newWindow.gBrowser, "load", true);
+
+  let tab = newWindow.gBrowser.selectedTab;
+  yield openRDM(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/responsive.html/test/browser/touch.html
@@ -0,0 +1,86 @@
+<!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 type="text/javascript;version=1.8">
+  "use strict";
+  let div = document.querySelector("div");
+  let initX, initY;
+  let previousEvent = "", touchendTime = 0;
+  let updatePreviousEvent = function (e) {
+    previousEvent = e.type;
+  };
+
+  div.style.transform = "none";
+  div.style.backgroundColor = "";
+
+  div.addEventListener("touchstart", function (evt) {
+    let touch = evt.changedTouches[0];
+    initX = touch.pageX;
+    initY = touch.pageY;
+    updatePreviousEvent(evt);
+  }, true);
+
+  div.addEventListener("touchmove", function (evt) {
+    let touch = evt.changedTouches[0];
+    let deltaX = touch.pageX - initX;
+    let deltaY = touch.pageY - initY;
+    div.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)";
+    updatePreviousEvent(evt);
+  }, true);
+
+  div.addEventListener("touchend", function (evt) {
+    if (!evt.touches.length) {
+      div.style.transform = "none";
+    }
+    touchendTime = performance.now();
+    updatePreviousEvent(evt);
+  }, true);
+
+  div.addEventListener("mouseenter", function (evt) {
+    div.style.backgroundColor = "red";
+    updatePreviousEvent(evt);
+  }, true);
+  div.addEventListener("mouseover", function(evt) {
+    div.style.backgroundColor = "red";
+    updatePreviousEvent(evt);
+  }, true);
+
+  div.addEventListener("mouseout", function (evt) {
+    div.style.backgroundColor = "blue";
+    updatePreviousEvent(evt);
+  }, true);
+
+  div.addEventListener("mouseleave", function (evt) {
+    div.style.backgroundColor = "blue";
+    updatePreviousEvent(evt);
+  }, true);
+
+  div.addEventListener("mousedown", function (evt) {
+    if (previousEvent === "touchend" && touchendTime !== 0) {
+      let now = performance.now();
+      div.dataset.isDelay = ((now - touchendTime) >= 300);
+    } else {
+      div.dataset.isDelay = false;
+    }
+    updatePreviousEvent(evt);
+  }, true);
+
+  div.addEventListener("mousemove", updatePreviousEvent, true);
+
+  div.addEventListener("mouseup", updatePreviousEvent, true);
+
+  div.addEventListener("click", updatePreviousEvent, true);
+</script>