Bug 1194224 - adding support for Shadow DOM in marionette. draft
authorYura Zenevich <yzenevich@mozilla.com>
Wed, 26 Aug 2015 15:52:50 -0400
changeset 288071 7ee77248182d4d43ebc1a899ad32041beb8a59a2
parent 288070 fea87cbeaa6b64510dff835549ed906fe405d558
child 288072 de5f020249e23b573baed38abda5af3881e4c7d2
push id4792
push useryura.zenevich@gmail.com
push dateWed, 26 Aug 2015 20:00:52 +0000
bugs1194224, 100644
milestone43.0a1
Bug 1194224 - adding support for Shadow DOM in marionette. --- testing/marionette/actions.js | 30 +-- .../marionette/tests/unit/test_shadow_dom.py | 58 +++++ .../client/marionette/tests/unit/unit-tests.ini | 2 + .../client/marionette/www/test_shadow_dom.html | 25 ++ testing/marionette/driver.js | 47 ++-- .../driver/marionette_driver/marionette.py | 15 ++ testing/marionette/elements.js | 76 ++++-- testing/marionette/listener.js | 261 ++++++++++++--------- 8 files changed, 348 insertions(+), 166 deletions(-) create mode 100644 testing/marionette/client/marionette/tests/unit/test_shadow_dom.py create mode 100644 testing/marionette/client/marionette/www/test_shadow_dom.html
testing/marionette/actions.js
testing/marionette/client/marionette/tests/unit/test_shadow_dom.py
testing/marionette/client/marionette/tests/unit/unit-tests.ini
testing/marionette/client/marionette/www/test_shadow_dom.html
testing/marionette/driver.js
testing/marionette/driver/marionette_driver/marionette.py
testing/marionette/elements.js
testing/marionette/listener.js
--- a/testing/marionette/actions.js
+++ b/testing/marionette/actions.js
@@ -35,37 +35,37 @@ this.ActionChain = function(utils, check
 
   // test utilities providing some event synthesis code
   this.utils = utils;
 };
 
 ActionChain.prototype.dispatchActions = function(
     args,
     touchId,
-    frame,
+    container,
     elementManager,
     callbacks,
     touchProvider) {
   // Some touch events code in the listener needs to do ipc, so we can't
   // share this code across chrome/content.
   if (touchProvider) {
     this.touchProvider = touchProvider;
   }
 
   this.elementManager = elementManager;
-  let commandArray = elementManager.convertWrappedArguments(args, frame);
+  let commandArray = elementManager.convertWrappedArguments(args, container);
   this.onSuccess = callbacks.onSuccess;
   this.onError = callbacks.onError;
-  this.frame = frame;
+  this.container = container;
 
   if (touchId == null) {
     touchId = this.nextTouchId++;
   }
 
-  if (!frame.document.createTouch) {
+  if (!container.frame.document.createTouch) {
     this.mouseEventsOnly = true;
   }
 
   let keyModifiers = {
     shiftKey: false,
     ctrlKey: false,
     altKey: false,
     metaKey: false
@@ -121,32 +121,32 @@ ActionChain.prototype.emitMouseEvent = f
     } else {
       mods = 0;
     }
 
     domUtils.sendMouseEvent(
         type,
         elClientX,
         elClientY,
-        button || 0, 
+        button || 0,
         clickCount || 1,
         mods,
         false,
         0,
         this.inputSource);
   }
 };
 
 /**
  * Reset any persisted values after a command completes.
  */
 ActionChain.prototype.resetValues = function() {
   this.onSuccess = null;
   this.onError = null;
-  this.frame = null;
+  this.container = null;
   this.elementManager = null;
   this.touchProvider = null;
   this.mouseEventsOnly = false;
 };
 
 /**
  * Function to emit touch events for each finger. e.g.
  * finger=[['press', id], ['wait', 5], ['release']] touchId represents
@@ -172,27 +172,27 @@ ActionChain.prototype.actions = function
     if (!(touchId in this.touchIds) && !this.mouseEventsOnly) {
       this.resetValues();
       throw new WebDriverError("Element has not been pressed");
     }
   }
 
   switch(command) {
     case "keyDown":
-      this.utils.sendKeyDown(pack[1], keyModifiers, this.frame);
+      this.utils.sendKeyDown(pack[1], keyModifiers, this.container.frame);
       this.actions(chain, touchId, i, keyModifiers);
       break;
 
     case "keyUp":
-      this.utils.sendKeyUp(pack[1], keyModifiers, this.frame);
+      this.utils.sendKeyUp(pack[1], keyModifiers, this.container.frame);
       this.actions(chain, touchId, i, keyModifiers);
       break;
 
     case "click":
-      el = this.elementManager.getKnownElement(pack[1], this.frame);
+      el = this.elementManager.getKnownElement(pack[1], this.container);
       let button = pack[2];
       let clickCount = pack[3];
       c = this.coordinates(el, null, null);
       this.mouseTap(el.ownerDocument, c.x, c.y, button, clickCount, keyModifiers);
       if (button == 2) {
         this.emitMouseEvent(el.ownerDocument, "contextmenu", c.x, c.y,
             button, clickCount, keyModifiers);
       }
@@ -212,17 +212,17 @@ ActionChain.prototype.actions = function
         throw new WebDriverError(
             "Invalid Command: press cannot follow an active touch event");
       }
 
       // look ahead to check if we're scrolling. Needed for APZ touch dispatching.
       if ((i != chain.length) && (chain[i][0].indexOf('move') !== -1)) {
         this.scrolling = true;
       }
-      el = this.elementManager.getKnownElement(pack[1], this.frame);
+      el = this.elementManager.getKnownElement(pack[1], this.container);
       c = this.coordinates(el, pack[2], pack[3]);
       touchId = this.generateEvents("press", c.x, c.y, null, el, keyModifiers);
       this.actions(chain, touchId, i, keyModifiers);
       break;
 
     case "release":
       this.generateEvents(
           "release",
@@ -231,17 +231,17 @@ ActionChain.prototype.actions = function
           touchId,
           null,
           keyModifiers);
       this.actions(chain, null, i, keyModifiers);
       this.scrolling =  false;
       break;
 
     case "move":
-      el = this.elementManager.getKnownElement(pack[1], this.frame);
+      el = this.elementManager.getKnownElement(pack[1], this.container);
       c = this.coordinates(el);
       this.generateEvents("move", c.x, c.y, touchId, null, keyModifiers);
       this.actions(chain, touchId, i, keyModifiers);
       break;
 
     case "moveByOffset":
       this.generateEvents(
           "move",
@@ -269,29 +269,29 @@ ActionChain.prototype.actions = function
         }
         this.checkTimer.initWithCallback(
             () => this.actions(chain, touchId, i, keyModifiers),
             time, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
       } else {
         this.actions(chain, touchId, i, keyModifiers);
       }
       break;
-  
+
     case "cancel":
       this.generateEvents(
           "cancel",
           this.lastCoordinates[0],
           this.lastCoordinates[1],
           touchId,
           null,
           keyModifiers);
       this.actions(chain, touchId, i, keyModifiers);
       this.scrolling = false;
       break;
-  
+
     case "longPress":
       this.generateEvents(
           "contextmenu",
           this.lastCoordinates[0],
           this.lastCoordinates[1],
           touchId,
           null,
           keyModifiers);
@@ -350,17 +350,17 @@ ActionChain.prototype.getCoordinateInfo 
  *     to the viewport.
  * @param {number} y
  *     Y coordinate of the location to generate the event that is relative
  *     to the viewport.
  */
 ActionChain.prototype.generateEvents = function(
     type, x, y, touchId, target, keyModifiers) {
   this.lastCoordinates = [x, y];
-  let doc = this.frame.document;
+  let doc = this.container.frame.document;
 
   switch (type) {
     case "tap":
       if (this.mouseEventsOnly) {
         this.mouseTap(
             touch.target.ownerDocument,
             touch.clientX,
             touch.clientY,
@@ -445,17 +445,17 @@ ActionChain.prototype.generateEvents = f
             this.touchIds[touchId].target, x, y, touchId);
         this.touchIds[touchId] = touch;
         this.touchProvider.emitTouchEvent("touchmove", touch);
       }
       break;
 
     case "contextmenu":
       this.isTap = false;
-      let event = this.frame.document.createEvent("MouseEvents");
+      let event = this.container.frame.document.createEvent("MouseEvents");
       if (this.mouseEventsOnly) {
         target = doc.elementFromPoint(this.lastCoordinates[0], this.lastCoordinates[1]);
       } else {
         target = this.touchIds[touchId].target;
       }
 
       let [clientX, clientY, pageX, pageY, screenX, screenY] =
           this.getCoordinateInfo(target, x, y);
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_shadow_dom.py
@@ -0,0 +1,58 @@
+# 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 import MarionetteTestCase
+from marionette_driver.errors import StaleElementException
+
+
+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("id", "host")
+        self.marionette.switch_to_shadow_root(self.host)
+        self.button = self.marionette.find_element("id", "button")
+
+    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()
+        # After switching back to top content, button should be stale
+        self.assertRaises(StaleElementException, self.button.click)
+
+    def test_shadow_dom_raises_stale_element_exception_when_button_remove(self):
+        self.marionette.execute_script(
+            'document.getElementById("host").shadowRoot.getElementById("button").remove();')
+        # After removing button from shadow DOM, button should be stale
+        self.assertRaises(StaleElementException, self.button.click)
+
+    def test_shadow_dom_raises_stale_element_exception_when_host_removed(self):
+        self.marionette.execute_script('document.getElementById("host").remove();')
+        # After removing shadow DOM host element, button should be stale
+        self.assertRaises(StaleElementException, self.button.click)
+
+    def test_inner_shadow_dom(self):
+        # Button in shadow root should be actionable
+        self.button.click()
+        self.inner_host = self.marionette.find_element("id", "inner-host")
+        self.marionette.switch_to_shadow_root(self.inner_host)
+        self.inner_button = self.marionette.find_element("id", "inner-button")
+        # Nested nutton in nested shadow root should be actionable
+        self.inner_button.click()
+        self.marionette.switch_to_shadow_root()
+        # After jumping back to parent shadow root, button should again be actionable but inner
+        # button should now be stale
+        self.button.click()
+        self.assertRaises(StaleElementException, self.inner_button.click)
+        self.marionette.switch_to_shadow_root()
+        # After switching back to top content, both buttons should now be stale
+        self.assertRaises(StaleElementException, self.button.click)
+        self.assertRaises(StaleElementException, self.inner_button.click)
--- a/testing/marionette/client/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/client/marionette/tests/unit/unit-tests.ini
@@ -153,8 +153,10 @@ b2g = false
 [test_teardown_context_preserved.py]
 b2g = false
 [test_file_upload.py]
 b2g = false
 skip-if = os == "win" # http://bugs.python.org/issue14574
 
 [test_execute_sandboxes.py]
 [test_using_permissions.py]
+
+[test_shadow_dom.py]
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/www/test_shadow_dom.html
@@ -0,0 +1,25 @@
+<!-- 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/. -->
+
+<!DOCTYPE html>
+
+<html>
+<meta charset="UTF-8">
+<head>
+<title>Marionette Test</title>
+</head>
+<body>
+  <div id="host"></div>
+  <script>
+    'use strict';
+    var host = document.getElementById('host');
+    var root = host.createShadowRoot();
+    root.innerHTML = '<button id="button">Foo</button>' +
+      '<div id="inner-host"></div>';
+    var innerHost = host.shadowRoot.getElementById('inner-host');
+    var innerRoot = innerHost.createShadowRoot();
+    innerRoot.innerHTML = '<button id="inner-button">Bar</button>';
+  </script>
+</body>
+</html>
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -786,17 +786,18 @@ GeckoDriver.prototype.createExecuteSandb
   this.sandboxes[sandboxName] = sb;
 };
 
 /**
  * Apply arguments sent from the client to the current (possibly reused)
  * execution sandbox.
  */
 GeckoDriver.prototype.applyArgumentsToSandbox = function(win, sb, args) {
-  sb.__marionetteParams = this.curBrowser.elementManager.convertWrappedArguments(args, win);
+  sb.__marionetteParams = this.curBrowser.elementManager.convertWrappedArguments(args,
+    { frame: win });
   sb.__namedArgs = this.curBrowser.elementManager.applyNamedArgs(args);
 };
 
 /**
  * Executes a script in the given sandbox.
  *
  * @param {Response} resp
  *     Response object given to the command calling this routine.
@@ -1651,17 +1652,17 @@ GeckoDriver.prototype.switchToFrame = fu
       checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
       return;
     }
 
     if (typeof cmd.parameters.element != "undefined") {
       if (this.curBrowser.elementManager.seenItems[cmd.parameters.element]) {
         // HTMLIFrameElement
         let wantedFrame = this.curBrowser.elementManager
-            .getKnownElement(cmd.parameters.element, curWindow);
+            .getKnownElement(cmd.parameters.element, { frame: curWindow });
         // Deal with an embedded xul:browser case
         if (wantedFrame.tagName == "xul:browser" || wantedFrame.tagName == "browser") {
           curWindow = wantedFrame.contentWindow;
           this.curFrame = curWindow;
           if (cmd.parameters.focus) {
             this.curFrame.focus();
           }
           checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
@@ -1832,17 +1833,17 @@ GeckoDriver.prototype.actionChain = func
       }
 
       let cbs = {};
       cbs.onSuccess = val => resp.value = val;
       cbs.onError = err => { throw err; };
 
       let win = this.getCurrentWindow();
       let elm = this.curBrowser.elementManager;
-      this.actions.dispatchActions(chain, nextId, win, elm, cbs);
+      this.actions.dispatchActions(chain, nextId, { frame: win }, elm, cbs);
       break;
 
     case Context.CONTENT:
       this.addFrameCloseListener("action chain");
       resp.value = yield this.listener.actionChain({chain: chain, nextId: nextId});
       break;
   }
 };
@@ -1877,17 +1878,17 @@ GeckoDriver.prototype.multiAction = func
  *     Value the client is looking for.
  */
 GeckoDriver.prototype.findElement = function(cmd, resp) {
   switch (this.context) {
     case Context.CHROME:
       resp.value = yield new Promise((resolve, reject) => {
         let win = this.getCurrentWindow();
         this.curBrowser.elementManager.find(
-            win,
+            { frame: win },
             cmd.parameters,
             this.searchTimeout,
             false /* all */,
             resolve,
             reject);
       }).then(null, e => { throw e; });
       break;
 
@@ -1929,17 +1930,17 @@ GeckoDriver.prototype.findChildElement =
  *     Value the client is looking for.
  */
 GeckoDriver.prototype.findElements = function(cmd, resp) {
   switch (this.context) {
     case Context.CHROME:
       resp.value = yield new Promise((resolve, reject) => {
         let win = this.getCurrentWindow();
         this.curBrowser.elementManager.find(
-            win,
+            { frame: win },
             cmd.parameters,
             this.searchTimeout,
             true /* all */,
             resolve,
             reject);
       }).then(null, e => { throw new NoSuchElementError(e.message); });
       break;
 
@@ -1985,17 +1986,17 @@ GeckoDriver.prototype.getActiveElement =
  */
 GeckoDriver.prototype.clickElement = function(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       // click atom fails, fall back to click() action
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, win);
+      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       el.click();
       break;
 
     case Context.CONTENT:
       // We need to protect against the click causing an OOP frame to close.
       // This fires the mozbrowserclose event when it closes so we need to
       // listen for it and then just send an error back. The person making the
       // call should be aware something isnt right and handle accordingly
@@ -2014,17 +2015,17 @@ GeckoDriver.prototype.clickElement = fun
  *     Name of the attribute to retrieve.
  */
 GeckoDriver.prototype.getElementAttribute = function(cmd, resp) {
   let {id, name} = cmd.parameters;
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, win);
+      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       resp.value = utils.getElementAttribute(el, name);
       break;
 
     case Context.CONTENT:
       resp.value = yield this.listener.getElementAttribute(id, name);
       break;
   }
 };
@@ -2038,17 +2039,17 @@ GeckoDriver.prototype.getElementAttribut
  */
 GeckoDriver.prototype.getElementText = function(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       // for chrome, we look at text nodes, and any node with a "label" field
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, win);
+      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       let lines = [];
       this.getVisibleText(el, lines);
       resp.value = lines.join("\n");
       break;
 
     case Context.CONTENT:
       resp.value = yield this.listener.getElementText(id);
       break;
@@ -2062,17 +2063,17 @@ GeckoDriver.prototype.getElementText = f
  *     Reference ID to the element that will be inspected.
  */
 GeckoDriver.prototype.getElementTagName = function(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, win);
+      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       resp.value = el.tagName.toLowerCase();
       break;
 
     case Context.CONTENT:
       resp.value = yield this.listener.getElementTagName(id);
       break;
   }
 };
@@ -2084,17 +2085,17 @@ GeckoDriver.prototype.getElementTagName 
  *     Reference ID to the element that will be inspected.
  */
 GeckoDriver.prototype.isElementDisplayed = function(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, win);
+      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       resp.value = utils.isElementDisplayed(el);
       break;
 
     case Context.CONTENT:
       resp.value = yield this.listener.isElementDisplayed(id);
       break;
   }
 };
