OLD | NEW |
| (Empty) |
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 } | |
OLD | NEW |