| /* 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); |
| } |
| } |