Bug 1083269, handle windows in a different process within waitForFocus, r=jmaher
authorNeil Deakin <neil@mozilla.com>
Mon, 29 Dec 2014 07:39:26 -0500
changeset 247333 09606692e15489a6d18a4f11404e9d99cf2a0afa
parent 247332 724f0a71d62171da1357e6c1f93453359e54206b
child 247334 8530f5b7443b0c801a8c145ca2a88975bdd20ffb
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjmaher
bugs1083269
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1083269, handle windows in a different process within waitForFocus, r=jmaher
testing/mochitest/tests/SimpleTest/SimpleTest.js
testing/mochitest/tests/browser/browser.ini
testing/mochitest/tests/browser/browser_waitForFocus.js
testing/mochitest/tests/browser/waitForFocusPage.html
--- a/testing/mochitest/tests/SimpleTest/SimpleTest.js
+++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js
@@ -655,22 +655,29 @@ window.setTimeout = function SimpleTest_
  */
 SimpleTest.requestFlakyTimeout = function (reason) {
     SimpleTest.is(typeof(reason), "string", "A valid string reason is expected");
     SimpleTest.isnot(reason, "", "Reason cannot be empty");
     SimpleTest._flakyTimeoutIsOK = true;
     SimpleTest._flakyTimeoutReason = reason;
 }
 
