Bug 1493108 - [marionette] Each command has to check the appropriate BrowsingContext for existence. r=marionette-reviewers,maja_zf,jdescottes
authorHenrik Skupin <mail@hskupin.info>
Tue, 15 Sep 2020 18:33:45 +0000
changeset 548812 bcf6908307f71714798acb55a7e8b41e8a2b9bfb
parent 548811 0438dfe8c6fd08a12aa169eb0261193d8f95c22b
child 548813 b9cfef2719954a66343b6baa68c2dc5bb7e936ac
push id126395
push userhskupin@mozilla.com
push dateTue, 15 Sep 2020 18:56:46 +0000
treeherderautoland@12fadd0009e4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarionette-reviewers, maja_zf, jdescottes
bugs1493108
milestone82.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 1493108 - [marionette] Each command has to check the appropriate BrowsingContext for existence. r=marionette-reviewers,maja_zf,jdescottes Given by the WebDriver specification each command has to check if the appropriate browsing context it operates in is still open. If it's not the case a "no such window" error will be thrown. Until now the code only checked the top-level browsing context for each of the supported commands. That means if the current browsing context was not open anymore a command running in a specific frame of the website has thrown unknown or type errors. That change makes our implementation WebDriver compliant. Differential Revision: https://phabricator.services.mozilla.com/D88951
testing/marionette/assert.js
testing/marionette/doc/Debugging.md
testing/marionette/driver.js
testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
testing/marionette/listener.js
testing/marionette/reftest.js
testing/marionette/test/unit/test_assert.js
--- a/testing/marionette/assert.js
+++ b/testing/marionette/assert.js
@@ -141,50 +141,36 @@ assert.content = function(context, msg =
   assert.that(
     c => c.toString() == "content",
     msg,
     error.UnsupportedOperationError
   )(context);
 };
 
 /**
- * Asserts that the {@link ChromeWindow} is open or that the {@link
- * browser.Context} has a content browser attached.
+ * Asserts that the {@link CanonicalBrowsingContext} is open.
  *
- * When passed in a {@link ChromeContext} this is equivalent to
- * testing that the associated <code>window</code> global is open,
- * and when given {@link browser.Context} it will test that the content
- * frame, represented by <code>&lt;xul:browser&gt;</code>, is
- * connected.
- *
- * @param {(ChromeWindow|browser.Context)} context
- *     Browsing context to test.
+ * @param {CanonicalBrowsingContext} browsingContext
+ *     Canonical browsing context to check.
  * @param {string=} msg
  *     Custom error message.
  *
- * @return {(ChromeWindow|browser.Context)}
- *     <var>context</var> is returned unaltered.
+ * @return {CanonicalBrowsingContext}
+ *     <var>browsingContext</var> is returned unaltered.
  *
  * @throws {NoSuchWindowError}
- *     If <var>context</var>'s <code>window</code> has been closed.
+ *     If <var>browsingContext</var> is no longer open.
  */
