Bug 1274274 - Decouple element retrieval methods; r=automatedtester
authorAndreas Tolfsen <ato@mozilla.com>
Fri, 20 May 2016 13:28:27 +0100
changeset 337930 2d585161b6b8c8088833c5a675faedfcb7f6388a
parent 337929 e15b4b4184eb36eafeb3f366a25e1911eb7f8978
child 337931 02d31625ba1406044fb706e87bc579c8fb222263
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersautomatedtester
bugs1274274
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 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
@@ -32,16 +32,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.
@@ -1341,17 +1351,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;
   }
 };
@@ -1642,64 +1652,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
@@ -36,17 +36,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;
 
@@ -1004,47 +1016,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.
  */
@@ -1264,17 +1294,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,
@@ -1357,17 +1387,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();