common/src/js/core/scripts/htmlutils.js in selenium-webdriver-0.0.14 vs common/src/js/core/scripts/htmlutils.js in selenium-webdriver-0.0.15

- old
+ new

@@ -1071,10 +1071,398 @@ } return { type: 'implicit', string: locator }; } /** + * An interface definition for XPath engine implementations; an instance of + * XPathEngine should be their prototype. Sub-implementations need only define + * overrides of the methods provided here. + */ +function XPathEngine() { +// public + this.doc = null; + + /** + * Returns whether the current runtime environment supports the use of this + * engine. Needs override. + */ + this.isAvailable = function() { return false; }; + + /** + * Sets the document to be used for evaluation. Always returns the current + * engine object so as to be chainable. + */ + this.setDocument = function(newDocument) { + this.doc = newDocument; + return this; + }; + + /** + * Returns a possibly-empty list of nodes. Needs override. + */ + this.selectNodes = function(xpath, contextNode, namespaceResolver) { + return []; + }; + + /** + * Returns a single node, or null if no nodes were selected. This default + * implementation simply returns the first result of selectNodes(), or + * null. + */ + this.selectSingleNode = function(xpath, contextNode, namespaceResolver) { + var nodes = this.selectNodes(xpath, contextNode, namespaceResolver); + return (nodes.length > 0 ? nodes[0] : null); + }; + + /** + * Returns the number of matching nodes. This default implementation simply + * returns the length of the result of selectNodes(), which should be + * adequate for most sub-implementations. + */ + this.countNodes = function(xpath, contextNode, namespaceResolver) { + return this.selectNodes(xpath, contextNode, namespaceResolver).length; + }; + + /** + * An optimization; likely to be a no-op for many implementations. Always + * returns the current engine object so as to be chainable. + */ + this.setIgnoreAttributesWithoutValue = function(ignore) { return this; }; +} + +/** + * Implements XPathEngine. + */ +function NativeEngine() { +// public + // Override + this.isAvailable = function() { + if (browserVersion && browserVersion.isIE) { + // javascript-xpath can fake out the check otherwise + return false; + } + + return this.doc && this.doc.evaluate; + }; + + // Override + this.selectNodes = function(xpath, contextNode, namespaceResolver) { + if (contextNode != this.doc) { + xpath = '.' + xpath; + } + + var nodes = []; + + try { + var xpathResult = this.doc.evaluate(xpath, contextNode, + namespaceResolver, 0, null); + } + catch (e) { + var msg = extractExceptionMessage(e); + throw new SeleniumError("Invalid xpath [1]: " + msg); + } + finally { + if (xpathResult == null) { + // If the result is null, we should still throw an Error. + throw new SeleniumError("Invalid xpath [2]: " + xpath); + } + } + + var node = xpathResult.iterateNext(); + + while (node) { + nodes.push(node); + node = xpathResult.iterateNext(); + } + + return nodes; + }; +} + +NativeEngine.prototype = new XPathEngine(); + +/** + * Implements XPathEngine. + */ +function AjaxsltEngine() { +// private + var ignoreAttributesWithoutValue = false; + + function selectLogic(xpath, contextNode, namespaceResolver, firstMatch) { + // DGF set xpathdebug = true (using getEval, if you like) to turn on JS + // XPath debugging + //xpathdebug = true; + var context; + + if (contextNode == this.doc) { + context = new ExprContext(this.doc); + } + else { + // provide false values to get the default constructor values + context = new ExprContext(contextNode, false, false, + contextNode.parentNode); + } + + context.setCaseInsensitive(true); + context.setIgnoreAttributesWithoutValue(ignoreAttributesWithoutValue); + context.setReturnOnFirstMatch(firstMatch); + + try { + var xpathObj = xpathParse(xpath); + } + catch (e) { + var msg = extractExceptionMessage(e); + throw new SeleniumError("Invalid xpath [3]: " + msg); + } + + var nodes = [] + var xpathResult = xpathObj.evaluate(context); + + if (xpathResult && xpathResult.value) { + for (var i = 0; i < xpathResult.value.length; ++i) { + nodes.push(xpathResult.value[i]); + } + } + + return nodes; + } + +// public + // Override + this.isAvailable = function() { return true; }; + + // Override + this.selectNodes = function(xpath, contextNode, namespaceResolver) { + return selectLogic(xpath, contextNode, namespaceResolver, false); + }; + + // Override + this.selectSingleNode = function(xpath, contextNode, namespaceResolver) { + var nodes = selectLogic(xpath, contextNode, namespaceResolver, true); + return (nodes.length > 0 ? nodes[0] : null); + }; + + // Override + this.setIgnoreAttributesWithoutValue = function(ignore) { + ignoreAttributesWithoutValue = ignore; + return this; + }; +} + +AjaxsltEngine.prototype = new XPathEngine(); + +/** + * Implements XPathEngine. + */ +function JavascriptXPathEngine() { +// private + var engineDoc = document; + +// public + // Override + this.isAvailable = function() { return true; }; + + // Override + this.selectNodes = function(xpath, contextNode, namespaceResolver) { + if (contextNode != this.doc) { + // Regarding use of the second argument to document.evaluate(): + // http://groups.google.com/group/comp.lang.javascript/browse_thread/thread/a59ce20639c74ba1/a9d9f53e88e5ebb5 + xpath = '.' + xpath; + } + + var nodes = []; + + try { + // When using the new and faster javascript-xpath library, we'll + // use the TestRunner's document object, not the App-Under-Test's + // document. The new library only modifies the TestRunner document + // with the new functionality. + var xpathResult = engineDoc.evaluate(xpath, contextNode, + namespaceResolver, 0, null); + } + catch (e) { + var msg = extractExceptionMessage(e); + throw new SeleniumError("Invalid xpath [1]: " + msg); + } + finally { + if (xpathResult == null) { + // If the result is null, we should still throw an Error. + throw new SeleniumError("Invalid xpath [2]: " + xpath); + } + } + + var node = xpathResult.iterateNext(); + + while (node) { + nodes.push(node); + node = xpathResult.iterateNext(); + } + + return nodes; + }; +} + +JavascriptXPathEngine.prototype = new XPathEngine(); + +/** + * An object responsible for handling XPath logic. New XPath engines can be + * registered to this evaluator on the fly. + * + * @param newDefaultEngineName the name of the default XPath engine. Must be + * a non-native engine that is always available. + * Defaults to 'ajaxslt'. + */ +function XPathEvaluator(newDefaultEngineName) { +// private + var nativeEngine = new NativeEngine(); + var defaultEngineName = newDefaultEngineName || 'ajaxslt'; + var engines = { + 'ajaxslt' : new AjaxsltEngine(), + 'javascript-xpath': new JavascriptXPathEngine(), + 'native' : nativeEngine + }; + + var currentEngineName = defaultEngineName; + var allowNativeXPath = true; + var ignoreAttributesWithoutValue = true; + + function preprocess(xpath) { + // Trim any trailing "/": not valid xpath, and remains from attribute + // locator. + if (xpath.charAt(xpath.length - 1) == '/') { + xpath = xpath.slice(0, -1); + } + // HUGE hack - remove namespace from xpath for IE + if (browserVersion && browserVersion.isIE) { + xpath = xpath.replace(/x:/g, '') + } + + return xpath; + } + + /** + * Returns the most sensible engine given the settings and the document + * object. + */ + function getEngineFor(inDocument) { + if (allowNativeXPath && + nativeEngine.setDocument(inDocument).isAvailable()) { + return nativeEngine; + } + + var currentEngine = engines[currentEngineName]; + + if (currentEngine && + currentEngine.setDocument(inDocument).isAvailable()) { + return currentEngine; + } + + return engines[defaultEngineName].setDocument(inDocument); + } + + /** + * Dispatches an XPath evaluation method on the relevant engine for the + * given document, and returns the result + */ + function dispatch(methodName, inDocument, xpath, contextNode, namespaceResolver) { + xpath = preprocess(xpath); + + if (! contextNode) { + contextNode = inDocument; + } + + return getEngineFor(inDocument) + .setIgnoreAttributesWithoutValue(ignoreAttributesWithoutValue) + [methodName](xpath, contextNode, namespaceResolver); + } + +// public + /** + * Registers a new engine by name, and returns whether the registration was + * successful. Each registered engine must be an instance of XPathEngine. + * The engines registered by default - "ajaxslt", "javascript-xpath", + * "native", and "default" - can't be overwritten. + */ + this.registerEngine = function(name, engine) { + // can't overwrite one of these + if (name == 'ajaxslt' || + name == 'javascript-xpath' || + name == 'native' || + name == 'default') { + return false; + } + + if (! (engine instanceof XPathEngine)) { + return false; + } + + engines[name] = engine; + return true; + } + + this.getRegisteredEngine = function(name) { + return engines[name]; + }; + + this.setCurrentEngine = function(name) { + if (name == 'default') { + currentEngineName = defaultEngineName; + } + else if (! engines[name]) { + return; + } + else { + currentEngineName = name; + } + }; + + this.getCurrentEngine = function() { + return currentEngineName || defaultEngineName; + }; + + this.setAllowNativeXPath = function(allow) { + allowNativeXPath = allow; + } + + this.isAllowNativeXPath = function() { + return allowNativeXPath; + } + + this.setIgnoreAttributesWithoutValue = function(ignore) { + ignoreAttributesWithoutValue = ignore; + }; + + this.isIgnoreAttributesWithoutValue = function() { + return ignoreAttributesWithoutValue; + }; + + this.selectNodes = function(inDocument, xpath, contextNode, namespaceResolver) { + return dispatch('selectNodes', inDocument, xpath, contextNode, + namespaceResolver); + }; + + this.selectSingleNode = function(inDocument, xpath, contextNode, namespaceResolver) { + return dispatch('selectSingleNode', inDocument, xpath, contextNode, + namespaceResolver); + }; + + this.countNodes = function(inDocument, xpath, contextNode, namespaceResolver) { + return dispatch('countNodes', inDocument, xpath, contextNode, + namespaceResolver); + }; + +// initialization + this.init(); +}; + +/** + * Gives the user an overridable hook for registering new XPath engines, for + * example from user extensions. + */ +XPathEvaluator.prototype.init = function() {}; + +/** * Evaluates an xpath on a document, and returns a list containing nodes in the * resulting nodeset. The browserbot xpath methods are now backed by this * function. A context node may optionally be provided, and the xpath will be * evaluated from that context. * @@ -1112,13 +1500,14 @@ * return the first match. The match, if any, will still * be returned in a list. Defaults to false. */ function eval_xpath(xpath, inDocument, opts) { - if (!opts) { + if (! opts) { var opts = {}; } + var contextNode = opts.contextNode ? opts.contextNode : inDocument; var namespaceResolver = opts.namespaceResolver ? opts.namespaceResolver : null; var xpathLibrary = opts.xpathLibrary @@ -1128,90 +1517,29 @@ var ignoreAttributesWithoutValue = (opts.ignoreAttributesWithoutValue != undefined) ? opts.ignoreAttributesWithoutValue : true; var returnOnFirstMatch = (opts.returnOnFirstMatch != undefined) ? opts.returnOnFirstMatch : false; - // Trim any trailing "/": not valid xpath, and remains from attribute - // locator. - if (xpath.charAt(xpath.length - 1) == '/') { - xpath = xpath.slice(0, -1); + if (! eval_xpath.xpathEvaluator) { + eval_xpath.xpathEvaluator = new XPathEvaluator(); } - // HUGE hack - remove namespace from xpath for IE - if (browserVersion && browserVersion.isIE) { - xpath = xpath.replace(/x:/g, '') - } - var nativeXpathAvailable = inDocument.evaluate; - var useNativeXpath = allowNativeXpath && nativeXpathAvailable; - var useDocumentEvaluate = useNativeXpath; - - // When using the new and faster javascript-xpath library, - // we'll use the TestRunner's document object, not the App-Under-Test's document. - // The new library only modifies the TestRunner document with the new - // functionality. - if (xpathLibrary == 'javascript-xpath' && !useNativeXpath) { - documentForXpath = document; - useDocumentEvaluate = true; - } else { - documentForXpath = inDocument; - } - var results = []; + var xpathEvaluator = eval_xpath.xpathEvaluator; - // this is either native xpath or javascript-xpath via TestRunner.evaluate - if (useDocumentEvaluate) { - try { - // Regarding use of the second argument to document.evaluate(): - // http://groups.google.com/group/comp.lang.javascript/browse_thread/thread/a59ce20639c74ba1/a9d9f53e88e5ebb5 - var xpathResult = documentForXpath - .evaluate((contextNode == inDocument ? xpath : '.' + xpath), - contextNode, namespaceResolver, 0, null); - } - catch (e) { - throw new SeleniumError("Invalid xpath [1]: " + extractExceptionMessage(e)); - } - finally{ - if (xpathResult == null) { - // If the result is null, we should still throw an Error. - throw new SeleniumError("Invalid xpath [2]: " + xpath); - } - } - var result = xpathResult.iterateNext(); - while (result) { - results.push(result); - result = xpathResult.iterateNext(); - } - return results; + xpathEvaluator.setCurrentEngine(xpathLibrary); + xpathEvaluator.setAllowNativeXPath(allowNativeXpath); + xpathEvaluator.setIgnoreAttributesWithoutValue(ignoreAttributesWithoutValue); + + if (returnOnFirstMatch) { + var result = xpathEvaluator.selectSingleNode(inDocument, xpath, + contextNode, namespaceResolver); } - - // If not, fall back to slower JavaScript implementation - // DGF set xpathdebug = true (using getEval, if you like) to turn on JS XPath debugging - //xpathdebug = true; - var context; - if (contextNode == inDocument) { - context = new ExprContext(inDocument); - } else { - // provide false values to get the default constructor values - context = new ExprContext(contextNode, false, false, - contextNode.parentNode); + var result = xpathEvaluator.selectNodes(inDocument, xpath, contextNode, + namespaceResolver); } - context.setCaseInsensitive(true); - context.setIgnoreAttributesWithoutValue(ignoreAttributesWithoutValue); - context.setReturnOnFirstMatch(returnOnFirstMatch); - var xpathObj; - try { - xpathObj = xpathParse(xpath); - } - catch (e) { - throw new SeleniumError("Invalid xpath [3]: " + extractExceptionMessage(e)); - } - var xpathResult = xpathObj.evaluate(context); - if (xpathResult && xpathResult.value) { - for (var i = 0; i < xpathResult.value.length; ++i) { - results.push(xpathResult.value[i]); - } - } - return results; + + return result; } /** * Returns the full resultset of a CSS selector evaluation. */