1 /* Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 * Use of this source code is governed by a BSD-style license that can be 3 * found in the LICENSE file. 4 * 5 * Helper javascript injected whenever a DomMutationEventObserver is created. 6 * 7 * This script uses MutationObservers to watch for changes to the DOM, then 8 * reports the event to the observer using the DomAutomationController. An 9 * anonymous namespace is used to prevent conflict with other Javascript. 10 * 11 * Args: 12 * automation_id: Automation id used to route DomAutomationController messages. 13 * observer_id: Id of the observer who will be receiving the messages. 14 * observer_type: One of 'add', 'remove', 'change', or 'exists'. 15 * xpath: XPath used to specify the DOM node of interest. 16 * attribute: If |expected_value| is provided, check if this attribute of the 17 * DOM node matches |expected value|. 18 * expected_value: If not null, regular expression to match with the value of 19 * |attribute| after the mutation. 20 */ 21 function(automation_id, observer_id, observer_type, xpath, attribute, 22 expected_value) { 23 24 /* Raise an event for the DomMutationEventObserver. */ 25 function raiseEvent() { 26 if (window.domAutomationController) { 27 console.log("Event sent to DomEventObserver with id=" + 28 observer_id + "."); 29 window.domAutomationController.sendWithId( 30 automation_id, "__dom_mutation_observer__:" + observer_id); 31 } 32 } 33 34 /* Calls raiseEvent if the expected node has been added to the DOM. 35 * 36 * Args: 37 * mutations: A list of mutation objects. 38 * observer: The mutation observer object associated with this callback. 39 */ 40 function addNodeCallback(mutations, observer) { 41 for (var j=0; j<mutations.length; j++) { 42 for (var i=0; i<mutations[j].addedNodes.length; i++) { 43 var node = mutations[j].addedNodes[i]; 44 if (xpathMatchesNode(node, xpath) && 45 nodeAttributeValueEquals(node, attribute, expected_value)) { 46 raiseEvent(); 47 observer.disconnect(); 48 delete observer; 49 return; 50 } 51 } 52 } 53 } 54 55 /* Calls raiseEvent if the expected node has been removed from the DOM. 56 * 57 * Args: 58 * mutations: A list of mutation objects. 59 * observer: The mutation observer object associated with this callback. 60 */ 61 function removeNodeCallback(mutations, observer) { 62 var node = firstXPathNode(xpath); 63 if (!node) { 64 raiseEvent(); 65 observer.disconnect(); 66 delete observer; 67 } 68 } 69 70 /* Calls raiseEvent if the given node has been changed to expected_value. 71 * 72 * Args: 73 * mutations: A list of mutation objects. 74 * observer: The mutation observer object associated with this callback. 75 */ 76 function changeNodeCallback(mutations, observer) { 77 for (var j=0; j<mutations.length; j++) { 78 if (nodeAttributeValueEquals(mutations[j].target, attribute, 79 expected_value)) { 80 raiseEvent(); 81 observer.disconnect(); 82 delete observer; 83 return; 84 } 85 } 86 } 87 88 /* Calls raiseEvent if the expected node exists in the DOM. 89 * 90 * Args: 91 * mutations: A list of mutation objects. 92 * observer: The mutation observer object associated with this callback. 93 */ 94 function existsNodeCallback(mutations, observer) { 95 if (findNodeMatchingXPathAndValue(xpath, attribute, expected_value)) { 96 raiseEvent(); 97 observer.disconnect(); 98 delete observer; 99 return; 100 } 101 } 102 103 /* Return true if the xpath matches the given node. 104 * 105 * Args: 106 * node: A node object from the DOM. 107 * xpath: An XPath used to compare with the DOM node. 108 */ 109 function xpathMatchesNode(node, xpath) { 110 var con = document.evaluate(xpath, document, null, 111 XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null); 112 var thisNode = con.iterateNext(); 113 while (thisNode) { 114 if (node == thisNode) { 115 return true; 116 } 117 thisNode = con.iterateNext(); 118 } 119 return false; 120 } 121 122 /* Returns the first node in the DOM that matches the xpath. 123 * 124 * Args: 125 * xpath: XPath used to specify the DOM node of interest. 126 */ 127 function firstXPathNode(xpath) { 128 return document.evaluate(xpath, document, null, 129 XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 130 } 131 132 /* Returns the first node in the DOM that matches the xpath. 133 * 134 * Args: 135 * xpath: XPath used to specify the DOM node of interest. 136 * attribute: The attribute to match |expected_value| against. 137 * expected_value: A regular expression to match with the node's 138 * |attribute|. If null the match always succeeds. 139 */ 140 function findNodeMatchingXPathAndValue(xpath, attribute, expected_value) { 141 var nodes = document.evaluate(xpath, document, null, 142 XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); 143 var node; 144 while ( (node = nodes.iterateNext()) ) { 145 if (nodeAttributeValueEquals(node, attribute, expected_value)) 146 return node; 147 } 148 return null; 149 } 150 151 /* Returns true if the node's |attribute| value is matched by the regular 152 * expression |expected_value|, false otherwise. 153 * 154 * Args: 155 * node: A node object from the DOM. 156 * attribute: The attribute to match |expected_value| against. 157 * expected_value: A regular expression to match with the node's 158 * |attribute|. If null the test always passes. 159 */ 160 function nodeAttributeValueEquals(node, attribute, expected_value) { 161 return expected_value == null || 162 (node[attribute] && RegExp(expected_value, "").test(node[attribute])); 163 } 164 165 /* Watch for a node matching xpath to be added to the DOM. 166 * 167 * Args: 168 * xpath: XPath used to specify the DOM node of interest. 169 */ 170 function observeAdd(xpath) { 171 window.domAutomationController.send("success"); 172 if (findNodeMatchingXPathAndValue(xpath, attribute, expected_value)) { 173 raiseEvent(); 174 console.log("Matching node in DOM, assuming it was previously added."); 175 return; 176 } 177 178 var obs = new MutationObserver(addNodeCallback); 179 obs.observe(document, 180 { childList: true, 181 attributes: true, 182 characterData: true, 183 subtree: true}); 184 } 185 186 /* Watch for a node matching xpath to be removed from the DOM. 187 * 188 * Args: 189 * xpath: XPath used to specify the DOM node of interest. 190 */ 191 function observeRemove(xpath) { 192 window.domAutomationController.send("success"); 193 if (!firstXPathNode(xpath)) { 194 raiseEvent(); 195 console.log("No matching node in DOM, assuming it was already removed."); 196 return; 197 } 198 199 var obs = new MutationObserver(removeNodeCallback); 200 obs.observe(document, 201 { childList: true, 202 attributes: true, 203 subtree: true}); 204 } 205 206 /* Watch for the textContent of a node matching xpath to change to 207 * expected_value. 208 * 209 * Args: 210 * xpath: XPath used to specify the DOM node of interest. 211 */ 212 function observeChange(xpath) { 213 var node = firstXPathNode(xpath); 214 if (!node) { 215 console.log("No matching node in DOM."); 216 window.domAutomationController.send( 217 "No DOM node matching xpath exists."); 218 return; 219 } 220 window.domAutomationController.send("success"); 221 222 var obs = new MutationObserver(changeNodeCallback); 223 obs.observe(node, 224 { childList: true, 225 attributes: true, 226 characterData: true, 227 subtree: true}); 228 } 229 230 /* Watch for a node matching xpath to exist in the DOM. 231 * 232 * Args: 233 * xpath: XPath used to specify the DOM node of interest. 234 */ 235 function observeExists(xpath) { 236 window.domAutomationController.send("success"); 237 if (findNodeMatchingXPathAndValue(xpath, attribute, expected_value)) { 238 raiseEvent(); 239 console.log("Node already exists in DOM."); 240 return; 241 } 242 243 var obs = new MutationObserver(existsNodeCallback); 244 obs.observe(document, 245 { childList: true, 246 attributes: true, 247 characterData: true, 248 subtree: true}); 249 } 250 251 /* Interpret arguments and launch the requested observer function. */ 252 function installMutationObserver() { 253 switch (observer_type) { 254 case "add": 255 observeAdd(xpath); 256 break; 257 case "remove": 258 observeRemove(xpath); 259 break; 260 case "change": 261 observeChange(xpath); 262 break; 263 case "exists": 264 observeExists(xpath); 265 break; 266 } 267 console.log("MutationObserver javscript injection completed."); 268 } 269 270 /* Ensure the DOM is loaded before attempting to create MutationObservers. */ 271 if (document.body) { 272 installMutationObserver(); 273 } else { 274 window.addEventListener("DOMContentLoaded", installMutationObserver, true); 275 } 276 } 277