Bug 1400256 - Marshal IPC messages to and from frame script. r?whimboo draft
authorAndreas Tolfsen <ato@sny.no>
Thu, 05 Oct 2017 17:57:17 +0100
changeset 680858 257efe81b35f1db6b86b0957e6af76ccc316a257
parent 680857 92ec306cef4632f4ecc0cc45636cdcbccaa5f43e
child 680859 140c775eb23475d19fe793b93500d14610e6fc73
push id84656
push userbmo:ato@sny.no
push dateMon, 16 Oct 2017 14:02:26 +0000
reviewerswhimboo
bugs1400256
milestone58.0a1
Bug 1400256 - Marshal IPC messages to and from frame script. r?whimboo MozReview-Commit-ID: BTDQDvu2pVE
testing/marionette/driver.js
testing/marionette/element.js
testing/marionette/evaluate.js
testing/marionette/legacyaction.js
testing/marionette/listener.js
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -19,16 +19,17 @@ Cu.import("chrome://marionette/content/a
 const {
   browser,
   WindowState,
 } = Cu.import("chrome://marionette/content/browser.js", {});
 Cu.import("chrome://marionette/content/capture.js");
 Cu.import("chrome://marionette/content/cert.js");
 Cu.import("chrome://marionette/content/cookie.js");
 const {
+  ChromeWebElement,
   element,
   WebElement,
 } = Cu.import("chrome://marionette/content/element.js", {});
 const {
   ElementNotInteractableError,
   InsecureCertificateError,
   InvalidArgumentError,
   InvalidCookieDomainError,
@@ -339,17 +340,17 @@ GeckoDriver.prototype.switchToGlobalMess
  *     <tt>Marionette:SUFFIX</tt>.
  * @param {Object=} msg
  *     Optional JSON serialisable object to send to the listener.
  * @param {number=} commandID
  *     Optional command ID to ensure synchronisity.
  */
 GeckoDriver.prototype.sendAsync = function(name, data, commandID) {
   name = "Marionette:" + name;
-  let payload = copy(data);
+  let payload = evaluate.toJSON(data, this.seenEls);
 
   // TODO(ato): When proxy.AsyncMessageChannel
   // is used for all chrome <-> content communication
   // this can be removed.
   if (commandID) {
     payload.commandID = commandID;
   }
 
@@ -598,17 +599,17 @@ GeckoDriver.prototype.registerBrowser = 
     // curBrowser holds all the registered frames in knownFrames
     this.curBrowser.register(id, be);
   }
 
   this.wins.set(id, listenerWindow);
   if (nullPrevious && (this.curBrowser.curFrameId !== null)) {
     this.sendAsync(
         "newSession",
-        this.capabilities.toJSON(),
+        this.capabilities,
         this.newSessionCommandId);
     if (this.curBrowser.isNewSession) {
       this.newSessionCommandId = null;
     }
   }
 
   return [id, this.capabilities.toJSON()];
 };
@@ -1679,28 +1680,25 @@ GeckoDriver.prototype.setWindowHandle = 
 GeckoDriver.prototype.getActiveFrame = function(cmd, resp) {
   assert.window(this.getCurrentWindow());
 
   switch (this.context) {
     case Context.CHROME:
       // no frame means top-level
       resp.body.value = null;
       if (this.curFrame) {
-        let elRef = this.curBrowser.seenEls
-            .add(this.curFrame.frameElement);
-        let el = element.makeWebElement(elRef);
-        resp.body.value = el;
+        resp.body.value = this.curBrowser.seenEls.add(
+            this.curFrame.frameElement);
       }
       break;
 
     case Context.CONTENT:
       resp.body.value = null;
       if (this.currentFrameElement !== null) {
-        let el = element.makeWebElement(this.currentFrameElement);
-        resp.body.value = el;
+        resp.body.value = this.currentFrameElement;
       }
       break;
   }
 };
 
 /**
  * Set the current browsing context for future commands to the parent
  * of the current browsing context.
@@ -1768,20 +1766,21 @@ GeckoDriver.prototype.switchToFrame = as
       if (focus) {
         this.mainFrame.focus();
       }
       checkTimer.initWithCallback(
           checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
       return;
     }
 
-    // by element
-    if (this.curBrowser.seenEls.has(element)) {
-      // HTMLIFrameElement
-      let wantedFrame = this.curBrowser.seenEls.get(element);
+    // by element (HTMLIFrameElement)
+    if (typeof element != "undefined") {
+      let webEl = WebElement.fromUUID(element, Context.CHROME);
+      let wantedFrame = this.curBrowser.seenEls.get(webEl);
+
       // Deal with an embedded xul:browser case
       if (wantedFrame.tagName == "xul:browser" ||
           wantedFrame.tagName == "browser") {
         curWindow = wantedFrame.contentWindow;
         this.curFrame = curWindow;
         if (focus) {
           this.curFrame.focus();
         }
@@ -1831,17 +1830,17 @@ GeckoDriver.prototype.switchToFrame = as
           checkTimer.initWithCallback(
               checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
           return;
         }
       }
     }
 
     switch (typeof id) {
-      case "string" :
+      case "string":
         let foundById = null;
         let frames = curWindow.document.getElementsByTagName("iframe");
         let numFrames = frames.length;
         for (let i = 0; i < numFrames; i++) {
           // give precedence to name
           let frame = frames[i];
           if (frame.getAttribute("name") == id) {
             foundFrame = i;
@@ -1851,16 +1850,17 @@ GeckoDriver.prototype.switchToFrame = as
             foundById = i;
           }
         }
         if (foundFrame === null && foundById !== null) {
           foundFrame = foundById;
           curWindow = frames[foundById].contentWindow;
         }
         break;
+
       case "number":
         if (typeof curWindow.frames[id] != "undefined") {
           foundFrame = id;
           let frameEl = curWindow.frames[foundFrame].frameElement;
           curWindow = frameEl.contentWindow;
         }
         break;
     }
@@ -1926,25 +1926,26 @@ GeckoDriver.prototype.setTimeouts = func
   this.timeouts = session.Timeouts.fromJSON(merged);
 };
 
 /** Single tap. */
 GeckoDriver.prototype.singleTap = async function(cmd) {
   assert.window(this.getCurrentWindow());
 
   let {id, x, y} = cmd.parameters;
+  let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.CHROME:
       throw new UnsupportedOperationError(
           "Command 'singleTap' is not yet available in chrome context");
 
     case Context.CONTENT:
       this.addFrameCloseListener("tap");
-      await this.listener.singleTap(id, x, y);
+      await this.listener.singleTap(webEl, x, y);
       break;
   }
 };
 
 /**
  * Perform a series of grouped actions at the specified points in time.
  *
  * @param {Array.<?>} actions
@@ -2063,91 +2064,90 @@ GeckoDriver.prototype.multiAction = asyn
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.findElement = async function(cmd, resp) {
   const win = assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
-  let strategy = cmd.parameters.using;
-  let expr = cmd.parameters.value;
+  let {using, value} = cmd.parameters;
+  let startNode;
+  if (typeof cmd.parameters.element != "undefined") {
+    startNode = WebElement.fromUUID(cmd.parameters.element, this.context);
+  }
+
   let opts = {
-    startNode: cmd.parameters.element,
+    startNode,
     timeout: this.timeouts.implicit,
     all: false,
   };
 
   switch (this.context) {
     case Context.CHROME:
-      if (!SUPPORTED_STRATEGIES.has(strategy)) {
-        throw new InvalidSelectorError(`Strategy not supported: ${strategy}`);
+      if (!SUPPORTED_STRATEGIES.has(using)) {
+        throw new InvalidSelectorError(`Strategy not supported: ${using}`);
       }
 
       let container = {frame: win};
       if (opts.startNode) {
         opts.startNode = this.curBrowser.seenEls.get(opts.startNode);
       }
-      let el = await element.find(container, strategy, expr, opts);
-      let elRef = this.curBrowser.seenEls.add(el);
-      let webEl = element.makeWebElement(elRef);
-
+      let el = await element.find(container, using, value, opts);
+      let webEl = this.curBrowser.seenEls.add(el);
       resp.body.value = webEl;
       break;
 
     case Context.CONTENT:
       resp.body.value = await this.listener.findElementContent(
-          strategy,
-          expr,
-          opts);
+          using, value, opts);
       break;
   }
 };
 
 /**
  * Find elements using the indicated search strategy.
  *
  * @param {string} using
  *     Indicates which search method to use.
  * @param {string} value
  *     Value the client is looking for.
  */
 GeckoDriver.prototype.findElements = async function(cmd, resp) {
-  let win = assert.window(this.getCurrentWindow());
-
-  let strategy = cmd.parameters.using;
-  let expr = cmd.parameters.value;
+  const win = assert.window(this.getCurrentWindow());
+
+  let {using, value} = cmd.parameters;
+  let startNode;
+  if (typeof cmd.parameters.element != "undefined") {
+    startNode = WebElement.fromUUID(cmd.parameters.element, this.context);
+  }
+
   let opts = {
-    startNode: cmd.parameters.element,
+    startNode,
     timeout: this.timeouts.implicit,
     all: true,
   };
 
   switch (this.context) {
     case Context.CHROME:
-      if (!SUPPORTED_STRATEGIES.has(strategy)) {
-        throw new InvalidSelectorError(`Strategy not supported: ${strategy}`);
+      if (!SUPPORTED_STRATEGIES.has(using)) {
+        throw new InvalidSelectorError(`Strategy not supported: ${using}`);
       }
 
       let container = {frame: win};
-      if (opts.startNode) {
+      if (startNode) {
         opts.startNode = this.curBrowser.seenEls.get(opts.startNode);
       }
-      let els = await element.find(container, strategy, expr, opts);
-
-      let elRefs = this.curBrowser.seenEls.addAll(els);
-      let webEls = elRefs.map(element.makeWebElement);
+      let els = await element.find(container, using, value, opts);
+      let webEls = this.curBrowser.seenEls.addAll(els);
       resp.body = webEls;
       break;
 
     case Context.CONTENT:
-      resp.body = await this.listener.findElementsContent(
-          cmd.parameters.using,
-          cmd.parameters.value,
-          opts);
+      resp.body = await this.listener.findElementsContent(using, value, opts);
       break;
   }
 };
 
 /**
  * Return the active element on the page.
  *
  * @return {WebElement}
@@ -2169,56 +2169,61 @@ GeckoDriver.prototype.getActiveElement =
 };
 
 /**
  * Send click event to element.
  *
  * @param {string} id
  *     Reference ID to the element that will be clicked.
  *
+ * @throws {InvalidArgumentError}
+ *     If element <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.clickElement = async function(cmd) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
-  let id = cmd.parameters.id;
+  let id = assert.string(cmd.parameters.id);
+  let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.CHROME:
-      let el = this.curBrowser.seenEls.get(id);
+      let el = this.curBrowser.seenEls.get(webEl);
       await interaction.clickElement(el, this.a11yChecks);
       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 is not right
       // and handle accordingly.
       this.addFrameCloseListener("click");
 
       let click = this.listener.clickElement(
-          {id, pageTimeout: this.timeouts.pageLoad});
+          {webElRef: webEl.toJSON(), pageTimeout: this.timeouts.pageLoad});
 
       // If a reload of the frame script 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(() => {
         let parameters = {
           // TODO(ato): Bug 1242595
           commandID: this.listener.activeMessageId,
           pageTimeout: this.timeouts.pageLoad,
           startTime: new Date().getTime(),
         };
         this.mm.broadcastAsyncMessage(
-            "Marionette:waitForPageLoaded" + this.curBrowser.curFrameId,
+            `Marionette:waitForPageLoaded${this.curBrowser.curFrameId}`,
             parameters);
       });
 
       await click;
       break;
   }
 };
 
@@ -2228,387 +2233,452 @@ GeckoDriver.prototype.clickElement = asy
  * @param {string} id
  *     Web element reference ID to the element that will be inspected.
  * @param {string} name
  *     Name of the attribute which value to retrieve.
  *
  * @return {string}
  *     Value of the attribute.
  *
+ * @throws {InvalidArgumentError}
+ *     If <var>id</var> or <var>name</var> are not strings.
+ * @throws {NoSuchElementError}
+ *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getElementAttribute = async function(cmd, resp) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
-  let {id, name} = cmd.parameters;
+  let id = assert.string(cmd.parameters.id);
+  let name = assert.string(cmd.parameters.name);
+  let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.CHROME:
-      let el = this.curBrowser.seenEls.get(id);
+      let el = this.curBrowser.seenEls.get(webEl);
       resp.body.value = el.getAttribute(name);
       break;
 
     case Context.CONTENT:
-      resp.body.value = await this.listener.getElementAttribute(id, name);
+      resp.body.value = await this.listener.getElementAttribute(webEl, name);
       break;
   }
 };
 
 /**
  * Returns the value of a property associated with given element.
  *
  * @param {string} id
  *     Web element reference ID to the element that will be inspected.
  * @param {string} name
  *     Name of the property which value to retrieve.
  *
  * @return {string}
  *     Value of the property.
  *
+ * @throws {InvalidArgumentError}
+ *     If <var>id</var> or <var>name</var> are not strings.
+ * @throws {NoSuchElementError}
+ *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getElementProperty = async function(cmd, resp) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
-  let {id, name} = cmd.parameters;
+  let id = assert.string(cmd.parameters.id);
+  let name = assert.string(cmd.parameters.name);
+  let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.CHROME:
-      let el = this.curBrowser.seenEls.get(id);
+      let el = this.curBrowser.seenEls.get(webEl);
       resp.body.value = el[name];
       break;
 
     case Context.CONTENT:
-      resp.body.value = await this.listener.getElementProperty(id, name);
+      resp.body.value = await this.listener.getElementProperty(webEl, name);
       break;
   }
 };
 
 /**
  * Get the text of an element, if any.  Includes the text of all child
  * elements.
  *
  * @param {string} id
  *     Reference ID to the element that will be inspected.
  *
  * @return {string}
  *     Element's text "as rendered".
  *
+ * @throws {InvalidArgumentError}
+ *     If <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getElementText = async function(cmd, resp) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
-  let id = cmd.parameters.id;
+  let id = assert.string(cmd.parameters.id);
+  let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.CHROME:
       // for chrome, we look at text nodes, and any node with a "label" field
-      let el = this.curBrowser.seenEls.get(id);
+      let el = this.curBrowser.seenEls.get(webEl);
       let lines = [];
       this.getVisibleText(el, lines);
       resp.body.value = lines.join("\n");
       break;
 
     case Context.CONTENT:
-      resp.body.value = await this.listener.getElementText(id);
+      resp.body.value = await this.listener.getElementText(webEl);
       break;
   }
 };
 
 /**
  * Get the tag name of the element.
  *
  * @param {string} id
  *     Reference ID to the element that will be inspected.
  *
  * @return {string}
  *     Local tag name of element.
  *
+ * @throws {InvalidArgumentError}
+ *     If <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getElementTagName = async function(cmd, resp) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
-  let id = cmd.parameters.id;
+  let id = assert.string(cmd.parameters.id);
+  let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.CHROME:
-      let el = this.curBrowser.seenEls.get(id);
+      let el = this.curBrowser.seenEls.get(webEl);
       resp.body.value = el.tagName.toLowerCase();
       break;
 
     case Context.CONTENT:
-      resp.body.value = await this.listener.getElementTagName(id);
+      resp.body.value = await this.listener.getElementTagName(webEl);
       break;
   }
 };
 
 /**
  * Check if element is displayed.
  *
  * @param {string} id
  *     Reference ID to the element that will be inspected.
  *
  * @return {boolean}
  *     True if displayed, false otherwise.
  *
+ * @throws {InvalidArgumentError}
+ *     If <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.isElementDisplayed = async function(cmd, resp) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
-  let id = cmd.parameters.id;
+  let id = assert.string(cmd.parameters.id);
+  let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.CHROME:
-      let el = this.curBrowser.seenEls.get(id);
+      let el = this.curBrowser.seenEls.get(webEl);
       resp.body.value = await interaction.isElementDisplayed(
           el, this.a11yChecks);
       break;
 
     case Context.CONTENT:
-      resp.body.value = await this.listener.isElementDisplayed(id);
+      resp.body.value = await this.listener.isElementDisplayed(webEl);
       break;
   }
 };
 
 /**
  * Return the property of the computed style of an element.
  *
  * @param {string} id
  *     Reference ID to the element that will be checked.
  * @param {string} propertyName
  *     CSS rule that is being requested.
  *
  * @return {string}
  *     Value of |propertyName|.
  *
+ * @throws {InvalidArgumentError}
+ *     If <var>id</var> or <var>propertyName</var> are not strings.
+ * @throws {NoSuchElementError}
+ *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getElementValueOfCssProperty = async function(
     cmd, resp) {
   const win = assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
-  let {id, propertyName: prop} = cmd.parameters;
+  let id = assert.string(cmd.parameters.id);
+  let prop = assert.string(cmd.parameters.propertyName);
+  let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.CHROME:
-      let el = this.curBrowser.seenEls.get(id);
+      let el = this.curBrowser.seenEls.get(webEl);
       let sty = win.document.defaultView.getComputedStyle(el);
       resp.body.value = sty.getPropertyValue(prop);
       break;
 
     case Context.CONTENT:
       resp.body.value = await this.listener
-          .getElementValueOfCssProperty(id, prop);
+          .getElementValueOfCssProperty(webEl, prop);
       break;
   }
 };
 
 /**
  * Check if element is enabled.
  *
  * @param {string} id
  *     Reference ID to the element that will be checked.
  *
  * @return {boolean}
  *     True if enabled, false if disabled.
  *
+ * @throws {InvalidArgumentError}
+ *     If <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.isElementEnabled = async function(cmd, resp) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
-  let id = cmd.parameters.id;
+  let id = assert.string(cmd.parameters.id);
+  let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.CHROME:
       // Selenium atom doesn't quite work here
-      let el = this.curBrowser.seenEls.get(id);
+      let el = this.curBrowser.seenEls.get(webEl);
       resp.body.value = await interaction.isElementEnabled(
           el, this.a11yChecks);
       break;
 
     case Context.CONTENT:
-      resp.body.value = await this.listener.isElementEnabled(id);
+      resp.body.value = await this.listener.isElementEnabled(webEl);
       break;
   }
 };
 
 /**
  * Check if element is selected.
  *
  * @param {string} id
  *     Reference ID to the element that will be checked.
  *
  * @return {boolean}
  *     True if selected, false if unselected.
  *
+ * @throws {InvalidArgumentError}
+ *     If <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.isElementSelected = async function(cmd, resp) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
-  let id = cmd.parameters.id;
+  let id = assert.string(cmd.parameters.id);
+  let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.CHROME:
       // Selenium atom doesn't quite work here
-      let el = this.curBrowser.seenEls.get(id);
+      let el = this.curBrowser.seenEls.get(webEl);
       resp.body.value = await interaction.isElementSelected(
           el, this.a11yChecks);
       break;
 
     case Context.CONTENT:
-      resp.body.value = await this.listener.isElementSelected(id);
+      resp.body.value = await this.listener.isElementSelected(webEl);
       break;
   }
 };
 
 /**
+ * @throws {InvalidArgumentError}
+ *     If <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getElementRect = async function(cmd, resp) {
   const win = assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
-  let id = cmd.parameters.id;
+  let id = assert.string(cmd.parameters.id);
+  let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.CHROME:
-      let el = this.curBrowser.seenEls.get(id);
+      let el = this.curBrowser.seenEls.get(webEl);
       let rect = el.getBoundingClientRect();
       resp.body = {
         x: rect.x + win.pageXOffset,
         y: rect.y + win.pageYOffset,
         width: rect.width,
         height: rect.height,
       };
       break;
 
     case Context.CONTENT:
-      resp.body = await this.listener.getElementRect(id);
+      resp.body = await this.listener.getElementRect(webEl);
       break;
   }
 };
 
 /**
  * Send key presses to element after focusing on it.
  *
  * @param {string} id
  *     Reference ID to the element that will be checked.
- * @param {string} value
+ * @param {string} text
  *     Value to send to the element.
  *
+ * @throws {InvalidArgumentError}
+ *     If <var>id</var> or <var>text</var> are not strings.
+ * @throws {NoSuchElementError}
+ *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.sendKeysToElement = async function(cmd) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
-  let {id, text} = cmd.parameters;
-  assert.string(text);
+  let id = assert.string(cmd.parameters.id);
+  let text = assert.string(cmd.parameters.text);
+  let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.CHROME:
-      let el = this.curBrowser.seenEls.get(id);
+      let el = this.curBrowser.seenEls.get(webEl);
       await interaction.sendKeysToElement(
           el, text, true, this.a11yChecks);
       break;
 
     case Context.CONTENT:
-      await this.listener.sendKeysToElement(id, text);
+      await this.listener.sendKeysToElement(webEl, text);
       break;
   }
 };
 
 /**
  * Clear the text of an element.
  *
  * @param {string} id
  *     Reference ID to the element that will be cleared.
  *
+ * @throws {InvalidArgumentError}
+ *     If <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ *     If element represented by reference <var>id</var> is unknown.
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.clearElement = async function(cmd) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
-  let id = cmd.parameters.id;
+  let id = assert.string(cmd.parameters.id);
+  let webEl = WebElement.fromUUID(id, this.context);
 
   switch (this.context) {
     case Context.CHROME:
       // the selenium atom doesn't work here
-      let el = this.curBrowser.seenEls.get(id);
+      let el = this.curBrowser.seenEls.get(webEl);
       if (el.nodeName == "textbox") {
         el.value = "";
       } else if (el.nodeName == "checkbox") {
         el.checked = false;
       }
       break;
 
     case Context.CONTENT:
-      await this.listener.clearElement(id);
+      await this.listener.clearElement(webEl);
       break;
   }
 };
 
 /**
  * Switch to shadow root of the given host element.
  *
- * @param {string} id element id.
+ * @param {string} id
+ *     Reference ID to the element.
+ *
+ * @throws {InvalidArgumentError}
+ *     If <var>id</var> is not a string.
+ * @throws {NoSuchElementError}
+ *     If element represented by reference <var>id</var> is unknown.
  */
 GeckoDriver.prototype.switchToShadowRoot = async function(cmd) {
   assert.content(this.context);
   assert.window(this.getCurrentWindow());
 
-  let id = cmd.parameters.id;
-  await this.listener.switchToShadowRoot(id);
+  let id = assert.string(cmd.parameters.id);
+  let webEl = WebElement.fromUUID(id, this.context);
+  await this.listener.switchToShadowRoot(webEl);
 };
 
 /**
  * Add a single cookie to the cookie store associated with the active
  * document's address.
  *
  * @param {Map.<string, (string|number|boolean)> cookie
  *     Cookie object.
@@ -2887,29 +2957,31 @@ GeckoDriver.prototype.takeScreenshot = f
   let win = assert.window(this.getCurrentWindow());
 
   let {id, highlights, full, hash} = cmd.parameters;
   highlights = highlights || [];
   let format = hash ? capture.Format.Hash : capture.Format.Base64;
 
   switch (this.context) {
     case Context.CHROME:
-      let highlightEls = highlights.map(
-          ref => this.curBrowser.seenEls.get(ref));
+      let highlightEls = highlights
+          .map(ref => WebElement.fromUUID(ref, Context.CHROME))
+          .map(webEl => this.curBrowser.seenEls.get(webEl));
 
       // viewport
       let canvas;
       if (!id && !full) {
         canvas = capture.viewport(win, highlightEls);
 
       // element or full document element
       } else {
         let node;
         if (id) {
-          node = this.curBrowser.seenEls.get(id);
+          let webEl = WebElement.fromUUID(id, Context.CHROME);
+          node = this.curBrowser.seenEls.get(webEl);
         } else {
           node = win.document.documentElement;
         }
 
         canvas = capture.element(node, highlightEls);
       }
 
       switch (format) {
@@ -3365,19 +3437,25 @@ GeckoDriver.prototype.receiveMessage = f
     case "Marionette:switchedToFrame":
       if (message.json.restorePrevious) {
         this.currentFrameElement = this.previousFrameElement;
       } else {
         // we don't arbitrarily save previousFrameElement, since
         // we allow frame switching after modals appear, which would
         // override this value and we'd lose our reference
         if (message.json.storePrevious) {
-          this.previousFrameElement = this.currentFrameElement;
+          this.previousFrameElement =
+              new ChromeWebElement(this.currentFrameElement);
         }
-        this.currentFrameElement = message.json.frameValue;
+        if (message.json.frameValue) {
+          this.currentFrameElement =
+              new ChromeWebElement(message.json.frameValue);
+        } else {
+          this.currentFrameElement = null;
+        }
       }
       break;
 
     case "Marionette:emitTouchEvent":
       globalMessageManager.broadcastAsyncMessage(
           "MarionetteMainListener:emitTouchEvent", message.json);
       break;
 
@@ -3387,17 +3465,17 @@ GeckoDriver.prototype.receiveMessage = f
       let rv = this.registerBrowser(wid, be);
       return rv;
 
     case "Marionette:listenersAttached":
       if (message.json.listenerId === this.curBrowser.curFrameId) {
         // If the frame script gets reloaded we need to call newSession.
         // In the case of desktop this just sets up a small amount of state
         // that doesn't change over the course of a session.
-        this.sendAsync("newSession", this.capabilities.toJSON());
+        this.sendAsync("newSession", this.capabilities);
         this.curBrowser.flushPendingCommands();
       }
       break;
   }
 };
 /* eslint-enable consistent-return */
 
 GeckoDriver.prototype.responseCompleted = function() {
@@ -3676,25 +3754,16 @@ GeckoDriver.prototype.commands = {
   "singleTap": GeckoDriver.prototype.singleTap,
   "switchToFrame": GeckoDriver.prototype.switchToFrame,
   "switchToParentFrame": GeckoDriver.prototype.switchToParentFrame,
   "switchToShadowRoot": GeckoDriver.prototype.switchToShadowRoot,
   "switchToWindow": GeckoDriver.prototype.switchToWindow,
   "takeScreenshot": GeckoDriver.prototype.takeScreenshot,
 };
 
-function copy(obj) {
-  if (Array.isArray(obj)) {
-    return obj.slice();
-  } else if (typeof obj == "object") {
-    return Object.assign({}, obj);
-  }
-  return obj;
-}
-
 function getOuterWindowId(win) {
   return win.QueryInterface(Ci.nsIInterfaceRequestor)
       .getInterface(Ci.nsIDOMWindowUtils)
       .outerWindowID;
 }
 
 /**
  * Exit fullscreen and wait for <var>window</var> to resize.
--- a/testing/marionette/element.js
+++ b/testing/marionette/element.js
@@ -666,23 +666,16 @@ element.isCollection = function(seq) {
     case "[object NodeList]":
       return true;
 
     default:
       return false;
   }
 };
 
-element.makeWebElement = function(uuid) {
-  return {
-    [element.Key]: uuid,
-    [element.LegacyKey]: uuid,
-  };
-};
-
 /**
  * Determines if <var>el</var> is stale.
  *
  * A stale element is an element no longer attached to the DOM or which
  * node document is not the active document of the current browsing
  * context.
  *
  * The currently selected browsing context, specified through
--- a/testing/marionette/evaluate.js
+++ b/testing/marionette/evaluate.js
@@ -13,17 +13,16 @@ Cu.import("resource://gre/modules/XPCOMU
 
 const {
   element,
   WebElement,
 } = Cu.import("chrome://marionette/content/element.js", {});
 const {
   JavaScriptError,
   ScriptTimeoutError,
-  WebDriverError,
 } = Cu.import("chrome://marionette/content/error.js", {});
 
 const log = Log.repository.getLogger("Marionette");
 
 this.EXPORTED_SYMBOLS = ["evaluate", "sandbox", "Sandboxes"];
 
 const ARGUMENTS = "__webDriverArguments";
 const CALLBACK = "__webDriverCallback";
--- a/testing/marionette/legacyaction.js
+++ b/testing/marionette/legacyaction.js
@@ -2,17 +2,20 @@
  * 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/. */
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 
-Cu.import("chrome://marionette/content/element.js");
+const {
+  element,
+  WebElement,
+} = Cu.import("chrome://marionette/content/element.js", {});
 Cu.import("chrome://marionette/content/evaluate.js");
 Cu.import("chrome://marionette/content/event.js");
 
 const CONTEXT_MENU_DELAY_PREF = "ui.click_hold_context_menus.delay";
 const DEFAULT_CONTEXT_MENU_DELAY = 750;  // ms
 
 this.EXPORTED_SYMBOLS = ["legacyaction"];
 
@@ -173,16 +176,17 @@ action.Chain.prototype.actions = functio
   if (i == chain.length) {
     cb(touchId || null);
     this.resetValues();
     return;
   }
 
   let pack = chain[i];
   let command = pack[0];
+  let webEl;
   let el;
   let c;
   i++;
 
   if (["press", "wait", "keyDown", "keyUp", "click"].indexOf(command) == -1) {
     // if mouseEventsOnly, then touchIds isn't used
     if (!(touchId in this.touchIds) && !this.mouseEventsOnly) {
       this.resetValues();
@@ -197,17 +201,18 @@ action.Chain.prototype.actions = functio
       break;
 
     case "keyUp":
       event.sendKeyUp(pack[1], keyModifiers, this.container.frame);
       this.actions(chain, touchId, i, keyModifiers, cb);
       break;
 
     case "click":
-      el = this.seenEls.get(pack[1]);
+      webEl = WebElement.fromUUID(pack[1], "content");
+      el = this.seenEls.get(webEl);
       let button = pack[2];
       let clickCount = pack[3];
       c = element.coordinates(el);
       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);
       }
@@ -228,17 +233,18 @@ action.Chain.prototype.actions = functio
             "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.seenEls.get(pack[1]);
+      webEl = WebElement.fromUUID(pack[1], "content");
+      el = this.seenEls.get(webEl);
       c = element.coordinates(el, pack[2], pack[3]);
       touchId = this.generateEvents("press", c.x, c.y, null, el, keyModifiers);
       this.actions(chain, touchId, i, keyModifiers, cb);
       break;
 
     case "release":
       this.generateEvents(
           "release",
@@ -247,17 +253,18 @@ action.Chain.prototype.actions = functio
           touchId,
           null,
           keyModifiers);
       this.actions(chain, null, i, keyModifiers, cb);
       this.scrolling =  false;
       break;
 
     case "move":
-      el = this.seenEls.get(pack[1]);
+      webEl = WebElement.fromUUID(pack[1], "content");
+      el = this.seenEls.get(webEl);
       c = element.coordinates(el);
       this.generateEvents("move", c.x, c.y, touchId, null, keyModifiers);
       this.actions(chain, touchId, i, keyModifiers, cb);
       break;
 
     case "moveByOffset":
       this.generateEvents(
           "move",
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -16,26 +16,30 @@ Cu.import("resource://gre/modules/FileUt
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 Cu.import("chrome://marionette/content/accessibility.js");
 Cu.import("chrome://marionette/content/action.js");
 Cu.import("chrome://marionette/content/atom.js");
 Cu.import("chrome://marionette/content/capture.js");
-Cu.import("chrome://marionette/content/element.js");
+const {
+  element,
+  WebElement,
+} = Cu.import("chrome://marionette/content/element.js", {});
 const {
   ElementNotInteractableError,
   error,
   InsecureCertificateError,
   InvalidArgumentError,
   InvalidElementStateError,
   InvalidSelectorError,
   NoSuchElementError,
   NoSuchFrameError,
+  pprint,
   TimeoutError,
   UnknownError,
 } = Cu.import("chrome://marionette/content/error.js", {});
 Cu.import("chrome://marionette/content/evaluate.js");
 Cu.import("chrome://marionette/content/event.js");
 const {ContentEventObserverService} = Cu.import("chrome://marionette/content/dom.js", {});
 Cu.import("chrome://marionette/content/interaction.js");
 Cu.import("chrome://marionette/content/legacyaction.js");
@@ -457,45 +461,43 @@ function registerSelf() {
     if (typeof listenerId != "undefined") {
       startListeners();
       sendAsyncMessage("Marionette:listenersAttached",
           {"listenerId": listenerId});
     }
   }
 }
 
-// Eventually we will not have a closure for every single command, but
-// use a generic dispatch for all listener commands.
+// Eventually we will not have a closure for every single command,
+// but use a generic dispatch for all listener commands.
 //
-// Perhaps one could even conceive having a separate instance of
-// CommandProcessor for the listener, because the code is mostly the same.
+// Worth nothing that this shares many characteristics with
+// server.TCPConnection#execute.  Perhaps this could be generalised
+// at the point.
 function dispatch(fn) {
   if (typeof fn != "function") {
     throw new TypeError("Provided dispatch handler is not a function");
   }
 
-  return function(msg) {
-    let id = msg.json.commandID;
+  return msg => {
+    const id = msg.json.commandID;
 
-    let req = (async () => {
-      if (typeof msg.json == "undefined" || msg.json instanceof Array) {
-        return fn.apply(null, msg.json);
-      }
-      return fn(msg.json);
-    })();
+    let req = new Promise(resolve => {
+      const args = evaluate.fromJSON(msg.json, seenEls, curContainer.frame);
 
-    let okOrValueResponse = rv => {
-      if (typeof rv == "undefined") {
-        sendOk(id);
+      let rv;
+      if (typeof args == "undefined" || args instanceof Array) {
+        rv = fn.apply(null, args);
       } else {
-        sendResponse(rv, id);
+        rv = fn(args);
       }
-    };
+      resolve(rv);
+    });
 
-    req.then(okOrValueResponse, err => sendError(err, id))
+    req.then(rv => sendResponse(rv, id), err => sendError(err, id))
         .catch(error.report);
   };
 }
 
 /**
  * Add a message listener that's tied to our listenerId.
  */
 function addMessageListenerId(messageName, handler) {
@@ -699,17 +701,18 @@ function sendToServer(uuid, data = undef
  * Send asynchronous reply with value to chrome.
  *
  * @param {Object} obj
  *     JSON serialisable object of arbitrary type and complexity.
  * @param {UUID} uuid
  *     Unique identifier of the request.
  */
 function sendResponse(obj, uuid) {
-  sendToServer(uuid, obj);
+  let payload = evaluate.toJSON(obj, seenEls);
+  sendToServer(uuid, payload);
 }
 
 /**
  * Send asynchronous reply to chrome.
  *
  * @param {UUID} uuid
  *     Unique identifier of the request.
  */
@@ -765,34 +768,24 @@ function checkForInterrupted() {
       sendSyncMessage("Marionette:switchToModalOrigin");
     }
     sendSyncMessage("Marionette:switchedToFrame", {restorePrevious: true});
   }
 }
 
 async function execute(script, args, timeout, opts) {
   opts.timeout = timeout;
-
   let sb = sandbox.createMutable(curContainer.frame);
-  let wargs = evaluate.fromJSON(
-      args, seenEls, curContainer.frame, curContainer.shadowRoot);
-  let res = await evaluate.sandbox(sb, script, wargs, opts);
-
-  return evaluate.toJSON(res, seenEls);
+  return evaluate.sandbox(sb, script, args, opts);
 }
 
 async function executeInSandbox(script, args, timeout, opts) {
   opts.timeout = timeout;
-
   let sb = sandboxes.get(opts.sandboxName, opts.newSandbox);
-  let wargs = evaluate.fromJSON(
-      args, seenEls, curContainer.frame, curContainer.shadowRoot);
-
-  let res = await evaluate.sandbox(sb, script, wargs, opts);
-  return evaluate.toJSON(res, seenEls);
+  return evaluate.sandbox(sb, script, args, opts);
 }
 
 function emitTouchEvent(type, touch) {
   if (!wasInterrupted()) {
     logger.info(`Emitting Touch event of type ${type} ` +
         `to element with id: ${touch.target.id} ` +
         `and tag name: ${touch.target.tagName} ` +
         `at coordinates (${touch.clientX}), ` +
@@ -843,18 +836,17 @@ function emitTouchEvent(type, touch) {
         1,
         0);
   }
 }
 
 /**
  * Function that perform a single tap
  */
-async function singleTap(id, corx, cory) {
-  let el = seenEls.get(id, curContainer.frame);
+async function singleTap(el, corx, cory) {
   // after this block, the element will be scrolled into view
   let visible = element.isVisible(el, corx, cory);
   if (!visible) {
     throw new ElementNotInteractableError(
         "Element is not currently visible and may not be manipulated");
   }
 
   let a11y = accessibility.get(capabilities.get("moz:accessibilityChecks"));
@@ -1079,18 +1071,17 @@ function setDispatch(batches, touches, b
 /**
  * Start multi-action.
  *
  * @param {Number} maxLen
  *     Longest action chain for one finger.
  */
 function multiAction(args, maxLen) {
   // unwrap the original nested array
-  let commandArray = evaluate.fromJSON(
-      args, seenEls, curContainer.frame, curContainer.shadowRoot);
+  let commandArray = evaluate.fromJSON(args, seenEls, curContainer.frame);
   let concurrentEvent = [];
   let temp;
   for (let i = 0; i < maxLen; i++) {
     let row = [];
     for (let j = 0; j < commandArray.length; j++) {
       if (typeof commandArray[j][i] != "undefined") {
         // add finger id to the front of each action,
         // i.e. [finger_id, action, element]
@@ -1132,17 +1123,16 @@ function cancelRequest() {
  * @param {number} pageTimeout
  *     Timeout in seconds the method has to wait for the page being
  *     finished loading.
  * @param {number} startTime
  *     Unix timestap when the navitation request got triggred.
  */
 function waitForPageLoaded(msg) {
   let {commandID, pageTimeout, startTime} = msg.json;
-
   loadListener.waitForLoadAfterFramescriptReload(
       commandID, pageTimeout, startTime);
 }
 
 /**
  * Navigate to the given URL.  The operation will be performed on the
  * current browsing context, which means it handles the case where we
  * navigate within an iframe.  All other navigation is handled by the driver
@@ -1267,250 +1257,204 @@ function getPageSource() {
  */
 async function findElementContent(strategy, selector, opts = {}) {
   if (!SUPPORTED_STRATEGIES.has(strategy)) {
     throw new InvalidSelectorError("Strategy not supported: " + strategy);
   }
 
   opts.all = false;
   if (opts.startNode) {
-    opts.startNode = seenEls.get(opts.startNode, curContainer.frame);
+    opts.startNode = opts.startNode;
   }
 
   let el = await element.find(curContainer, strategy, selector, opts);
-  let elRef = seenEls.add(el);
-  let webEl = element.makeWebElement(elRef);
-  return webEl;
+  return seenEls.add(el);
 }
 
 /**
  * Find elements in the current browsing context's document using the
  * given search strategy.
  */
 async function findElementsContent(strategy, selector, opts = {}) {
   if (!SUPPORTED_STRATEGIES.has(strategy)) {
     throw new InvalidSelectorError("Strategy not supported: " + strategy);
   }
 
   opts.all = true;
-  if (opts.startNode) {
-    opts.startNode = seenEls.get(opts.startNode, curContainer.frame);
-  }
-
   let els = await element.find(curContainer, strategy, selector, opts);
-  let elRefs = seenEls.addAll(els);
-  let webEls = elRefs.map(element.makeWebElement);
+  let webEls = seenEls.addAll(els);
   return webEls;
 }
 
 /** Find and return the active element on the page. */
 function getActiveElement() {
   let el = curContainer.frame.document.activeElement;
   return evaluate.toJSON(el, seenEls);
 }
 
 /**
  * Send click event to element.
  *
  * @param {number} commandID
  *     ID of the currently handled message between the driver and
  *     listener.
- * @param {WebElement} id
+ * @param {WebElement} el
  *     Reference to the web element to click.
  * @param {number} pageTimeout
  *     Timeout in milliseconds the method has to wait for the page being
  *     finished loading.
  */
 function clickElement(msg) {
-  let {commandID, id, pageTimeout} = msg.json;
+  let {commandID, webElRef, pageTimeout} = msg.json;
+  let webEl = WebElement.fromJSON(webElRef);
+  let el = seenEls.get(webEl, curContainer.frame);
 
   try {
     let loadEventExpected = true;
 
-    let target = getElementAttribute(id, "target");
+    let target = getElementAttribute(el, "target");
 
     if (target === "_blank") {
       loadEventExpected = false;
     }
 
     loadListener.navigate(() => {
       return interaction.clickElement(
-          seenEls.get(id, curContainer.frame),
+          el,
           capabilities.get("moz:accessibilityChecks"),
           capabilities.get("moz:webdriverClick")
       );
     }, commandID, pageTimeout, loadEventExpected, true);
 
   } catch (e) {
     sendError(e, commandID);
   }
 }
 
-function getElementAttribute(id, name) {
-  let el = seenEls.get(id, curContainer.frame);
+function getElementAttribute(el, name) {
   if (element.isBooleanAttribute(el, name)) {
     if (el.hasAttribute(name)) {
       return "true";
     }
     return null;
   }
   return el.getAttribute(name);
 }
 
-function getElementProperty(id, name) {
-  let el = seenEls.get(id, curContainer.frame);
+function getElementProperty(el, name) {
   return typeof el[name] != "undefined" ? el[name] : null;
 }
 
 /**
- * Get the text of this element. This includes text from child elements.
- *
- * @param {WebElement} id
- *     Reference to web element.
- *
- * @return {string}
- *     Text of element.
+ * Get the text of this element.  This includes text from child
+ * elements.
  */
-function getElementText(id) {
-  let el = seenEls.get(id, curContainer.frame);
+function getElementText(el) {
   return atom.getElementText(el, curContainer.frame);
 }
 
 /**
  * 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 = seenEls.get(id, curContainer.frame);
+function getElementTagName(el) {
   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 = seenEls.get(id, curContainer.frame);
+function isElementDisplayed(el) {
   return interaction.isElementDisplayed(
       el, capabilities.get("moz:accessibilityChecks"));
 }
 
 /**
  * Retrieves the computed value of the given CSS property of the given
  * web element.
- *
- * @param {String} 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 = seenEls.get(id, curContainer.frame);
+function getElementValueOfCssProperty(el, prop) {
   let st = curContainer.frame.document.defaultView.getComputedStyle(el);
   return st.getPropertyValue(prop);
 }
 
 /**
  * Get the position and dimensions 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 = seenEls.get(id, curContainer.frame);
+function getElementRect(el) {
   let clientRect = el.getBoundingClientRect();
   return {
     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 = seenEls.get(id, curContainer.frame);
+function isElementEnabled(el) {
   return interaction.isElementEnabled(
       el, capabilities.get("moz:accessibilityChecks"));
 }
 
 /**
  * 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 = seenEls.get(id, curContainer.frame);
+function isElementSelected(el) {
   return interaction.isElementSelected(
       el, capabilities.get("moz:accessibilityChecks"));
 }
 
-async function sendKeysToElement(id, val) {
-  let el = seenEls.get(id, curContainer.frame);
+async function sendKeysToElement(el, val) {
   if (el.type == "file") {
     await interaction.uploadFile(el, val);
   } else if ((el.type == "date" || el.type == "time") &&
       Preferences.get("dom.forms.datetime")) {
     interaction.setFormControlValue(el, val);
   } else {
     await interaction.sendKeysToElement(
         el, val, false, capabilities.get("moz:accessibilityChecks"));
   }
 }
 
 /** Clear the text of an element. */
-function clearElement(id) {
+function clearElement(el) {
   try {
-    let el = seenEls.get(id, curContainer.frame);
     if (el.type == "file") {
       el.value = null;
     } else {
       atom.clearElement(el, curContainer.frame);
     }
   } catch (e) {
     // Bug 964738: Newer atoms contain status codes which makes wrapping
     // this in an error prototype that has a status property unnecessary
     if (e.name == "InvalidElementStateError") {
       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) {
+/** Switch the current context to the specified host's Shadow DOM. */
+function switchToShadowRoot(el) {
+  if (!el) {
     // 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;
       try {
         parent = curContainer.shadowRoot.host;
       } catch (e) {
         // There is a chance that host element is dead and we are trying to
@@ -1521,35 +1465,33 @@ function switchToShadowRoot(id) {
       while (parent && !(parent instanceof curContainer.frame.ShadowRoot)) {
         parent = parent.parentNode;
       }
       curContainer.shadowRoot = parent;
     }
     return;
   }
 
-  let foundShadowRoot;
-  let hostEl = seenEls.get(id, curContainer.frame);
-  foundShadowRoot = hostEl.shadowRoot;
+  let foundShadowRoot = el.shadowRoot;
   if (!foundShadowRoot) {
-    throw new NoSuchElementError("Unable to locate shadow root: " + id);
+    throw new NoSuchElementError(pprint`Unable to locate shadow root: ${el}`);
   }
   curContainer.shadowRoot = foundShadowRoot;
 }
 
 /**
  * Switch to the parent frame of the current frame. If the frame is the
  * top most is the current frame then no action will happen.
  */
 function switchToParentFrame(msg) {
   curContainer.frame = curContainer.frame.parent;
   let parentElement = seenEls.add(curContainer.frame);
 
   sendSyncMessage(
-      "Marionette:switchedToFrame", {frameValue: parentElement});
+      "Marionette:switchedToFrame", {frameValue: parentElement.uuid});
 
   sendOk(msg.json.commandID);
 }
 
 /**
  * Switch to frame given either the server-assigned element id,
  * its index in window.frames, or the iframe's name or id.
  */
@@ -1585,23 +1527,27 @@ function switchToFrame(msg) {
     if (msg.json.focus == true) {
       curContainer.frame.focus();
     }
 
     sendOk(commandID);
     return;
   }
 
-  let id = msg.json.element;
-  if (seenEls.has(id)) {
+  let webEl;
+  if (typeof msg.json.element != "undefined") {
+    webEl = WebElement.fromUUID(msg.json.element, "content");
+  }
+  if (webEl && seenEls.has(webEl)) {
     let wantedFrame;
     try {
-      wantedFrame = seenEls.get(id, curContainer.frame);
+      wantedFrame = seenEls.get(webEl, curContainer.frame);
     } catch (e) {
       sendError(e, commandID);
+      return;
     }
 
     if (frames.length > 0) {
       for (let i = 0; i < frames.length; i++) {
         // use XPCNativeWrapper to compare elements; see bug 834266
         let frameEl = frames[i].frameElement;
         let wrappedItem = new XPCNativeWrapper(frameEl);
         let wrappedWanted = new XPCNativeWrapper(wantedFrame);
@@ -1626,17 +1572,17 @@ function switchToFrame(msg) {
           curContainer.frame = iframes[i];
           foundFrame = i;
         }
       }
     }
   }
 
   if (foundFrame === null) {
-    if (typeof(msg.json.id) === "number") {
+    if (typeof msg.json.id === "number") {
       try {
         foundFrame = frames[msg.json.id].frameElement;
         if (foundFrame !== null) {
           curContainer.frame = foundFrame;
           foundFrame = seenEls.add(curContainer.frame);
         } else {
           // If foundFrame is null at this point then we have the top
           // level browsing context so should treat it accordingly.
@@ -1668,19 +1614,18 @@ function switchToFrame(msg) {
     let failedFrame = msg.json.id || msg.json.element;
     let err = new NoSuchFrameError(`Unable to locate frame: ${failedFrame}`);
     sendError(err, commandID);
     return;
   }
 
   // send a synchronous message to let the server update the currently active
   // frame element (for getActiveFrame)
-  let frameValue = evaluate.toJSON(
-      curContainer.frame.wrappedJSObject, seenEls)[element.Key];
-  sendSyncMessage("Marionette:switchedToFrame", {"frameValue": frameValue});
+  let frameWebEl = seenEls.add(curContainer.frame.wrappedJSObject);
+  sendSyncMessage("Marionette:switchedToFrame", {"frameValue": frameWebEl.uuid});
 
   if (curContainer.frame.contentWindow === null) {
     // The frame we want to switch to is a remote/OOP frame;
     // notify our parent to handle the switch
     curContainer.frame = content;
     let rv = {win: parWindow, frame: foundFrame};
     sendResponse(rv, commandID);
 
@@ -1723,29 +1668,32 @@ function switchToFrame(msg) {
  */
 function takeScreenshot(format, opts = {}) {
   let id = opts.id;
   let full = !!opts.full;
   let highlights = opts.highlights || [];
   let scroll = !!opts.scroll;
 
   let win = curContainer.frame;
-  let highlightEls = highlights.map(ref => seenEls.get(ref, win));
 
   let canvas;
+  let highlightEls = highlights
+      .map(ref => WebElement.fromUUID(ref, "content"))
+      .map(webEl => seenEls.get(webEl, win));
 
   // viewport
   if (!id && !full) {
     canvas = capture.viewport(win, highlightEls);
 
   // element or full document element
   } else {
     let el;
     if (id) {
-      el = seenEls.get(id, win);
+      let webEl = WebElement.fromUUID(id, "content");
+      el = seenEls.get(webEl, win);
       if (scroll) {
         element.scrollIntoView(el);
       }
     } else {
       el = win.document.documentElement;
     }
 
     canvas = capture.element(el, highlightEls);