Commit a31f833d authored by Neil Deakin's avatar Neil Deakin
Browse files

Bug 1712900, rework waitForFocus/promiseFocus so that it works with fission, r=hsivonen

Instead of using a framescript, change promiseFocus to use the SpecialPowers actor. In addition, promiseFocus may now take a browsing context to focus a specific subframe's window. If that subframe is deeply nested, messages will be passed back and forth between the parent and each successive subframe to get to the one subframe that is desired to be focused. Similarly, this process is used when focusing a higher level frame that already has its focused element set to a subframe.

Differential Revision: https://phabricator.services.mozilla.com/D116387
parent 7676d879
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -12,3 +12,4 @@ skip-if =
  os == "linux" && bits == 64 # Bug 1663506
  os == "mac" && webrender && debug # Bug 1663506
  os == "win" && bits == 64 # Bug 1663506
[browser_promisefocus.js]
+259 −0
Original line number Diff line number Diff line
// Opens another window and switches focus between them.
add_task(async function test_window_focus() {
  let window2 = await BrowserTestUtils.openNewBrowserWindow();
  ok(!document.hasFocus(), "hasFocus after open second window");
  ok(window2.document.hasFocus(), "hasFocus after open second window");
  is(
    Services.focus.activeWindow,
    window2,
    "activeWindow after open second window"
  );
  is(
    Services.focus.focusedWindow,
    window2,
    "focusedWindow after open second window"
  );

  await SimpleTest.promiseFocus(window);
  ok(document.hasFocus(), "hasFocus after promiseFocus on window");
  ok(!window2.document.hasFocus(), "hasFocus after promiseFocus on window");
  is(
    Services.focus.activeWindow,
    window,
    "activeWindow after promiseFocus on window"
  );
  is(
    Services.focus.focusedWindow,
    window,
    "focusedWindow after promiseFocus on window"
  );

  await SimpleTest.promiseFocus(window2);
  ok(!document.hasFocus(), "hasFocus after promiseFocus on second window");
  ok(
    window2.document.hasFocus(),
    "hasFocus after promiseFocus on second window"
  );
  is(
    Services.focus.activeWindow,
    window2,
    "activeWindow after promiseFocus on second window"
  );
  is(
    Services.focus.focusedWindow,
    window2,
    "focusedWindow after promiseFocus on second window"
  );

  await BrowserTestUtils.closeWindow(window2);

  // If the window is already focused, this should just return.
  await SimpleTest.promiseFocus(window);
  await SimpleTest.promiseFocus(window);
});

// Opens two tabs and ensures that focus can be switched to the browser.
add_task(async function test_tab_focus() {
  let tab1 = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    "data:text/html,<input>"
  );

  let tab2 = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    "data:text/html,<input>"
  );

  gURLBar.focus();

  await SimpleTest.promiseFocus(tab2.linkedBrowser);
  is(
    document.activeElement,
    tab2.linkedBrowser,
    "Browser is focused after promiseFocus"
  );

  await SpecialPowers.spawn(tab1.linkedBrowser, [], () => {
    Assert.equal(
      Services.focus.activeBrowsingContext,
      null,
      "activeBrowsingContext in child process in hidden tab"
    );
    Assert.equal(
      Services.focus.focusedWindow,
      null,
      "focusedWindow in child process in hidden tab"
    );
    Assert.ok(
      !content.document.hasFocus(),
      "hasFocus in child process in hidden tab"
    );
  });

  await SpecialPowers.spawn(tab2.linkedBrowser, [], () => {
    Assert.equal(
      Services.focus.activeBrowsingContext,
      content.browsingContext,
      "activeBrowsingContext in child process in visible tab"
    );
    Assert.equal(
      Services.focus.focusedWindow,
      content.window,
      "focusedWindow in child process in visible tab"
    );
    Assert.ok(
      content.document.hasFocus(),
      "hasFocus in child process in visible tab"
    );
  });

  BrowserTestUtils.removeTab(tab1);
  BrowserTestUtils.removeTab(tab2);
});

