Bug 1326174 - For unsupported commands in chrome context throw UnsupportedOperationError draft
authorHenrik Skupin <mail@hskupin.info>
Thu, 29 Dec 2016 14:04:16 +0100
changeset 454598 52778396fd7c2bc0773d3ccbe93a1adc7bde494c
parent 454520 87efd66165ddaa1b97608b92cd651b73c11aca6f
child 540761 3c909e6489c31a7d03843cd40c1c14cd71590d16
push id39987
push userbmo:hskupin@gmail.com
push dateThu, 29 Dec 2016 19:49:05 +0000
bugs1326174
milestone53.0a1
Bug 1326174 - For unsupported commands in chrome context throw UnsupportedOperationError Commands which are not (yet) supported in chrome context have to throw an UnsupportedOperationError to indicate that they cannot be used instead of silently eating failures. MozReview-Commit-ID: 2eFYAfKaQpD
testing/marionette/assert.js
testing/marionette/driver.js
testing/marionette/harness/marionette_harness/tests/unit/test_cookies.py
testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
testing/marionette/harness/marionette_harness/tests/unit/test_shadow_dom.py
--- a/testing/marionette/assert.js
+++ b/testing/marionette/assert.js
@@ -3,16 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
 
 Cu.import("chrome://marionette/content/error.js");
 
 this.EXPORTED_SYMBOLS = ["assert"];
 
 const isFennec = () => AppConstants.platform == "android";
 const isB2G = () => AppConstants.MOZ_B2G;
 const isFirefox = () => Services.appinfo.name == "Firefox";
