Path: blob/trunk/javascript/atoms/locators/xpath.js
2884 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('goog.array');39goog.require('goog.dom');40goog.require('goog.dom.NodeType');41goog.require('goog.userAgent');42goog.require('goog.userAgent.product');4344/**45* XPathResult enum values. These are defined separately since46* the context running this script may not support the XPathResult47* type.48* @enum {number}49* @see http://www.w3.org/TR/DOM-Level-3-XPath/xpath.html#XPathResult50* @private51*/52// TODO: Move this enum back to bot.locators.xpath namespace.53// The problem is that we alias bot.locators.xpath in locators.js, while54// we set the flag --collapse_properties (http://goo.gl/5W6cP).55// The compiler should have thrown the error anyways, it's a bug that it fails56// only when introducing this enum.57// Solution: remove --collapse_properties from the js_binary rule or58// use goog.exportSymbol to export the public methods and get rid of the alias.59bot.locators.XPathResult_ = {60ORDERED_NODE_SNAPSHOT_TYPE: 7,61FIRST_ORDERED_NODE_TYPE: 962};636465/**66* Default XPath namespace resolver.67* @private68*/69bot.locators.xpath.DEFAULT_RESOLVER_ = (function () {70var namespaces = { svg: 'http://www.w3.org/2000/svg' };71return function (prefix) {72return namespaces[prefix] || null;73};74})();757677/**78* Evaluates an XPath expression using a W3 XPathEvaluator.79* @param {!(Document|Element)} node The document or element to perform the80* search under.81* @param {string} path The xpath to search for.82* @param {!bot.locators.XPathResult_} resultType The desired result type.83* @return {XPathResult} The XPathResult or null if the root's ownerDocument84* does not support XPathEvaluators.85* @private86* @see http://www.w3.org/TR/DOM-Level-3-XPath/xpath.html#XPathEvaluator-evaluate87*/88bot.locators.xpath.evaluate_ = function (node, path, resultType) {89var doc = goog.dom.getOwnerDocument(node);9091if (!doc.documentElement) {92// document is not loaded yet93return null;94}9596try {97var resolver = doc.createNSResolver ?98doc.createNSResolver(doc.documentElement) :99bot.locators.xpath.DEFAULT_RESOLVER_;100101if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(7)) {102// IE6, and only IE6, has an issue where calling a custom function103// directly attached to the document object does not correctly propagate104// thrown errors. So in that case *only* we will use apply().105return doc.evaluate.call(doc, path, node, resolver, resultType, null);106107} else {108if (!goog.userAgent.IE || goog.userAgent.isDocumentModeOrHigher(9)) {109var reversedNamespaces = {};110var allNodes = doc.getElementsByTagName("*");111for (var i = 0; i < allNodes.length; ++i) {112var n = allNodes[i];113var ns = n.namespaceURI;114if (ns && !reversedNamespaces[ns]) {115var prefix = n.lookupPrefix(ns);116if (!prefix) {117var m = ns.match('.*/(\\w+)/?$');118if (m) {119prefix = m[1];120} else {121prefix = 'xhtml';122}123}124reversedNamespaces[ns] = prefix;125}126}127var namespaces = {};128for (var key in reversedNamespaces) {129namespaces[reversedNamespaces[key]] = key;130}131resolver = function (prefix) {132return namespaces[prefix] || null;133};134}135136try {137return doc.evaluate(path, node, resolver, resultType, null);138} catch (te) {139if (te.name === 'TypeError') {140// fallback to simplified implementation141resolver = doc.createNSResolver ?142doc.createNSResolver(doc.documentElement) :143bot.locators.xpath.DEFAULT_RESOLVER_;144return doc.evaluate(path, node, resolver, resultType, null);145} else {146throw te;147}148}149}150} catch (ex) {151// The Firefox XPath evaluator can throw an exception if the document is152// queried while it's in the midst of reloading, so we ignore it. In all153// other cases, we assume an invalid xpath has caused the exception.154if (!(goog.userAgent.GECKO && ex.name == 'NS_ERROR_ILLEGAL_VALUE')) {155throw new bot.Error(bot.ErrorCode.INVALID_SELECTOR_ERROR,156'Unable to locate an element with the xpath expression ' + path +157' because of the following error:\n' + ex);158}159}160};161162163/**164* @param {Node|undefined} node Node to check whether it is an Element.165* @param {string} path XPath expression to include in the error message.166* @private167*/168bot.locators.xpath.checkElement_ = function (node, path) {169if (!node || node.nodeType != goog.dom.NodeType.ELEMENT) {170throw new bot.Error(bot.ErrorCode.INVALID_SELECTOR_ERROR,171'The result of the xpath expression "' + path +172'" is: ' + node + '. It should be an element.');173}174};175176177/**178* Find an element by using an xpath expression179* @param {string} target The xpath to search for.180* @param {!(Document|Element)} root The document or element to perform the181* search under.182* @return {Element} The first matching element found in the DOM, or null if no183* such element could be found.184*/185bot.locators.xpath.single = function (target, root) {186187function selectSingleNode() {188var result = bot.locators.xpath.evaluate_(root, target,189bot.locators.XPathResult_.FIRST_ORDERED_NODE_TYPE);190191if (result) {192var node = result.singleNodeValue;193return node || null;194} else if (root.selectSingleNode) {195var doc = goog.dom.getOwnerDocument(root);196if (doc.setProperty) {197doc.setProperty('SelectionLanguage', 'XPath');198}199return root.selectSingleNode(target);200}201return null;202}203204var node = selectSingleNode();205if (!goog.isNull(node)) {206bot.locators.xpath.checkElement_(node, target);207}208return /** @type {Element} */ (node);209};210211212/**213* Find elements by using an xpath expression214* @param {string} target The xpath to search for.215* @param {!(Document|Element)} root The document or element to perform the216* search under.217* @return {!IArrayLike} All matching elements, or an empty list.218*/219bot.locators.xpath.many = function (target, root) {220221function selectNodes() {222var result = bot.locators.xpath.evaluate_(root, target,223bot.locators.XPathResult_.ORDERED_NODE_SNAPSHOT_TYPE);224if (result) {225var count = result.snapshotLength;226var results = [];227for (var i = 0; i < count; ++i) {228results.push(result.snapshotItem(i));229}230return results;231} else if (root.selectNodes) {232var doc = goog.dom.getOwnerDocument(root);233if (doc.setProperty) {234doc.setProperty('SelectionLanguage', 'XPath');235}236return root.selectNodes(target);237}238return [];239}240241var nodes = selectNodes();242goog.array.forEach(nodes, function (n) {243bot.locators.xpath.checkElement_(n, target);244});245return /** @type {!IArrayLike} */ (nodes);246};247248249