// Opens a document with a nested hierarchy of frames using initChildFrames and
// focuses each child iframe in turn.
add_task(async function test_subframes_focus() {
  let tab = await BrowserTestUtils.openNewForegroundTab(
    gBrowser,
    OOP_BASE_PAGE_URI
  );

  const markup = "<input>";

  let browser = tab.linkedBrowser;
  let browsingContexts = await initChildFrames(browser, markup);

  for (let blurSubframe of [true, false]) {
    for (let index = browsingContexts.length - 1; index >= 0; index--) {
      let bc = browsingContexts[index];

      // Focus each browsing context in turn. Do this twice, once when the window
      // is not already focused, and once when it is already focused.
      for (let step = 0; step < 2; step++) {
        let desc =
          "within child frame " +
          index +
          " step " +
          step +
          " blur subframe " +
          blurSubframe +
          " ";

        info(desc + "start");
        await SimpleTest.promiseFocus(bc, false, blurSubframe);

        let expectedFocusedBC = bc;
        // Becuase we are iterating backwards through the iframes, when we get to a frame
        // that contains the iframe we just tested, focusing it will keep the child
        // iframe focused as well, so we need to account for this when verifying which
        // child iframe is focused. For the root frame (index 0), the iframe nested
        // two items down will actually be focused.
        // If blurSubframe is true however, the iframe focus in the parent will be cleared,
        // so the focused window should be the parent instead.
        if (!blurSubframe) {
          if (index == 0) {
            expectedFocusedBC = browsingContexts[index + 2];
          } else if (index == 3 || index == 1) {
            expectedFocusedBC = browsingContexts[index + 1];
          }
        }
        is(
          Services.focus.focusedContentBrowsingContext,
          expectedFocusedBC,
          desc +
            " focusedContentBrowsingContext" +
            ":: " +
            Services.focus.focusedContentBrowsingContext?.id +
            "," +
            expectedFocusedBC?.id
        );

        // If the processes don't match, then the child iframe is an out-of-process iframe.
        let oop =
          expectedFocusedBC.currentWindowGlobal.osPid !=
          bc.currentWindowGlobal.osPid;
        await SpecialPowers.spawn(
          bc,
          [
            index,
            desc,
            expectedFocusedBC != bc ? expectedFocusedBC : null,
            oop,
          ],
          (num, descChild, childBC, isOop) => {
            Assert.equal(
              Services.focus.activeBrowsingContext,
              content.browsingContext.top,
              descChild + "activeBrowsingContext"
            );
            if (!isOop) {
              // XXXndeakin bug 1709125 will fix this for oop mode.
              Assert.ok(
                content.document.hasFocus(),
                descChild + "hasFocus: " + content.browsingContext.id
              );
            }

            // If a child browsing context is expected to be focused, the focusedWindow
            // should be set to that instead and the active element should be an iframe.
            // Otherwise, the focused window should be this window, and the active
            // element should be the document's body element.
            if (childBC) {
              // Only check this for in-process iframes.
              // XXX This fails in fission mode. The frame structure is:
              //    A1
              //      -> B
              //      -> A2
              // where A and B are two processes. The frame A2 starts out focused. When B is
              // focused, A1's focus is updated correctly, however if window.focus() is then
              // called on A1, process A's focusedWindow is set to A2, yet its active
              // focused element is the frame for B.
              if (!isOop) {
                Assert.equal(
                  Services.focus.focusedWindow,
                  childBC.window,
                  descChild + "focusedWindow"
                );
              }
              Assert.equal(
                content.document.activeElement.localName,
                "iframe",
                descChild + "activeElement"
              );
            } else {
              Assert.equal(
                Services.focus.focusedWindow,
                content.window,
                descChild + "focusedWindow"
              );
              Assert.equal(
                content.document.activeElement,
                content.document.body,
                descChild + "activeElement"
              );
            }
          }
        );
      }
    }
  }

  // Focus the top window without blurring the browser.
  await SimpleTest.promiseFocus(window, false, false);
  is(
    document.activeElement.localName,
    "browser",
    "focus after blurring browser blur subframe false"
  );

  // Now, focus the top window, blurring the browser.
  await SimpleTest.promiseFocus(window, false, true);
  is(
    document.activeElement,
    document.body,
    "focus after blurring browser blur subframe true"
  );

  BrowserTestUtils.removeTab(tab);
});
+3 −1
Original line number Diff line number Diff line
@@ -147,7 +147,9 @@ function anchoredPopupShown(event)
  event.target.hidePopup();
}

