/* * 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 $() { 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 $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 = $A(arguments), __method = args.shift(), object = args.shift(); var retval = function() { return __method.apply(object, args.concat($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; }; // 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.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*/) { 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 \m */ 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) { 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) { 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.info('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.info('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 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) { var tempLink = window.document.createElement("link"); tempLink.href = url; // this will canonicalize the href return tempLink.href; } 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); }; }, /** * "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) + "$"; }; var Assert = { fail: function(message) { throw new AssertionFailedError(message); }, /* * Assert.equals(comment?, expected, actual) */ 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.matches(comment?, pattern, actual) */ 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) */ 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); 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', 'main'); 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) }