-assert.open = function(context, msg = "") {
-  // TODO: The contentBrowser uses a cached tab, which is only updated when
-  // switchToTab is called. Because of that an additional check is needed to
-  // make sure that the chrome window has not already been closed.
-  if (context instanceof browser.Context) {
-    assert.open(context.window);
-  }
-
+assert.open = function(browsingContext, msg = "") {
   msg = msg || "Browsing context has been discarded";
   return assert.that(
-    ctx => ctx && !ctx.closed,
+    browsingContext => !!browsingContext?.currentWindowGlobal,
     msg,
     error.NoSuchWindowError
-  )(context);
+  )(browsingContext);
 };
 
 /**
  * Asserts that there is no current user prompt.
  *
  * @param {modal.Dialog} dialog
  *     Reference to current dialogue.
  * @param {string=} msg
--- a/testing/marionette/doc/Debugging.md
+++ b/testing/marionette/doc/Debugging.md
@@ -48,18 +48,16 @@ It will prompt you when to start to allo
 breakpoints.  It will also prompt you between each test.
 
 You can also use the `debugger;` statement anywhere in chrome code
 to add a breakpoint.  In this example, a breakpoint will be added
 whenever the `WebDriver:GetPageSource` command is called:
 
 	GeckoDriver.prototype.getPageSource = async function() {
 	  debugger;
-	  const win = assert.open(this.getCurrentWindow());
-	  await this._handleUserPrompts();

 	}
 
 To not be prompted at the start of the test run or between tests,
 you can set the `marionette.debugging.clicktostart` preference to
 false this way:
 
 	% ./mach marionette-test --pref 'marionette.debugging.clicktostart:false' --jsdebugger
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -386,39 +386,44 @@ GeckoDriver.prototype.getActor = functio
 
 /**
  * Get the selected BrowsingContext for the current context.
  *
  * @param {Object} options
  * @param {Context=} options.context
  *     Context (content or chrome) for which to retrieve the browsing context.
  *     Defaults to the current one.
+ * @param {boolean=} options.parent
+ *     If set to true return the window's parent browsing context,
+ *     otherwise the one from the currently selected frame. Defaults to false.
  * @param {boolean=} options.top
- *     If set to true use the window's top-level browsing context,
+ *     If set to true return the window's top-level browsing context,
  *     otherwise the one from the currently selected frame. Defaults to false.
  *
  * @return {BrowsingContext}
  *     The browsing context.
  */
 GeckoDriver.prototype.getBrowsingContext = function(options = {}) {
-  const { context = this.context, top = false } = options;
+  const { context = this.context, parent = false, top = false } = options;
 
   let browsingContext = null;
   if (context === Context.Chrome) {
     browsingContext = this.chromeBrowsingContext;
   } else {
     browsingContext = this.contentBrowsingContext;
   }
 
+  if (browsingContext && parent) {
+    browsingContext = browsingContext.parent;
+  }
+
   if (browsingContext && top) {
     browsingContext = browsingContext.top;
   }
 
-  logger.trace(`Using browsing context ${browsingContext?.id}`);
-
   return browsingContext;
 };
 
 /**
  * Get the currently selected window.
  *
  * It will return the outer {@link ChromeWindow} previously selected by
  * window handle through {@link #switchToWindow}, or the first window that
@@ -985,21 +990,23 @@ GeckoDriver.prototype.getContext = funct
  *     Filename of the client's program where this script is evaluated.
  * @param {number=} line
  *     Line in the client's program where this script is evaluated.
  *
  * @return {(string|boolean|number|object|WebElement)}
  *     Return value from the script, or null which signifies either the
  *     JavaScript notion of null or undefined.
  *
+ * @throws {JavaScriptError}
+ *     If an {@link Error} was thrown whilst evaluating the script.
+ * @throws {NoSuchWindowError}
+ *     Browsing context has been discarded.
  * @throws {ScriptTimeoutError}
  *     If the script was interrupted due to reaching the session's
  *     script timeout.
- * @throws {JavaScriptError}
- *     If an {@link Error} was thrown whilst evaluating the script.
  */
 GeckoDriver.prototype.executeScript = async function(cmd) {
   let { script, args } = cmd.parameters;
   let opts = {
     script: cmd.parameters.script,
     args: cmd.parameters.args,
     sandboxName: cmd.parameters.sandbox,
     newSandbox: cmd.parameters.newSandbox,
@@ -1050,21 +1057,23 @@ GeckoDriver.prototype.executeScript = as
  *     Filename of the client's program where this script is evaluated.
  * @param {number=} line
  *     Line in the client's program where this script is evaluated.
  *
  * @return {(string|boolean|number|object|WebElement)}
  *     Return value from the script, or null which signifies either the
  *     JavaScript notion of null or undefined.
  *
+ * @throws {JavaScriptError}
+ *     If an Error was thrown whilst evaluating the script.
+ * @throws {NoSuchWindowError}
+ *     Browsing context has been discarded.
  * @throws {ScriptTimeoutError}
  *     If the script was interrupted due to reaching the session's
  *     script timeout.
- * @throws {JavaScriptError}
- *     If an Error was thrown whilst evaluating the script.
  */
 GeckoDriver.prototype.executeAsyncScript = async function(cmd) {
   let { script, args } = cmd.parameters;
   let opts = {
     script: cmd.parameters.script,
     args: cmd.parameters.args,
     sandboxName: cmd.parameters.sandbox,
     newSandbox: cmd.parameters.newSandbox,
@@ -1082,17 +1091,17 @@ GeckoDriver.prototype.execute_ = async f
   {
     sandboxName = null,
     newSandbox = false,
     file = "",
     line = 0,
     async = false,
   } = {}
 ) {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   assert.string(script, pprint`Expected "script" to be a string: ${script}`);
   assert.array(args, pprint`Expected script args to be an array: ${args}`);
   if (sandboxName !== null) {
     assert.string(
       sandboxName,
       pprint`Expected sandbox name to be a string: ${sandboxName}`
@@ -1161,26 +1170,26 @@ GeckoDriver.prototype.execute_ = async f
  *
  * In chrome context it will change the current window's location to
  * the supplied URL and wait until document.readyState equals "complete"
  * or the page timeout duration has elapsed.
  *
  * @param {string} url
  *     URL to navigate to.
  *
- * @throws {UnsupportedOperationError}
- *     Not available in current context.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ *     Not available in current context.
  */
 GeckoDriver.prototype.navigateTo = async function(cmd) {
   assert.content(this.context);
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ context: Context.Content, top: true }));
   await this._handleUserPrompts();
 
   let validURL;
   try {
     validURL = new URL(cmd.parameters.url);
   } catch (e) {
     throw new error.InvalidArgumentError(`Malformed URL: ${e.message}`);
   }
@@ -1214,17 +1223,17 @@ GeckoDriver.prototype.navigateTo = async
  * of the current resource.
  *
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getCurrentUrl = async function() {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
   await this._handleUserPrompts();
 
   const url = await this._getCurrentURL();
   return url.href;
 };
 
 /**
  * Gets the current title of the window.
@@ -1233,72 +1242,81 @@ GeckoDriver.prototype.getCurrentUrl = as
  *     Document title of the top-level browsing context.
  *
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getTitle = async function() {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
   await this._handleUserPrompts();
 
   return this.title;
 };
 
-/** Gets the current type of the window. */
+/**
+ * Gets the current type of the window.
+ *
+ * @return {string}
+ *     Type of window
+ *
+ * @throws {NoSuchWindowError}
+ *     Top-level browsing context has been discarded.
+ */
 GeckoDriver.prototype.getWindowType = function() {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
 
   return this.windowType;
 };
 
 /**
  * Gets the page source of the content document.
  *
  * @return {string}
  *     String serialisation of the DOM of the current browsing context's
  *     active document.
  *
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getPageSource = async function() {
-  const win = assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   switch (this.context) {
     case Context.Chrome:
-      let s = new win.XMLSerializer();
+      const win = this.getCurrentWindow();
+      const s = new win.XMLSerializer();
       return s.serializeToString(win.document);
 
     case Context.Content:
       return this.listener.getPageSource();
 
     default:
       throw new TypeError(`Unknown context: ${this.context}`);
   }
 };
 
 /**
  * Cause the browser to traverse one step backward in the joint history
  * of the current browsing context.
  *
- * @throws {UnsupportedOperationError}
- *     Not available in current context.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ *     Not available in current context.
  */
 GeckoDriver.prototype.goBack = async function() {
   assert.content(this.context);
-  assert.open(this.curBrowser);
+  assert.open(this.getBrowsingContext({ top: true }));
   await this._handleUserPrompts();
 
   const browsingContext = this.getBrowsingContext({ context: Context.Content });
 
   // If there is no history, just return
   if (!browsingContext.top.embedderElement?.canGoBack) {
     return;
   }
@@ -1307,26 +1325,26 @@ GeckoDriver.prototype.goBack = async fun
     browsingContext.goBack();
   });
 };
 
 /**
  * Cause the browser to traverse one step forward in the joint history
  * of the current browsing context.
  *
- * @throws {UnsupportedOperationError}
- *     Not available in current context.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ *     Not available in current context.
  */
 GeckoDriver.prototype.goForward = async function() {
   assert.content(this.context);
-  assert.open(this.curBrowser);
+  assert.open(this.getBrowsingContext({ top: true }));
   await this._handleUserPrompts();
 
   const browsingContext = this.getBrowsingContext({ context: Context.Content });
 
   // If there is no history, just return
   if (!browsingContext.top.embedderElement?.canGoForward) {
     return;
   }
@@ -1335,26 +1353,26 @@ GeckoDriver.prototype.goForward = async 
     browsingContext.goForward();
   });
 };
 
 /**
  * Causes the browser to reload the page in current top-level browsing
  * context.
  *
- * @throws {UnsupportedOperationError}
- *     Not available in current context.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ *     Not available in current context.
  */
 GeckoDriver.prototype.refresh = async function() {
   assert.content(this.context);
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
   await this._handleUserPrompts();
 
   // We need to move to the top frame before navigating
   await this.listener.switchToFrame();
 
   const browsingContext = this.getBrowsingContext({ context: Context.Content });
   await navigate.waitForNavigationCompleted(this, () => {
     navigate.refresh(browsingContext);
@@ -1401,17 +1419,17 @@ GeckoDriver.prototype.getIdForBrowser = 
  *
  * @return {string}
  *     Unique window handle.
  *
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  */
 GeckoDriver.prototype.getWindowHandle = function() {
-  assert.open(this.curBrowser);
+  assert.open(this.getBrowsingContext({ context: Context.Content, top: true }));
 
   return this.curBrowser.curFrameId.toString();
 };
 
 /**
  * Get a list of top-level browsing contexts. On desktop this typically
  * corresponds to the set of open tabs for browser windows, or the window
  * itself for non-browser chrome windows.
@@ -1438,17 +1456,17 @@ GeckoDriver.prototype.getWindowHandles =
  *     Unique window handle.
  *
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnknownError}
  *     Internal browsing context reference not found
  */
 GeckoDriver.prototype.getChromeWindowHandle = function() {
-  assert.open(this.getCurrentWindow({ context: Context.Chrome }));
+  assert.open(this.getBrowsingContext({ context: Context.Chrome, top: true }));
 
   for (let i in this.browsers) {
     if (this.curBrowser == this.browsers[i]) {
       return i;
     }
   }
 
   throw new error.UnknownError("Invalid browsing context");
@@ -1477,17 +1495,17 @@ GeckoDriver.prototype.getChromeWindowHan
  *     of browser window.
  *
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getWindowRect = async function() {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
   await this._handleUserPrompts();
 
   return this.curBrowser.rect;
 };
 
 /**
  * Set the window position and size of the browser on the operating
  * system window manager.
@@ -1506,30 +1524,31 @@ GeckoDriver.prototype.getWindowRect = as
  *     Width to resize the window to.
  * @param {number} height
  *     Height to resize the window to.
  *
  * @return {Object.<string, number>}
  *     Object with `x` and `y` coordinates and `width` and `height`
  *     dimensions.
  *
- * @throws {UnsupportedOperationError}
- *     Not applicable to application.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ *     Not applicable to application.
  */
 GeckoDriver.prototype.setWindowRect = async function(cmd) {
   assert.firefox();
-  const win = assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
   await this._handleUserPrompts();
 
   let { x, y, width, height } = cmd.parameters;
 
+  const win = this.getCurrentWindow();
   switch (WindowState.from(win.windowState)) {
     case WindowState.Fullscreen:
       await exitFullscreen(win);
       break;
 
     case WindowState.Maximized:
     case WindowState.Minimized:
       await restoreWindow(win);
@@ -1706,28 +1725,30 @@ GeckoDriver.prototype.setWindowHandle = 
   }
 };
 
 /**
  * Set the current browsing context for future commands to the parent
  * of the current browsing context.
  *
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.switchToParentFrame = async function() {
-  assert.open(this.getCurrentWindow());
-  await this._handleUserPrompts();
+  let browsingContext = this.getBrowsingContext();
+  if (browsingContext && !browsingContext.parent) {
+    return;
+  }
+
+  browsingContext = assert.open(browsingContext?.parent);
 
   if (MarionettePrefs.useActors) {
-    const { browsingContext } = await this.getActor().switchToParentFrame();
     this.contentBrowsingContext = browsingContext;
-
     return;
   }
 
   await this.listener.switchToParentFrame();
 };
 
 /**
  * Switch to a given frame within the current window.
@@ -1736,30 +1757,30 @@ GeckoDriver.prototype.switchToParentFram
  *     Focus the frame if set to true. Defaults to false.
  * @param {(string|Object)=} element
  *     A web element reference of the frame or its element id.
  * @param {number=} id
  *     The index of the frame to switch to.
  *     If both element and id are not defined, switch to top-level frame.
  *
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.switchToFrame = async function(cmd) {
   const { element: el, focus = false, id } = cmd.parameters;
 
-  assert.open(this.getCurrentWindow());
-  await this._handleUserPrompts();
-
   if (typeof id == "number") {
     assert.unsignedShort(id, `Expected id to be unsigned short, got ${id}`);
   }
 
+  assert.open(this.getBrowsingContext({ top: id == null && el == null }));
+  await this._handleUserPrompts();
+
   // Bug 1495063: Elements should be passed as WebElement reference
   let byFrame;
   if (typeof el == "string") {
     byFrame = WebElement.fromUUID(el, this.context);
   } else if (el) {
     byFrame = WebElement.fromJSON(el);
   }
 
@@ -1847,17 +1868,17 @@ GeckoDriver.prototype.getTimeouts = func
 GeckoDriver.prototype.setTimeouts = function(cmd) {
   // merge with existing timeouts
   let merged = Object.assign(this.timeouts.toJSON(), cmd.parameters);
   this.timeouts = Timeouts.fromJSON(merged);
 };
 
 /** Single tap. */
 GeckoDriver.prototype.singleTap = async function(cmd) {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
 
   let { id, x, y } = cmd.parameters;
   let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.Chrome:
       throw new error.UnsupportedOperationError(
         "Command 'singleTap' is not yet available in chrome context"
@@ -1870,86 +1891,86 @@ GeckoDriver.prototype.singleTap = async 
 };
 
 /**
  * Perform a series of grouped actions at the specified points in time.
  *
  * @param {Array.<?>} actions
  *     Array of objects that each represent an action sequence.
  *
+ * @throws {NoSuchWindowError}
+ *     Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ *     A modal dialog is open, blocking this operation.
  * @throws {UnsupportedOperationError}
  *     Not yet available in current context.
- * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
- * @throws {UnexpectedAlertOpenError}
- *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.performActions = async function(cmd) {
   assert.content(
     this.context,
     "Command 'performActions' is not yet available in chrome context"
   );
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let actions = cmd.parameters.actions;
   await this.listener.performActions({ actions });
 };
 
 /**
  * Release all the keys and pointer buttons that are currently depressed.
  *
+ * @throws {NoSuchWindowError}
+ *     Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ *     A modal dialog is open, blocking this operation.
  * @throws {UnsupportedOperationError}
  *     Not available in current context.
- * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
- * @throws {UnexpectedAlertOpenError}
- *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.releaseActions = async function() {
   assert.content(this.context);
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
   await this._handleUserPrompts();
 
   await this.listener.releaseActions();
 };
 
 /**
  * An action chain.
  *
  * @param {Object} value
  *     A nested array where the inner array represents each event,
  *     and the outer array represents a collection of events.
  *
  * @return {number}
  *     Last touch ID.
  *
+ * @throws {NoSuchWindowError}
+ *     Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ *     A modal dialog is open, blocking this operation.
  * @throws {UnsupportedOperationError}
  *     Not applicable to application.
- * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
- * @throws {UnexpectedAlertOpenError}
- *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.actionChain = async function(cmd) {
-  const win = assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let { chain, nextId } = cmd.parameters;
 
   switch (this.context) {
     case Context.Chrome:
       // be conservative until this has a use case and is established
       // to work as expected in Fennec
       assert.firefox();
 
       return this.legacyactions.dispatchActions(
         chain,
         nextId,
-        { frame: win },
+        { frame: this.getCurrentWindow() },
         this.curBrowser.seenEls
       );
 
     case Context.Content:
       return this.listener.actionChain(chain, nextId);
 
     default:
       throw new TypeError(`Unknown context: ${this.context}`);
@@ -1959,53 +1980,53 @@ GeckoDriver.prototype.actionChain = asyn
 /**
  * A multi-action chain.
  *
  * @param {Object} value
  *     A nested array where the inner array represents eache vent,
  *     the middle array represents a collection of events for each
  *     finger, and the outer array represents all fingers.
  *
+ * @throws {NoSuchWindowError}
+ *     Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ *     A modal dialog is open, blocking this operation.
  * @throws {UnsupportedOperationError}
  *     Not available in current context.
- * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
- * @throws {UnexpectedAlertOpenError}
- *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.multiAction = async function(cmd) {
   assert.content(this.context);
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let { value, max_length } = cmd.parameters; // eslint-disable-line camelcase
   await this.listener.multiAction(value, max_length);
 };
 
 /**
  * Find an element using the indicated search strategy.
  *
  * @param {string} using
  *     Indicates which search method to use.
  * @param {string} value
  *     Value the client is looking for.
  *
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.findElement = async function(cmd) {
   const { element: el, using, value } = cmd.parameters;
 
   if (!SUPPORTED_STRATEGIES.has(using)) {
     throw new error.InvalidSelectorError(`Strategy not supported: ${using}`);
   }
 
-  const win = assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let startNode;
   if (typeof el != "undefined") {
     startNode = WebElement.fromUUID(el, this.context);
   }
 
   let opts = {
@@ -2015,17 +2036,17 @@ GeckoDriver.prototype.findElement = asyn
   };
 
   if (MarionettePrefs.useActors) {
     return this.getActor().findElement(using, value, opts);
   }
 
   switch (this.context) {
     case Context.Chrome:
-      let container = { frame: win };
+      let container = { frame: this.getCurrentWindow() };
       if (opts.startNode) {
         opts.startNode = this.curBrowser.seenEls.get(opts.startNode);
       }
       let el = await element.find(container, using, value, opts);
       return this.curBrowser.seenEls.add(el);
 
     case Context.Content:
       return this.listener.findElementContent(using, value, opts);
@@ -2037,25 +2058,28 @@ GeckoDriver.prototype.findElement = asyn
 
 /**
  * Find elements using the indicated search strategy.
  *
  * @param {string} using
  *     Indicates which search method to use.
  * @param {string} value
  *     Value the client is looking for.
+ *
+ * @throws {NoSuchWindowError}
+ *     Browsing context has been discarded.
  */
 GeckoDriver.prototype.findElements = async function(cmd) {
   const { element: el, using, value } = cmd.parameters;
 
   if (!SUPPORTED_STRATEGIES.has(using)) {
     throw new error.InvalidSelectorError(`Strategy not supported: ${using}`);
   }
 
-  const win = assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let startNode;
   if (typeof el != "undefined") {
     startNode = WebElement.fromUUID(el, this.context);
   }
 
   let opts = {
@@ -2065,17 +2089,17 @@ GeckoDriver.prototype.findElements = asy
   };
 
   if (MarionettePrefs.useActors) {
     return this.getActor().findElements(using, value, opts);
   }
 
   switch (this.context) {
     case Context.Chrome:
-      let container = { frame: win };
+      let container = { frame: this.getCurrentWindow() };
       if (startNode) {
         opts.startNode = this.curBrowser.seenEls.get(opts.startNode);
       }
       let els = await element.find(container, using, value, opts);
       return this.curBrowser.seenEls.addAll(els);
 
     case Context.Content:
       return this.listener.findElementsContent(using, value, opts);
@@ -2087,51 +2111,51 @@ GeckoDriver.prototype.findElements = asy
 
 /**
  * Return the active element in the document.
  *
  * @return {WebElement}
  *     Active element of the current browsing context's document
  *     element, if the document element is non-null.
  *
- * @throws {UnsupportedOperationError}
- *     Not available in current context.
- * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
- * @throws {UnexpectedAlertOpenError}
- *     A modal dialog is open, blocking this operation.
  * @throws {NoSuchElementError}
  *     If the document does not have an active element, i.e. if
  *     its document element has been deleted.
+ * @throws {NoSuchWindowError}
+ *     Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ *     A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ *     Not available in current context.
  */
 GeckoDriver.prototype.getActiveElement = async function() {
   assert.content(this.context);
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   return this.listener.getActiveElement();
 };
 
 /**
  * Send click event to element.
  *
  * @param {string} id
  *     Reference ID to the element that will be clicked.
  *
  * @throws {InvalidArgumentError}
  *     If element <var>id</var> is not a string.
  * @throws {NoSuchElementError}
  *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.clickElement = async function(cmd) {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let id = assert.string(cmd.parameters.id);
   let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.Chrome:
       let el = this.curBrowser.seenEls.get(webEl);
@@ -2170,22 +2194,22 @@ GeckoDriver.prototype.clickElement = asy
  * @return {string}
  *     Value of the attribute.
  *
  * @throws {InvalidArgumentError}
  *     If <var>id</var> or <var>name</var> are not strings.
  * @throws {NoSuchElementError}
  *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getElementAttribute = async function(cmd) {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   const id = assert.string(cmd.parameters.id);
   const name = assert.string(cmd.parameters.name);
   const webEl = WebElement.fromUUID(id, this.context);
 
   if (MarionettePrefs.useActors) {
     return this.getActor().getElementAttribute(webEl, name);
@@ -2215,22 +2239,22 @@ GeckoDriver.prototype.getElementAttribut
  * @return {string}
  *     Value of the property.
  *
  * @throws {InvalidArgumentError}
  *     If <var>id</var> or <var>name</var> are not strings.
  * @throws {NoSuchElementError}
  *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getElementProperty = async function(cmd) {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   const id = assert.string(cmd.parameters.id);
   const name = assert.string(cmd.parameters.name);
   const webEl = WebElement.fromUUID(id, this.context);
 
   if (MarionettePrefs.useActors) {
     return this.getActor().getElementProperty(webEl, name);
@@ -2259,22 +2283,22 @@ GeckoDriver.prototype.getElementProperty
  * @return {string}
  *     Element's text "as rendered".
  *
  * @throws {InvalidArgumentError}
  *     If <var>id</var> is not a string.
  * @throws {NoSuchElementError}
  *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getElementText = async function(cmd) {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let id = assert.string(cmd.parameters.id);
   let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.Chrome:
       // for chrome, we look at text nodes, and any node with a "label" field
@@ -2300,22 +2324,22 @@ GeckoDriver.prototype.getElementText = a
  * @return {string}
  *     Local tag name of element.
  *
  * @throws {InvalidArgumentError}
  *     If <var>id</var> is not a string.
  * @throws {NoSuchElementError}
  *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getElementTagName = async function(cmd) {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let id = assert.string(cmd.parameters.id);
   let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.Chrome:
       let el = this.curBrowser.seenEls.get(webEl);
@@ -2338,22 +2362,22 @@ GeckoDriver.prototype.getElementTagName 
  * @return {boolean}
  *     True if displayed, false otherwise.
  *
  * @throws {InvalidArgumentError}
  *     If <var>id</var> is not a string.
  * @throws {NoSuchElementError}
  *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.isElementDisplayed = async function(cmd) {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let id = assert.string(cmd.parameters.id);
   let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.Chrome:
       let el = this.curBrowser.seenEls.get(webEl);
@@ -2378,33 +2402,34 @@ GeckoDriver.prototype.isElementDisplayed
  * @return {string}
  *     Value of |propertyName|.
  *
  * @throws {InvalidArgumentError}
  *     If <var>id</var> or <var>propertyName</var> are not strings.
  * @throws {NoSuchElementError}
  *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getElementValueOfCssProperty = async function(cmd) {
-  const win = assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let id = assert.string(cmd.parameters.id);
   let prop = assert.string(cmd.parameters.propertyName);
   let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.Chrome:
-      let el = this.curBrowser.seenEls.get(webEl);
-      let sty = win.document.defaultView.getComputedStyle(el);
-      return sty.getPropertyValue(prop);
+      const win = this.getCurrentWindow();
+      const el = this.curBrowser.seenEls.get(webEl);
+      const style = win.document.defaultView.getComputedStyle(el);
+      return style.getPropertyValue(prop);
 
     case Context.Content:
       return this.listener.getElementValueOfCssProperty(webEl, prop);
 
     default:
       throw new TypeError(`Unknown context: ${this.context}`);
   }
 };
@@ -2418,22 +2443,22 @@ GeckoDriver.prototype.getElementValueOfC
  * @return {boolean}
  *     True if enabled, false if disabled.
  *
  * @throws {InvalidArgumentError}
  *     If <var>id</var> is not a string.
  * @throws {NoSuchElementError}
  *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.isElementEnabled = async function(cmd) {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let id = assert.string(cmd.parameters.id);
   let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.Chrome:
       // Selenium atom doesn't quite work here
@@ -2457,22 +2482,22 @@ GeckoDriver.prototype.isElementEnabled =
  * @return {boolean}
  *     True if selected, false if unselected.
  *
  * @throws {InvalidArgumentError}
  *     If <var>id</var> is not a string.
  * @throws {NoSuchElementError}
  *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.isElementSelected = async function(cmd) {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let id = assert.string(cmd.parameters.id);
   let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.Chrome:
       // Selenium atom doesn't quite work here
@@ -2488,31 +2513,32 @@ GeckoDriver.prototype.isElementSelected 
 };
 
 /**
  * @throws {InvalidArgumentError}
  *     If <var>id</var> is not a string.
  * @throws {NoSuchElementError}
  *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getElementRect = async function(cmd) {
-  const win = assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let id = assert.string(cmd.parameters.id);
   let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.Chrome:
-      let el = this.curBrowser.seenEls.get(webEl);
-      let rect = el.getBoundingClientRect();
+      const win = this.getCurrentWindow();
+      const el = this.curBrowser.seenEls.get(webEl);
+      const rect = el.getBoundingClientRect();
       return {
         x: rect.x + win.pageXOffset,
         y: rect.y + win.pageYOffset,
         width: rect.width,
         height: rect.height,
       };
 
     case Context.Content:
@@ -2531,22 +2557,22 @@ GeckoDriver.prototype.getElementRect = a
  * @param {string} text
  *     Value to send to the element.
  *
  * @throws {InvalidArgumentError}
  *     If `id` or `text` are not strings.
  * @throws {NoSuchElementError}
  *     If element represented by reference `id` is unknown.
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.sendKeysToElement = async function(cmd) {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let id = assert.string(cmd.parameters.id);
   let text = assert.string(cmd.parameters.text);
   let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.Chrome:
@@ -2571,22 +2597,22 @@ GeckoDriver.prototype.sendKeysToElement 
  * @param {string} id
  *     Reference ID to the element that will be cleared.
  *
  * @throws {InvalidArgumentError}
  *     If <var>id</var> is not a string.
  * @throws {NoSuchElementError}
  *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
+ *     Browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.clearElement = async function(cmd) {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let id = assert.string(cmd.parameters.id);
   let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.Chrome:
       // the selenium atom doesn't work here
@@ -2612,20 +2638,22 @@ GeckoDriver.prototype.clearElement = asy
  *
  * @param {string=} id
  *     Reference ID to the element.
  *
  * @throws {InvalidArgumentError}
  *     If <var>id</var> is not a string.
  * @throws {NoSuchElementError}
  *     If element represented by reference <var>id</var> is unknown.
+ * @throws {NoSuchWindowError}
+ *     Browsing context has been discarded.
  */
 GeckoDriver.prototype.switchToShadowRoot = async function(cmd) {
   assert.content(this.context);
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
 
   let id = cmd.parameters.id;
   let webEl = null;
   if (id != null) {
     assert.string(id);
     webEl = WebElement.fromUUID(id, this.context);
   }
   await this.listener.switchToShadowRoot(webEl);
@@ -2633,29 +2661,29 @@ GeckoDriver.prototype.switchToShadowRoot
 
 /**
  * Add a single cookie to the cookie store associated with the active
  * document's address.
  *
  * @param {Map.<string, (string|number|boolean)> cookie
  *     Cookie object.
  *
- * @throws {UnsupportedOperationError}
- *     Not available in current context.
- * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
- * @throws {UnexpectedAlertOpenError}
- *     A modal dialog is open, blocking this operation.
  * @throws {InvalidCookieDomainError}
  *     If <var>cookie</var> is for a different domain than the active
  *     document's host.
+ * @throws {NoSuchWindowError}
+ *     Bbrowsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ *     A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ *     Not available in current context.
  */
 GeckoDriver.prototype.addCookie = async function(cmd) {
   assert.content(this.context);
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let { protocol, hostname } = await this._getCurrentURL();
 
   const networkSchemes = ["ftp:", "http:", "https:"];
   if (!networkSchemes.includes(protocol)) {
     throw new error.InvalidCookieDomainError("Document is cookie-averse");
   }
@@ -2666,66 +2694,66 @@ GeckoDriver.prototype.addCookie = async 
 };
 
 /**
  * Get all the cookies for the current domain.
  *
  * This is the equivalent of calling <code>document.cookie</code> and
  * parsing the result.
  *
+ * @throws {NoSuchWindowError}
+ *     Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ *     A modal dialog is open, blocking this operation.
  * @throws {UnsupportedOperationError}
  *     Not available in current context.
- * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
- * @throws {UnexpectedAlertOpenError}
- *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getCookies = async function() {
   assert.content(this.context);
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let { hostname, pathname } = await this._getCurrentURL();
   return [...cookie.iter(hostname, pathname)];
 };
 
 /**
  * Delete all cookies that are visible to a document.
  *
+ * @throws {NoSuchWindowError}
+ *     Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ *     A modal dialog is open, blocking this operation.
  * @throws {UnsupportedOperationError}
  *     Not available in current context.
- * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
- * @throws {UnexpectedAlertOpenError}
- *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.deleteAllCookies = async function() {
   assert.content(this.context);
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let { hostname, pathname } = await this._getCurrentURL();
   for (let toDelete of cookie.iter(hostname, pathname)) {
     cookie.remove(toDelete);
   }
 };
 
 /**
  * Delete a cookie by name.
  *
+ * @throws {NoSuchWindowError}
+ *     Browsing context has been discarded.
+ * @throws {UnexpectedAlertOpenError}
+ *     A modal dialog is open, blocking this operation.
  * @throws {UnsupportedOperationError}
  *     Not available in current context.
- * @throws {NoSuchWindowError}
- *     Top-level browsing context has been discarded.
- * @throws {UnexpectedAlertOpenError}
- *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.deleteCookie = async function(cmd) {
   assert.content(this.context);
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext());
   await this._handleUserPrompts();
 
   let { hostname, pathname } = await this._getCurrentURL();
   let name = assert.string(cmd.parameters.name);
   for (let c of cookie.iter(hostname, pathname)) {
     if (c.name === name) {
       cookie.remove(c);
     }
@@ -2743,19 +2771,22 @@ GeckoDriver.prototype.deleteCookie = asy
  *     in foreground (focused) or background (not focused). Defaults to false.
  * @param {boolean=} private
  *     Optional flag, which gets only evaluated for type `window`. True if the
  *     new top-level browsing context should be a private window.
  *     Defaults to false.
  *
  * @return {Object.<string, string>}
  *     Handle and type of the new browsing context.
+ *
+ * @throws {NoSuchWindowError}
+ *     Top-level browsing context has been discarded.
  */
 GeckoDriver.prototype.newWindow = async function(cmd) {
-  assert.open(this.getCurrentWindow({ context: Context.Content }));
+  assert.open(this.getBrowsingContext({ top: true }));
   await this._handleUserPrompts();
 
   let focus = false;
   if (typeof cmd.parameters.focus != "undefined") {
     focus = assert.boolean(
       cmd.parameters.focus,
       pprint`Expected "focus" to be a boolean, got ${cmd.parameters.focus}`
     );
@@ -2821,17 +2852,17 @@ GeckoDriver.prototype.newWindow = async 
  *     Unique window handles of remaining windows.
  *
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.close = async function() {
-  assert.open(this.getCurrentWindow({ context: Context.Content }));
+  assert.open(this.getBrowsingContext({ context: Context.Content, top: true }));
   await this._handleUserPrompts();
 
   let nwins = 0;
 
   for (let win of this.windows) {
     // For browser windows count the tabs. Otherwise take the window itself.
     let tabbrowser = browser.getTabBrowser(win);
     if (tabbrowser && tabbrowser.tabs) {
@@ -2858,20 +2889,23 @@ GeckoDriver.prototype.close = async func
  * Close the currently selected chrome window.
  *
  * If it is the last window currently open, the chrome window will not be
  * closed to prevent a shutdown of the application. Instead the returned
  * list of chrome window handles is empty.
  *
  * @return {Array.<string>}
  *     Unique chrome window handles of remaining chrome windows.
+ *
+ * @throws {NoSuchWindowError}
+ *     Top-level browsing context has been discarded.
  */
 GeckoDriver.prototype.closeChromeWindow = async function() {
   assert.firefox();
-  assert.open(this.getCurrentWindow({ context: Context.Chrome }));
+  assert.open(this.getBrowsingContext({ context: Context.Chrome, top: true }));
 
   let nwins = 0;
 
   // eslint-disable-next-line
   for (let _ of this.windows) {
     nwins++;
   }
 
@@ -2951,32 +2985,37 @@ GeckoDriver.prototype.deleteSession = fu
  *     True if the user requests a hash of the image data. Defaults to false.
  * @param {boolean=} scroll
  *     Scroll to element if |id| is provided. Defaults to true.
  *
  * @return {string}
  *     If <var>hash</var> is false, PNG image encoded as Base64 encoded
  *     string.  If <var>hash</var> is true, hex digest of the SHA-256
  *     hash of the Base64 encoded string.
+ *
+ * @throws {NoSuchWindowError}
+ *     Top-level browsing context has been discarded.
  */
 GeckoDriver.prototype.takeScreenshot = async function(cmd) {
-  let win = assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
   await this._handleUserPrompts();
 
   let { id, full, hash, scroll } = cmd.parameters;
   let format = hash ? capture.Format.Hash : capture.Format.Base64;
 
   full = typeof full == "undefined" ? true : full;
   scroll = typeof scroll == "undefined" ? true : scroll;
 
   let webEl = id ? WebElement.fromUUID(id, this.context) : null;
 
   // Only consider full screenshot if no element has been specified
   full = webEl ? false : full;
 
+  const win = this.getCurrentWindow();
+
   let rect;
   switch (this.context) {
     case Context.Chrome:
       if (id) {
         let el = this.curBrowser.seenEls.get(webEl, win);
         rect = el.getBoundingClientRect();
       } else if (full) {
         const docEl = win.document.documentElement;
@@ -3022,38 +3061,46 @@ GeckoDriver.prototype.takeScreenshot = a
 };
 
 /**
  * Get the current browser orientation.
  *
  * Will return one of the valid primary orientation values
  * portrait-primary, landscape-primary, portrait-secondary, or
  * landscape-secondary.
+ *
+ * @throws {NoSuchWindowError}
+ *     Top-level browsing context has been discarded.
  */
 GeckoDriver.prototype.getScreenOrientation = function() {
   assert.fennec();
-  let win = assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
+
+  const win = this.getCurrentWindow();
 
   return win.screen.mozOrientation;
 };
 
 /**
  * Set the current browser orientation.
  *
  * The supplied orientation should be given as one of the valid
  * orientation values.  If the orientation is unknown, an error will
  * be raised.
  *
  * Valid orientations are "portrait" and "landscape", which fall
  * back to "portrait-primary" and "landscape-primary" respectively,
  * and "portrait-secondary" as well as "landscape-secondary".
+ *
+ * @throws {NoSuchWindowError}
+ *     Top-level browsing context has been discarded.
  */
 GeckoDriver.prototype.setScreenOrientation = function(cmd) {
   assert.fennec();
-  let win = assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
 
   const ors = [
     "portrait",
     "landscape",
     "portrait-primary",
     "landscape-primary",
     "portrait-secondary",
     "landscape-secondary",
@@ -3061,44 +3108,46 @@ GeckoDriver.prototype.setScreenOrientati
 
   let or = String(cmd.parameters.orientation);
   assert.string(or);
   let mozOr = or.toLowerCase();
   if (!ors.includes(mozOr)) {
     throw new error.InvalidArgumentError(`Unknown screen orientation: ${or}`);
   }
 
+  const win = this.getCurrentWindow();
   if (!win.screen.mozLockOrientation(mozOr)) {
     throw new error.WebDriverError(`Unable to set screen orientation: ${or}`);
   }
 };
 
 /**
  * Synchronously minimizes the user agent window as if the user pressed
  * the minimize button.
  *
  * No action is taken if the window is already minimized.
  *
  * Not supported on Fennec.
  *
  * @return {Object.<string, number>}
  *     Window rect and window state.
  *
- * @throws {UnsupportedOperationError}
- *     Not available for current application.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ *     Not available for current application.
  */
 GeckoDriver.prototype.minimizeWindow = async function() {
   assert.firefox();
-  const win = assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
   await this._handleUserPrompts();
 
+  const win = this.getCurrentWindow();
   switch (WindowState.from(win.windowState)) {
     case WindowState.Fullscreen:
       await exitFullscreen(win);
       break;
 
     case WindowState.Maximized:
       await restoreWindow(win);
       break;
@@ -3129,28 +3178,29 @@ GeckoDriver.prototype.minimizeWindow = a
  *
  * No action is taken if the window is already maximized.
  *
  * Not supported on Fennec.
  *
  * @return {Object.<string, number>}
  *     Window rect.
  *
- * @throws {UnsupportedOperationError}
- *     Not available for current application.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ *     Not available for current application.
  */
 GeckoDriver.prototype.maximizeWindow = async function() {
   assert.firefox();
-  const win = assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
   await this._handleUserPrompts();
 
+  const win = this.getCurrentWindow();
   switch (WindowState.from(win.windowState)) {
     case WindowState.Fullscreen:
       await exitFullscreen(win);
       break;
 
     case WindowState.Minimized:
       await restoreWindow(win);
       break;
@@ -3180,28 +3230,29 @@ GeckoDriver.prototype.maximizeWindow = a
  *
  * No action is taken if the window is already in full screen mode.
  *
  * Not supported on Fennec.
  *
  * @return {Map.<string, number>}
  *     Window rect.
  *
- * @throws {UnsupportedOperationError}
- *     Not available for current application.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
+ * @throws {UnsupportedOperationError}
+ *     Not available for current application.
  */
 GeckoDriver.prototype.fullscreenWindow = async function() {
   assert.firefox();
-  const win = assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
   await this._handleUserPrompts();
 
+  const win = this.getCurrentWindow();
   switch (WindowState.from(win.windowState)) {
     case WindowState.Maximized:
     case WindowState.Minimized:
       await restoreWindow(win);
       break;
   }
 
   if (WindowState.from(win.windowState) != WindowState.Fullscreen) {
@@ -3220,53 +3271,64 @@ GeckoDriver.prototype.fullscreenWindow =
   await new IdlePromise(win);
 
   return this.curBrowser.rect;
 };
 
 /**
  * Dismisses a currently displayed tab modal, or returns no such alert if
  * no modal is displayed.
+ *
+ * @throws {NoSuchWindowError}
+ *     Top-level browsing context has been discarded.
  */
 GeckoDriver.prototype.dismissDialog = async function() {
-  let win = assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
   this._checkIfAlertIsPresent();
 
-  let dialogClosed = waitForEvent(win, "DOMModalDialogClosed");
-
-  let { button0, button1 } = this.dialog.ui;
+  const win = this.getCurrentWindow();
+  const dialogClosed = waitForEvent(win, "DOMModalDialogClosed");
+
+  const { button0, button1 } = this.dialog.ui;
   (button1 ? button1 : button0).click();
 
   await dialogClosed;
   await new IdlePromise(win);
 };
 
 /**
  * Accepts a currently displayed tab modal, or returns no such alert if
  * no modal is displayed.
+ *
+ * @throws {NoSuchWindowError}
+ *     Top-level browsing context has been discarded.
  */
 GeckoDriver.prototype.acceptDialog = async function() {
-  let win = assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
   this._checkIfAlertIsPresent();
 
-  let dialogClosed = waitForEvent(win, "DOMModalDialogClosed");
-
-  let { button0 } = this.dialog.ui;
+  const win = this.getCurrentWindow();
+  const dialogClosed = waitForEvent(win, "DOMModalDialogClosed");
+
+  const { button0 } = this.dialog.ui;
   button0.click();
 
   await dialogClosed;
   await new IdlePromise(win);
 };
 
 /**
  * Returns the message shown in a currently displayed modal, or returns
  * a no such alert error if no modal is currently displayed.
+ *
+ * @throws {NoSuchWindowError}
+ *     Top-level browsing context has been discarded.
  */
 GeckoDriver.prototype.getTextFromDialog = function() {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
   this._checkIfAlertIsPresent();
 
   return this.dialog.ui.infoBody.textContent;
 };
 
 /**
  * Set the user prompt's value field.
  *
@@ -3277,22 +3339,24 @@ GeckoDriver.prototype.getTextFromDialog 
  *
  * @param {string} text
  *     Input to the user prompt's value field.
  *
  * @throws {ElementNotInteractableError}
  *     If the current user prompt is an alert or confirm.
  * @throws {NoSuchAlertError}
  *     If there is no current user prompt.
+ * @throws {NoSuchWindowError}
+ *     Top-level browsing context has been discarded.
  * @throws {UnsupportedOperationError}
  *     If the current user prompt is something other than an alert,
  *     confirm, or a prompt.
  */
 GeckoDriver.prototype.sendKeysToDialog = async function(cmd) {
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
   this._checkIfAlertIsPresent();
 
   let text = assert.string(cmd.parameters.text);
   let promptType = this.dialog.args.promptType;
 
   switch (promptType) {
     case "alert":
     case "confirm":
@@ -3680,20 +3744,23 @@ GeckoDriver.prototype.teardownReftest = 
  *     to fit the paper size.
  * @param {boolean=} printBackground
  *     Print background graphics. Defaults to false.
  * @param {number=} scale
  *     Scale of the webpage rendering. Defaults to 1.
  *
  * @return {string}
  *     Base64 encoded PDF representing printed document
+ *
+ * @throws {NoSuchWindowError}
+ *     Top-level browsing context has been discarded.
  */
 GeckoDriver.prototype.print = async function(cmd) {
   assert.content(this.context);
-  assert.open(this.getCurrentWindow());
+  assert.open(this.getBrowsingContext({ top: true }));
   await this._handleUserPrompts();
 
   const settings = print.addDefaultSettings(cmd.parameters);
   for (let prop of ["top", "bottom", "left", "right"]) {
     assert.positiveNumber(
       settings.margin[prop],
       pprint`margin.${prop} is not a positive number`
     );
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
@@ -10,17 +10,16 @@ import os
 from six.moves.urllib.parse import quote
 
 from marionette_driver import By, errors, expected, Wait
 from marionette_driver.keys import Keys
 from marionette_driver.marionette import Alert
 from marionette_harness import (
     MarionetteTestCase,
     run_if_manage_instance,
-    skip,
     WindowManagerMixin,
 )
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 
 BLACK_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==' # noqa
 RED_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=' # noqa
@@ -58,17 +57,16 @@ class BaseNavigationTestCase(WindowManag
         self.new_tab = self.open_tab()
         self.marionette.switch_to_window(self.new_tab)
         Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
             lambda _: self.history_length == 1,
             message="The newly opened tab doesn't have a browser history length of 1")
 
     def tearDown(self):
         self.marionette.timeout.reset()
-        self.marionette.switch_to_parent_frame()
 
         self.close_all_tabs()
 
         super(BaseNavigationTestCase, self).tearDown()
 
     @property
     def history_length(self):
         return self.marionette.execute_script("return window.history.length;")
@@ -159,32 +157,34 @@ class TestNavigate(BaseNavigationTestCas
         frame = self.marionette.find_element(By.NAME, "third")
         self.marionette.switch_to_frame(frame)
         self.assertRaises(errors.NoSuchElementException,
                           self.marionette.find_element, By.NAME, "third")
 
         self.marionette.navigate(self.test_page_frameset)
         self.marionette.find_element(By.NAME, "third")
 
-    @skip("https://bugzilla.mozilla.org/show_bug.cgi?id=1255946")
     def test_navigate_top_frame_from_nested_context(self):
-        self.marionette.navigate(inline("""<title>foo</title>
-<iframe src="{}">""".format(inline("""<title>bar</title>
-<a href="{}" target=_top>consume top frame</a>""".format(inline("<title>baz</title>"))))))
-
-        self.assertEqual("foo", self.marionette.title)
+        sub_frame = inline("""
+          <title>bar</title>
+          <a href="{}" target="_top">consume top frame</a>
+        """.format(self.test_page_remote))
+        top_frame = inline("""
+          <title>foo</title>
+          <iframe src="{}">
+        """.format(sub_frame))
 
-        bar = self.marionette.find_element(By.TAG_NAME, "iframe")
-        self.marionette.switch_to_frame(bar)
-        self.assertEqual("foo", self.marionette.title)
+        self.marionette.navigate(top_frame)
+        frame = self.marionette.find_element(By.TAG_NAME, "iframe")
+        self.marionette.switch_to_frame(frame)
 
-        consume = self.marionette.find_element(By.TAG_NAME, "a")
-        consume.click()
+        link = self.marionette.find_element(By.TAG_NAME, "a")
+        link.click()
 
-        self.assertEqual("baz", self.marionette.title)
+        self.assertEqual(self.marionette.get_url(), self.test_page_remote)
 
     def test_invalid_url(self):
         with self.assertRaises(errors.MarionetteException):
             self.marionette.navigate("foo")
         with self.assertRaises(errors.MarionetteException):
             self.marionette.navigate("thisprotocoldoesnotexist://")
 
     def test_find_element_state_complete(self):
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -43,27 +43,32 @@ XPCOMUtils.defineLazyModuleGetters(this,
 XPCOMUtils.defineLazyGetter(this, "logger", () => Log.getWithPrefix(contentId));
 XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
 
 const contentFrameMessageManager = this;
 const contentId = content.docShell.browsingContext.id;
 
 const curContainer = {
   _frame: null,
+  _parentFrame: null,
   shadowRoot: null,
 
   get frame() {
     return this._frame;
   },
 
   set frame(frame) {
     this._frame = frame;
+    this._parentFrame = frame.parent;
+    this.id = frame.browsingContext.id;
+    this.shadowRoot = null;
+  },
 
-    this.id = this._frame.browsingContext.id;
-    this.shadowRoot = null;
+  get parentFrame() {
+    return this._parentFrame;
   },
 };
 
 // Listen for click event to indicate one click has happened, so actions
 // code can send dblclick event
 addEventListener("click", event.DoubleClickTracker.setClick);
 addEventListener("dblclick", event.DoubleClickTracker.resetClick);
 addEventListener("unload", event.DoubleClickTracker.resetClick, true);
@@ -891,17 +896,17 @@ function switchToShadowRoot(el) {
   curContainer.shadowRoot = foundShadowRoot;
 }
 
 /**
  * Switch to the parent frame of the current frame. If the frame is the
  * top most is the current frame then no action will happen.
  */
 function switchToParentFrame(msg) {
-  curContainer.frame = curContainer.frame.parent;
+  curContainer.frame = curContainer.parentFrame;
 
   sendSyncMessage("Marionette:switchedToFrame", {
     browsingContextId: curContainer.id,
   });
 
   sendOk(msg.json.commandID);
 }
 
--- a/testing/marionette/reftest.js
+++ b/testing/marionette/reftest.js
@@ -90,17 +90,18 @@ reftest.Runner = class {
    *     will be opened during the reftest run, where that's
    *     greater than 1.
    * @param {string} screenshotMode
    *     String enum representing when screenshots should be taken
    */
   setup(urlCount, screenshotMode, isPrint = false) {
     this.isPrint = isPrint;
 
-    this.parentWindow = assert.open(this.driver.getCurrentWindow());
+    assert.open(this.driver.getBrowsingContext({ top: true }));
+    this.parentWindow = this.driver.getCurrentWindow();
 
     this.screenshotMode =
       SCREENSHOT_MODE[screenshotMode] || SCREENSHOT_MODE.unexpected;
 
     this.urlCount = Object.keys(urlCount || {}).reduce(
       (map, key) => map.set(key, urlCount[key]),
       new Map()
     );
--- a/testing/marionette/test/unit/test_assert.js
+++ b/testing/marionette/test/unit/test_assert.js
@@ -140,19 +140,19 @@ add_test(function test_string() {
   assert.string(`bar`);
   Assert.throws(() => assert.string(42), /InvalidArgumentError/);
   Assert.throws(() => assert.string(42, "custom"), /custom/);
 
   run_next_test();
 });
 
 add_test(function test_open() {
-  assert.open({ closed: false });
+  assert.open({ currentWindowGlobal: {} });
 
-  for (let typ of [null, undefined, { closed: true }]) {
+  for (let typ of [null, undefined, { currentWindowGlobal: null }]) {
     Assert.throws(() => assert.open(typ), /NoSuchWindowError/);
   }
 
   Assert.throws(() => assert.open(null, "custom"), /custom/);
 
   run_next_test();
 });