@@ -2108,17 +2109,17 @@ GeckoDriver.prototype.isElementDisplayed
  *     CSS rule that is being requested.
  */
 GeckoDriver.prototype.getElementValueOfCssProperty = function(cmd, resp) {
   let {id, propertyName: prop} = cmd.parameters;
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, win);
+      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       let sty = win.document.defaultView.getComputedStyle(el, null);
       resp.value = sty.getPropertyValue(prop);
       break;
 
     case Context.CONTENT:
       resp.value = yield this.listener.getElementValueOfCssProperty(id, prop);
       break;
   }
@@ -2132,17 +2133,17 @@ GeckoDriver.prototype.getElementValueOfC
  */
 GeckoDriver.prototype.isElementEnabled = function(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       // Selenium atom doesn't quite work here
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, win);
+      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       resp.value = !(!!el.disabled);
       break;
 
     case Context.CONTENT:
       resp.value = yield this.listener.isElementEnabled(id);
       break;
   }
 },
@@ -2155,17 +2156,17 @@ GeckoDriver.prototype.isElementEnabled =
  */
 GeckoDriver.prototype.isElementSelected = function(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       // Selenium atom doesn't quite work here
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, win);
+      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       if (typeof el.checked != "undefined") {
         resp.value = !!el.checked;
       } else if (typeof el.selected != "undefined") {
         resp.value = !!el.selected;
       } else {
         resp.value = true;
       }
       break;
@@ -2177,34 +2178,34 @@ GeckoDriver.prototype.isElementSelected 
 };
 
 GeckoDriver.prototype.getElementSize = function(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, win);
+      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       let rect = el.getBoundingClientRect();
       resp.value = {width: rect.width, height: rect.height};
       break;
 
     case Context.CONTENT:
       resp.value = yield this.listener.getElementSize(id);
       break;
   }
 };
 
 GeckoDriver.prototype.getElementRect = function(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, win);
+      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       let rect = el.getBoundingClientRect();
       resp.value = {
         x: rect.x + win.pageXOffset,
         y: rect.y + win.pageYOffset,
         width: rect.width,
         height: rect.height
       };
       break;
@@ -2228,17 +2229,17 @@ GeckoDriver.prototype.sendKeysToElement 
 
   if (!value) {
     throw new InvalidArgumentError(`Expected character sequence: ${value}`);
   }
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, win);
+      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       utils.sendKeysToElement(
           win,
           el,
           value,
           () => {},
           e => { throw e; },
           cmd.id,
           true /* ignore visibility check */);
@@ -2294,17 +2295,17 @@ GeckoDriver.prototype.setTestName = func
  */
 GeckoDriver.prototype.clearElement = function(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       // the selenium atom doesn't work here
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, win);
+      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       if (el.nodeName == "textbox") {
         el.value = "";
       } else if (el.nodeName == "checkbox") {
         el.checked = false;
       }
       break;
 
     case Context.CONTENT:
