/* * Copyright 2004 ThoughtWorks, Inc * * Licensed 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. * */ // This script contains a badly-organised collection of miscellaneous // functions that really better homes. function classCreate() { return function() { this.initialize.apply(this, arguments); } } function objectExtend(destination, source) { for (var property in source) { destination[property] = source[property]; } return destination; } function sel$() { var results = [], element; for (var i = 0; i < arguments.length; i++) { element = arguments[i]; if (typeof element == 'string') element = document.getElementById(element); results[results.length] = element; } return results.length < 2 ? results[0] : results; } function sel$A(iterable) { if (!iterable) return []; if (iterable.toArray) { return iterable.toArray(); } else { var results = []; for (var i = 0; i < iterable.length; i++) results.push(iterable[i]); return results; } } function fnBind() { var args = sel$A(arguments), __method = args.shift(), object = args.shift(); var retval = function() { return __method.apply(object, args.concat(sel$A(arguments))); } retval.__method = __method; return retval; } function fnBindAsEventListener(fn, object) { var __method = fn; return function(event) { return __method.call(object, event || window.event); } } function removeClassName(element, name) { var re = new RegExp("\\b" + name + "\\b", "g"); element.className = element.className.replace(re, ""); } function addClassName(element, name) { element.className = element.className + ' ' + name; } function elementSetStyle(element, style) { for (var name in style) { var value = style[name]; if (value == null) value = ""; element.style[name] = value; } } function elementGetStyle(element, style) { var value = element.style[style]; if (!value) { if (document.defaultView && document.defaultView.getComputedStyle) { var css = document.defaultView.getComputedStyle(element, null); value = css ? css.getPropertyValue(style) : null; } else if (element.currentStyle) { value = element.currentStyle[style]; } } /** DGF necessary? if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) if (Element.getStyle(element, 'position') == 'static') value = 'auto'; */ return value == 'auto' ? null : value; } String.prototype.trim = function() { var result = this.replace(/^\s+/g, ""); // strip leading return result.replace(/\s+$/g, ""); // strip trailing }; String.prototype.lcfirst = function() { return this.charAt(0).toLowerCase() + this.substr(1); }; String.prototype.ucfirst = function() { return this.charAt(0).toUpperCase() + this.substr(1); }; String.prototype.startsWith = function(str) { return this.indexOf(str) == 0; }; /** * Given a string literal that would appear in an XPath, puts it in quotes and * returns it. Special consideration is given to literals who themselves * contain quotes. It's possible for a concat() expression to be returned. */ String.prototype.quoteForXPath = function() { if (/\'/.test(this)) { if (/\"/.test(this)) { // concat scenario var pieces = []; var a = "'", b = '"', c; for (var i = 0, j = 0; i < this.length;) { if (this.charAt(i) == a) { // encountered a quote that cannot be contained in current // quote, so need to flip-flop quoting scheme if (j < i) { pieces.push(a + this.substring(j, i) + a); j = i; } c = a; a = b; b = c; } else { ++i; } } pieces.push(a + this.substring(j) + a); return 'concat(' + pieces.join(', ') + ')'; } else { // quote with doubles return '"' + this + '"'; } } // quote with singles return "'" + this + "'"; }; // Returns the text in this element function getText(element) { var text = ""; var isRecentFirefox = (browserVersion.isFirefox && browserVersion.firefoxVersion >= "1.5"); if (isRecentFirefox || browserVersion.isKonqueror || browserVersion.isSafari || browserVersion.isOpera) { text = getTextContent(element); } else if (element.textContent) { text = element.textContent; } else if (element.innerText) { text = element.innerText; } text = normalizeNewlines(text); text = normalizeSpaces(text); return text.trim(); } function getTextContent(element, preformatted) { if (element.style && (element.style.visibility == 'hidden' || element.style.display == 'none')) return ''; if (element.nodeType == 3 /*Node.TEXT_NODE*/) { var text = element.data; if (!preformatted) { text = text.replace(/\n|\r|\t/g, " "); } return text; } if (element.nodeType == 1 /*Node.ELEMENT_NODE*/ && element.nodeName != 'SCRIPT') { var childrenPreformatted = preformatted || (element.tagName == "PRE"); var text = ""; for (var i = 0; i < element.childNodes.length; i++) { var child = element.childNodes.item(i); text += getTextContent(child, childrenPreformatted); } // Handle block elements that introduce newlines // -- From HTML spec: // // // TODO: should potentially introduce multiple newlines to separate blocks if (element.tagName == "P" || element.tagName == "BR" || element.tagName == "HR" || element.tagName == "DIV") { text += "\n"; } return text; } return ''; } /** * Convert all newlines to \n */ function normalizeNewlines(text) { return text.replace(/\r\n|\r/g, "\n"); } /** * Replace multiple sequential spaces with a single space, and then convert   to space. */ function normalizeSpaces(text) { // IE has already done this conversion, so doing it again will remove multiple nbsp if (browserVersion.isIE) { return text; } // Replace multiple spaces with a single space // TODO - this shouldn't occur inside PRE elements text = text.replace(/\ +/g, " "); // Replace   with a space var nbspPattern = new RegExp(String.fromCharCode(160), "g"); if (browserVersion.isSafari) { return replaceAll(text, String.fromCharCode(160), " "); } else { return text.replace(nbspPattern, " "); } } function replaceAll(text, oldText, newText) { while (text.indexOf(oldText) != -1) { text = text.replace(oldText, newText); } return text; } function xmlDecode(text) { text = text.replace(/"/g, '"'); text = text.replace(/'/g, "'"); text = text.replace(/</g, "<"); text = text.replace(/>/g, ">"); text = text.replace(/&/g, "&"); return text; } // Sets the text in this element function setText(element, text) { if (element.textContent != null) { element.textContent = text; } else if (element.innerText != null) { element.innerText = text; } } // Get the value of an element function getInputValue(inputElement) { if (inputElement.type) { if (inputElement.type.toUpperCase() == 'CHECKBOX' || inputElement.type.toUpperCase() == 'RADIO') { return (inputElement.checked ? 'on' : 'off'); } } if (inputElement.value == null) { throw new SeleniumError("This element has no value; is it really a form field?"); } return inputElement.value; } /* Fire an event in a browser-compatible manner */ function triggerEvent(element, eventType, canBubble, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) { canBubble = (typeof(canBubble) == undefined) ? true : canBubble; if (element.fireEvent && element.ownerDocument && element.ownerDocument.createEventObject) { // IE var evt = createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown); element.fireEvent('on' + eventType, evt); } else { var evt = document.createEvent('HTMLEvents'); try { evt.shiftKey = shiftKeyDown; evt.metaKey = metaKeyDown; evt.altKey = altKeyDown; evt.ctrlKey = controlKeyDown; } catch (e) { // On Firefox 1.0, you can only set these during initMouseEvent or initKeyEvent // we'll have to ignore them here LOG.exception(e); } evt.initEvent(eventType, canBubble, true); element.dispatchEvent(evt); } } function getKeyCodeFromKeySequence(keySequence) { var match = /^\\(\d{1,3})$/.exec(keySequence); if (match != null) { return match[1]; } match = /^.$/.exec(keySequence); if (match != null) { return match[0].charCodeAt(0); } // this is for backward compatibility with existing tests // 1 digit ascii codes will break however because they are used for the digit chars match = /^\d{2,3}$/.exec(keySequence); if (match != null) { return match[0]; } throw new SeleniumError("invalid keySequence"); } function createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) { var evt = element.ownerDocument.createEventObject(); evt.shiftKey = shiftKeyDown; evt.metaKey = metaKeyDown; evt.altKey = altKeyDown; evt.ctrlKey = controlKeyDown; return evt; } function triggerKeyEvent(element, eventType, keySequence, canBubble, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) { var keycode = getKeyCodeFromKeySequence(keySequence); canBubble = (typeof(canBubble) == undefined) ? true : canBubble; if (element.fireEvent && element.ownerDocument && element.ownerDocument.createEventObject) { // IE var keyEvent = createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown); keyEvent.keyCode = keycode; element.fireEvent('on' + eventType, keyEvent); } else { var evt; if (window.KeyEvent) { evt = document.createEvent('KeyEvents'); evt.initKeyEvent(eventType, true, true, window, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown, keycode, keycode); } else { evt = document.createEvent('UIEvents'); evt.shiftKey = shiftKeyDown; evt.metaKey = metaKeyDown; evt.altKey = altKeyDown; evt.ctrlKey = controlKeyDown; evt.initUIEvent(eventType, true, true, window, 1); evt.keyCode = keycode; evt.which = keycode; } element.dispatchEvent(evt); } } function removeLoadListener(element, command) { LOG.debug('Removing loadListenter for ' + element + ', ' + command); if (window.removeEventListener) element.removeEventListener("load", command, true); else if (window.detachEvent) element.detachEvent("onload", command); } function addLoadListener(element, command) { LOG.debug('Adding loadListenter for ' + element + ', ' + command); var augmentedCommand = function() { command.call(this, element); } if (window.addEventListener && !browserVersion.isOpera) element.addEventListener("load", augmentedCommand, true); else if (window.attachEvent) element.attachEvent("onload", augmentedCommand); } /** * Override the broken getFunctionName() method from JsUnit * This file must be loaded _after_ the jsunitCore.js */ function getFunctionName(aFunction) { var regexpResult = aFunction.toString().match(/function (\w*)/); if (regexpResult && regexpResult[1]) { return regexpResult[1]; } return 'anonymous'; } function getDocumentBase(doc) { var bases = document.getElementsByTagName("base"); if (bases && bases.length && bases[0].href) { return bases[0].href; } return ""; } function getTagName(element) { var tagName; if (element && element.tagName && element.tagName.toLowerCase) { tagName = element.tagName.toLowerCase(); } return tagName; } function selArrayToString(a) { if (isArray(a)) { // DGF copying the array, because the array-like object may be a non-modifiable nodelist var retval = []; for (var i = 0; i < a.length; i++) { var item = a[i]; var replaced = new String(item).replace(/([,\\])/g, '\\$1'); retval[i] = replaced; } return retval; } return new String(a); } function isArray(x) { return ((typeof x) == "object") && (x["length"] != null); } function absolutify(url, baseUrl) { /** returns a relative url in its absolute form, given by baseUrl. * * This function is a little odd, because it can take baseUrls that * aren't necessarily directories. It uses the same rules as the HTML * <base> tag; if the baseUrl doesn't end with "/", we'll assume * that it points to a file, and strip the filename off to find its * base directory. * * So absolutify("foo", "http://x/bar") will return "http://x/foo" (stripping off bar), * whereas absolutify("foo", "http://x/bar/") will return "http://x/bar/foo" (preserving bar). * Naturally absolutify("foo", "http://x") will return "http://x/foo", appropriately. * * @param url the url to make absolute; if this url is already absolute, we'll just return that, unchanged * @param baseUrl the baseUrl from which we'll absolutify, following the rules above. * @return 'url' if it was already absolute, or the absolutized version of url if it was not absolute. */ // DGF isn't there some library we could use for this? if (/^\w+:/.test(url)) { // it's already absolute return url; } var loc; try { loc = parseUrl(baseUrl); } catch (e) { // is it an absolute windows file path? let's play the hero in that case if (/^\w:\\/.test(baseUrl)) { baseUrl = "file:///" + baseUrl.replace(/\\/g, "/"); loc = parseUrl(baseUrl); } else { throw new SeleniumError("baseUrl wasn't absolute: " + baseUrl); } } loc.search = null; loc.hash = null; // if url begins with /, then that's the whole pathname if (/^\//.test(url)) { loc.pathname = url; var result = reassembleLocation(loc); return result; } // if pathname is null, then we'll just append "/" + the url if (!loc.pathname) { loc.pathname = "/" + url; var result = reassembleLocation(loc); return result; } // if pathname ends with /, just append url if (/\/$/.test(loc.pathname)) { loc.pathname += url; var result = reassembleLocation(loc); return result; } // if we're here, then the baseUrl has a pathname, but it doesn't end with / // in that case, we replace everything after the final / with the relative url loc.pathname = loc.pathname.replace(/[^\/\\]+$/, url); var result = reassembleLocation(loc); return result; } var URL_REGEX = /^((\w+):\/\/)(([^:]+):?([^@]+)?@)?([^\/\?:]*):?(\d+)?(\/?[^\?#]+)?\??([^#]+)?#?(.+)?/; function parseUrl(url) { var fields = ['url', null, 'protocol', null, 'username', 'password', 'host', 'port', 'pathname', 'search', 'hash']; var result = URL_REGEX.exec(url); if (!result) { throw new SeleniumError("Invalid URL: " + url); } var loc = new Object(); for (var i = 0; i < fields.length; i++) { var field = fields[i]; if (field == null) { continue; } loc[field] = result[i]; } return loc; } function reassembleLocation(loc) { if (!loc.protocol) { throw new Error("Not a valid location object: " + o2s(loc)); } var protocol = loc.protocol; protocol = protocol.replace(/:$/, ""); var url = protocol + "://"; if (loc.username) { url += loc.username; if (loc.password) { url += ":" + loc.password; } url += "@"; } if (loc.host) { url += loc.host; } if (loc.port) { url += ":" + loc.port; } if (loc.pathname) { url += loc.pathname; } if (loc.search) { url += "?" + loc.search; } if (loc.hash) { var hash = loc.hash; hash = loc.hash.replace(/^#/, ""); url += "#" + hash; } return url; } function canonicalize(url) { if(url == "about:blank") { return url; } var tempLink = window.document.createElement("link"); tempLink.href = url; // this will canonicalize the href on most browsers var loc = parseUrl(tempLink.href) if (!/\/\.\.\//.test(loc.pathname)) { return tempLink.href; } // didn't work... let's try it the hard way var originalParts = loc.pathname.split("/"); var newParts = []; newParts.push(originalParts.shift()); for (var i = 0; i < originalParts.length; i++) { var part = originalParts[i]; if (".." == part) { newParts.pop(); continue; } newParts.push(part); } loc.pathname = newParts.join("/"); return reassembleLocation(loc); } function extractExceptionMessage(ex) { if (ex == null) return "null exception"; if (ex.message != null) return ex.message; if (ex.toString && ex.toString() != null) return ex.toString(); } function describe(object, delimiter) { var props = new Array(); for (var prop in object) { try { props.push(prop + " -> " + object[prop]); } catch (e) { props.push(prop + " -> [htmlutils: ack! couldn't read this property! (Permission Denied?)]"); } } return props.join(delimiter || '\n'); } var PatternMatcher = function(pattern) { this.selectStrategy(pattern); }; PatternMatcher.prototype = { selectStrategy: function(pattern) { this.pattern = pattern; var strategyName = 'glob'; // by default if (/^([a-z-]+):(.*)/.test(pattern)) { var possibleNewStrategyName = RegExp.$1; var possibleNewPattern = RegExp.$2; if (PatternMatcher.strategies[possibleNewStrategyName]) { strategyName = possibleNewStrategyName; pattern = possibleNewPattern; } } var matchStrategy = PatternMatcher.strategies[strategyName]; if (!matchStrategy) { throw new SeleniumError("cannot find PatternMatcher.strategies." + strategyName); } this.strategy = matchStrategy; this.matcher = new matchStrategy(pattern); }, matches: function(actual) { return this.matcher.matches(actual + ''); // Note: appending an empty string avoids a Konqueror bug } }; /** * A "static" convenience method for easy matching */ PatternMatcher.matches = function(pattern, actual) { return new PatternMatcher(pattern).matches(actual); }; PatternMatcher.strategies = { /** * Exact matching, e.g. "exact:***" */ exact: function(expected) { this.expected = expected; this.matches = function(actual) { return actual == this.expected; }; }, /** * Match by regular expression, e.g. "regexp:^[0-9]+$" */ regexp: function(regexpString) { this.regexp = new RegExp(regexpString); this.matches = function(actual) { return this.regexp.test(actual); }; }, regex: function(regexpString) { this.regexp = new RegExp(regexpString); this.matches = function(actual) { return this.regexp.test(actual); }; }, regexpi: function(regexpString) { this.regexp = new RegExp(regexpString, "i"); this.matches = function(actual) { return this.regexp.test(actual); }; }, regexi: function(regexpString) { this.regexp = new RegExp(regexpString, "i"); this.matches = function(actual) { return this.regexp.test(actual); }; }, /** * "globContains" (aka "wildmat") patterns, e.g. "glob:one,two,*", * but don't require a perfect match; instead succeed if actual * contains something that matches globString. * Making this distinction is motivated by a bug in IE6 which * leads to the browser hanging if we implement *TextPresent tests * by just matching against a regular expression beginning and * ending with ".*". The globcontains strategy allows us to satisfy * the functional needs of the *TextPresent ops more efficiently * and so avoid running into this IE6 freeze. */ globContains: function(globString) { this.regexp = new RegExp(PatternMatcher.regexpFromGlobContains(globString)); this.matches = function(actual) { return this.regexp.test(actual); }; }, /** * "glob" (aka "wildmat") patterns, e.g. "glob:one,two,*" */ glob: function(globString) { this.regexp = new RegExp(PatternMatcher.regexpFromGlob(globString)); this.matches = function(actual) { return this.regexp.test(actual); }; } }; PatternMatcher.convertGlobMetaCharsToRegexpMetaChars = function(glob) { var re = glob; re = re.replace(/([.^$+(){}\[\]\\|])/g, "\\$1"); re = re.replace(/\?/g, "(.|[\r\n])"); re = re.replace(/\*/g, "(.|[\r\n])*"); return re; }; PatternMatcher.regexpFromGlobContains = function(globContains) { return PatternMatcher.convertGlobMetaCharsToRegexpMetaChars(globContains); }; PatternMatcher.regexpFromGlob = function(glob) { return "^" + PatternMatcher.convertGlobMetaCharsToRegexpMetaChars(glob) + "$"; }; if (!this["Assert"]) Assert = {}; Assert.fail = function(message) { throw new AssertionFailedError(message); }; /* * Assert.equals(comment?, expected, actual) */ Assert.equals = function() { var args = new AssertionArguments(arguments); if (args.expected === args.actual) { return; } Assert.fail(args.comment + "Expected '" + args.expected + "' but was '" + args.actual + "'"); }; Assert.assertEquals = Assert.equals; /* * Assert.matches(comment?, pattern, actual) */ Assert.matches = function() { var args = new AssertionArguments(arguments); if (PatternMatcher.matches(args.expected, args.actual)) { return; } Assert.fail(args.comment + "Actual value '" + args.actual + "' did not match '" + args.expected + "'"); } /* * Assert.notMtches(comment?, pattern, actual) */ Assert.notMatches = function() { var args = new AssertionArguments(arguments); if (!PatternMatcher.matches(args.expected, args.actual)) { return; } Assert.fail(args.comment + "Actual value '" + args.actual + "' did match '" + args.expected + "'"); } // Preprocess the arguments to allow for an optional comment. function AssertionArguments(args) { if (args.length == 2) { this.comment = ""; this.expected = args[0]; this.actual = args[1]; } else { this.comment = args[0] + "; "; this.expected = args[1]; this.actual = args[2]; } } function AssertionFailedError(message) { this.isAssertionFailedError = true; this.isSeleniumError = true; this.message = message; this.failureMessage = message; } function SeleniumError(message) { var error = new Error(message); if (typeof(arguments.caller) != 'undefined') { // IE, not ECMA var result = ''; for (var a = arguments.caller; a != null; a = a.caller) { result += '> ' + a.callee.toString() + '\n'; if (a.caller == a) { result += '*'; break; } } error.stack = result; } error.isSeleniumError = true; return error; } function highlight(element) { var highLightColor = "yellow"; if (element.originalColor == undefined) { // avoid picking up highlight element.originalColor = elementGetStyle(element, "background-color"); } elementSetStyle(element, {"backgroundColor" : highLightColor}); window.setTimeout(function() { try { //if element is orphan, probably page of it has already gone, so ignore if (!element.parentNode) { return; } elementSetStyle(element, {"backgroundColor" : element.originalColor}); } catch (e) {} // DGF unhighlighting is very dangerous and low priority }, 200); } // for use from vs.2003 debugger function o2s(obj) { var s = ""; for (key in obj) { var line = key + "->" + obj[key]; line.replace("\n", " "); s += line + "\n"; } return s; } var seenReadyStateWarning = false; function openSeparateApplicationWindow(url, suppressMozillaWarning) { // resize the Selenium window itself window.resizeTo(1200, 500); window.moveTo(window.screenX, 0); var appWindow = window.open(url + '?start=true', 'selenium_main_app_window'); if (appWindow == null) { var errorMessage = "Couldn't open app window; is the pop-up blocker enabled?" LOG.error(errorMessage); throw new Error("Couldn't open app window; is the pop-up blocker enabled?"); } try { var windowHeight = 500; if (window.outerHeight) { windowHeight = window.outerHeight; } else if (document.documentElement && document.documentElement.offsetHeight) { windowHeight = document.documentElement.offsetHeight; } if (window.screenLeft && !window.screenX) window.screenX = window.screenLeft; if (window.screenTop && !window.screenY) window.screenY = window.screenTop; appWindow.resizeTo(1200, screen.availHeight - windowHeight - 60); appWindow.moveTo(window.screenX, window.screenY + windowHeight + 25); } catch (e) { LOG.error("Couldn't resize app window"); LOG.exception(e); } if (!suppressMozillaWarning && window.document.readyState == null && !seenReadyStateWarning) { alert("Beware! Mozilla bug 300992 means that we can't always reliably detect when a new page has loaded. Install the Selenium IDE extension or the readyState extension available from selenium.openqa.org to make page load detection more reliable."); seenReadyStateWarning = true; } return appWindow; } var URLConfiguration = classCreate(); objectExtend(URLConfiguration.prototype, { initialize: function() { }, _isQueryParameterTrue: function (name) { var parameterValue = this._getQueryParameter(name); if (parameterValue == null) return false; if (parameterValue.toLowerCase() == "true") return true; if (parameterValue.toLowerCase() == "on") return true; return false; }, _getQueryParameter: function(searchKey) { var str = this.queryString if (str == null) return null; var clauses = str.split('&'); for (var i = 0; i < clauses.length; i++) { var keyValuePair = clauses[i].split('=', 2); var key = unescape(keyValuePair[0]); if (key == searchKey) { return unescape(keyValuePair[1]); } } return null; }, _extractArgs: function() { var str = SeleniumHTARunner.commandLine; if (str == null || str == "") return new Array(); var matches = str.match(/(?:\"([^\"]+)\"|(?!\"([^\"]+)\")(\S+))/g); // We either want non quote stuff ([^"]+) surrounded by quotes // or we want to look-ahead, see that the next character isn't // a quoted argument, and then grab all the non-space stuff // this will return for the line: "foo" bar // the results "\"foo\"" and "bar" // So, let's unquote the quoted arguments: var args = new Array; for (var i = 0; i < matches.length; i++) { args[i] = matches[i]; args[i] = args[i].replace(/^"(.*)"$/, "$1"); } return args; }, isMultiWindowMode:function() { return this._isQueryParameterTrue('multiWindow'); }, getBaseUrl:function() { return this._getQueryParameter('baseUrl'); } }); function safeScrollIntoView(element) { if (element.scrollIntoView) { element.scrollIntoView(false); return; } // TODO: work out how to scroll browsers that don't support // scrollIntoView (like Konqueror) } /** * Returns the absolute time represented as an offset of the current time. * Throws a SeleniumException if timeout is invalid. * * @param timeout the number of milliseconds from "now" whose absolute time * to return */ function getTimeoutTime(timeout) { var now = new Date().getTime(); var timeoutLength = parseInt(timeout); if (isNaN(timeoutLength)) { throw new SeleniumError("Timeout is not a number: '" + timeout + "'"); } return now + timeoutLength; } /** * Returns true iff the current environment is the IDE, and is not the chrome * runner launched by the IDE. */ function is_IDE() { var locstr = window.location.href; if (locstr.indexOf('chrome://selenium-ide-testrunner') == 0) { return false; } return (typeof(SeleniumIDE) != 'undefined'); } /** * Logs a message if the Logger exists, and does nothing if it doesn't exist. * * @param level the level to log at * @param msg the message to log */ function safe_log(level, msg) { try { LOG[level](msg); } catch (e) { // couldn't log! } } /** * Displays a warning message to the user appropriate to the context under * which the issue is encountered. This is primarily used to avoid popping up * alert dialogs that might pause an automated test suite. * * @param msg the warning message to display */ function safe_alert(msg) { if (is_IDE()) { alert(msg); } } /** * Returns true iff the given element represents a link with a javascript * href attribute, and does not have an onclick attribute defined. * * @param element the element to test */ function hasJavascriptHref(element) { if (getTagName(element) != 'a') { return false; } if (element.getAttribute('onclick')) { return false; } if (! element.href) { return false; } if (! /\s*javascript:/i.test(element.href)) { return false; } return true; } /** * Returns the given element, or its nearest ancestor, that satisfies * hasJavascriptHref(). Returns null if none is found. * * @param element the element whose ancestors to test */ function getAncestorOrSelfWithJavascriptHref(element) { if (hasJavascriptHref(element)) { return element; } if (element.parentNode == null) { return null; } return getAncestorOrSelfWithJavascriptHref(element.parentNode); } //****************************************************************************** // Locator evaluation support /** * Parses a Selenium locator, returning its type and the unprefixed locator * string as an object. * * @param locator the locator to parse */ function parse_locator(locator) { var result = locator.match(/^([A-Za-z]+)=(.+)/); if (result) { return { type: result[1].toLowerCase(), string: result[2] }; } 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(); /** * Cache class. * * @param newMaxSize the maximum number of entries to keep in the cache. */ function BoundedCache(newMaxSize) { var maxSize = newMaxSize; var map = {}; var size = 0; var counter = -1; /** * Adds a key-value pair to the cache. If the cache is at its size limit, * the least-recently used entry is evicted. */ this.put = function(key, value) { if (map[key]) { // entry already exists map[key] = { usage: ++counter, value: value }; } else { map[key] = { usage: ++counter, value: value }; ++size; if (size > maxSize) { // remove the least recently used item var minUsage = counter; var keyToRemove = key; for (var key in map) { if (map[key].usage < minUsage) { minUsage = map[key].usage; keyToRemove = key; } } this.remove(keyToRemove); } } }; /** * Returns a cache item by its key, and updates its use status. */ this.get = function(key) { if (map[key]) { map[key].usage = ++counter; return map[key].value; } return null; }; /** * Removes a cache item by its key. */ this.remove = function(key) { if (map[key]) { delete map[key]; --size; if (size == 0) { counter = -1; } } } /** * Clears all entries in the cache. */ this.clear = function() { map = {}; size = 0; counter = -1; }; } /////////////////////////////////////////////////////////////////////////////// /** * Builds and returns closures that take a document and return a node. */ function FinderBuilder(newDocument) { // private var doc = newDocument; function buildIdFinder(e) { if (e.id) { var id = e.id; return (function(targetDoc) { return targetDoc.getElementById(id); }); } return null; } function buildTagNameFinder(e) { var elements = doc.getElementsByTagName(e.tagName); for (var i = 0, n = elements.length; i < n; ++i) { if (elements[i] === e) { // both the descendant axis and getElementsByTagName() should // return elements in document order; hence the following index // operation is possible return (function(targetDoc) { return targetDoc.getElementsByTagName(e.tagName)[i]; }); } } return null; } // public this.setDocument = function(newDocument) { doc = newDocument; return this; }; this.build = function(e) { return ( buildIdFinder(e) || buildTagNameFinder(e) ); }; } /////////////////////////////////////////////////////////////////////////////// /** * @param newEngine the XPath engine used to navigate this document */ function MirroredDocument() { // private var originalDoc; var reflectionDoc; var namespaceResolver; var finderBuilder = new FinderBuilder(); var pastReflections = new BoundedCache(50); var jQuery = new JQueryWrapper(); /** * Appends elements represented by the given HTML to the given parent * element. All