Path: blob/trunk/javascript/atoms/locators/xpath.js
3985 views
// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617/**18* @fileoverview Functions to locate elements by XPath.19*20* <p>The locator implementations below differ from the Closure functions21* goog.dom.xml.{selectSingleNode,selectNodes} in three important ways:22* <ol>23* <li>they do not refer to "document" which is undefined in the context of a24* Firefox extension;25* <li> they use a default NsResolver for browsers that do not provide26* document.createNSResolver (e.g. Android); and27* <li> they prefer document.evaluate to node.{selectSingleNode,selectNodes}28* because the latter silently return nothing when the xpath resolves to a29* non-Node type, limiting the error-checking the implementation can provide.30* </ol>31*/3233goog.provide('bot.locators.xpath');3435goog.require('bot');36goog.require('bot.Error');37goog.require('bot.ErrorCode');38goog.require('bot.locators');39goog.require('goog.array');40goog.require('goog.dom');41goog.require('goog.dom.NodeType');42goog.require('goog.userAgent');43goog.require('goog.userAgent.product');4445/**46* XPathResult enum values. These are defined separately since47* the context running this script may not support the XPathResult48* type.49* @enum {number}50* @see http://www.w3.org/TR/DOM-Level-3-XPath/xpath.html#XPathResult51* @private52*/53// TODO: Move this enum back to bot.locators.xpath namespace.54// The problem is that we alias bot.locators.xpath in locators.js, while55// we set the flag --collapse_properties (http://goo.gl/5W6cP).56// The compiler should have thrown the error anyways, it's a bug that it fails57// only when introducing this enum.58// Solution: remove --collapse_properties from the js_binary rule or59// use goog.exportSymbol to export the public methods and get rid of the alias.60bot.locators.XPathResult_ = {61ORDERED_NODE_SNAPSHOT_TYPE: 7,62FIRST_ORDERED_NODE_TYPE: 963};646566/**67* Default XPath namespace resolver.68* @private69*/70bot.locators.xpath.DEFAULT_RESOLVER_ = (function () {71var namespaces = { svg: 'http://www.w3.org/2000/svg' };72return function (prefix) {73return namespaces[prefix] || null;74};75})();767778/**79* Evaluates an XPath expression using a W3 XPathEvaluator.80* @param {!(Document|Element)} node The document or element to perform the81* search under.82* @param {string} path The xpath to search for.83* @param {!bot.locators.XPathResult_} resultType The desired result type.84* @return {XPathResult} The XPathResult or null if the root's ownerDocument85* does not support XPathEvaluators.86* @private87* @see http://www.w3.org/TR/DOM-Level-3-XPath/xpath.html#XPathEvaluator-evaluate88*/89bot.locators.xpath.evaluate_ = function (node, path, resultType) {90var doc = goog.dom.getOwnerDocument(node);9192if (!doc.documentElement) {93// document is not loaded yet94return null;95}9697try {98var resolver = doc.createNSResolver ?99doc.createNSResolver(doc.documentElement) :100bot.locators.xpath.DEFAULT_RESOLVER_;101102if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(7)) {103// IE6, and only IE6, has an issue where calling a custom function104// directly attached to the document object does not correctly propagate105// thrown errors. So in that case *only* we will use apply().106return doc.evaluate.call(doc, path, node, resolver, resultType, null);107108} else {109if (!goog.userAgent.IE || goog.userAgent.isDocumentModeOrHigher(9)) {110var reversedNamespaces = {};111var allNodes = doc.getElementsByTagName("*");112for (var i = 0; i < allNodes.length; ++i) {113var n = allNodes[i];114var ns = n.namespaceURI;115if (ns && !reversedNamespaces[ns]) {116var prefix = n.lookupPrefix(ns);117if (!prefix) {118var m = ns.match('.*/(\\w+)/?$');119if (m) {120prefix = m[1];121} else {122prefix = 'xhtml';123}124}125reversedNamespaces[ns] = prefix;126}127}128var namespaces = {};129for (var key in reversedNamespaces) {130namespaces[reversedNamespaces[key]] = key;131}132resolver = function (prefix) {133return namespaces[prefix] || null;134};135}136137try {138return doc.evaluate(path, node, resolver, resultType, null);139} catch (te) {140if (te.name === 'TypeError') {141// fallback to simplified implementation142resolver = doc.createNSResolver ?143doc.createNSResolver(doc.documentElement) :144bot.locators.xpath.DEFAULT_RESOLVER_;145return doc.evaluate(path, node, resolver, resultType, null);146} else {147throw te;148}149}150}151} catch (ex) {152// The Firefox XPath evaluator can throw an exception if the document is153// queried while it's in the midst of reloading, so we ignore it. In all154// other cases, we assume an invalid xpath has caused the exception.155if (!(goog.userAgent.GECKO && ex.name == 'NS_ERROR_ILLEGAL_VALUE')) {156throw new bot.Error(bot.ErrorCode.INVALID_SELECTOR_ERROR,157'Unable to locate an element with the xpath expression ' + path +158' because of the following error:\n' + ex);159}160}161};162163164/**165* @param {Node|undefined} node Node to check whether it is an Element.166* @param {string} path XPath expression to include in the error message.167* @private168*/169bot.locators.xpath.checkElement_ = function (node, path) {170if (!node || node.nodeType != goog.dom.NodeType.ELEMENT) {171throw new bot.Error(bot.ErrorCode.INVALID_SELECTOR_ERROR,172'The result of the xpath expression "' + path +173'" is: ' + node + '. It should be an element.');174}175};176177178/**179* Find an element by using an xpath expression180* @param {string} target The xpath to search for.181* @param {!(Document|Element)} root The document or element to perform the182* search under.183* @return {Element} The first matching element found in the DOM, or null if no184* such element could be found.185*/186bot.locators.xpath.single = function (target, root) {187188function selectSingleNode() {189var result = bot.locators.xpath.evaluate_(root, target,190bot.locators.XPathResult_.FIRST_ORDERED_NODE_TYPE);191192if (result) {193var node = result.singleNodeValue;194return node || null;195} else if (root.selectSingleNode) {196var doc = goog.dom.getOwnerDocument(root);197if (doc.setProperty) {198doc.setProperty('SelectionLanguage', 'XPath');199}200return root.selectSingleNode(target);201}202return null;203}204205var node = selectSingleNode();206if (node !== null) {207bot.locators.xpath.checkElement_(node, target);208}209return /** @type {Element} */ (node);210};211212213/**214* Find elements by using an xpath expression215* @param {string} target The xpath to search for.216* @param {!(Document|Element)} root The document or element to perform the217* search under.218* @return {!IArrayLike} All matching elements, or an empty list.219*/220bot.locators.xpath.many = function (target, root) {221222function selectNodes() {223var result = bot.locators.xpath.evaluate_(root, target,224bot.locators.XPathResult_.ORDERED_NODE_SNAPSHOT_TYPE);225if (result) {226var count = result.snapshotLength;227var results = [];228for (var i = 0; i < count; ++i) {229results.push(result.snapshotItem(i));230}231return results;232} else if (root.selectNodes) {233var doc = goog.dom.getOwnerDocument(root);234if (doc.setProperty) {235doc.setProperty('SelectionLanguage', 'XPath');236}237return root.selectNodes(target);238}239return [];240}241242var nodes = selectNodes();243goog.array.forEach(nodes, function (n) {244bot.locators.xpath.checkElement_(n, target);245});246return /** @type {!IArrayLike} */ (nodes);247};248249250