@@ -25,60 +26,79 @@ this.assert = {};
  *
  * @param {string=} msg
  *     Custom error message.
  *
  * @throws {UnsupportedOperationError}
  *     If current browser is not Firefox.
  */
 assert.firefox = function (msg = "") {
-  msg = msg || "Expected Firefox";
+  msg = msg || "Only supported in Firefox";
   assert.that(isFirefox, msg, UnsupportedOperationError)();
 };
 
 /**
  * Asserts that the current browser is Fennec, or Firefox for Android.
  *
  * @param {string=} msg
  *     Custom error message.
  *
  * @throws {UnsupportedOperationError}
  *     If current browser is not Fennec.
  */
 assert.fennec = function (msg = "") {
-  msg = msg || "Expected Fennec";
+  msg = msg || "Only supported in Fennec";
   assert.that(isFennec, msg, UnsupportedOperationError)();
 };
 
 /**
  * Asserts that the current browser is B2G.
  *
  * @param {string=} msg
  *     Custom error message.
  *
  * @throws {UnsupportedOperationError}
  *     If the current browser is not B2G.
  */
 assert.b2g = function (msg = "") {
-  msg = msg || "Expected B2G"
+  msg = msg || "Only supported in B2G";
   assert.that(isB2G, msg, UnsupportedOperationError)();
 };
 
 /**
+ * Asserts that the current |context| is content.
+ *
+ * @param {string} context
+ *     Context to test.
+ * @param {string=} msg
+ *     Custom error message.
+ *
+ * @return {string}
+ *     |context| is returned unaltered.
+ *
+ * @throws {UnsupportedOperationError}
+ *     If |context| is not content.
+ */
+assert.content = function (context, msg = "") {
+  msg = msg || "Only supported in content context";
+  assert.that(c => c.toString() == "content", msg, UnsupportedOperationError)(context);
+}
+
+/**
  * Asserts that the current browser is a mobile browser, that is either
  * B2G or Fennec.
  *
  * @param {string=} msg
  *     Custom error message.
  *
  * @throws {UnsupportedOperationError}
  *     If the current browser is not B2G or Fennec.
  */
 assert.mobile = function (msg = "") {
-  msg = msg || "Expected Fennec or B2G";
+  msg = msg || "Only supported in Fennec or B2G";
   assert.that(() => isFennec() || isB2G(), msg, UnsupportedOperationError)();
 };
 
 /**
  * Asserts that |obj| is defined.
  *
  * @param {?} obj
  *     Value to test.
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -1010,42 +1010,36 @@ GeckoDriver.prototype.executeJSScript = 
  * 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.
  */
 GeckoDriver.prototype.get = function*(cmd, resp) {
+  assert.content(this.context);
+
   let url = cmd.parameters.url;
 
-  switch (this.context) {
-    case Context.CONTENT:
-      let get = this.listener.get({url: url, pageTimeout: this.pageTimeout});
-      // TODO(ato): Bug 1242595
-      let id = this.listener.activeMessageId;
+  let get = this.listener.get({url: url, pageTimeout: this.pageTimeout});
+  // TODO(ato): Bug 1242595
+  let id = this.listener.activeMessageId;
 
-      // If a remoteness update interrupts our page load, this will never return
-      // We need to re-issue this request to correctly poll for readyState and
-      // send errors.
-      this.curBrowser.pendingCommands.push(() => {
-        cmd.parameters.command_id = id;
-        cmd.parameters.pageTimeout = this.pageTimeout;
-        this.mm.broadcastAsyncMessage(
-            "Marionette:pollForReadyState" + this.curBrowser.curFrameId,
-            cmd.parameters);
-      });
+  // If a remoteness update interrupts our page load, this will never return
+  // We need to re-issue this request to correctly poll for readyState and
+  // send errors.
+  this.curBrowser.pendingCommands.push(() => {
+    cmd.parameters.command_id = id;
+    cmd.parameters.pageTimeout = this.pageTimeout;
+    this.mm.broadcastAsyncMessage(
+        "Marionette:pollForReadyState" + this.curBrowser.curFrameId,
+        cmd.parameters);
+  });
 
-      yield get;
-      break;
-
-    case Context.CHROME:
-      throw new UnsupportedOperationError("Cannot navigate in chrome context");
-      break;
-  }
+  yield get;
 };
 
 /**
  * Get a string representing the current URL.
  *
  * On Desktop this returns a string representation of the URL of the
  * current top level browsing context.  This is equivalent to
  * document.location.href.
@@ -1096,26 +1090,32 @@ GeckoDriver.prototype.getPageSource = fu
     case Context.CONTENT:
       resp.body.value = yield this.listener.getPageSource();
       break;
   }
 };
 
 /** Go back in history. */
 GeckoDriver.prototype.goBack = function*(cmd, resp) {
+  assert.content(this.context);
+
   yield this.listener.goBack();
 };
 
 /** Go forward in history. */
 GeckoDriver.prototype.goForward = function*(cmd, resp) {
+  assert.content(this.context);
+
   yield this.listener.goForward();
 };
 
 /** Refresh the page. */
 GeckoDriver.prototype.refresh = function*(cmd, resp) {
+  assert.content(this.context);
+
   yield this.listener.refresh();
 };
 
 /**
  * Get the current window's handle. On desktop this typically corresponds
  * to the currently selected tab.
  *
  * Return an opaque server-assigned identifier to this window that
@@ -1265,19 +1265,17 @@ GeckoDriver.prototype.getWindowPosition 
  * @param {number} y
  *     Y coordinate of the top/left of the window that it will be
  *     moved to.
  *
  * @return {Object.<string, number>}
  *     Object with |x| and |y| coordinates.
  */
 GeckoDriver.prototype.setWindowPosition = function (cmd, resp) {
-  if (this.appName != "Firefox") {
-    throw new UnsupportedOperationError("Unable to set the window position on mobile");
-  }
+  assert.firefox()
 
   let {x, y} = cmd.parameters;
   assert.positiveInteger(x);
   assert.positiveInteger(y);
 
   let win = this.getCurrentWindow();
   win.moveTo(x, y);
 
@@ -1613,17 +1611,18 @@ GeckoDriver.prototype.setTimeouts = func
 };
 
 /** Single tap. */
 GeckoDriver.prototype.singleTap = function*(cmd, resp) {
   let {id, x, y} = cmd.parameters;
 
   switch (this.context) {
     case Context.CHROME:
-      throw new WebDriverError("Command 'singleTap' is not available in chrome context");
+      throw new UnsupportedOperationError(
+          "Command 'singleTap' is not yet available in chrome context");
 
     case Context.CONTENT:
       this.addFrameCloseListener("tap");
       yield this.listener.singleTap(id, x, y);
       break;
   }
 };
 