@@ -2322,16 +2323,27 @@ GeckoDriver.prototype.clearElement = fun
  *
  * @return {Object.<string, number>}
  *     A point containing X and Y coordinates as properties.
  */
 GeckoDriver.prototype.getElementLocation = function(cmd, resp) {
   return this.listener.getElementLocation(cmd.parameters.id);
 };
 
+/**
+ * Switch to shadow root of the given host element.
+ *
+ * @param {string} id element id.
+ */
+GeckoDriver.prototype.switchToShadowRoot = function(cmd, resp) {
+  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) {
   yield this.listener.addCookie({cookie: cmd.parameters.cookie});
 };
 
 /**
  * Get all the cookies for the current domain.
  *
@@ -3014,16 +3026,17 @@ GeckoDriver.prototype.commands = {
   "getChromeWindowHandles": GeckoDriver.prototype.getChromeWindowHandles,
   "getCurrentWindowHandles": GeckoDriver.prototype.getWindowHandles,  // Selenium 2 compat
   "getWindows":  GeckoDriver.prototype.getWindowHandles,  // deprecated
   "getWindowPosition": GeckoDriver.prototype.getWindowPosition,
   "setWindowPosition": GeckoDriver.prototype.setWindowPosition,
   "getActiveFrame": GeckoDriver.prototype.getActiveFrame,
   "switchToFrame": GeckoDriver.prototype.switchToFrame,
   "switchToWindow": GeckoDriver.prototype.switchToWindow,
+  "switchToShadowRoot": GeckoDriver.prototype.switchToShadowRoot,
   "deleteSession": GeckoDriver.prototype.deleteSession,
   "importScript": GeckoDriver.prototype.importScript,
   "clearImportedScripts": GeckoDriver.prototype.clearImportedScripts,
   "getAppCacheStatus": GeckoDriver.prototype.getAppCacheStatus,
   "close": GeckoDriver.prototype.close,
   "closeWindow": GeckoDriver.prototype.close,  // deprecated
   "closeChromeWindow": GeckoDriver.prototype.closeChromeWindow,
   "setTestName": GeckoDriver.prototype.setTestName,
--- a/testing/marionette/driver/marionette_driver/marionette.py
+++ b/testing/marionette/driver/marionette_driver/marionette.py
@@ -1264,16 +1264,31 @@ class Marionette(object):
         """
         body = {"focus": focus}
         if isinstance(frame, HTMLElement):
             body["element"] = frame.id
         elif frame is not None:
             body["id"] = frame
         self._send_message("switchToFrame", body)
 
+    def switch_to_shadow_root(self, host=None):
+        """Switch the current context to the specified host's Shadow DOM.
+        Subsequent commands will operate in the context of the specified Shadow
+        DOM, if applicable.
+
+        :param host: A reference to the host element containing Shadow DOM.
+            This can be an ``HTMLElement``. If you call
+            ``switch_to_shadow_root`` without an argument, it will switch to the
+            parent Shadow DOM or the top-level frame.
+        """
+        body = {}
+        if isinstance(host, HTMLElement):
+            body["id"] = host.id
+        return self._send_message("switchToShadowRoot", body)
+
     def get_url(self):
         """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.
 
         When in the context of the chrome, this returns the canonical
--- a/testing/marionette/elements.js
+++ b/testing/marionette/elements.js
@@ -238,48 +238,83 @@ ElementManager.prototype = {
     return id;
   },
 
   /**
    * Retrieve element from its unique ID
    *
    * @param String id
    *        The DOM reference ID
-   * @param nsIDOMWindow win
-   *        The window that contains the element
+   * @param nsIDOMWindow, ShadowRoot container
+   *        The window and an optional shadow root that contains the element
    *
    * @returns nsIDOMElement
    *        Returns the element or throws Exception if not found
    */
