Allow overlapping sync and async startup requests
[chromium-blink-merge.git] / chrome / test / pyautolib / dom_mutation_observer.js
blobced5ec3f026615374fbb280541b2e8e9b3b3d902
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) {
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   }
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   }
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   }
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   }
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   }
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   }
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   }
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   }
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   }
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     }
178     var obs = new MutationObserver(addNodeCallback);
179     obs.observe(document,
180         { childList: true,
181           attributes: true,
182           characterData: true,
183           subtree: true});
184   }
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     }
199     var obs = new MutationObserver(removeNodeCallback);
200     obs.observe(document,
201         { childList: true,
202           attributes: true,
203           subtree: true});
204   }
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");
222     var obs = new MutationObserver(changeNodeCallback);
223     obs.observe(node,
224         { childList: true,
225           attributes: true,
226           characterData: true,
227           subtree: true});
228   }
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     }
243     var obs = new MutationObserver(existsNodeCallback);
244     obs.observe(document,
245         { childList: true,
246           attributes: true,
247           characterData: true,
248           subtree: true});
249   }
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   }
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   }