Javascript  |  277行  |  8.58 KB

/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 *
 * Helper javascript injected whenever a DomMutationEventObserver is created.
 *
 * This script uses MutationObservers to watch for changes to the DOM, then
 * reports the event to the observer using the DomAutomationController. An
 * anonymous namespace is used to prevent conflict with other Javascript.
 *
 * Args:
 *  automation_id: Automation id used to route DomAutomationController messages.
 *  observer_id: Id of the observer who will be receiving the messages.
 *  observer_type: One of 'add', 'remove', 'change', or 'exists'.
 *  xpath: XPath used to specify the DOM node of interest.
 *  attribute: If |expected_value| is provided, check if this attribute of the
 *      DOM node matches |expected value|.
 *  expected_value: If not null, regular expression to match with the value of
 *      |attribute| after the mutation.
 */
function(automation_id, observer_id, observer_type, xpath, attribute,
         expected_value) {

  /* Raise an event for the DomMutationEventObserver. */
  function raiseEvent() {
    if (window.domAutomationController) {
      console.log("Event sent to DomEventObserver with id=" +
                  observer_id + ".");
      window.domAutomationController.sendWithId(
          automation_id, "__dom_mutation_observer__:" + observer_id);
    }
  }

  /* Calls raiseEvent if the expected node has been added to the DOM.
   *
   * Args:
   *  mutations: A list of mutation objects.
   *  observer: The mutation observer object associated with this callback.
   */
  function addNodeCallback(mutations, observer) {
    for (var j=0; j<mutations.length; j++) {
      for (var i=0; i<mutations[j].addedNodes.length; i++) {
        var node = mutations[j].addedNodes[i];
        if (xpathMatchesNode(node, xpath) &&
            nodeAttributeValueEquals(node, attribute, expected_value)) {
          raiseEvent();
          observer.disconnect();
          delete observer;
          return;
        }
      }
    }
  }

  /* Calls raiseEvent if the expected node has been removed from the DOM.
   *
   * Args:
   *  mutations: A list of mutation objects.
   *  observer: The mutation observer object associated with this callback.
   */
  function removeNodeCallback(mutations, observer) {
    var node = firstXPathNode(xpath);
    if (!node) {
      raiseEvent();
      observer.disconnect();
      delete observer;
    }
  }

  /* Calls raiseEvent if the given node has been changed to expected_value.
   *
   * Args:
   *  mutations: A list of mutation objects.
   *  observer: The mutation observer object associated with this callback.
   */
  function changeNodeCallback(mutations, observer) {
    for (var j=0; j<mutations.length; j++) {
      if (nodeAttributeValueEquals(mutations[j].target, attribute,
                                   expected_value)) {
        raiseEvent();
        observer.disconnect();
        delete observer;
        return;
      }
    }
  }

  /* Calls raiseEvent if the expected node exists in the DOM.
   *
   * Args:
   *  mutations: A list of mutation objects.
   *  observer: The mutation observer object associated with this callback.
   */
  function existsNodeCallback(mutations, observer) {
    if (findNodeMatchingXPathAndValue(xpath, attribute, expected_value)) {
      raiseEvent();
      observer.disconnect();
      delete observer;
      return;
    }
  }

  /* Return true if the xpath matches the given node.
   *
   * Args:
   *  node: A node object from the DOM.
   *  xpath: An XPath used to compare with the DOM node.
   */
  function xpathMatchesNode(node, xpath) {
    var con = document.evaluate(xpath, document, null,
        XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
    var thisNode = con.iterateNext();
    while (thisNode) {
      if (node == thisNode) {
        return true;
      }
      thisNode = con.iterateNext();
    }
    return false;
  }

  /* Returns the first node in the DOM that matches the xpath.
   *
   * Args:
   *  xpath: XPath used to specify the DOM node of interest.
   */
  function firstXPathNode(xpath) {
    return document.evaluate(xpath, document, null,
        XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  }

  /* Returns the first node in the DOM that matches the xpath.
   *
   * Args:
   *  xpath: XPath used to specify the DOM node of interest.
   *  attribute: The attribute to match |expected_value| against.
   *  expected_value: A regular expression to match with the node's
   *      |attribute|. If null the match always succeeds.
   */
  function findNodeMatchingXPathAndValue(xpath, attribute, expected_value) {
    var nodes = document.evaluate(xpath, document, null,
                                  XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
    var node;
    while ( (node = nodes.iterateNext()) ) {
      if (nodeAttributeValueEquals(node, attribute, expected_value))
        return node;
    }
    return null;
  }

  /* Returns true if the node's |attribute| value is matched by the regular
   * expression |expected_value|, false otherwise.
   *
   * Args:
   *  node: A node object from the DOM.
   *  attribute: The attribute to match |expected_value| against.
   *  expected_value: A regular expression to match with the node's
   *      |attribute|. If null the test always passes.
   */
  function nodeAttributeValueEquals(node, attribute, expected_value) {
    return expected_value == null ||
        (node[attribute] && RegExp(expected_value, "").test(node[attribute]));
  }

  /* Watch for a node matching xpath to be added to the DOM.
   *
   * Args:
   *  xpath: XPath used to specify the DOM node of interest.
   */
  function observeAdd(xpath) {
    window.domAutomationController.send("success");
    if (findNodeMatchingXPathAndValue(xpath, attribute, expected_value)) {
      raiseEvent();
      console.log("Matching node in DOM, assuming it was previously added.");
      return;
    }

    var obs = new MutationObserver(addNodeCallback);
    obs.observe(document,
        { childList: true,
          attributes: true,
          characterData: true,
          subtree: true});
  }

  /* Watch for a node matching xpath to be removed from the DOM.
   *
   * Args:
   *  xpath: XPath used to specify the DOM node of interest.
   */
  function observeRemove(xpath) {
    window.domAutomationController.send("success");
    if (!firstXPathNode(xpath)) {
      raiseEvent();
      console.log("No matching node in DOM, assuming it was already removed.");
      return;
    }

    var obs = new MutationObserver(removeNodeCallback);
    obs.observe(document,
        { childList: true,
          attributes: true,
          subtree: true});
  }

  /* Watch for the textContent of a node matching xpath to change to
   * expected_value.
   *
   * Args:
   *  xpath: XPath used to specify the DOM node of interest.
   */
  function observeChange(xpath) {
    var node = firstXPathNode(xpath);
    if (!node) {
      console.log("No matching node in DOM.");
      window.domAutomationController.send(
          "No DOM node matching xpath exists.");
      return;
    }
    window.domAutomationController.send("success");

    var obs = new MutationObserver(changeNodeCallback);
    obs.observe(node,
        { childList: true,
          attributes: true,
          characterData: true,
          subtree: true});
  }

  /* Watch for a node matching xpath to exist in the DOM.
   *
   * Args:
   *  xpath: XPath used to specify the DOM node of interest.
   */
  function observeExists(xpath) {
    window.domAutomationController.send("success");
    if (findNodeMatchingXPathAndValue(xpath, attribute, expected_value)) {
      raiseEvent();
      console.log("Node already exists in DOM.");
      return;
    }

    var obs = new MutationObserver(existsNodeCallback);
    obs.observe(document,
        { childList: true,
          attributes: true,
          characterData: true,
          subtree: true});
  }

  /* Interpret arguments and launch the requested observer function. */
  function installMutationObserver() {
    switch (observer_type) {
      case "add":
        observeAdd(xpath);
        break;
      case "remove":
        observeRemove(xpath);
        break;
      case "change":
        observeChange(xpath);
        break;
      case "exists":
        observeExists(xpath);
        break;
    }
    console.log("MutationObserver javscript injection completed.");
  }

  /* Ensure the DOM is loaded before attempting to create MutationObservers. */
  if (document.body) {
    installMutationObserver();
  } else {
    window.addEventListener("DOMContentLoaded", installMutationObserver, true);
  }
}