-  getKnownElement: function EM_getKnownElement(id, win) {
+  getKnownElement: function EM_getKnownElement(id, container) {
     let el = this.seenItems[id];
     if (!el) {
       throw new JavaScriptError("Element has not been seen before. Id given was " + id);
     }
     try {
       el = el.get();
     }
     catch(e) {
       el = null;
       delete this.seenItems[id];
     }
     // use XPCNativeWrapper to compare elements; see bug 834266
-    let wrappedWin = XPCNativeWrapper(win);
+    let wrappedFrame = XPCNativeWrapper(container.frame);
+    let wrappedShadowRoot;
+    if (container.shadowRoot) {
+      wrappedShadowRoot = XPCNativeWrapper(container.shadowRoot);
+    }
+
     if (!el ||
-        !(XPCNativeWrapper(el).ownerDocument == wrappedWin.document) ||
-        (XPCNativeWrapper(el).compareDocumentPosition(wrappedWin.document.documentElement) &
-         DOCUMENT_POSITION_DISCONNECTED)) {
+        !(XPCNativeWrapper(el).ownerDocument == wrappedFrame.document) ||
+        this.isDisconnected(XPCNativeWrapper(el), wrappedShadowRoot,
+          wrappedFrame)) {
       throw new StaleElementReferenceError(
           "The element reference is stale. Either the element " +
           "is no longer attached to the DOM or the page has been refreshed.");
     }
     return el;
   },
 
   /**
+   * Check if the element is detached from the current frame as well as the
+   * optional shadow root (when inside a Shadow DOM context).
+   * @param nsIDOMElement el
+   *        element to be checked
+   * @param ShadowRoot shadowRoot
+   *        an optional shadow root containing an element
+   * @param nsIDOMWindow frame
+   *        window that contains the element or the current host of the shadow
+   *        root.
+   * @return {Boolean} a flag indicating that the element is disconnected
+   */
+  isDisconnected: function EM_isDisconnected(el, shadowRoot, frame) {
+    if (shadowRoot && frame.ShadowRoot) {
+      if (el.compareDocumentPosition(shadowRoot) &
+        DOCUMENT_POSITION_DISCONNECTED) {
+        return true;
+      }
+      // Looking for next possible ShadowRoot ancestor
+      let parent = shadowRoot.host;
+      while (parent && !(parent instanceof frame.ShadowRoot)) {
+        parent = parent.parentNode;
+      }
+      return this.isDisconnected(shadowRoot.host, parent, frame);
+    } else {
+      return el.compareDocumentPosition(frame.document.documentElement) &
+        DOCUMENT_POSITION_DISCONNECTED;
+    }
+  },
+
+  /**
    * Convert values to primitives that can be transported over the
    * Marionette protocol.
    *
    * This function implements the marshaling algorithm defined in the
    * WebDriver specification:
    *
    *     https://dvcs.w3.org/hg/webdriver/raw-file/tip/webdriver-spec.html#synchronous-javascript-execution
    *
@@ -332,54 +367,54 @@ ElementManager.prototype = {
     return result;
   },
 
   /**
    * Convert any ELEMENT references in 'args' to the actual elements
    *
    * @param object args
    *        Arguments passed in by client
-   * @param nsIDOMWindow win
-   *        The window that contains the elements
+   * @param nsIDOMWindow, ShadowRoot container
+   *        The window and an optional shadow root that contains the element
    *
    * @returns object
    *        Returns the objects passed in by the client, with the
    *        reference IDs replaced by the actual elements.
    */
-  convertWrappedArguments: function EM_convertWrappedArguments(args, win) {
+  convertWrappedArguments: function EM_convertWrappedArguments(args, container) {
     let converted;
     switch (typeof(args)) {
       case 'number':
       case 'string':
       case 'boolean':
         converted = args;
         break;
       case 'object':
         if (args == null) {
           converted = null;
         }
         else if (Object.prototype.toString.call(args) == '[object Array]') {
           converted = [];
           for (let i in args) {
-            converted.push(this.convertWrappedArguments(args[i], win));
+            converted.push(this.convertWrappedArguments(args[i], container));
           }
         }
         else if (((typeof(args[this.elementKey]) === 'string') && args.hasOwnProperty(this.elementKey)) ||
                  ((typeof(args[this.w3cElementKey]) === 'string') &&
                      args.hasOwnProperty(this.w3cElementKey))) {
           let elementUniqueIdentifier = args[this.w3cElementKey] ? args[this.w3cElementKey] : args[this.elementKey];
-          converted = this.getKnownElement(elementUniqueIdentifier,  win);
+          converted = this.getKnownElement(elementUniqueIdentifier, container);
           if (converted == null) {
             throw new WebDriverError(`Unknown element: ${elementUniqueIdentifier}`);
           }
         }
         else {
           converted = {};
           for (let prop in args) {
-            converted[prop] = this.convertWrappedArguments(args[prop], win);
+            converted[prop] = this.convertWrappedArguments(args[prop], container);
           }
         }
         break;
     }
     return converted;
   },
 
   /*
@@ -411,18 +446,18 @@ ElementManager.prototype = {
     return namedArgs;
   },
 
   /**
    * Find an element or elements starting at the document root or
    * given node, using the given search strategy. Search
    * will continue until the search timelimit has been reached.
    *
-   * @param nsIDOMWindow win
-   *        The window to search in
+   * @param nsIDOMWindow, ShadowRoot container
+   *        The window and an optional shadow root that contains the element
    * @param object values
    *        The 'using' member of values will tell us which search
    *        method to use. The 'value' member tells us the value we
    *        are looking for.
    *        If this object has an 'element' member, this will be used
    *        as the start node instead of the document root
    *        If this object has a 'time' member, this number will be
    *        used to see if we have hit the search timelimit.
@@ -432,25 +467,26 @@ ElementManager.prototype = {
    * @param function on_success
    *        Callback used when operating is successful.
    * @param function on_error
    *        Callback to invoke when an error occurs.
    *
    * @return nsIDOMElement or list of nsIDOMElements
    *        Returns the element(s) by calling the on_success function.
    */
-  find: function EM_find(win, values, searchTimeout, all, on_success, on_error, command_id) {
+  find: function EM_find(container, values, searchTimeout, all, on_success, on_error, command_id) {
     let startTime = values.time ? values.time : new Date().getTime();
+    let rootNode = container.shadowRoot || container.frame.document;
     let startNode = (values.element != undefined) ?
-                    this.getKnownElement(values.element, win) : win.document;
+                    this.getKnownElement(values.element, container) : rootNode;
     if (this.elementStrategies.indexOf(values.using) < 0) {
       throw new InvalidSelectorError("No such strategy: " + values.using);
     }
-    let found = all ? this.findElements(values.using, values.value, win.document, startNode) :
-                      this.findElement(values.using, values.value, win.document, startNode);
+    let found = all ? this.findElements(values.using, values.value, rootNode, startNode) :
+                      this.findElement(values.using, values.value, rootNode, startNode);
     let type = Object.prototype.toString.call(found);
     let isArrayLike = ((type == '[object Array]') || (type == '[object HTMLCollection]') || (type == '[object NodeList]'));
     if (found == null || (isArrayLike && found.length <= 0)) {
       if (!searchTimeout || new Date().getTime() - startTime > searchTimeout) {
         if (all) {
           on_success([], command_id); // findElements should return empty list
         } else {
           // Format message depending on strategy if necessary
@@ -459,17 +495,17 @@ ElementManager.prototype = {
             message = "Unable to locate anonymous children";
           } else if (values.using == ANON_ATTRIBUTE) {
             message = "Unable to locate anonymous element: " + JSON.stringify(values.value);
           }
           on_error(new NoSuchElementError(message), command_id);
         }
       } else {
         values.time = startTime;
-        this.timer.initWithCallback(this.find.bind(this, win, values,
+        this.timer.initWithCallback(this.find.bind(this, container, values,
                                                    searchTimeout, all,
                                                    on_success, on_error,
                                                    command_id),
                                     100,
                                     Components.interfaces.nsITimer.TYPE_ONE_SHOT);
       }
     } else {
       if (isArrayLike) {
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -30,19 +30,19 @@ loader.loadSubScript("chrome://marionett
 let marionetteLogObj = new MarionetteLogObj();
 
 let isB2G = false;
 
 let marionetteTestName;
 let winUtil = content.QueryInterface(Ci.nsIInterfaceRequestor)
     .getInterface(Ci.nsIDOMWindowUtils);
 let listenerId = null; // unique ID of this listener
-let curFrame = content;
-let isRemoteBrowser = () => curFrame.contentWindow !== null;
-let previousFrame = null;
+let curContainer = { frame: content, shadowRoot: null };
+let isRemoteBrowser = () => curContainer.frame.contentWindow !== null;
+let previousContainer = null;
 let elementManager = new ElementManager([]);
 let accessibility = new Accessibility();
 let actions = new ActionChain(utils, checkForInterrupted);
 let importedScripts = null;
 
 // Contains the last file input element that was the target of
 // sendKeysToElement.
 let fileInputElement;
@@ -79,19 +79,19 @@ let multiLast = {};
 Cu.import("resource://gre/modules/Log.jsm");
 let logger = Log.repository.getLogger("Marionette");
 logger.info("loaded listener.js");
 let modalHandler = function() {
   // This gets called on the system app only since it receives the mozbrowserprompt event
   sendSyncMessage("Marionette:switchedToFrame", { frameValue: null, storePrevious: true });
   let isLocal = sendSyncMessage("MarionetteFrame:handleModal", {})[0].value;
   if (isLocal) {
-    previousFrame = curFrame;
+    previousContainer = curContainer;
   }
-  curFrame = content;
+  curContainer = { frame: content, shadowRoot: null };
 };
 
 /**
  * Called when listener is first started up.
  * The listener sends its unique window ID and its current URI to the actor.
  * If the actor returns an ID, we start the listeners. Otherwise, nothing happens.
  */
 function registerSelf() {
@@ -118,17 +118,17 @@ function registerSelf() {
     }
   }
 }
 
 function emitTouchEventForIFrame(message) {
   message = message.json;
   let identifier = actions.nextTouchId;
 
-  let domWindowUtils = curFrame.
+  let domWindowUtils = curContainer.frame.
     QueryInterface(Components.interfaces.nsIInterfaceRequestor).
     getInterface(Components.interfaces.nsIDOMWindowUtils);
   var ratio = domWindowUtils.screenPixelsPerCSSPixel;
 
   var typeForUtils;
   switch (message.type) {
     case 'touchstart':
       typeForUtils = domWindowUtils.TOUCH_CONTACT;
@@ -207,16 +207,17 @@ let isElementEnabledFn = dispatch(isElem
 let getCurrentUrlFn = dispatch(getCurrentUrl);
 let findElementContentFn = dispatch(findElementContent);
 let findElementsContentFn = dispatch(findElementsContent);
 let isElementSelectedFn = dispatch(isElementSelected);
 let getElementLocationFn = dispatch(getElementLocation);
 let clearElementFn = dispatch(clearElement);
 let isElementDisplayedFn = dispatch(isElementDisplayed);
 let getElementValueOfCssPropertyFn = dispatch(getElementValueOfCssProperty);
+let switchToShadowRootFn = dispatch(switchToShadowRoot);
 
 /**
  * Start all message listeners
  */
 function startListeners() {
   addMessageListenerId("Marionette:receiveFiles", receiveFiles);
   addMessageListenerId("Marionette:newSession", newSession);
   addMessageListenerId("Marionette:executeScript", executeScript);
@@ -246,16 +247,17 @@ function startListeners() {
   addMessageListenerId("Marionette:getElementSize", getElementSizeFn);  // deprecated
   addMessageListenerId("Marionette:getElementRect", getElementRectFn);
   addMessageListenerId("Marionette:isElementEnabled", isElementEnabledFn);
   addMessageListenerId("Marionette:isElementSelected", isElementSelectedFn);
   addMessageListenerId("Marionette:sendKeysToElement", sendKeysToElement);
   addMessageListenerId("Marionette:getElementLocation", getElementLocationFn); //deprecated
   addMessageListenerId("Marionette:clearElement", clearElementFn);
   addMessageListenerId("Marionette:switchToFrame", switchToFrame);
+  addMessageListenerId("Marionette:switchToShadowRoot", switchToShadowRootFn);
   addMessageListenerId("Marionette:deleteSession", deleteSession);
   addMessageListenerId("Marionette:sleepSession", sleepSession);
   addMessageListenerId("Marionette:emulatorCmdResult", emulatorCmdResult);
   addMessageListenerId("Marionette:importScript", importScript);
   addMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus);
   addMessageListenerId("Marionette:setTestName", setTestName);
   addMessageListenerId("Marionette:takeScreenshot", takeScreenshot);
   addMessageListenerId("Marionette:addCookie", addCookie);
@@ -351,34 +353,35 @@ function deleteSession(msg) {
   removeMessageListenerId("Marionette:getElementSize", getElementSizeFn); // deprecated
   removeMessageListenerId("Marionette:getElementRect", getElementRectFn);
   removeMessageListenerId("Marionette:isElementEnabled", isElementEnabledFn);
   removeMessageListenerId("Marionette:isElementSelected", isElementSelectedFn);
   removeMessageListenerId("Marionette:sendKeysToElement", sendKeysToElement);
   removeMessageListenerId("Marionette:getElementLocation", getElementLocationFn);
   removeMessageListenerId("Marionette:clearElement", clearElementFn);
   removeMessageListenerId("Marionette:switchToFrame", switchToFrame);
+  removeMessageListenerId("Marionette:switchToShadowRoot", switchToShadowRootFn);
   removeMessageListenerId("Marionette:deleteSession", deleteSession);
   removeMessageListenerId("Marionette:sleepSession", sleepSession);
   removeMessageListenerId("Marionette:emulatorCmdResult", emulatorCmdResult);
   removeMessageListenerId("Marionette:importScript", importScript);
   removeMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus);
   removeMessageListenerId("Marionette:setTestName", setTestName);
   removeMessageListenerId("Marionette:takeScreenshot", takeScreenshot);
   removeMessageListenerId("Marionette:addCookie", addCookie);
   removeMessageListenerId("Marionette:getCookies", getCookies);
   removeMessageListenerId("Marionette:deleteAllCookies", deleteAllCookies);
   removeMessageListenerId("Marionette:deleteCookie", deleteCookie);
   if (isB2G) {
     content.removeEventListener("mozbrowsershowmodalprompt", modalHandler, false);
   }
   elementManager.reset();
-  // reset frame to the top-most frame
-  curFrame = content;
-  curFrame.focus();
+  // reset container frame to the top-most frame
+  curContainer = { frame: content, shadowRoot: null };
+  curContainer.frame.focus();
   actions.touchIds = {};
 }
 
 /*
  * Helper methods
  */
 
 /**
@@ -422,49 +425,49 @@ function sendError(err, cmdId) {
   sendToServer("Marionette:error", null, {error: err}, cmdId);
 }
 
 /**
  * Clear test values after completion of test
  */
 function resetValues() {
   sandboxes = {};
-  curFrame = content;
+  curContainer = { frame: content, shadowRoot: null };
   actions.mouseEventsOnly = false;
 }
 
 /**
  * Dump a logline to stdout. Prepends logline with a timestamp.
  */
 function dumpLog(logline) {
   dump(Date.now() + " Marionette: " + logline);
 }
 
 /**
  * Check if our context was interrupted
  */
 function wasInterrupted() {
-  if (previousFrame) {
+  if (previousContainer) {
     let element = content.document.elementFromPoint((content.innerWidth/2), (content.innerHeight/2));
     if (element.id.indexOf("modal-dialog") == -1) {
       return true;
     }
     else {
       return false;
     }
   }
   return sendSyncMessage("MarionetteFrame:getInterruptedState", {})[0].value;
 }
 
 function checkForInterrupted() {
     if (wasInterrupted()) {
-      if (previousFrame) {
-        //if previousFrame is set, then we're in a single process environment
-        curFrame = actions.frame = previousFrame;
-        previousFrame = null;
+      if (previousContainer) {
+        // if previousContainer is set, then we're in a single process environment
+        curContainer = actions.container = previousContainer;
+        previousContainer = null;
       }
       else {
         //else we're in OOP environment, so we'll switch to the original OOP frame
         sendSyncMessage("Marionette:switchToModalOrigin");
       }
       sendSyncMessage("Marionette:switchedToFrame", { restorePrevious: true });
     }
 }
@@ -507,21 +510,21 @@ function createExecuteContentSandbox(win
       sandbox[fn] = mn[fn].bind(mn);
     } else {
       sandbox[fn] = mn[fn];
     }
   });
 
   sandbox.asyncComplete = (obj, id) => {
     if (id == asyncTestCommandId) {
-      curFrame.removeEventListener("unload", onunload, false);
-      curFrame.clearTimeout(asyncTestTimeoutId);
+      curContainer.frame.removeEventListener("unload", onunload, false);
+      curContainer.frame.clearTimeout(asyncTestTimeoutId);
 
       if (inactivityTimeoutId != null) {
-        curFrame.clearTimeout(inactivityTimeoutId);
+        curContainer.frame.clearTimeout(inactivityTimeoutId);
       }
 
       sendSyncMessage("Marionette:shareData",
           {log: elementManager.wrapValue(marionetteLogObj.getLogs())});
       marionetteLogObj.clearLogs();
 
       if (error.isError(obj)) {
         sendError(obj, id);
@@ -557,36 +560,36 @@ function createExecuteContentSandbox(win
 /**
  * Execute the given script either as a function body (executeScript)
  * or directly (for mochitest like JS Marionette tests).
  */
 function executeScript(msg, directInject) {
   // Set up inactivity timeout.
   if (msg.json.inactivityTimeout) {
     let setTimer = function() {
-      inactivityTimeoutId = curFrame.setTimeout(function() {
+      inactivityTimeoutId = curContainer.frame.setTimeout(function() {
         sendError(new ScriptTimeoutError("timed out due to inactivity"), asyncTestCommandId);
       }, msg.json.inactivityTimeout);
    };
 
     setTimer();
     heartbeatCallback = function() {
-      curFrame.clearTimeout(inactivityTimeoutId);
+      curContainer.frame.clearTimeout(inactivityTimeoutId);
       setTimer();
     };
   }
 
   asyncTestCommandId = msg.json.command_id;
   let script = msg.json.script;
   sandboxName = msg.json.sandboxName;
 
   if (msg.json.newSandbox ||
       !(sandboxName in sandboxes) ||
-      (sandboxes[sandboxName].window != curFrame)) {
-    createExecuteContentSandbox(curFrame, msg.json.timeout);
+      (sandboxes[sandboxName].window != curContainer.frame)) {
+    createExecuteContentSandbox(curContainer.frame, msg.json.timeout);
     if (!sandboxes[sandboxName]) {
       sendError(new WebDriverError("Could not create sandbox!"), asyncTestCommandId);
       return;
     }
   } else {
     sandboxes[sandboxName].asyncTestCommandId = asyncTestCommandId;
   }
 
@@ -612,17 +615,17 @@ function executeScript(msg, directInject
       }
       else {
         sendResponse({value: elementManager.wrapValue(res)}, asyncTestCommandId);
       }
     }
     else {
       try {
         sandbox.__marionetteParams = Cu.cloneInto(elementManager.convertWrappedArguments(
-          msg.json.args, curFrame), sandbox, { wrapReflectors: true });
+          msg.json.args, curContainer), sandbox, { wrapReflectors: true });
       } catch (e) {
         sendError(e, asyncTestCommandId);
         return;
       }
 
       script = "let __marionetteFunc = function(){" + script + "};" +
                    "__marionetteFunc.apply(null, __marionetteParams);";
       if (importedScripts.exists()) {
@@ -706,73 +709,73 @@ function executeJSScript(msg) {
  * For executeJSScript, it will return a message only when the finish() method is called.
  * For executeAsync, it will return a response when marionetteScriptFinished/arguments[arguments.length-1]
  * method is called, or if it times out.
  */
 function executeWithCallback(msg, useFinish) {
   // Set up inactivity timeout.
   if (msg.json.inactivityTimeout) {
     let setTimer = function() {
-      inactivityTimeoutId = curFrame.setTimeout(function() {
+      inactivityTimeoutId = curContainer.frame.setTimeout(function() {
         sandbox.asyncComplete(new ScriptTimeoutError("timed out due to inactivity"), asyncTestCommandId);
       }, msg.json.inactivityTimeout);
     };
 
     setTimer();
     heartbeatCallback = function() {
-      curFrame.clearTimeout(inactivityTimeoutId);
+      curContainer.frame.clearTimeout(inactivityTimeoutId);
       setTimer();
     };
   }
 
   let script = msg.json.script;
   asyncTestCommandId = msg.json.command_id;
   sandboxName = msg.json.sandboxName;
 
   onunload = function() {
     sendError(new JavaScriptError("unload was called"), asyncTestCommandId);
   };
-  curFrame.addEventListener("unload", onunload, false);
+  curContainer.frame.addEventListener("unload", onunload, false);
 
   if (msg.json.newSandbox ||
       !(sandboxName in sandboxes) ||
-      (sandboxes[sandboxName].window != curFrame)) {
-    createExecuteContentSandbox(curFrame, msg.json.timeout);
+      (sandboxes[sandboxName].window != curContainer.frame)) {
+    createExecuteContentSandbox(curContainer.frame, msg.json.timeout);
     if (!sandboxes[sandboxName]) {
       sendError(new JavaScriptError("Could not create sandbox!"), asyncTestCommandId);
       return;
     }
   }
   else {
     sandboxes[sandboxName].asyncTestCommandId = asyncTestCommandId;
   }
   let sandbox = sandboxes[sandboxName];
   sandbox.tag = script;
 
-  asyncTestTimeoutId = curFrame.setTimeout(function() {
+  asyncTestTimeoutId = curContainer.frame.setTimeout(function() {
     sandbox.asyncComplete(new ScriptTimeoutError("timed out"), asyncTestCommandId);
   }, msg.json.timeout);
 
-  originalOnError = curFrame.onerror;
-  curFrame.onerror = function errHandler(msg, url, line) {
+  originalOnError = curContainer.frame.onerror;
+  curContainer.frame.onerror = function errHandler(msg, url, line) {
     sandbox.asyncComplete(new JavaScriptError(msg + "@" + url + ", line " + line), asyncTestCommandId);
-    curFrame.onerror = originalOnError;
+    curContainer.frame.onerror = originalOnError;
   };
 
   let scriptSrc;
   if (useFinish) {
     if (msg.json.timeout == null || msg.json.timeout == 0) {
       sendError(new TimeoutError("Please set a timeout"), asyncTestCommandId);
     }
     scriptSrc = script;
   }
   else {
     try {
       sandbox.__marionetteParams = Cu.cloneInto(elementManager.convertWrappedArguments(
-        msg.json.args, curFrame), sandbox, { wrapReflectors: true });
+        msg.json.args, curContainer), sandbox, { wrapReflectors: true });
     } catch (e) {
       sendError(e, asyncTestCommandId);
       return;
     }
 
     scriptSrc = "__marionetteParams.push(marionetteScriptFinished);" +
                 "let __marionetteFunc = function() { " + script + "};" +
                 "__marionetteFunc.apply(null, __marionetteParams); ";
@@ -802,17 +805,17 @@ function executeWithCallback(msg, useFin
 
 /**
  * This function creates a touch event given a touch type and a touch
  */
 function emitTouchEvent(type, touch) {
   if (!wasInterrupted()) {
     let loggingInfo = "emitting Touch event of type " + type + " to element with id: " + touch.target.id + " and tag name: " + touch.target.tagName + " at coordinates (" + touch.clientX + ", " + touch.clientY + ") relative to the viewport";
     dumpLog(loggingInfo);
-    var docShell = curFrame.document.defaultView.
+    var docShell = curContainer.frame.document.defaultView.
                    QueryInterface(Components.interfaces.nsIInterfaceRequestor).
                    getInterface(Components.interfaces.nsIWebNavigation).
                    QueryInterface(Components.interfaces.nsIDocShell);
     if (docShell.asyncPanZoomEnabled && actions.scrolling) {
       // if we're in APZ and we're scrolling, we must use injectTouchEvent to dispatch our touchmove events
       let index = sendSyncMessage("MarionetteFrame:getCurrentFrameId");
       // only call emitTouchEventForIFrame if we're inside an iframe.
       if (index != null) {
@@ -828,17 +831,17 @@ function emitTouchEvent(type, touch) {
     // we get here if we're not in asyncPacZoomEnabled land, or if we're the main process
     /*
     Disabled per bug 888303
     marionetteLogObj.log(loggingInfo, "TRACE");
     sendSyncMessage("Marionette:shareData",
                     {log: elementManager.wrapValue(marionetteLogObj.getLogs())});
     marionetteLogObj.clearLogs();
     */
-    let domWindowUtils = curFrame.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils);
+    let domWindowUtils = curContainer.frame.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils);
     domWindowUtils.sendTouchEvent(type, [touch.identifier], [touch.clientX], [touch.clientY], [touch.radiusX], [touch.radiusY], [touch.rotationAngle], [touch.force], 1, 0);
   }
 }
 
 /**
  * This function generates a pair of coordinates relative to the viewport given a
  * target element and coordinates relative to that element's top-left corner.
  * @param 'x', and 'y' are the relative to the target.
@@ -861,16 +864,17 @@ function coordinates(target, x, y) {
 
 /**
  * This function returns true if the given coordinates are in the viewport.
  * @param 'x', and 'y' are the coordinates relative to the target.
  *        If they are not specified, then the center of the target is used.
  */
 function elementInViewport(el, x, y) {
   let c = coordinates(el, x, y);
+  let curFrame = curContainer.frame;
   let viewPort = {top: curFrame.pageYOffset,
                   left: curFrame.pageXOffset,
                   bottom: (curFrame.pageYOffset + curFrame.innerHeight),
                   right:(curFrame.pageXOffset + curFrame.innerWidth)};
   return (viewPort.left <= c.x + curFrame.pageXOffset &&
           c.x + curFrame.pageXOffset <= viewPort.right &&
           viewPort.top <= c.y + curFrame.pageYOffset &&
           c.y + curFrame.pageYOffset <= viewPort.bottom);
@@ -912,27 +916,27 @@ function checkVisible(el, x, y) {
 
 
 /**
  * Function that perform a single tap
  */
 function singleTap(msg) {
   let command_id = msg.json.command_id;
   try {
-    let el = elementManager.getKnownElement(msg.json.id, curFrame);
+    let el = elementManager.getKnownElement(msg.json.id, curContainer);
     let acc = accessibility.getAccessibleObject(el, true);
     // after this block, the element will be scrolled into view
     let visible = checkVisible(el, msg.json.corx, msg.json.cory);
     checkVisibleAccessibility(acc, visible);
     if (!visible) {
       sendError(new ElementNotVisibleError("Element is not currently visible and may not be manipulated"), command_id);
       return;
     }
     checkActionableAccessibility(acc);
-    if (!curFrame.document.createTouch) {
+    if (!curContainer.frame.document.createTouch) {
       actions.mouseEventsOnly = true;
     }
     let c = coordinates(el, msg.json.corx, msg.json.cory);
     if (!actions.mouseEventsOnly) {
       let touchId = actions.nextTouchId++;
       let touch = createATouch(el, c.x, c.y, touchId);
       emitTouchEvent('touchstart', touch);
       emitTouchEvent('touchend', touch);
@@ -952,17 +956,17 @@ function singleTap(msg) {
  * @param Boolean enabled element's enabled state
  */
 function checkEnabledAccessibility(accesible, element, enabled) {
   if (!accesible) {
     return;
   }
   let disabledAccessibility = accessibility.matchState(
     accesible, 'STATE_UNAVAILABLE');
-  let explorable = curFrame.document.defaultView.getComputedStyle(
+  let explorable = curContainer.frame.document.defaultView.getComputedStyle(
     element, null).getPropertyValue('pointer-events') !== 'none';
   let message;
 
   if (!explorable && !disabledAccessibility) {
     message = 'Element is enabled but is not explorable via the ' +
       'accessibility API';
   } else if (enabled && disabledAccessibility) {
     message = 'Element is enabled but disabled via the accessibility API';
@@ -1071,17 +1075,17 @@ function actionChain(msg) {
   let touchProvider = {};
   touchProvider.createATouch = createATouch;
   touchProvider.emitTouchEvent = emitTouchEvent;
 
   try {
     actions.dispatchActions(
         args,
         touchId,
-        curFrame,
+        curContainer,
         elementManager,
         callbacks,
         touchProvider);
   } catch (e) {
     sendError(e, command_id);
   }
 }
 
@@ -1152,32 +1156,32 @@ function setDispatch(batches, touches, c
   batchIndex++;
   // loop through the batch
   for (let i = 0; i < batch.length; i++) {
     pack = batch[i];
     touchId = pack[0];
     command = pack[1];
     switch (command) {
       case 'press':
-        el = elementManager.getKnownElement(pack[2], curFrame);
+        el = elementManager.getKnownElement(pack[2], curContainer);
         c = coordinates(el, pack[3], pack[4]);
         touch = createATouch(el, c.x, c.y, touchId);
         multiLast[touchId] = touch;
         touches.push(touch);
         emitMultiEvents('touchstart', touch, touches);
         break;
       case 'release':
         touch = multiLast[touchId];
         // the index of the previous touch for the finger may change in the touches array
         touchIndex = touches.indexOf(touch);
         touches.splice(touchIndex, 1);
         emitMultiEvents('touchend', touch, touches);
         break;
       case 'move':
-        el = elementManager.getKnownElement(pack[2], curFrame);
+        el = elementManager.getKnownElement(pack[2], curContainer);
         c = coordinates(el);
         touch = createATouch(multiLast[touchId].target, c.x, c.y, touchId);
         touchIndex = touches.indexOf(lastTouch);
         touches[touchIndex] = touch;
         multiLast[touchId] = touch;
         emitMultiEvents('touchmove', touch, touches);
         break;
       case 'moveByOffset':
@@ -1221,17 +1225,17 @@ function setDispatch(batches, touches, c
  */
 function multiAction(msg) {
   let command_id = msg.json.command_id;
   let args = msg.json.value;
   // maxlen is the longest action chain for one finger
   let maxlen = msg.json.maxlen;
   try {
     // unwrap the original nested array
-    let commandArray = elementManager.convertWrappedArguments(args, curFrame);
+    let commandArray = elementManager.convertWrappedArguments(args, curContainer);
     let concurrentEvent = [];
     let temp;
     for (let i = 0; i < maxlen; i++) {
       let row = [];
       for (let j = 0; j < commandArray.length; j++) {
         if (commandArray[j][i] != undefined) {
           // add finger id to the front of each action, i.e. [finger_id, action, element]
           temp = commandArray[j][i];
@@ -1265,28 +1269,29 @@ function pollForReadyState(msg, start, c
   }
 
   let end = null;
   function checkLoad() {
     navTimer.cancel();
     end = new Date().getTime();
     let aboutErrorRegex = /about:.+(error)\?/;
     let elapse = end - start;
+    let doc = curContainer.frame.document;
     if (pageTimeout == null || elapse <= pageTimeout) {
-      if (curFrame.document.readyState == "complete") {
+      if (doc.readyState == "complete") {
         callback();
         sendOk(command_id);
-      } else if (curFrame.document.readyState == "interactive" &&
-                 aboutErrorRegex.exec(curFrame.document.baseURI) &&
-                 !curFrame.document.baseURI.startsWith(url)) {
+      } else if (doc.readyState == "interactive" &&
+                 aboutErrorRegex.exec(doc.baseURI) &&
+                 !doc.baseURI.startsWith(url)) {
         // We have reached an error url without requesting it.
         callback();
         sendError(new UnknownError("Error loading page"), command_id);
-      } else if (curFrame.document.readyState == "interactive" &&
-                 curFrame.document.baseURI.startsWith("about:")) {
+      } else if (doc.readyState == "interactive" &&
+                 doc.baseURI.startsWith("about:")) {
         callback();
         sendOk(command_id);
       } else {
         navTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
       }
     } else {
       callback();
       sendError(new TimeoutError("Error loading page, timed out (checkLoad)"), command_id);
@@ -1304,33 +1309,33 @@ function pollForReadyState(msg, start, c
 function get(msg) {
   let start = new Date().getTime();
 
   // Prevent DOMContentLoaded events from frames from invoking this
   // code, unless the event is coming from the frame associated with
   // the current window (i.e. someone has used switch_to_frame).
   onDOMContentLoaded = function onDOMContentLoaded(event) {
     if (!event.originalTarget.defaultView.frameElement ||
-        event.originalTarget.defaultView.frameElement == curFrame.frameElement) {
+        event.originalTarget.defaultView.frameElement == curContainer.frame.frameElement) {
       pollForReadyState(msg, start, () => {
         removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
         onDOMContentLoaded = null;
       });
     }
   };
 
   function timerFunc() {
     removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
     sendError(new TimeoutError("Error loading page, timed out (onDOMContentLoaded)"), msg.json.command_id);
   }
   if (msg.json.pageTimeout != null) {
     navTimer.initWithCallback(timerFunc, msg.json.pageTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
   }
   addEventListener("DOMContentLoaded", onDOMContentLoaded, false);
-  curFrame.location = msg.json.url;
+  curContainer.frame.location = msg.json.url;
 }
 
  /**
  * Cancel the polling and remove the event listener associated with a current
  * navigation request in case we're interupted by an onbeforeunload handler
  * and navigation doesn't complete.
  */
 function cancelRequest() {
@@ -1340,118 +1345,118 @@ function cancelRequest() {
   }
 }
 
 /**
  * Get URL of the top-level browsing context.
  */
 function getCurrentUrl(isB2G) {
   if (isB2G) {
-    return curFrame.location.href;
+    return curContainer.frame.location.href;
   } else {
     return content.location.href;
   }
 }
 
 /**
  * Get the title of the current browsing context.
  */
 function getTitle() {
-  return curFrame.top.document.title;
+  return curContainer.frame.top.document.title;
 }
 
 /**
  * Get source of the current browsing context's DOM.
  */
 function getPageSource() {
-  let XMLSerializer = curFrame.XMLSerializer;
-  let source = new XMLSerializer().serializeToString(curFrame.document);
+  let XMLSerializer = curContainer.frame.XMLSerializer;
+  let source = new XMLSerializer().serializeToString(curContainer.frame.document);
   return source;
 }
 
 /**
  * Cause the browser to traverse one step backward in the joint history
  * of the current top-level browsing context.
  */
 function goBack() {
-  curFrame.history.back();
+  curContainer.frame.history.back();
 }
 
 /**
  * Go forward in history
  */
 function goForward(msg) {
-  curFrame.history.forward();
+  curContainer.frame.history.forward();
   sendOk(msg.json.command_id);
 }
 
 /**
  * Refresh the page
  */
 function refresh(msg) {
   let command_id = msg.json.command_id;
-  curFrame.location.reload(true);
+  curContainer.frame.location.reload(true);
   let listen = function() {
     removeEventListener("DOMContentLoaded", arguments.callee, false);
     sendOk(command_id);
   };
   addEventListener("DOMContentLoaded", listen, false);
 }
 
 /**
  * Find an element in the current browsing context's document using the
  * given search strategy.
  */
 function findElementContent(opts) {
   return new Promise((resolve, reject) => {
     elementManager.find(
-        curFrame,
+        curContainer,
         opts,
         opts.searchTimeout,
         false /* all */,
         resolve,
         reject);
   });
 }
 
 /**
  * Find elements in the current browsing context's document using the
  * given search strategy.
  */
 function findElementsContent(opts) {
   return new Promise((resolve, reject) => {
     elementManager.find(
-        curFrame,
+        curContainer,
         opts,
         opts.searchTimeout,
         true /* all */,
         resolve,
         reject);
   });
 }
 
 /**
  * Find and return the active element on the page.
  *
  * @return {WebElement}
  *     Reference to web element.
  */
 function getActiveElement() {
-  let el = curFrame.document.activeElement;
+  let el = curContainer.frame.document.activeElement;
   return elementManager.addToKnownElements(el);
 }
 
 /**
  * Send click event to element.
  *
  * @param {WebElement} id
  *     Reference to the web element to click.
  */
 function clickElement(id) {
-  let el = elementManager.getKnownElement(id, curFrame);
+  let el = elementManager.getKnownElement(id, curContainer);
   let acc = accessibility.getAccessibleObject(el, true);
   let visible = checkVisible(el);
   checkVisibleAccessibility(acc, visible);
   if (!visible) {
     throw new ElementNotVisibleError("Element is not visible");
   }
   checkActionableAccessibility(acc);
   if (utils.isElementEnabled(el)) {
@@ -1469,56 +1474,56 @@ function clickElement(id) {
  *     Reference to the web element to get the attribute of.
  * @param {string} name
  *     Name of the attribute.
  *
  * @return {string}
  *     The value of the attribute.
  */
 function getElementAttribute(id, name) {
-  let el = elementManager.getKnownElement(id, curFrame);
+  let el = elementManager.getKnownElement(id, curContainer);
   return utils.getElementAttribute(el, name);
 }
 
 /**
  * Get the text of this element. This includes text from child elements.
  *
  * @param {WebElement} id
  *     Reference to web element.
  *
  * @return {string}
  *     Text of element.
  */
 function getElementText(id) {
-  let el = elementManager.getKnownElement(id, curFrame);
+  let el = elementManager.getKnownElement(id, curContainer);
   return utils.getElementText(el);
 }
 
 /**
  * Get the tag name of an element.
  *
  * @param {WebElement} id
  *     Reference to web element.
  *
  * @return {string}
  *     Tag name of element.
  */
 function getElementTagName(id) {
-  let el = elementManager.getKnownElement(id, curFrame);
+  let el = elementManager.getKnownElement(id, curContainer);
   return el.tagName.toLowerCase();
 }
 
 /**
  * Determine the element displayedness of the given web element.
  *
  * Also performs additional accessibility checks if enabled by session
  * capability.
  */
 function isElementDisplayed(id) {
-  let el = elementManager.getKnownElement(id, curFrame);
+  let el = elementManager.getKnownElement(id, curContainer);
   let displayed = utils.isElementDisplayed(el);
   checkVisibleAccessibility(accessibility.getAccessibleObject(el), displayed);
   return displayed;
 }
 
 /**
  * Retrieves the computed value of the given CSS property of the given
  * web element.
@@ -1527,129 +1532,129 @@ function isElementDisplayed(id) {
  *     Web element reference.
  * @param {String} prop
  *     The CSS property to get.
  *
  * @return {String}
  *     Effective value of the requested CSS property.
  */
 function getElementValueOfCssProperty(id, prop) {
-  let el = elementManager.getKnownElement(id, curFrame);
-  let st = curFrame.document.defaultView.getComputedStyle(el, null);
+  let el = elementManager.getKnownElement(id, curContainer);
+  let st = curContainer.frame.document.defaultView.getComputedStyle(el, null);
   return st.getPropertyValue(prop);
 }
 
 /**
  * Get the size of the element.
  *
  * @param {WebElement} id
  *     Web element reference.
  *
  * @return {Object.<string, number>}
  *     The width/height dimensions of th element.
  */
 function getElementSize(id) {
-  let el = elementManager.getKnownElement(id, curFrame);
+  let el = elementManager.getKnownElement(id, curContainer);
   let clientRect = el.getBoundingClientRect();
   return {width: clientRect.width, height: clientRect.height};
 }
 
 /**
  * Get the size of the element.
  *
  * @param {WebElement} id
  *     Reference to web element.
  *
  * @return {Object.<string, number>}
  *     The x, y, width, and height properties of the element.
  */
 function getElementRect(id) {
-  let el = elementManager.getKnownElement(id, curFrame);
+  let el = elementManager.getKnownElement(id, curContainer);
   let clientRect = el.getBoundingClientRect();
   return {
-    x: clientRect.x + curFrame.pageXOffset,
-    y: clientRect.y  + curFrame.pageYOffset,
+    x: clientRect.x + curContainer.frame.pageXOffset,
+    y: clientRect.y  + curContainer.frame.pageYOffset,
     width: clientRect.width,
     height: clientRect.height
   };
 }
 
 /**
  * Check if element is enabled.
  *
  * @param {WebElement} id
  *     Reference to web element.
  *
  * @return {boolean}
  *     True if enabled, false otherwise.
  */
 function isElementEnabled(id) {
-  let el = elementManager.getKnownElement(id, curFrame);
+  let el = elementManager.getKnownElement(id, curContainer);
   let enabled = utils.isElementEnabled(el);
   checkEnabledAccessibility(
     accessibility.getAccessibleObject(el), el, enabled);
   return enabled;
 }
 
 /**
  * Determines if the referenced element is selected or not.
  *
  * This operation only makes sense on input elements of the Checkbox-
  * and Radio Button states, or option elements.
  */
 function isElementSelected(id) {
-  let el = elementManager.getKnownElement(id, curFrame);
+  let el = elementManager.getKnownElement(id, curContainer);
     let selected = utils.isElementSelected(el);
     checkSelectedAccessibility(accessibility.getAccessibleObject(el), selected);
   return selected;
 }
 
 /**
  * Send keys to element
  */
 function sendKeysToElement(msg) {
   let command_id = msg.json.command_id;
   let val = msg.json.value;
 
   try {
-    let el = elementManager.getKnownElement(msg.json.id, curFrame);
+    let el = elementManager.getKnownElement(msg.json.id, curContainer);
     // Element should be actionable from the accessibility standpoint to be able
     // to send keys to it.
     checkActionableAccessibility(accessibility.getAccessibleObject(el, true));
     if (el.type == "file") {
       let p = val.join("");
       fileInputElement = el;
       // In e10s, we can only construct File objects in the parent process,
       // so pass the filename to driver.js, which in turn passes them back
       // to this frame script in receiveFiles.
       sendSyncMessage("Marionette:getFiles",
                       {value: p, command_id: command_id});
     } else {
-      utils.sendKeysToElement(curFrame, el, val, sendOk, sendError, command_id);
+      utils.sendKeysToElement(curContainer.frame, el, val, sendOk, sendError, command_id);
     }
   } catch (e) {
     sendError(e, command_id);
   }
 }
 
 /**
  * Get the element's top left-hand corner point.
  */
 function getElementLocation(id) {
-  let el = elementManager.getKnownElement(id, curFrame);
+  let el = elementManager.getKnownElement(id, curContainer);
   let rect = el.getBoundingClientRect();
   return {x: rect.left, y: rect.top};
 }
 
 /**
  * Clear the text of an element.
  */
 function clearElement(id) {
   try {
-    let el = elementManager.getKnownElement(id, curFrame);
+    let el = elementManager.getKnownElement(id, curContainer);
     if (el.type == "file") {
       el.value = null;
     } else {
       utils.clearElement(el);
     }
   } catch (e) {
     // Bug 964738: Newer atoms contain status codes which makes wrapping
     // this in an error prototype that has a status property unnecessary
@@ -1657,148 +1662,176 @@ function clearElement(id) {
       throw new InvalidElementStateError(e.message);
     } else {
       throw e;
     }
   }
 }
 
 /**
+ * Switch the current context to the specified host's Shadow DOM.
+ * @param {WebElement} id
+ *     Reference to web element.
+ */
+function switchToShadowRoot(id) {
+  if (!id) {
+    // If no host element is passed, attempt to find a parent shadow root or, if
+    // none found, unset the current shadow root
+    if (curContainer.shadowRoot) {
+      let parent = curContainer.shadowRoot.host;
+      while (parent && !(parent instanceof curContainer.frame.ShadowRoot)) {
+        parent = parent.parentNode;
+      }
+      curContainer.shadowRoot = parent;
+    }
+    return;
+  }
+
+  let foundShadowRoot;
+  let hostEl = elementManager.getKnownElement(id, curContainer);
+  foundShadowRoot = hostEl.shadowRoot;
+  if (!foundShadowRoot) {
+    throw new NoSuchElementError('Unable to locate shadow root: ' + msg.json.element);
+  }
+  curContainer.shadowRoot = foundShadowRoot;
+}
+
+/**
  * Switch to frame given either the server-assigned element id,
  * its index in window.frames, or the iframe's name or id.
  */
 function switchToFrame(msg) {
   let command_id = msg.json.command_id;
   function checkLoad() {
     let errorRegex = /about:.+(error)|(blocked)\?/;
-    if (curFrame.document.readyState == "complete") {
+    if (curContainer.frame.document.readyState == "complete") {
       sendOk(command_id);
       return;
-    } else if (curFrame.document.readyState == "interactive" &&
-        errorRegex.exec(curFrame.document.baseURI)) {
+    } else if (curContainer.frame.document.readyState == "interactive" &&
+        errorRegex.exec(curContainer.frame.document.baseURI)) {
       sendError(new UnknownError("Error loading page"), command_id);
       return;
     }
     checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
   }
   let foundFrame = null;
   let frames = [];
   let parWindow = null;
-  // Check of the curFrame reference is dead
+  // Check of the curContainer.frame reference is dead
   try {
-    frames = curFrame.frames;
+    frames = curContainer.frame.frames;
     //Until Bug 761935 lands, we won't have multiple nested OOP iframes. We will only have one.
     //parWindow will refer to the iframe above the nested OOP frame.
-    parWindow = curFrame.QueryInterface(Ci.nsIInterfaceRequestor)
+    parWindow = curContainer.frame.QueryInterface(Ci.nsIInterfaceRequestor)
                       .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
   } catch (e) {
     // We probably have a dead compartment so accessing it is going to make Firefox
     // very upset. Let's now try redirect everything to the top frame even if the
     // user has given us a frame since search doesnt look up.
     msg.json.id = null;
     msg.json.element = null;
   }
 
   if ((msg.json.id === null || msg.json.id === undefined) && (msg.json.element == null)) {
     // returning to root frame
     sendSyncMessage("Marionette:switchedToFrame", { frameValue: null });
 
-    curFrame = content;
+    curContainer.frame = content;
     if(msg.json.focus == true) {
-      curFrame.focus();
+      curContainer.frame.focus();
     }
 
     checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
     return;
   }
   if (msg.json.element != undefined) {
     if (elementManager.seenItems[msg.json.element] != undefined) {
       let wantedFrame;
       try {
-        wantedFrame = elementManager.getKnownElement(msg.json.element, curFrame); //Frame Element
+        wantedFrame = elementManager.getKnownElement(msg.json.element, curContainer); //Frame Element
       } catch (e) {
         sendError(e, command_id);
       }
 
       if (frames.length > 0) {
         for (let i = 0; i < frames.length; i++) {
           // use XPCNativeWrapper to compare elements; see bug 834266
           if (XPCNativeWrapper(frames[i].frameElement) == XPCNativeWrapper(wantedFrame)) {
-            curFrame = frames[i].frameElement;
+            curContainer.frame = frames[i].frameElement;
             foundFrame = i;
           }
         }
       }
       if (foundFrame === null) {
         // Either the frame has been removed or we have a OOP frame
         // so lets just get all the iframes and do a quick loop before
         // throwing in the towel
-        let iframes = curFrame.document.getElementsByTagName("iframe");
+        let iframes = curContainer.frame.document.getElementsByTagName("iframe");
         for (var i = 0; i < iframes.length; i++) {
           if (XPCNativeWrapper(iframes[i]) == XPCNativeWrapper(wantedFrame)) {
-            curFrame = iframes[i];
+            curContainer.frame = iframes[i];
             foundFrame = i;
           }
         }
       }
     }
   }
   if (foundFrame === null) {
     if (typeof(msg.json.id) === 'number') {
       try {
         foundFrame = frames[msg.json.id].frameElement;
         if (foundFrame !== null) {
-          curFrame = foundFrame;
-          foundFrame = elementManager.addToKnownElements(curFrame);
+          curContainer.frame = foundFrame;
+          foundFrame = elementManager.addToKnownElements(curContainer.frame);
         }
         else {
           // If foundFrame is null at this point then we have the top level browsing
           // context so should treat it accordingly.
           sendSyncMessage("Marionette:switchedToFrame", { frameValue: null});
-          curFrame = content;
+          curContainer.frame = content;
           if(msg.json.focus == true) {
-            curFrame.focus();
+            curContainer.frame.focus();
           }
 
           checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
           return;
         }
       } catch (e) {
         // Since window.frames does not return OOP frames it will throw
         // and we land up here. Let's not give up and check if there are
         // iframes and switch to the indexed frame there
-        let iframes = curFrame.document.getElementsByTagName("iframe");
+        let iframes = curContainer.frame.document.getElementsByTagName("iframe");
         if (msg.json.id >= 0 && msg.json.id < iframes.length) {
-          curFrame = iframes[msg.json.id];
+          curContainer.frame = iframes[msg.json.id];
           foundFrame = msg.json.id;
         }
       }
     }
   }
 
   if (foundFrame === null) {
     sendError(new NoSuchFrameError("Unable to locate frame: " + (msg.json.id || msg.json.element)), command_id);
     return true;
   }
 
   // send a synchronous message to let the server update the currently active
   // frame element (for getActiveFrame)
-  let frameValue = elementManager.wrapValue(curFrame.wrappedJSObject)['ELEMENT'];
+  let frameValue = elementManager.wrapValue(curContainer.frame.wrappedJSObject)['ELEMENT'];
   sendSyncMessage("Marionette:switchedToFrame", { frameValue: frameValue });
 
   let rv = null;
-  if (curFrame.contentWindow === null) {
+  if (curContainer.frame.contentWindow === null) {
     // The frame we want to switch to is a remote/OOP frame;
     // notify our parent to handle the switch
-    curFrame = content;
+    curContainer.frame = content;
     rv = {win: parWindow, frame: foundFrame};
   } else {
-    curFrame = curFrame.contentWindow;
+    curContainer.frame = curContainer.frame.contentWindow;
     if (msg.json.focus)
-      curFrame.focus();
+      curContainer.frame.focus();
     checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
   }
 
   sendResponse({value: rv}, command_id);
 }
  /**
   * Add a cookie to the document
   */
@@ -1807,34 +1840,34 @@ function addCookie(msg) {
   if (!cookie.expiry) {
     var date = new Date();
     var thePresent = new Date(Date.now());
     date.setYear(thePresent.getFullYear() + 20);
     cookie.expiry = date.getTime() / 1000;  // Stored in seconds.
   }
 
   if (!cookie.domain) {
-    var location = curFrame.document.location;
+    var location = curContainer.frame.document.location;
     cookie.domain = location.hostname;
   } else {
-    var currLocation = curFrame.location;
+    var currLocation = curContainer.frame.location;
     var currDomain = currLocation.host;
     if (currDomain.indexOf(cookie.domain) == -1) {
       sendError(new InvalidCookieDomainError("You may only set cookies for the current domain"), msg.json.command_id);
     }
   }
 
   // The cookie's domain may include a port. Which is bad. Remove it
   // We'll catch ip6 addresses by mistake. Since no-one uses those
   // this will be okay for now. See Bug 814416
   if (cookie.domain.match(/:\d+$/)) {
     cookie.domain = cookie.domain.replace(/:\d+$/, '');
   }
 
-  var document = curFrame.document;
+  var document = curContainer.frame.document;
   if (!document || !document.contentType.match(/html/i)) {
     sendError(new UnableToSetCookieError("You may only set cookies on html documents"), msg.json.command_id);
   }
 
   let added = sendSyncMessage("Marionette:addCookie", {value: cookie});
   if (added[0] !== true) {
     sendError(new UnableToSetCookieError(), msg.json.command_id);
     return;
@@ -1842,17 +1875,17 @@ function addCookie(msg) {
   sendOk(msg.json.command_id);
 }
 
 /**
  * Get all cookies for the current domain.
  */
 function getCookies(msg) {
   var toReturn = [];
-  var cookies = getVisibleCookies(curFrame.location);
+  var cookies = getVisibleCookies(curContainer.frame.location);
   for (let cookie of cookies) {
     var expires = cookie.expires;
     if (expires == 0) {  // Session cookie, don't return an expiry.
       expires = null;
     } else if (expires == 1) { // Date before epoch time, cap to epoch.
       expires = 0;
     }
     toReturn.push({
@@ -1868,17 +1901,17 @@ function getCookies(msg) {
   sendResponse({value: toReturn}, msg.json.command_id);
 }
 
 /**
  * Delete a cookie by name
  */
 function deleteCookie(msg) {
   let toDelete = msg.json.name;
-  let cookies = getVisibleCookies(curFrame.location);
+  let cookies = getVisibleCookies(curContainer.frame.location);
   for (let cookie of cookies) {
     if (cookie.name == toDelete) {
       let deleted = sendSyncMessage("Marionette:deleteCookie", {value: cookie});
       if (deleted[0] !== true) {
         sendError(new UnknownError("Could not delete cookie: " + msg.json.name), msg.json.command_id);
         return;
       }
     }
@@ -1886,17 +1919,17 @@ function deleteCookie(msg) {
 
   sendOk(msg.json.command_id);
 }
 
 /**
  * Delete all the visibile cookies on a page
  */
 function deleteAllCookies(msg) {
-  let cookies = getVisibleCookies(curFrame.location);
+  let cookies = getVisibleCookies(curContainer.frame.location);
   for (let cookie of cookies) {
     let deleted = sendSyncMessage("Marionette:deleteCookie", {value: cookie});
     if (!deleted[0]) {
       sendError(new UnknownError("Could not delete cookie: " + JSON.stringify(cookie)), msg.json.command_id);
       return;
     }
   }
   sendOk(msg.json.command_id);
@@ -1908,17 +1941,17 @@ function deleteAllCookies(msg) {
 function getVisibleCookies(location) {
   let currentPath = location.pathname || '/';
   let result = sendSyncMessage("Marionette:getVisibleCookies",
                                {value: [currentPath, location.hostname]});
   return result[0];
 }
 
 function getAppCacheStatus(msg) {
-  sendResponse({ value: curFrame.applicationCache.status },
+  sendResponse({ value: curContainer.frame.applicationCache.status },
                msg.json.command_id);
 }
 
 // emulator callbacks
 let _emu_cb_id = 0;
 let _emu_cbs = {};
 
 function runEmulatorCmd(cmd, callback) {
@@ -1983,47 +2016,47 @@ function importScript(msg) {
  * If given an array of web element references in
  * <code>msg.json.highlights</code>, a red box will be painted around
  * them to highlight their position.
  */
 function takeScreenshot(msg) {
   let node = null;
   if (msg.json.id) {
     try {
-      node = elementManager.getKnownElement(msg.json.id, curFrame)
+      node = elementManager.getKnownElement(msg.json.id, curContainer)
     }
     catch (e) {
       sendResponse(e.message, e.code, e.stack, msg.json.command_id);
       return;
     }
   }
   else {
-    node = curFrame;
+    node = curContainer.frame;
   }
   let highlights = msg.json.highlights;
 
-  var document = curFrame.document;
+  var document = curContainer.frame.document;
   var rect, win, width, height, left, top;
   // node can be either a window or an arbitrary DOM node
-  if (node == curFrame) {
+  if (node == curContainer.frame) {
     // node is a window
     win = node;
     if (msg.json.full) {
       // the full window
       width = document.body.scrollWidth;
       height = document.body.scrollHeight;
       top = 0;
       left = 0;
     }
     else {
       // only the viewport
       width = document.documentElement.clientWidth;
       height = document.documentElement.clientHeight;
-      left = curFrame.pageXOffset;
-      top = curFrame.pageYOffset;
+      left = curContainer.frame.pageXOffset;
+      top = curContainer.frame.pageYOffset;
     }
   }
   else {
     // node is an arbitrary DOM node
     win = node.ownerDocument.defaultView;
     rect = node.getBoundingClientRect();
     width = rect.width;
     height = rect.height;
@@ -2042,17 +2075,17 @@ function takeScreenshot(msg) {
   // This section is for drawing a red rectangle around each element
   // passed in via the highlights array
   if (highlights) {
     ctx.lineWidth = "2";
     ctx.strokeStyle = "red";
     ctx.save();
 
     for (var i = 0; i < highlights.length; ++i) {
-      var elem = elementManager.getKnownElement(highlights[i], curFrame);
+      var elem = elementManager.getKnownElement(highlights[i], curContainer);
       rect = elem.getBoundingClientRect();
 
       var offsetY = -top;
       var offsetX = -left;
 
       // Draw the rectangle
       ctx.strokeRect(rect.left + offsetX,
                      rect.top + offsetY,