SimpleTest.executeSoon(() => {
  window.arguments[0].SimpleTest.waitForFocus(doTest, window);
});
]]></script>

<resizer id="outside" dir="bottomend" element="outside-container"/>
+89 −237
Original line number Diff line number Diff line
@@ -928,255 +928,118 @@ SimpleTest.requestFlakyTimeout = function(reason) {
  SimpleTest._flakyTimeoutReason = reason;
};

SimpleTest._pendingWaitForFocusCount = 0;

/**
 * Version of waitForFocus that returns a promise. The Promise will
 * not resolve to the focused window, as it might be a CPOW (and Promises
 * cannot be resolved with CPOWs). If you require the focused window,
 * you should use waitForFocus instead.
 */
SimpleTest.promiseFocus = function(targetWindow, expectBlankPage) {
  return new Promise(function(resolve, reject) {
    SimpleTest.waitForFocus(
      win => {
        // Just resolve, without passing the window (see bug 1233497)
        resolve();
      },
      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.
 * If the page is not yet loaded, waits for the load event. If the page is
 * not yet focused, focuses and waits for the window to be focused.
 * 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
 * focused window may be a descendant of targetWindow.
 * The target object should be specified if it is different than 'window'. The
 * actual focused window may be a descendant window of aObject.
 *
 * @param callback
 *        function called when load and focus are complete
 * @param targetWindow
 *        optional window to be loaded and focused, defaults to 'window'.
 *        This may also be a <browser> element, in which case the window within
 *        that browser will be focused. This cannot be a window CPOW.
 * @param aObject
 *        Optional object to be focused, and may be either:
 *          window - a window object to focus
 *          browser - a <browser>/<iframe> element. The top-level window
 *                    within the frame will be focused.
 *          browsing context - a browsing context containing a window to focus
 *        If not specified, defaults to the global 'window'.
 * @param expectBlankPage
 *        true if targetWindow.location is 'about:blank'. Defaults to false
 *        True if targetWindow.location is 'about:blank'. Defaults to false
 * @param aBlurSubframe
 *        If true, and a subframe within the window to focus is focused, blur
 *        it so that the specified window or browsing context will receive
 *        focus events.
 * @returns The browsing context that was focused.
 */
SimpleTest.waitForFocus = function(callback, targetWindow, expectBlankPage) {
  // 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.
  /* eslint-disable mozilla/use-services */
  function waitForFocusInner(targetWin, isChildProcess, expectBlank) {
    /* 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);
      }
    }

    function focusedWindow() {
      if (isChildProcess) {
        return Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager)
          .focusedWindow;
      }
      return SpecialPowers.focusedWindow();
    }

    function getHref(aWindow) {
      return isChildProcess
        ? aWindow.location.href
        : SpecialPowers.getPrivilegedProps(aWindow, "location.href");
    }

    /* 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 (expectBlank != (event.target.location == "about:blank")) {
              return;
            }
SimpleTest.promiseFocus = async function(
  aObject,
  aExpectBlankPage = false,
  aBlurSubframe = false
) {
  let browser;
  let browsingContext;
  let windowToFocus;

            loaded = true;
          } else if (event.type == "focus") {
            focused = true;
  if (!aObject) {
    aObject = window;
  }

          event.currentTarget.removeEventListener(
            event.type,
            focusedOrLoaded,
            true
          );
        }

        if (loaded && focused && !finished) {
          finished = true;
          if (isChildProcess) {
            sendAsyncMessage("WaitForFocus:ChildFocused", {});
          } else {
            SimpleTest._pendingWaitForFocusCount--;
            SimpleTest.executeSoon(function() {
              callback(targetWin);
  async function waitForEvent(aTarget, aEventName) {
    return new Promise(resolve => {
      aTarget.addEventListener(aEventName, resolve, {
        capture: true,
        once: true,
      });
    });
  }
        }
      } 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 = expectBlank
        ? getHref(desiredWindow) == "about:blank"
        : getHref(desiredWindow) != "about:blank" &&
          desiredWindow.document.readyState == "complete";
      if (!loaded) {
  if (SpecialPowers.wrap(Window).isInstance(aObject)) {
    windowToFocus = aObject;

    let isBlank = windowToFocus.location.href == "about:blank";
    if (
      aExpectBlankPage != isBlank ||
      windowToFocus.document.readyState != "complete"
    ) {
      info("must wait for load");
        desiredWindow.addEventListener("load", focusedOrLoaded, true);
      await waitForEvent(windowToFocus, "load");
    }

      var childDesiredWindow = {};
      if (isChildProcess) {
        var fm = Cc["@mozilla.org/focus-manager;1"].getService(
          Ci.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. */
      if (isChildProcess) {
        focused = focusedWindow() == childDesiredWindow;
    if (SpecialPowers.wrap(Element).isInstance(aObject)) {
      // assume this is a browser/iframe element
      browsingContext = aObject.browsingContext;
    } else {
        focused = SpecialPowers.compare(focusedWindow(), childDesiredWindow);
      }
      if (!focused) {
        info("must wait for focus");
        childDesiredWindow.addEventListener("focus", focusedOrLoaded, true);
        if (isChildProcess) {
          childDesiredWindow.focus();
        } else {
          SpecialPowers.focus(childDesiredWindow);
        }
      browsingContext = aObject;
    }

      focusedOrLoaded(null);
    browser =
      browsingContext == aObject ? aObject.top.embedderElement : aObject;
    windowToFocus = browser.ownerGlobal;
  }

    if (isChildProcess) {
      /* This message is used when an inner child frame must be focused. */
      addMessageListener("WaitForFocus:FocusChild", function focusChild(msg) {
        removeMessageListener("WaitForFocus:FocusChild", focusChild);
        finished = false;
        waitForLoadAndFocusOnWindow(msg.objects.child);
      });
    }

    waitForLoadAndFocusOnWindow(targetWin);
  if (!windowToFocus.document.hasFocus()) {
    info("must wait for focus");
    let focusPromise = waitForEvent(windowToFocus.document, "focus");
    SpecialPowers.focus(windowToFocus);
    await focusPromise;
  }

  SimpleTest._pendingWaitForFocusCount++;
  if (!targetWindow) {
    targetWindow = window;
  if (browser) {
    if (windowToFocus.document.activeElement != browser) {
      browser.focus();
    }

  expectBlankPage = !!expectBlankPage;
    info("must wait for focus in content");

  // If this is a request to focus a remote child window, the request must
  // be forwarded to the child process.
  //
  // Even if the real |Components| doesn't exist, we might shim in a simple JS
  // placebo for compat. An easy way to differentiate this from the real thing
  // is whether the property is read-only or not.  The real |Components|
  // property is read-only.
  var c = Object.getOwnPropertyDescriptor(window, "Components");
  var Ci;
  if (c && c.value && !c.writable) {
    // eslint-disable-next-line mozilla/use-cc-etc
    Ci = Components.interfaces;
    // Make sure that the child process thinks it is focused as well.
    await SpecialPowers.ensureFocus(browsingContext, aBlurSubframe);
  } else {
    Ci = SpecialPowers.Ci;
    if (aBlurSubframe) {
      SpecialPowers.clearFocus(windowToFocus);
    }

  var browser = null;
  if (
    typeof XULElement != "undefined" &&
    targetWindow instanceof XULElement &&
    targetWindow.localName == "browser"
  ) {
    browser = targetWindow;
    browsingContext = windowToFocus.browsingContext;
  }

  if (browser && browser.isRemoteBrowser) {
    browser.messageManager.addMessageListener(
      "WaitForFocus:ChildFocused",
      function waitTest(msg) {
        browser.messageManager.removeMessageListener(
          "WaitForFocus:ChildFocused",
          waitTest
        );
        SimpleTest._pendingWaitForFocusCount--;
        setTimeout(callback, 0, browser);
      }
    );
  // Some tests rely on this delay, likely expecting layout or paint to occur.
  await new Promise(resolve => {
    SimpleTest.executeSoon(resolve);
  });

    // Serialize the waitForFocusInner function and run it in the child process.
    var frameScript =
      "data:,(" +
      waitForFocusInner.toString() +
      ")(content, true, " +
      expectBlankPage +
      ");";
    browser.messageManager.loadFrameScript(frameScript, true);
    browser.focus();
  } else {
    // Otherwise, this is an attempt to focus a single process or parent window,
    // so pass false for isChildProcess.
    if (browser) {
      targetWindow = browser.contentWindow;
    }
  return browsingContext;
};

    waitForFocusInner(targetWindow, false, expectBlankPage);
  }
/**
 * Version of promiseFocus that uses a callback. For compatibility,
 * the callback is passed one argument, the window that was focused.
 * If the focused window is not in the same process, null is supplied.
 */
SimpleTest.waitForFocus = function(callback, aObject, expectBlankPage) {
  SimpleTest.promiseFocus(aObject, expectBlankPage).then(focusedBC => {
    callback(focusedBC?.window);
  });
};
/* eslint-enable mozilla/use-services */

@@ -1556,17 +1419,6 @@ SimpleTest.finish = function() {
        "expectUncaughtException was called but no uncaught exception was detected!"
      );
    }
    if (SimpleTest._pendingWaitForFocusCount != 0) {
      SimpleTest.is(
        SimpleTest._pendingWaitForFocusCount,
        0,
        "[SimpleTest.finish()] waitForFocus() was called a " +
          "different number of times from the number of " +
          "callbacks run.  Maybe the test terminated " +
          "prematurely -- be sure to use " +
          "SimpleTest.waitForExplicitFinish()."
      );
    }
    if (SimpleTest._tests.length == 0) {
      SimpleTest.ok(
        false,
+59 −0
Original line number Diff line number Diff line
@@ -99,3 +99,62 @@ add_task(async function() {

  gBrowser.removeCurrentTab();
});

// Tests focusing the sidebar, which is in a parent process subframe
// and then switching the focus to another window.
add_task(async function() {
  await SidebarUI.show("viewBookmarksSidebar");

  gURLBar.focus();

  // Focus the sidebar.
  await SimpleTest.promiseFocus(SidebarUI.browser);
  is(
    document.activeElement,
    document.getElementById("sidebar"),
    "sidebar focused"
  );
  ok(
    document.activeElement.contentDocument.hasFocus(),
    "sidebar document hasFocus"
  );

  // Focus the sidebar again, which should cause no change.
  await SimpleTest.promiseFocus(SidebarUI.browser);
  is(
    document.activeElement,
    document.getElementById("sidebar"),
    "sidebar focused"
  );
  ok(
    document.activeElement.contentDocument.hasFocus(),
    "sidebar document hasFocus"
  );

  // Focus another window. The sidebar should no longer be focused.
  let window2 = await BrowserTestUtils.openNewBrowserWindow();
  is(
    document.activeElement,
    document.getElementById("sidebar"),
    "sidebar focused after window 2 opened"
  );
  ok(
    !document.activeElement.contentDocument.hasFocus(),
    "sidebar document hasFocus after window 2 opened"
  );

  // Focus the first window again and the sidebar should be focused again.
  await SimpleTest.promiseFocus(window);
  is(
    document.activeElement,
    document.getElementById("sidebar"),
    "sidebar focused after window1 refocused"
  );
  ok(
    document.activeElement.contentDocument.hasFocus(),
    "sidebar document hasFocus after window1 refocused"
  );

  await BrowserTestUtils.closeWindow(window2);
  await SidebarUI.hide();
});
Loading