@@ -1635,27 +1634,35 @@ GeckoDriver.prototype.singleTap = functi
  *
  * @throws {UnsupportedOperationError}
  *     If the command is made in chrome context.
  */
 GeckoDriver.prototype.performActions = function(cmd, resp) {
   switch (this.context) {
     case Context.CHROME:
       throw new UnsupportedOperationError(
-          "Command 'performActions' is not available in chrome context");
+          "Command 'performActions' is not yet available in chrome context");
+
     case Context.CONTENT:
       return this.listener.performActions({"actions": cmd.parameters.actions});
   }
 };
 
 /**
  * Release all the keys and pointer buttons that are currently depressed.
  */
 GeckoDriver.prototype.releaseActions = function(cmd, resp) {
-  return this.listener.releaseActions();
+  switch (this.context) {
+    case Context.CHROME:
+      throw new UnsupportedOperationError(
+          "Command 'releaseActions' is not yet available in chrome context");
+
+    case Context.CONTENT:
+        return 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.
@@ -1663,22 +1670,19 @@ GeckoDriver.prototype.releaseActions = f
  * @return {number}
  *     Last touch ID.
  */
 GeckoDriver.prototype.actionChain = function*(cmd, resp) {
   let {chain, nextId} = cmd.parameters;
 
   switch (this.context) {
     case Context.CHROME:
-      if (this.appName != "Firefox") {
-        // be conservative until this has a use case and is established
-        // to work as expected on b2g/fennec
-        throw new WebDriverError(
-            "Command 'actionChain' is not available in chrome context");
-      }
+      // be conservative until this has a use case and is established
+      // to work as expected in Fennec
+      assert.firefox()
 
       let win = this.getCurrentWindow();
       resp.body.value = yield this.legacyactions.dispatchActions(
           chain, nextId, {frame: win}, this.curBrowser.seenEls);
       break;
 
     case Context.CONTENT:
       this.addFrameCloseListener("action chain");
@@ -1693,17 +1697,18 @@ GeckoDriver.prototype.actionChain = func
  * @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.
  */
 GeckoDriver.prototype.multiAction = function*(cmd, resp) {
   switch (this.context) {
     case Context.CHROME:
-      throw new WebDriverError("Command 'multiAction' is not available in chrome context");
+      throw new UnsupportedOperationError(
+          "Command 'multiAction' is not yet available in chrome context");
 
     case Context.CONTENT:
       this.addFrameCloseListener("multi action chain");
       yield this.listener.multiAction(cmd.parameters.value, cmd.parameters.max_length);
       break;
   }
 };
 
@@ -1790,17 +1795,25 @@ GeckoDriver.prototype.findElements = fun
           cmd.parameters.value,
           opts);
       break;
   }
 };
 
 /** Return the active element on the page. */
 GeckoDriver.prototype.getActiveElement = function*(cmd, resp) {
-  resp.body.value = yield this.listener.getActiveElement();
+  switch (this.context) {
+    case Context.CHROME:
+      throw new UnsupportedOperationError(
+          "Command 'getActiveElement' is not yet available in chrome context");
+
+    case Context.CONTENT:
+      resp.body.value = yield this.listener.getActiveElement();
+      break;
+  }
 };
 
 /**
  * Send click event to element.
  *
  * @param {string} id
  *     Reference ID to the element that will be clicked.
  */
@@ -2107,82 +2120,95 @@ GeckoDriver.prototype.clearElement = fun
 };
 
 /**
  * Switch to shadow root of the given host element.
  *
  * @param {string} id element id.
  */
 GeckoDriver.prototype.switchToShadowRoot = function*(cmd, resp) {
+  assert.content(this.context)
+
   let id;
   if (cmd.parameters) { id = cmd.parameters.id; }
   yield this.listener.switchToShadowRoot(id);
 };
 
 /** Add a cookie to the document. */
 GeckoDriver.prototype.addCookie = function*(cmd, resp) {
+  assert.content(this.context)
+
   let cb = msg => {
     this.mm.removeMessageListener("Marionette:addCookie", cb);
     let cookie = msg.json;
     Services.cookies.add(
         cookie.domain,
         cookie.path,
         cookie.name,
         cookie.value,
         cookie.secure,
         cookie.httpOnly,
         cookie.session,
         cookie.expiry,
         {}); // originAttributes
     return true;
   };
+
   this.mm.addMessageListener("Marionette:addCookie", cb);
   yield this.listener.addCookie(cmd.parameters.cookie);
 };
 
 /**
  * Get all the cookies for the current domain.
  *
  * This is the equivalent of calling {@code document.cookie} and parsing
  * the result.
  */
 GeckoDriver.prototype.getCookies = function*(cmd, resp) {
+  assert.content(this.context)
+
   resp.body = yield this.listener.getCookies();
 };
 
 /** Delete all cookies that are visible to a document. */
 GeckoDriver.prototype.deleteAllCookies = function*(cmd, resp) {
+  assert.content(this.context)
+
   let cb = msg => {
     let cookie = msg.json;
     cookieManager.remove(
         cookie.host,
         cookie.name,
         cookie.path,
         false,
         cookie.originAttributes);
     return true;
   };
+
   this.mm.addMessageListener("Marionette:deleteCookie", cb);
   yield this.listener.deleteAllCookies();
   this.mm.removeMessageListener("Marionette:deleteCookie", cb);
 };
 
 /** Delete a cookie by name. */
 GeckoDriver.prototype.deleteCookie = function*(cmd, resp) {
+  assert.content(this.context)
+
   let cb = msg => {
     this.mm.removeMessageListener("Marionette:deleteCookie", cb);
     let cookie = msg.json;
     cookieManager.remove(
         cookie.host,
         cookie.name,
         cookie.path,
         false,
         cookie.originAttributes);
     return true;
   };
+
   this.mm.addMessageListener("Marionette:deleteCookie", cb);
   yield this.listener.deleteCookie(cmd.parameters.name);
 };
 
 /**
  * Close the current window, ending the session if it's the last
  * window currently open.
  *
@@ -2342,17 +2368,25 @@ GeckoDriver.prototype.sessionTearDown = 
  * the session and responding "ok".
  */
 GeckoDriver.prototype.deleteSession = function (cmd, resp) {
   this.sessionTearDown();
 };
 
 /** Returns the current status of the Application Cache. */
 GeckoDriver.prototype.getAppCacheStatus = function* (cmd, resp) {
-  resp.body.value = yield this.listener.getAppCacheStatus();
+  switch (this.context) {
+    case Context.CHROME:
+      throw new UnsupportedOperationError(
+          "Command 'getAppCacheStatus' is not yet available in chrome context");
+
+    case Context.CONTENT:
+      resp.body.value = yield this.listener.getAppCacheStatus();
+      break;
+  }
 };
 
 /**
  * Import script to the JS evaluation runtime.
  *
  * Imported scripts are exposed in the contexts of all subsequent
  * calls to {@code executeScript}, {@code executeAsyncScript}, and
  * {@code executeJSScript} by prepending them to the evaluated script.
@@ -2460,19 +2494,18 @@ GeckoDriver.prototype.takeScreenshot = f
 /**
  * Get the current browser orientation.
  *
  * Will return one of the valid primary orientation values
  * portrait-primary, landscape-primary, portrait-secondary, or
  * landscape-secondary.
  */
 GeckoDriver.prototype.getScreenOrientation = function (cmd, resp) {
-  if (this.appName == "Firefox") {
-    throw new UnsupportedOperationError();
-  }
+  assert.fennec();
+
   resp.body.value = this.getCurrentWindow().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
@@ -2489,17 +2522,17 @@ GeckoDriver.prototype.setScreenOrientati
     "portrait", "landscape",
     "portrait-primary", "landscape-primary",
     "portrait-secondary", "landscape-secondary",
   ];
 
   let or = String(cmd.parameters.orientation);
   assert.string(or);
   let mozOr = or.toLowerCase();
