Bug 1274274 - Decouple element retrieval methods; r?automatedtester draft
authorAndreas Tolfsen <ato@mozilla.com>
Fri, 20 May 2016 13:28:27 +0100
changeset 370715 9d19d9e82de19ad31cea2aa1f33e010126be1350
parent 370714 467b0d255265f75f321f2b45e92495a902f20b5e
child 370716 78079075037133904c128a65d5481aee8d9ebeb5
push id19137
push userbmo:ato@mozilla.com
push dateWed, 25 May 2016 09:03:54 +0000
reviewersautomatedtester
bugs1274274
milestone49.0a1
Bug 1274274 - Decouple element retrieval methods; r?automatedtester Moves element retrieval methods from ElementManager to the testing/marionette/element.js module itself. This means some more work needs to be done by the caller, but avoids bloat by ensuring ElementManager does not end up as a super-object. MozReview-Commit-ID: 5LGe0vpSWwS
testing/marionette/driver.js
testing/marionette/element.js
testing/marionette/listener.js
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -33,16 +33,26 @@ Cu.import("chrome://marionette/content/s
 
 this.EXPORTED_SYMBOLS = ["GeckoDriver", "Context"];
 
 var FRAME_SCRIPT = "chrome://marionette/content/listener.js";
 const BROWSER_STARTUP_FINISHED = "browser-delayed-startup-finished";
 const CLICK_TO_START_PREF = "marionette.debugging.clicktostart";
 const CONTENT_LISTENER_PREF = "marionette.contentListener";
 
+const SUPPORTED_STRATEGIES = new Set([
+  element.Strategy.ClassName,
+  element.Strategy.Selector,
+  element.Strategy.ID,
+  element.Strategy.TagName,
+  element.Strategy.XPath,
+  element.Strategy.Anon,
+  element.Strategy.AnonAttribute,
+]);
+
 const logger = Log.repository.getLogger("Marionette");
 const globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"]
     .getService(Ci.nsIMessageBroadcaster);
 
 // This is used to prevent newSession from returning before the telephony
 // API's are ready; see bug 792647.  This assumes that marionette-server.js
 // will be loaded before the 'system-message-listener-ready' message
 // is fired.  If this stops being true, this approach will have to change.
@@ -1349,17 +1359,17 @@ GeckoDriver.prototype.switchToWindow = f
 
 GeckoDriver.prototype.getActiveFrame = function(cmd, resp) {
   switch (this.context) {
     case Context.CHROME:
       // no frame means top-level
       resp.body.value = null;
       if (this.curFrame) {
         resp.body.value = this.curBrowser.elementManager
-            .addToKnownElements(this.curFrame.frameElement);
+            .add(this.curFrame.frameElement);
       }
       break;
 
     case Context.CONTENT:
       resp.body.value = this.currentFrameElement;
       break;
   }
 };
@@ -1650,64 +1660,82 @@ GeckoDriver.prototype.multiAction = func
  * Find an element using the indicated search strategy.
  *
  * @param {string} using
  *     Indicates which search method to use.
  * @param {string} value
  *     Value the client is looking for.
  */
 GeckoDriver.prototype.findElement = function*(cmd, resp) {
+  let strategy = cmd.parameters.using;
+  let expr = cmd.parameters.value;
   let opts = {
     startNode: cmd.parameters.element,
     timeout: this.searchTimeout,
     all: false,
   };
 
   switch (this.context) {
     case Context.CHROME:
+      if (!SUPPORTED_STRATEGIES.has(strategy)) {
+        throw new InvalidSelectorError(`Strategy not supported: ${strategy}`);
+      }
+
       let container = {frame: this.getCurrentWindow()};
-      resp.body.value = yield this.curBrowser.elementManager.find(
-          container,
-          cmd.parameters.using,
-          cmd.parameters.value,
-          opts);
+      if (opts.startNode) {
+        opts.startNode = this.curBrowser.elementManager.getKnownElement(opts.startNode, container);
+      }
+      let el = yield element.find(container, strategy, expr, opts);
+      let elRef = this.curBrowser.elementManager.add(el);
+      let webEl = element.makeWebElement(elRef);
+
+      resp.body.value = webEl;
       break;
 
     case Context.CONTENT:
       resp.body.value = yield this.listener.findElementContent(
-          cmd.parameters.using,
-          cmd.parameters.value,
+          strategy,
+          expr,
           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 = function*(cmd, resp) {
+  let strategy = cmd.parameters.using;
+  let expr = cmd.parameters.value;
   let opts = {
     startNode: cmd.parameters.element,
     timeout: this.searchTimeout,
     all: true,
   };
 
   switch (this.context) {
     case Context.CHROME:
+      if (!SUPPORTED_STRATEGIES.has(strategy)) {
+        throw new InvalidSelectorError(`Strategy not supported: ${strategy}`);
+      }
+
       let container = {frame: this.getCurrentWindow()};
-      resp.body = yield this.curBrowser.elementManager.find(
-          container,
-          cmd.parameters.using,
-          cmd.parameters.value,
-          opts);
+      if (opts.startNode) {
+        opts.startNode = this.curBrowser.elementManager.getKnownElement(opts.startNode, container);
+      }
+      let els = yield element.find(container, strategy, expr, opts);
+
+      let elRefs = this.curBrowser.elementManager.addAll(els);
+      let webEls = elRefs.map(element.makeWebElement);
+      resp.body = webEls;
       break;
 
     case Context.CONTENT:
       resp.body = yield this.listener.findElementsContent(
           cmd.parameters.using,
           cmd.parameters.value,
           opts);
       break;
--- a/testing/marionette/element.js
+++ b/testing/marionette/element.js
@@ -53,60 +53,74 @@ element.Strategy = {
   Name: "name",
   LinkText: "link text",
   PartialLinkText: "partial link text",
   TagName: "tag name",
   XPath: "xpath",
   Anon: "anon",
   AnonAttribute: "anon attribute",
 };
-element.Strategies = new Set(Object.values(element.Strategy));
 
-this.ElementManager = function ElementManager(unsupportedStrategies = []) {
+this.ElementManager = function ElementManager() {
   this.seenItems = {};
   this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-
-  this.supportedStrategies = new Set(element.Strategies);
-  for (let s of unsupportedStrategies) {
-    this.supportedStrategies.delete(s);
-  }
 };
 
 ElementManager.prototype = {
   /**
    * Reset values
    */
   reset: function EM_clear() {
     this.seenItems = {};
   },
 
   /**
-  * Add element to list of seen elements
-  *
-  * @param nsIDOMElement element
-  *        The element to add
+   * Make a collection of elements seen.
+   *
+   * The oder of the returned web element references is guaranteed to
+   * match that of the collection passed in.
+   *
+   * @param {NodeList} els
+   *     Sequence of elements to add to set of seen elements.
+   *
+   * @return {Array.<WebElement>}
+   *     List of the web element references associated with each element
+   *     from |els|.
+   */
+  addAll: function(els) {
+    let add = this.add.bind(this);
+    return [...els].map(add);
+  },
+
+  /**
+  * Make an element seen.
   *
-  * @return string
-  *        Returns the server-assigned reference ID
+  * @param {nsIDOMElement} el
+  *    Element to add to set of seen elements.
+  *
+  * @return {string}
+  *     Web element reference associated with element.
   */
-  addToKnownElements: function EM_addToKnownElements(el) {
+  add: function(el) {
     for (let i in this.seenItems) {
-      let foundEl = null;
+      let foundEl;
       try {
         foundEl = this.seenItems[i].get();
       } catch (e) {}
+
       if (foundEl) {
         if (XPCNativeWrapper(foundEl) == XPCNativeWrapper(el)) {
           return i;
         }
       } else {
         // cleanup reference to GC'd element
         delete this.seenItems[i];
       }
     }
+
     let id = element.generateUUID();
     this.seenItems[id] = Cu.getWeakReference(el);
     return id;
   },
 
   /**
    * Retrieve element from its unique ID
    *
@@ -216,17 +230,17 @@ ElementManager.prototype = {
             result.push(this.wrapValue(val[i]));
 
           }
         }
         else if (val == null) {
           result = null;
         }
         else if (val.nodeType == 1) {
-          let elementId = this.addToKnownElements(val);
+          let elementId = this.add(val);
           result = {[element.LegacyKey]: elementId, [element.Key]: elementId};
         }
         else {
           result = {};
           for (let prop in val) {
             try {
               result[prop] = this.wrapValue(val[prop]);
             } catch (e if (e.result == Cr.NS_ERROR_NOT_IMPLEMENTED)) {
@@ -284,342 +298,325 @@ ElementManager.prototype = {
           for (let prop in args) {
             converted[prop] = this.convertWrappedArguments(args[prop], container);
           }
         }
         break;
     }
     return converted;
   },
+};
 
-  /**
-   * Find a single element or a collection of elements starting at the
-   * document root or a given node.
-   *
-   * If |timeout| is above 0, an implicit search technique is used.
-   * This will wait for the duration of |timeout| for the element
-   * to appear in the DOM.
-   *
-   * See the |element.Strategy| enum for a full list of supported
-   * search strategies that can be passed to |strategy|.
-   *
-   * Available flags for |opts|:
-   *
-   *     |all|
-   *       If true, a multi-element search selector is used and a sequence
-   *       of elements will be returned.  Otherwise a single element.
-   *
-   *     |timeout|
-   *       Duration to wait before timing out the search.  If |all| is
-   *       false, a NoSuchElementError is thrown if unable to find
-   *       the element within the timeout duration.
-   *
-   *     |startNode|
-   *       Element to use as the root of the search.
-   *
-   * @param {Object.<string, Window>} container
-   *     Window object and an optional shadow root that contains the
-   *     root shadow DOM element.
-   * @param {string} strategy
-   *     Search strategy whereby to locate the element(s).
-   * @param {string} selector
-   *     Selector search pattern.  The selector must be compatible with
-   *     the chosen search |strategy|.
-   * @param {Object.<string, ?>} opts
-   *     Options.
-   *
-   * @return {Promise: (WebElement|Array<WebElement>)}
-   *     Single element or a sequence of elements.
-   *
-   * @throws InvalidSelectorError
-   *     If |strategy| is unknown.
-   * @throws InvalidSelectorError
-   *     If |selector| is malformed.
-   * @throws NoSuchElementError
-   *     If a single element is requested, this error will throw if the
-   *     element is not found.
-   */
-  find: function(container, strategy, selector, opts = {}) {
-    opts.all = !!opts.all;
-    opts.timeout = opts.timeout || 0;
+/**
+ * Find a single element or a collection of elements starting at the
+ * document root or a given node.
+ *
+ * If |timeout| is above 0, an implicit search technique is used.
+ * This will wait for the duration of |timeout| for the element
+ * to appear in the DOM.
+ *
+ * See the |element.Strategy| enum for a full list of supported
+ * search strategies that can be passed to |strategy|.
+ *
+ * Available flags for |opts|:
+ *
+ *     |all|
+ *       If true, a multi-element search selector is used and a sequence
+ *       of elements will be returned.  Otherwise a single element.
+ *
+ *     |timeout|
+ *       Duration to wait before timing out the search.  If |all| is
+ *       false, a NoSuchElementError is thrown if unable to find
+ *       the element within the timeout duration.
+ *
+ *     |startNode|
+ *       Element to use as the root of the search.
+ *
+ * @param {Object.<string, Window>} container
+ *     Window object and an optional shadow root that contains the
+ *     root shadow DOM element.
+ * @param {string} strategy
+ *     Search strategy whereby to locate the element(s).
+ * @param {string} selector
+ *     Selector search pattern.  The selector must be compatible with
+ *     the chosen search |strategy|.
+ * @param {Object.<string, ?>} opts
+ *     Options.
+ *
+ * @return {Promise: (nsIDOMElement|Array<nsIDOMElement>)}
+ *     Single element or a sequence of elements.
+ *
+ * @throws InvalidSelectorError
+ *     If |strategy| is unknown.
+ * @throws InvalidSelectorError
+ *     If |selector| is malformed.
+ * @throws NoSuchElementError
+ *     If a single element is requested, this error will throw if the
+ *     element is not found.
+ */
+element.find = function(container, strategy, selector, opts = {}) {
+  opts.all = !!opts.all;
+  opts.timeout = opts.timeout || 0;
 
-    let searchFn;
-    if (opts.all) {
-      searchFn = this.findElements.bind(this);
-    } else {
-      searchFn = this.findElement.bind(this);
-    }
+  let searchFn;
+  if (opts.all) {
+    searchFn = findElements.bind(this);
+  } else {
+    searchFn = findElement.bind(this);
+  }
 
-    return new Promise((resolve, reject) => {
-      let findElements = implicitlyWaitFor(
-          () => this.find_(container, strategy, selector, searchFn, opts),
-          opts.timeout);
+  return new Promise((resolve, reject) => {
+    let findElements = implicitlyWaitFor(
+        () => find_(container, strategy, selector, searchFn, opts),
+        opts.timeout);
 
-      findElements.then(foundEls => {
-        // the following code ought to be moved into findElement
-        // and findElements when bug 1254486 is addressed
-        if (!opts.all && (!foundEls || foundEls.length == 0)) {
-          let msg;
-          switch (strategy) {
-            case element.Strategy.AnonAttribute:
-              msg = "Unable to locate anonymous element: " + JSON.stringify(selector);
-              break;
+    findElements.then(foundEls => {
+      // the following code ought to be moved into findElement
+      // and findElements when bug 1254486 is addressed
+      if (!opts.all && (!foundEls || foundEls.length == 0)) {
+        let msg;
+        switch (strategy) {
+          case element.Strategy.AnonAttribute:
+            msg = "Unable to locate anonymous element: " + JSON.stringify(selector);
+            break;
 
-            default:
-              msg = "Unable to locate element: " + selector;
-          }
-
-          reject(new NoSuchElementError(msg));
-        }
-
-        // serialise elements for return
-        let rv = [];
-        for (let el of foundEls) {
-          let ref = this.addToKnownElements(el);
-          let we = element.makeWebElement(ref);
-          rv.push(we);
+          default:
+            msg = "Unable to locate element: " + selector;
         }
 
-        if (opts.all) {
-          resolve(rv);
-        }
-        resolve(rv[0]);
-      }, reject);
-    });
-  },
+        reject(new NoSuchElementError(msg));
+      }
+
+      if (opts.all) {
+        resolve(foundEls);
+      }
+      resolve(foundEls[0]);
+    }, reject);
+  });
+};
 
-  find_: function(container, strategy, selector, searchFn, opts) {
-    let rootNode = container.shadowRoot || container.frame.document;
-    let startNode;
-    if (opts.startNode) {
-      startNode = this.getKnownElement(opts.startNode, container);
-    } else {
-      startNode = rootNode;
-    }
+function find_(container, strategy, selector, searchFn, opts) {
+  let rootNode = container.shadowRoot || container.frame.document;
+  let startNode = opts.startNode || rootNode;
 
-    if (!this.supportedStrategies.has(strategy)) {
-      throw new InvalidSelectorError("Strategy not supported: " + strategy);
-    }
+  let res;
+  try {
+    res = searchFn(strategy, selector, rootNode, startNode);
+  } catch (e) {
+    throw new InvalidSelectorError(
+        `Given ${strategy} expression "${selector}" is invalid: ${e}`);
+  }
 
-    let res;
-    try {
-      res = searchFn(strategy, selector, rootNode, startNode);
-    } catch (e) {
-      throw new InvalidSelectorError(
-          `Given ${strategy} expression "${selector}" is invalid`);
-    }
-
-    if (element.isElementCollection(res)) {
-      return res;
-    } else if (res) {
-      return [res];
-    }
-    return [];
-  },
+  if (element.isElementCollection(res)) {
+    return res;
+  } else if (res) {
+    return [res];
+  }
+  return [];
+}
 
-  /**
-   * Find a value by XPATH
-   *
-   * @param nsIDOMElement root
-   *        Document root
-   * @param string value
-   *        XPATH search string
-   * @param nsIDOMElement node
-   *        start node
-   *
-   * @return nsIDOMElement
-   *        returns the found element
-   */
-  findByXPath: function EM_findByXPath(root, value, node) {
-    return root.evaluate(value, node, null,
-            Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
-  },
+/**
+ * Find a value by XPATH
+ *
+ * @param nsIDOMElement root
+ *        Document root
+ * @param string value
+ *        XPATH search string
+ * @param nsIDOMElement node
+ *        start node
+ *
+ * @return nsIDOMElement
+ *        returns the found element
+ */
+function findByXPath(root, value, node) {
+  return root.evaluate(value, node, null,
+          Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
+}
 
-  /**
-   * Find values by XPATH
-   *
-   * @param nsIDOMElement root
-   *        Document root
-   * @param string value
-   *        XPATH search string
-   * @param nsIDOMElement node
-   *        start node
-   *
-   * @return object
-   *        returns a list of found nsIDOMElements
-   */
-  findByXPathAll: function EM_findByXPathAll(root, value, node) {
-    let values = root.evaluate(value, node, null,
-                      Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
-    let elements = [];
-    let element = values.iterateNext();
-    while (element) {
-      elements.push(element);
-      element = values.iterateNext();
-    }
-    return elements;
-  },
+/**
+ * Find values by XPATH
+ *
+ * @param nsIDOMElement root
+ *        Document root
+ * @param string value
+ *        XPATH search string
+ * @param nsIDOMElement node
+ *        start node
+ *
+ * @return object
+ *        returns a list of found nsIDOMElements
+ */
+function findByXPathAll(root, value, node) {
+  let values = root.evaluate(value, node, null,
+                    Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
+  let elements = [];
+  let element = values.iterateNext();
+  while (element) {
+    elements.push(element);
+    element = values.iterateNext();
+  }
+  return elements;
+}
 
-  /**
-   * Finds a single element.
-   *
-   * @param {element.Strategy} using
-   *     Selector strategy to use.
-   * @param {string} value
-   *     Selector expression.
-   * @param {DOMElement} rootNode
-   *     Document root.
-   * @param {DOMElement=} startNode
-   *     Optional node from which to start searching.
-   *
-   * @return {DOMElement}
-   *     Found elements.
-   *
-   * @throws {InvalidSelectorError}
-   *     If strategy |using| is not recognised.
-   * @throws {Error}
-   *     If selector expression |value| is malformed.
-   */
-  findElement: function(using, value, rootNode, startNode) {
-    switch (using) {
-      case element.Strategy.ID:
-        if (startNode.getElementById) {
-          return startNode.getElementById(value);
-        }
-        return this.findByXPath(rootNode, `.//*[@id="${value}"]`, startNode);
+/**
+ * Finds a single element.
+ *
+ * @param {element.Strategy} using
+ *     Selector strategy to use.
+ * @param {string} value
+ *     Selector expression.
+ * @param {DOMElement} rootNode
+ *     Document root.
+ * @param {DOMElement=} startNode
+ *     Optional node from which to start searching.
+ *
+ * @return {DOMElement}
+ *     Found elements.
+ *
+ * @throws {InvalidSelectorError}
+ *     If strategy |using| is not recognised.
+ * @throws {Error}
+ *     If selector expression |value| is malformed.
+ */
+function findElement(using, value, rootNode, startNode) {
+  switch (using) {
+    case element.Strategy.ID:
+      if (startNode.getElementById) {
+        return startNode.getElementById(value);
+      }
+      return findByXPath(rootNode, `.//*[@id="${value}"]`, startNode);
 
-      case element.Strategy.Name:
-        if (startNode.getElementsByName) {
-          return startNode.getElementsByName(value)[0];
-        }
-        return this.findByXPath(rootNode, `.//*[@name="${value}"]`, startNode);
+    case element.Strategy.Name:
+      if (startNode.getElementsByName) {
+        return startNode.getElementsByName(value)[0];
+      }
+      return findByXPath(rootNode, `.//*[@name="${value}"]`, startNode);
 
-      case element.Strategy.ClassName:
-        // works for >= Firefox 3
-        return  startNode.getElementsByClassName(value)[0];
-
-      case element.Strategy.TagName:
-        // works for all elements
-        return startNode.getElementsByTagName(value)[0];
+    case element.Strategy.ClassName:
+      // works for >= Firefox 3
+      return  startNode.getElementsByClassName(value)[0];
 
-      case element.Strategy.XPath:
-        return  this.findByXPath(rootNode, value, startNode);
+    case element.Strategy.TagName:
+      // works for all elements
+      return startNode.getElementsByTagName(value)[0];
+
+    case element.Strategy.XPath:
+      return  findByXPath(rootNode, value, startNode);
 
-      // TODO(ato): Rewrite this, it's hairy:
-      case element.Strategy.LinkText:
-      case element.Strategy.PartialLinkText:
-        let el;
-        let allLinks = startNode.getElementsByTagName("A");
-        for (let i = 0; i < allLinks.length && !el; i++) {
-          let text = allLinks[i].text;
-          if (using == element.Strategy.PartialLinkText) {
-            if (text.indexOf(value) != -1) {
-              el = allLinks[i];
-            }
-          } else if (text == value) {
+    // TODO(ato): Rewrite this, it's hairy:
+    case element.Strategy.LinkText:
+    case element.Strategy.PartialLinkText:
+      let el;
+      let allLinks = startNode.getElementsByTagName("A");
+      for (let i = 0; i < allLinks.length && !el; i++) {
+        let text = allLinks[i].text;
+        if (using == element.Strategy.PartialLinkText) {
+          if (text.indexOf(value) != -1) {
             el = allLinks[i];
           }
+        } else if (text == value) {
+          el = allLinks[i];
         }
-        return el;
-
-      case element.Strategy.Selector:
-        try {
-          return startNode.querySelector(value);
-        } catch (e) {
-          throw new InvalidSelectorError(`${e.message}: "${value}"`);
-        }
+      }
+      return el;
 
-      case element.Strategy.Anon:
-        return rootNode.getAnonymousNodes(startNode);
+    case element.Strategy.Selector:
+      try {
+        return startNode.querySelector(value);
+      } catch (e) {
+        throw new InvalidSelectorError(`${e.message}: "${value}"`);
+      }
 
-      case element.Strategy.AnonAttribute:
-        let attr = Object.keys(value)[0];
-        return rootNode.getAnonymousElementByAttribute(startNode, attr, value[attr]);
+    case element.Strategy.Anon:
+      return rootNode.getAnonymousNodes(startNode);
 
-      default:
-        throw new InvalidSelectorError(`No such strategy: ${using}`);
-    }
-},
+    case element.Strategy.AnonAttribute:
+      let attr = Object.keys(value)[0];
+      return rootNode.getAnonymousElementByAttribute(startNode, attr, value[attr]);
+
+    default:
+      throw new InvalidSelectorError(`No such strategy: ${using}`);
+  }
+};
 
-  /**
-   * Find multiple elements.
-   *
-   * @param {element.Strategy} using
-   *     Selector strategy to use.
-   * @param {string} value
-   *     Selector expression.
-   * @param {DOMElement} rootNode
-   *     Document root.
-   * @param {DOMElement=} startNode
-   *     Optional node from which to start searching.
-   *
-   * @return {DOMElement}
-   *     Found elements.
-   *
-   * @throws {InvalidSelectorError}
-   *     If strategy |using| is not recognised.
-   * @throws {Error}
-   *     If selector expression |value| is malformed.
-   */
-  findElements: function(using, value, rootNode, startNode) {
-    switch (using) {
-      case element.Strategy.ID:
-        value = `.//*[@id="${value}"]`;
+/**
+ * Find multiple elements.
+ *
+ * @param {element.Strategy} using
+ *     Selector strategy to use.
+ * @param {string} value
+ *     Selector expression.
+ * @param {DOMElement} rootNode
+ *     Document root.
+ * @param {DOMElement=} startNode
+ *     Optional node from which to start searching.
+ *
+ * @return {DOMElement}
+ *     Found elements.
+ *
+ * @throws {InvalidSelectorError}
+ *     If strategy |using| is not recognised.
+ * @throws {Error}
+ *     If selector expression |value| is malformed.
+ */
+function findElements(using, value, rootNode, startNode) {
+  switch (using) {
+    case element.Strategy.ID:
+      value = `.//*[@id="${value}"]`;
 
-      // fall through
-      case element.Strategy.XPath:
-        return this.findByXPathAll(rootNode, value, startNode);
+    // fall through
+    case element.Strategy.XPath:
+      return findByXPathAll(rootNode, value, startNode);
 
-      case element.Strategy.Name:
-        if (startNode.getElementsByName) {
-          return startNode.getElementsByName(value);
-        }
-        return this.findByXPathAll(rootNode, `.//*[@name="${value}"]`, startNode);
-
-      case element.Strategy.ClassName:
-        return startNode.getElementsByClassName(value);
+    case element.Strategy.Name:
+      if (startNode.getElementsByName) {
+        return startNode.getElementsByName(value);
+      }
+      return findByXPathAll(rootNode, `.//*[@name="${value}"]`, startNode);
 
-      case element.Strategy.TagName:
-        return startNode.getElementsByTagName(value);
+    case element.Strategy.ClassName:
+      return startNode.getElementsByClassName(value);
+
+    case element.Strategy.TagName:
+      return startNode.getElementsByTagName(value);
 
-      case element.Strategy.LinkText:
-      case element.Strategy.PartialLinkText:
-        let els = [];
-        let allLinks = startNode.getElementsByTagName("A");
-        for (let i = 0; i < allLinks.length; i++) {
-          let text = allLinks[i].text;
-          if (using == element.Strategy.PartialLinkText) {
-            if (text.indexOf(value) != -1) {
-              els.push(allLinks[i]);
-            }
-          } else if (text == value) {
+    case element.Strategy.LinkText:
+    case element.Strategy.PartialLinkText:
+      let els = [];
+      let allLinks = startNode.getElementsByTagName("A");
+      for (let i = 0; i < allLinks.length; i++) {
+        let text = allLinks[i].text;
+        if (using == element.Strategy.PartialLinkText) {
+          if (text.indexOf(value) != -1) {
             els.push(allLinks[i]);
           }
+        } else if (text == value) {
+          els.push(allLinks[i]);
         }
-        return els;
+      }
+      return els;
 
-      case element.Strategy.Selector:
-        return Array.slice(startNode.querySelectorAll(value));
+    case element.Strategy.Selector:
+      return startNode.querySelectorAll(value);
 
-      case element.Strategy.Anon:
-        return rootNode.getAnonymousNodes(startNode);
+    case element.Strategy.Anon:
+      return rootNode.getAnonymousNodes(startNode);
 
-      case element.Strategy.AnonAttribute:
-        let attr = Object.keys(value)[0];
-        let el = rootNode.getAnonymousElementByAttribute(startNode, attr, value[attr]);
-        if (el) {
-          return [el];
-        }
-        return [];
+    case element.Strategy.AnonAttribute:
+      let attr = Object.keys(value)[0];
+      let el = rootNode.getAnonymousElementByAttribute(startNode, attr, value[attr]);
+      if (el) {
+        return [el];
+      }
+      return [];
 
-      default:
-        throw new InvalidSelectorError(`No such strategy: ${using}`);
-    }
-  },
-};
+    default:
+      throw new InvalidSelectorError(`No such strategy: ${using}`);
+  }
+}
 
 /**
  * Runs function off the main thread until its return value is truthy
  * or the provided timeout is reached.  The function is guaranteed to be
  * run at least once, irregardless of the timeout.
  *
  * A truthy return value constitutes a truthful boolean, positive number,
  * object, or non-empty array.
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -37,17 +37,29 @@ var isB2G = false;
 
 var marionetteTestName;
 var winUtil = content.QueryInterface(Ci.nsIInterfaceRequestor)
     .getInterface(Ci.nsIDOMWindowUtils);
 var listenerId = null; // unique ID of this listener
 var curContainer = { frame: content, shadowRoot: null };
 var isRemoteBrowser = () => curContainer.frame.contentWindow !== null;
 var previousContainer = null;
+
 var elementManager = new ElementManager();
+var SUPPORTED_STRATEGIES = new Set([
+  element.Strategy.ClassName,
+  element.Strategy.Selector,
+  element.Strategy.ID,
+  element.Strategy.Name,
+  element.Strategy.LinkText,
+  element.Strategy.PartialLinkText,
+  element.Strategy.TagName,
+  element.Strategy.XPath,
+]);
+
 var capabilities = {};
 
 var actions = new action.Chain(checkForInterrupted);
 
 // Contains the last file input element that was the target of
 // sendKeysToElement.
 var fileInputElement;
 
@@ -1007,47 +1019,65 @@ function refresh(msg) {
   };
   addEventListener("DOMContentLoaded", listen, false);
 }
 
 /**
  * Find an element in the current browsing context's document using the
  * given search strategy.
  */
-function findElementContent(strategy, selector, opts = {}) {
+function* findElementContent(strategy, selector, opts = {}) {
+  if (!SUPPORTED_STRATEGIES.has(strategy)) {
+    throw new InvalidSelectorError("Strategy not supported: " + strategy);
+  }
+
   opts.all = false;
-  return elementManager.find(
-      curContainer,
-      strategy,
-      selector,
-      opts);
+  if (opts.startNode) {
+    opts.startNode = elementManager.getKnownElement(opts.startNode, curContainer);
+  }
+
+  let el = yield element.find(curContainer, strategy, selector, opts);
+  let elRef = elementManager.add(el);
+  let webEl = element.makeWebElement(elRef);
+  return webEl;
 }
 
 /**
  * Find elements in the current browsing context's document using the
  * given search strategy.
  */
-function findElementsContent(strategy, selector, opts = {}) {
+function* findElementsContent(strategy, selector, opts = {}) {
+  if (!SUPPORTED_STRATEGIES.has(strategy)) {
+    throw new InvalidSelectorError("Strategy not supported: " + strategy);
+  }
+
   opts.all = true;
-  return elementManager.find(
-      curContainer,
-      strategy,
-      selector,
-      opts);
+  if (opts.startNode) {
+    opts.startNode = elementManager.getKnownElement(opts.startNode, curContainer);
+  }
+
+  let els = yield element.find(curContainer, strategy, selector, opts);
+  let elRefs = elementManager.addAll(els);
+  let webEls = elRefs.map(element.makeWebElement);
+  return webEls;
 }
 
 /**
  * Find and return the active element on the page.
  *
  * @return {WebElement}
  *     Reference to web element.
  */
 function getActiveElement() {
   let el = curContainer.frame.document.activeElement;
-  return elementManager.addToKnownElements(el);
+  let elRef = elementManager.add(el);
+  // TODO(ato): This incorrectly returns
+  // the element's associated UUID as a string
+  // instead of a web element.
+  return elRef;
 }
 
 /**
  * Send click event to element.
  *
  * @param {WebElement} id
  *     Reference to the web element to click.
  */
@@ -1267,17 +1297,17 @@ function switchToShadowRoot(id) {
 
 /**
  * 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) {
    let command_id = msg.json.command_id;
    curContainer.frame = curContainer.frame.parent;
-   let parentElement = elementManager.addToKnownElements(curContainer.frame);
+   let parentElement = elementManager.add(curContainer.frame);
 
    sendSyncMessage("Marionette:switchedToFrame", { frameValue: parentElement });
 
    sendOk(msg.json.command_id);
  }
 
 /**
  * Switch to frame given either the server-assigned element id,
@@ -1360,17 +1390,17 @@ function switchToFrame(msg) {
     }
   }
   if (foundFrame === null) {
     if (typeof(msg.json.id) === 'number') {
       try {
         foundFrame = frames[msg.json.id].frameElement;
         if (foundFrame !== null) {
           curContainer.frame = foundFrame;
-          foundFrame = elementManager.addToKnownElements(curContainer.frame);
+          foundFrame = elementManager.add(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});
           curContainer.frame = content;
           if(msg.json.focus == true) {
             curContainer.frame.focus();