-SimpleTest.waitForFocus_started = false;
-SimpleTest.waitForFocus_loaded = false;
-SimpleTest.waitForFocus_focused = false;
 SimpleTest._pendingWaitForFocusCount = 0;
 
 /**
+ * Version of waitForFocus that returns a promise.
+ */
+SimpleTest.promiseFocus = function *(targetWindow, expectBlankPage)
+{
+    return new Promise(function (resolve, reject) {
+        SimpleTest.waitForFocus(win => resolve(win), targetWindow, expectBlankPage);
+    });
+}
+
+/**
  * If the page is not yet loaded, waits for the load event. In addition, if
  * the page is not yet focused, focuses and waits for the window to be
  * focused. Calls the callback when completed. If the current page is
  * 'about:blank', then the page is assumed to not yet be loaded. Pass true for
  * expectBlankPage to not make this assumption if you expect a blank page to
  * be present.
  *
  * targetWindow should be specified if it is different than 'window'. The actual
@@ -679,90 +686,185 @@ SimpleTest._pendingWaitForFocusCount = 0
  * @param callback
  *        function called when load and focus are complete
  * @param targetWindow
  *        optional window to be loaded and focused, defaults to 'window'
  * @param expectBlankPage
  *        true if targetWindow.location is 'about:blank'. Defaults to false
  */
 SimpleTest.waitForFocus = function (callback, targetWindow, expectBlankPage) {
-    SimpleTest._pendingWaitForFocusCount++;
-    if (!targetWindow)
-      targetWindow = window;
+    // A separate method is used that is serialized and passed to the child
+    // process via loadFrameScript. Once the child window is focused, the
+    // child will send the WaitForFocus:ChildFocused notification to the parent.
+    // If a child frame in a child process must be focused, a
+    // WaitForFocus:FocusChild message is then sent to the child to focus that
+    // child. This message is used so that the child frame can be passed to it.
+    function waitForFocusInner(targetWindow, isChildProcess, expectBlankPage)
+    {
+      /* Indicates whether the desired targetWindow has loaded or focused. The
+         finished flag is set when the callback has been called and is used to
+         reject extraneous events from invoking the callback again. */
+      var loaded = false, focused = false, finished = false;
+
+      function info(msg) {
+          if (!isChildProcess) {
+              SimpleTest.info(msg);
+          }
+      }
 
-    SimpleTest.waitForFocus_started = false;
-    expectBlankPage = !!expectBlankPage;
+      function focusedWindow() {
+          if (isChildProcess) {
+              return Components.classes["@mozilla.org/focus-manager;1"].
+                      getService(Components.interfaces.nsIFocusManager).focusedWindow;
+          }
+          return SpecialPowers.focusedWindow();
+      }
+
+      function getHref(aWindow) {
+          return isChildProcess ? aWindow.location.href :
+                                  SpecialPowers.getPrivilegedProps(aWindow, 'location.href');
+      }
 
-    var childTargetWindow = SpecialPowers.getFocusedElementForWindow(targetWindow, true);
+      /* Event listener for the load or focus events. It will also be called with
+         event equal to null to check if the page is already focused and loaded. */
+      function focusedOrLoaded(event) {
+          try {
+              if (event) {
+                  if (event.type == "load") {
+                      if (expectBlankPage != (event.target.location == "about:blank")) {
+                          return;
+                      }
+
+                      loaded = true;
+                  } else if (event.type == "focus") {
+                      focused = true;
+                  }
+
+                  event.currentTarget.removeEventListener(event.type, focusedOrLoaded, true);
+              }
 
-    function info(msg) {
-        SimpleTest.info(msg);
-    }
-    function getHref(aWindow) {
-      return SpecialPowers.getPrivilegedProps(aWindow, 'location.href');
-    }
+              if (loaded && focused && !finished) {
+                  finished = true;
+                  if (isChildProcess) {
+                      sendAsyncMessage("WaitForFocus:ChildFocused", {}, null);
+                  } else {
+                      SimpleTest._pendingWaitForFocusCount--;
+                      SimpleTest.executeSoon(function() { callback(targetWindow) });
+                  }
+              }
+          } catch (e) {
+              if (!isChildProcess) {
+                  SimpleTest.ok(false, "Exception caught in focusedOrLoaded: " + e.message +
+                                ", at: " + e.fileName + " (" + e.lineNumber + ")");
+              }
+          }
+      }
+
+      function waitForLoadAndFocusOnWindow(desiredWindow) {
+          /* If the current document is about:blank and we are not expecting a blank
+             page (or vice versa), and the document has not yet loaded, wait for the
+             page to load. A common situation is to wait for a newly opened window
+             to load its content, and we want to skip over any intermediate blank
+             pages that load. This issue is described in bug 554873. */
+          loaded = expectBlankPage ?
+                     getHref(desiredWindow) == "about:blank" :
+                     getHref(desiredWindow) != "about:blank" &&
+                         desiredWindow.document.readyState == "complete";
+          if (!loaded) {
+              info("must wait for load");
+              desiredWindow.addEventListener("load", focusedOrLoaded, true);
+          }
 
-    function maybeRunTests() {
-        if (SimpleTest.waitForFocus_loaded &&
-            SimpleTest.waitForFocus_focused &&
-            !SimpleTest.waitForFocus_started) {
-            SimpleTest._pendingWaitForFocusCount--;
-            SimpleTest.waitForFocus_started = true;
-            SimpleTest.executeSoon(function() { callback(targetWindow) });
-        }
+          var childDesiredWindow = { };
+          if (isChildProcess) {
+              var fm = Components.classes["@mozilla.org/focus-manager;1"].
+                         getService(Components.interfaces.nsIFocusManager);
+              fm.getFocusedElementForWindow(desiredWindow, true, childDesiredWindow);
+              childDesiredWindow = childDesiredWindow.value;
+          } else {
+              childDesiredWindow = SpecialPowers.getFocusedElementForWindow(desiredWindow, true);
+          }
+
+          /* If this is a child frame, ensure that the frame is focused. */
+          focused = (focusedWindow() == childDesiredWindow);
+          if (!focused) {
+              info("must wait for focus");
+              desiredWindow.addEventListener("focus", focusedOrLoaded, true);
+              if (isChildProcess) {
+                  childDesiredWindow.focus();
+              }
+              else {
+                  SpecialPowers.focus(childDesiredWindow);
+              }
+          }
+
+          focusedOrLoaded(null);
+      }
+
+      if (isChildProcess) {
+          /* This message is used when an inner child frame must be focused. */
+          addMessageListener("WaitForFocus:FocusChild", function focusChild(msg) {
+              removeMessageListener("WaitForFocus:ChildFocused", focusChild);
+              finished = false;
+              waitForLoadAndFocusOnWindow(msg.objects.child);
+          });
+      }
+
+      waitForLoadAndFocusOnWindow(targetWindow);
     }
 
-    function waitForEvent(event) {
-        try {
-            // Check to make sure that this isn't a load event for a blank or
-            // non-blank page that wasn't desired.
-            if (event.type == "load" && (expectBlankPage != (event.target.location == "about:blank")))
-                return;
-
-            SimpleTest["waitForFocus_" + event.type + "ed"] = true;
-            var win = (event.type == "load") ? targetWindow : childTargetWindow;
-            win.removeEventListener(event.type, waitForEvent, true);
-            maybeRunTests();
-        } catch (e) {
-            SimpleTest.ok(false, "Exception caught in waitForEvent: " + e.message +
-                ", at: " + e.fileName + " (" + e.lineNumber + ")");
-        }
+    SimpleTest._pendingWaitForFocusCount++;
+    if (!targetWindow) {
+        targetWindow = window;
     }
 
-    // If the current document is about:blank and we are not expecting a blank
-    // page (or vice versa), and the document has not yet loaded, wait for the
-    // page to load. A common situation is to wait for a newly opened window
-    // to load its content, and we want to skip over any intermediate blank
-    // pages that load. This issue is described in bug 554873.
-    SimpleTest.waitForFocus_loaded =
-        expectBlankPage ?
-            getHref(targetWindow) == "about:blank" :
-            getHref(targetWindow) != "about:blank" && targetWindow.document.readyState == "complete";
-    if (!SimpleTest.waitForFocus_loaded) {
-        info("must wait for load");
-        targetWindow.addEventListener("load", waitForEvent, true);
+    expectBlankPage = !!expectBlankPage;
+
+    // If this is a request to focus a remote child window, the request must
+    // be forwarded to the child process.
+    // XXXndeakin now sure what this issue with Components.utils is about, but
+    // browser tests require the former and plain tests require the latter.
+    var Cu = Components.utils || SpecialPowers.Cu;
+    if (Cu.isCrossProcessWrapper(targetWindow)) {
+        // Look for a tabbrowser and see if targetWindow corresponds to one
+        // within that tabbrowser. If not, just return.
+        var tabBrowser = document.getElementsByTagName("tabbrowser")[0] || null;
+        var remoteBrowser = tabBrowser ? tabBrowser.getBrowserForContentWindow(targetWindow.top) : null;
+        if (!remoteBrowser) {
+            SimpleTest.info("child process window cannot be focused");
+            return;
+        }
+
+        // If a subframe in a child process needs to be focused, first focus the
+        // parent frame, then send a WaitForFocus:FocusChild message to the child
+        // containing the subframe to focus.
+        var mustFocusSubframe = (targetWindow != targetWindow.top);
+        remoteBrowser.messageManager.addMessageListener("WaitForFocus:ChildFocused", function waitTest(msg) {
+            if (mustFocusSubframe) {
+                mustFocusSubframe = false;
+                var mm = gBrowser.selectedBrowser.messageManager;
+                mm.sendAsyncMessage("WaitForFocus:FocusChild", {}, { child: targetWindow } );
+            }
+            else {
+                remoteBrowser.messageManager.removeMessageListener("WaitForFocus:ChildFocused", waitTest);
+                setTimeout(callback, 0, targetWindow);
+            }
+        });
+
+        // Serialize the waitForFocusInner function and run it in the child.
+        var frameScript = "data:,(" + waitForFocusInner.toString() +
+                          ")(content, true, " + expectBlankPage + ");";
+        remoteBrowser.messageManager.loadFrameScript(frameScript, true);
+        remoteBrowser.focus();
+        return;
     }
 
-    // Check if the desired window is already focused.
-    var focusedChildWindow = null;
-    if (SpecialPowers.activeWindow()) {
-        focusedChildWindow = SpecialPowers.getFocusedElementForWindow(SpecialPowers.activeWindow(), true);
-    }
-
-    // If this is a child frame, ensure that the frame is focused.
-    SimpleTest.waitForFocus_focused = (focusedChildWindow == childTargetWindow);
-    if (SimpleTest.waitForFocus_focused) {
-        // If the frame is already focused and loaded, call the callback directly.
-        maybeRunTests();
-    }
-    else {
-        info("must wait for focus");
-        childTargetWindow.addEventListener("focus", waitForEvent, true);
-        SpecialPowers.focus(childTargetWindow);
-    }
+    // Otherwise, this is an attempt to focus a parent process window, so pass
+    // false for isChildProcess.
+    waitForFocusInner(targetWindow, false, expectBlankPage);
 };
 
 SimpleTest.waitForClipboard_polls = 0;
 
 /*
  * Polls the clipboard waiting for the expected value. A known value different than
  * the expected value is put on the clipboard first (and also polled for) so we
  * can be sure the value we get isn't just the expected value because it was already
--- a/testing/mochitest/tests/browser/browser.ini
+++ b/testing/mochitest/tests/browser/browser.ini
@@ -8,19 +8,21 @@ support-files =
 [browser_pass.js]
 [browser_parameters.js]
 [browser_popupNode.js]
 [browser_popupNode_check.js]
 [browser_privileges.js]
 [browser_sanityException.js]
 [browser_sanityException2.js]
 skip-if = e10s
+[browser_waitForFocus.js]
 [browser_getTestFile.js]
 support-files =
   test-dir/*
+  waitForFocusPage.html
 
 # Disabled because it would take too long, useful to check functionality though.
 #  browser_requestLongerTimeout.js
 [browser_zz_fail_openwindow.js]
 skip-if = true # this catches outside of the main loop to find an extra window
 [browser_fail.js]
 skip-if = true
 [browser_fail_add_task.js]
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/tests/browser/browser_waitForFocus.js
@@ -0,0 +1,56 @@
+
+const gBaseURL = "https://example.com/browser/testing/mochitest/tests/browser/";
+
+function *promiseTabLoadEvent(tab, url)
+{
+  return new Promise(function (resolve, reject) {
+    function handleLoadEvent(event) {
+      if (event.originalTarget != tab.linkedBrowser.contentDocument ||
+          event.target.location.href == "about:blank" ||
+          (url && event.target.location.href != url)) {
+        return;
+      }
+
+      tab.linkedBrowser.removeEventListener("load", handleLoadEvent, true);
+      resolve(event);
+    }
+
+    tab.linkedBrowser.addEventListener("load", handleLoadEvent, true, true);
+    if (url)
+      tab.linkedBrowser.loadURI(url);
+  });
+}
+
+// Load a new blank tab
+add_task(function *() {
+  let tab = gBrowser.addTab();
+  gBrowser.selectedTab = tab;
+
+  let browser = gBrowser.getBrowserForTab(tab);
+
+  yield SimpleTest.promiseFocus(browser.contentWindowAsCPOW, true);
+
+  is(document.activeElement, browser, "Browser is focused when about:blank is loaded");
+
+  gBrowser.removeCurrentTab();
+  gURLBar.focus();
+});
+
+// Load a tab with a subframe inside it and wait until the subframe is focused
+add_task(function *() {
+  let tab = gBrowser.addTab();
+  gBrowser.selectedTab = tab;
+
+  let browser = gBrowser.getBrowserForTab(tab);
+  yield promiseTabLoadEvent(tab, gBaseURL + "waitForFocusPage.html");
+
+  yield SimpleTest.promiseFocus(browser.contentWindowAsCPOW);
+
+  is(document.activeElement, browser, "Browser is focused when page is loaded");
+
+  yield SimpleTest.promiseFocus(browser.contentWindowAsCPOW.frames[0]);
+
+  is(browser.contentDocumentAsCPOW.activeElement.localName, "iframe", "Child iframe is focused");
+
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/tests/browser/waitForFocusPage.html
@@ -0,0 +1,4 @@
+<body>
+  <input>
+  <iframe id="f" src="data:text/plain,Test" width=80 height=80></iframe>
+</body>