blob: ced5ec3f026615374fbb280541b2e8e9b3b3d902 [file] [log] [blame]
/* 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);
}
}