Home | History | Annotate | Download | only in pyautolib
      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