-  if (!ors.include(mozOr)) {
+  if (!ors.includes(mozOr)) {
     throw new InvalidArgumentError(`Unknown screen orientation: ${or}`);
   }
 
   let win = this.getCurrentWindow();
   if (!win.screen.mozLockOrientation(mozOr)) {
     throw new WebDriverError(`Unable to set screen orientation: ${or}`);
   }
 };
@@ -2520,36 +2553,32 @@ GeckoDriver.prototype.getWindowSize = fu
 /**
  * Set the size of the browser window currently in focus.
  *
  * Not supported on B2G. The supplied width and height values refer to
  * the window outerWidth and outerHeight values, which include scroll
  * bars, title bars, etc.
  */
 GeckoDriver.prototype.setWindowSize = function (cmd, resp) {
-  if (this.appName != "Firefox") {
-    throw new UnsupportedOperationError();
-  }
+  assert.firefox()
 
   let {width, height} = cmd.parameters;
   let win = this.getCurrentWindow();
   win.resizeTo(width, height);
   this.getWindowSize(cmd, resp);
 };
 
 /**
  * Maximizes the user agent window as if the user pressed the maximise
  * button.
  *
  * Not Supported on B2G or Fennec.
  */
 GeckoDriver.prototype.maximizeWindow = function (cmd, resp) {
-  if (this.appName != "Firefox") {
-    throw new UnsupportedOperationError();
-  }
+  assert.firefox()
 
   let win = this.getCurrentWindow();
   win.maximize()
 };
 
 /**
  * Dismisses a currently displayed tab modal, or returns no such alert if
  * no modal is displayed.
@@ -2636,51 +2665,45 @@ GeckoDriver.prototype.acceptConnections 
   this._server.acceptConnections = cmd.parameters.value;
 }
 
 /**
  * Quits Firefox with the provided flags and tears down the current
  * session.
  */
 GeckoDriver.prototype.quitApplication = function (cmd, resp) {
-  if (this.appName != "Firefox") {
-    throw new WebDriverError("In app initiated quit only supported in Firefox");
-  }
+  assert.firefox()
 
   let flags = Ci.nsIAppStartup.eAttemptQuit;
   for (let k of cmd.parameters.flags || []) {
     flags |= Ci.nsIAppStartup[k];
   }
 
   this._server.acceptConnections = false;
   resp.send();
 
   this.sessionTearDown();
   Services.startup.quit(flags);
 };
 
 GeckoDriver.prototype.installAddon = function (cmd, resp) {
-  if (this.appName != "Firefox") {
-    throw new UnsupportedOperationError();
-  }
+  assert.firefox()
 
   let path = cmd.parameters.path;
   let temp = cmd.parameters.temporary || false;
   if (typeof path == "undefined" || typeof path != "string" ||
       typeof temp != "boolean") {
     throw InvalidArgumentError();
   }
 
   return addon.install(path, temp);
 };
 
 GeckoDriver.prototype.uninstallAddon = function (cmd, resp) {
-  if (this.appName != "Firefox") {
-    throw new UnsupportedOperationError();
-  }
+  assert.firefox()
 
   let id = cmd.parameters.id;
   if (typeof id == "undefined" || typeof id != "string") {
     throw new InvalidArgumentError();
   }
 
   return addon.uninstall(id);
 };
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_cookies.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_cookies.py
@@ -1,16 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import calendar
 import random
 import time
 
