// 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. * *

The locator implementations below differ from the Closure functions * goog.dom.xml.{selectSingleNode,selectNodes} in three important ways: *

    *
  1. they do not refer to "document" which is undefined in the context of a * Firefox extension; *
  2. they use a default NsResolver for browsers that do not provide * document.createNSResolver (e.g. Android); and *
  3. 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. *
*/ goog.provide('bot.locators.xpath'); goog.require('bot'); goog.require('bot.Error'); goog.require('bot.ErrorCode'); goog.require('bot.locators'); 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 (node !== null) { 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); };