blob: c67c077cd4bfa0b99029446f7cc18d4bd86ceaaf [file] [log] [blame] [edit]
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
/**
* @fileoverview Functions to locate elements by XPath.
*
* <p>The locator implementations below differ from the Closure functions
* goog.dom.xml.{selectSingleNode,selectNodes} in three important ways:
* <ol>
* <li>they do not refer to "document" which is undefined in the context of a
* Firefox extension;
* <li> they use a default NsResolver for browsers that do not provide
* document.createNSResolver (e.g. Android); and
* <li> they prefer document.evaluate to node.{selectSingleNode,selectNodes}
* because the latter silently return nothing when the xpath resolves to a
* non-Node type, limiting the error-checking the implementation can provide.
* </ol>
*/
goog.provide('bot.locators.xpath');
goog.require('bot');
goog.require('bot.Error');
goog.require('bot.ErrorCode');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.userAgent');
goog.require('goog.userAgent.product');
/**
* XPathResult enum values. These are defined separately since
* the context running this script may not support the XPathResult
* type.
* @enum {number}
* @see http://www.w3.org/TR/DOM-Level-3-XPath/xpath.html#XPathResult
* @private
*/
// TODO: Move this enum back to bot.locators.xpath namespace.
// The problem is that we alias bot.locators.xpath in locators.js, while
// we set the flag --collapse_properties (http://goo.gl/5W6cP).
// The compiler should have thrown the error anyways, it's a bug that it fails
// only when introducing this enum.
// Solution: remove --collapse_properties from the js_binary rule or
// use goog.exportSymbol to export the public methods and get rid of the alias.
bot.locators.XPathResult_ = {
ORDERED_NODE_SNAPSHOT_TYPE: 7,
FIRST_ORDERED_NODE_TYPE: 9
};
/**
* Default XPath namespace resolver.
* @private
*/
bot.locators.xpath.DEFAULT_RESOLVER_ = (function () {
var namespaces = { svg: 'http://www.w3.org/2000/svg' };
return function (prefix) {
return namespaces[prefix] || null;
};
})();
/**
* Evaluates an XPath expression using a W3 XPathEvaluator.
* @param {!(Document|Element)} node The document or element to perform the
* search under.
* @param {string} path The xpath to search for.
* @param {!bot.locators.XPathResult_} resultType The desired result type.
* @return {XPathResult} The XPathResult or null if the root's ownerDocument
* does not support XPathEvaluators.
* @private
* @see http://www.w3.org/TR/DOM-Level-3-XPath/xpath.html#XPathEvaluator-evaluate
*/
bot.locators.xpath.evaluate_ = function (node, path, resultType) {
var doc = goog.dom.getOwnerDocument(node);
if (!doc.documentElement) {
// document is not loaded yet
return null;
}
try {
var resolver = doc.createNSResolver ?
doc.createNSResolver(doc.documentElement) :
bot.locators.xpath.DEFAULT_RESOLVER_;
if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(7)) {
// IE6, and only IE6, has an issue where calling a custom function
// directly attached to the document object does not correctly propagate
// thrown errors. So in that case *only* we will use apply().
return doc.evaluate.call(doc, path, node, resolver, resultType, null);
} else {
if (!goog.userAgent.IE || goog.userAgent.isDocumentModeOrHigher(9)) {
var reversedNamespaces = {};
var allNodes = doc.getElementsByTagName("*");
for (var i = 0; i < allNodes.length; ++i) {
var n = allNodes[i];
var ns = n.namespaceURI;
if (ns && !reversedNamespaces[ns]) {
var prefix = n.lookupPrefix(ns);
if (!prefix) {
var m = ns.match('.*/(\\w+)/?$');
if (m) {
prefix = m[1];
} else {
prefix = 'xhtml';
}
}
reversedNamespaces[ns] = prefix;
}
}
var namespaces = {};
for (var key in reversedNamespaces) {
namespaces[reversedNamespaces[key]] = key;
}
resolver = function (prefix) {
return namespaces[prefix] || null;
};
}
try {
return doc.evaluate(path, node, resolver, resultType, null);
} catch (te) {
if (te.name === 'TypeError') {
// fallback to simplified implementation
resolver = doc.createNSResolver ?
doc.createNSResolver(doc.documentElement) :
bot.locators.xpath.DEFAULT_RESOLVER_;
return doc.evaluate(path, node, resolver, resultType, null);
} else {
throw te;
}
}
}
} catch (ex) {
// The Firefox XPath evaluator can throw an exception if the document is
// queried while it's in the midst of reloading, so we ignore it. In all
// other cases, we assume an invalid xpath has caused the exception.
if (!(goog.userAgent.GECKO && ex.name == 'NS_ERROR_ILLEGAL_VALUE')) {
throw new bot.Error(bot.ErrorCode.INVALID_SELECTOR_ERROR,
'Unable to locate an element with the xpath expression ' + path +
' because of the following error:\n' + ex);
}
}
};
/**
* @param {Node|undefined} node Node to check whether it is an Element.
* @param {string} path XPath expression to include in the error message.
* @private
*/
bot.locators.xpath.checkElement_ = function (node, path) {
if (!node || node.nodeType != goog.dom.NodeType.ELEMENT) {
throw new bot.Error(bot.ErrorCode.INVALID_SELECTOR_ERROR,
'The result of the xpath expression "' + path +
'" is: ' + node + '. It should be an element.');
}
};
/**
* Find an element by using an xpath expression
* @param {string} target The xpath to search for.
* @param {!(Document|Element)} root The document or element to perform the
* search under.
* @return {Element} The first matching element found in the DOM, or null if no
* such element could be found.
*/
bot.locators.xpath.single = function (target, root) {
function selectSingleNode() {
var result = bot.locators.xpath.evaluate_(root, target,
bot.locators.XPathResult_.FIRST_ORDERED_NODE_TYPE);
if (result) {
var node = result.singleNodeValue;
return node || null;
} else if (root.selectSingleNode) {
var doc = goog.dom.getOwnerDocument(root);
if (doc.setProperty) {
doc.setProperty('SelectionLanguage', 'XPath');
}
return root.selectSingleNode(target);
}
return null;
}
var node = selectSingleNode();
if (!goog.isNull(node)) {
bot.locators.xpath.checkElement_(node, target);
}
return /** @type {Element} */ (node);
};
/**
* Find elements by using an xpath expression
* @param {string} target The xpath to search for.
* @param {!(Document|Element)} root The document or element to perform the
* search under.
* @return {!IArrayLike} All matching elements, or an empty list.
*/
bot.locators.xpath.many = function (target, root) {
function selectNodes() {
var result = bot.locators.xpath.evaluate_(root, target,
bot.locators.XPathResult_.ORDERED_NODE_SNAPSHOT_TYPE);
if (result) {
var count = result.snapshotLength;
var results = [];
for (var i = 0; i < count; ++i) {
results.push(result.snapshotItem(i));
}
return results;
} else if (root.selectNodes) {
var doc = goog.dom.getOwnerDocument(root);
if (doc.setProperty) {
doc.setProperty('SelectionLanguage', 'XPath');
}
return root.selectNodes(target);
}
return [];
}
var nodes = selectNodes();
goog.array.forEach(nodes, function (n) {
bot.locators.xpath.checkElement_(n, target);
});
return /** @type {!IArrayLike} */ (nodes);
};