+from marionette_driver.errors import UnsupportedOperationException
 from marionette_harness import MarionetteTestCase
 
 
 class CookieTest(MarionetteTestCase):
 
     def setUp(self):
         MarionetteTestCase.setUp(self)
         test_url = self.marionette.absolute_url('test.html')
@@ -31,16 +32,27 @@ class CookieTest(MarionetteTestCase):
 
     def test_adding_a_cookie_that_expired_in_the_past(self):
         cookie = self.COOKIE_A.copy()
         cookie["expiry"] = calendar.timegm(time.gmtime()) - 1
         self.marionette.add_cookie(cookie)
         cookies = self.marionette.get_cookies()
         self.assertEquals(0, len(cookies))
 
+    def test_chrome_error(self):
+        with self.marionette.using_context("chrome"):
+            self.assertRaises(UnsupportedOperationException,
+                              self.marionette.add_cookie, self.COOKIE_A)
+            self.assertRaises(UnsupportedOperationException,
+                              self.marionette.delete_cookie, self.COOKIE_A)
+            self.assertRaises(UnsupportedOperationException,
+                              self.marionette.delete_all_cookies)
+            self.assertRaises(UnsupportedOperationException,
+                              self.marionette.get_cookies)
+
     def test_delete_all_cookie(self):
         self.marionette.add_cookie(self.COOKIE_A)
         cookie_returned = str(self.marionette.execute_script("return document.cookie"))
         print cookie_returned
         self.assertTrue(self.COOKIE_A["name"] in cookie_returned)
         self.marionette.delete_all_cookies()
         self.assertFalse(self.marionette.get_cookies())
 
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py
@@ -44,19 +44,21 @@ class TestNavigate(WindowManagerMixin, M
 
     def test_navigate(self):
         self.marionette.navigate(self.test_doc)
         self.assertNotEqual("about:", self.location_href)
         self.assertEqual("Marionette Test", self.marionette.title)
 
     def test_navigate_chrome_error(self):
         with self.marionette.using_context("chrome"):
-            self.assertRaisesRegexp(
-                errors.UnsupportedOperationException, "Cannot navigate in chrome context",
-                self.marionette.navigate, "about:blank")
+            self.assertRaises(errors.UnsupportedOperationException,
+                              self.marionette.navigate, "about:blank")
+            self.assertRaises(errors.UnsupportedOperationException, self.marionette.go_back)
+            self.assertRaises(errors.UnsupportedOperationException, self.marionette.go_forward)
+            self.assertRaises(errors.UnsupportedOperationException, self.marionette.refresh)
 
     def test_get_current_url_returns_top_level_browsing_context_url(self):
         self.marionette.navigate(self.iframe_doc)
         self.assertEqual(self.iframe_doc, self.location_href)
         frame = self.marionette.find_element(By.CSS_SELECTOR, "#test_iframe")
         self.marionette.switch_to_frame(frame)
         self.assertEqual(self.iframe_doc, self.marionette.get_url())
 
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_shadow_dom.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_shadow_dom.py
@@ -1,32 +1,38 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from marionette_driver.by import By
 from marionette_driver.errors import (
     NoSuchElementException,
-    StaleElementException
+    StaleElementException,
+    UnsupportedOperationException,
 )
 
 from marionette_harness import MarionetteTestCase
 
 
 class TestShadowDom(MarionetteTestCase):
 
     def setUp(self):
         MarionetteTestCase.setUp(self)
         self.marionette.enforce_gecko_prefs({"dom.webcomponents.enabled": True})
         self.marionette.navigate(self.marionette.absolute_url("test_shadow_dom.html"))
 
         self.host = self.marionette.find_element(By.ID, "host")
         self.marionette.switch_to_shadow_root(self.host)
         self.button = self.marionette.find_element(By.ID, "button")
 
+    def test_chrome_error(self):
+        with self.marionette.using_context("chrome"):
+            self.assertRaises(UnsupportedOperationException,
+                              self.marionette.switch_to_shadow_root)
+
     def test_shadow_dom(self):
         # Button in shadow root should be actionable
         self.button.click()
 
     def test_shadow_dom_after_switch_away_from_shadow_root(self):
         # Button in shadow root should be actionable
         self.button.click()
         self.marionette.switch_to_shadow_root()