/* * 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. 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) { element.textContent = text; } else if (element.innerText) { element.innerText = text; } } // Get the value of an element function getInputValue(inputElement) { if (inputElement.type.toUpperCase() == 'CHECKBOX' || inputElement.type.toUpperCase() == 'RADIO') { return (inputElement.checked ? 'on' : 'off'); } return inputElement.value; } /* Fire an event in a browser-compatible manner */ function triggerEvent(element, eventType, canBubble) { canBubble = (typeof(canBubble) == undefined) ? true : canBubble; if (element.fireEvent) { element.fireEvent('on' + eventType); } else { var evt = document.createEvent('HTMLEvents'); 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 SeleniumError("invalid keySequence"); } function triggerKeyEvent(element, eventType, keySequence, canBubble) { var keycode = getKeyCodeFromKeySequence(keySequence); canBubble = (typeof(canBubble) == undefined) ? true : canBubble; if (element.fireEvent) { keyEvent = element.ownerDocument.createEventObject(); keyEvent.keyCode = keycode; element.fireEvent('on' + eventType, keyEvent); } else { var evt; if (window.KeyEvent) { evt = document.createEvent('KeyEvents'); evt.initKeyEvent(eventType, true, true, window, false, false, false, false, keycode, keycode); } else { evt = document.createEvent('UIEvents'); evt.initUIEvent(eventType, true, true, window, 1); evt.keyCode = keycode; } element.dispatchEvent(evt); } } /* Fire a mouse event in a browser-compatible manner */ function triggerMouseEvent(element, eventType, canBubble, clientX, clientY) { clientX = clientX ? clientX : 0; clientY = clientY ? clientY : 0; // TODO: set these attributes -- they don't seem to be needed by the initial test cases, but that could change... var screenX = 0; var screenY = 0; canBubble = (typeof(canBubble) == undefined) ? true : canBubble; if (element.fireEvent) { LOG.error("element has fireEvent"); if (!screenX && !screenY && !clientX && !clientY) { element.fireEvent('on' + eventType); } else { var ieEvent = element.ownerDocument.createEventObject(); ieEvent.detail = 0; ieEvent.screenX = screenX; ieEvent.screenY = screenY; ieEvent.clientX = clientX; ieEvent.clientY = clientY; ieEvent.ctrlKey = false; ieEvent.altKey = false; ieEvent.shiftKey = false; ieEvent.metaKey = false; ieEvent.button = 1; ieEvent.relatedTarget = null; // when we go this route, window.event is never set to contain the event we have just created. // ideally we could just slide it in as follows in the try-block below, but this normally // doesn't work. This is why I try to avoid this code path, which is only required if we need to // set attributes on the event (e.g., clientX). try { window.event = ieEvent; } catch(e) { // getting an "Object does not support this action or property" error. Save the event away // for future reference. // TODO: is there a way to update window.event? // work around for http://jira.openqa.org/browse/SEL-280 -- make the event available somewhere: selenium.browserbot.getCurrentWindow().selenium_event = ieEvent; } element.fireEvent('on' + eventType, ieEvent); } } else { LOG.error("element doesn't have fireEvent"); var evt = document.createEvent('MouseEvents'); if (evt.initMouseEvent) { LOG.error("element has initMouseEvent"); //Safari evt.initMouseEvent(eventType, canBubble, true, document.defaultView, 1, screenX, screenY, clientX, clientY, false, false, false, false, 0, null) } else { LOG.error("element doesen't has initMouseEvent"); // TODO we should be initialising other mouse-event related attributes here evt.initEvent(eventType, canBubble, true); } 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); if (window.addEventListener && !browserVersion.isOpera) element.addEventListener("load", command, true); else if (window.attachEvent) element.attachEvent("onload", command); } /** * 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 describe(object, delimiter) { var props = new Array(); for (var prop in object) { props.push(prop + " -> " + object[prop]); } 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; } var Effect = new Object(); Object.extend(Effect, { highlight : function(element) { var highLightColor = "yellow"; if (element.originalColor == undefined) { // avoid picking up highlight element.originalColor = Element.getStyle(element, "background-color"); } Element.setStyle(element, {"background-color" : highLightColor}); window.setTimeout(function() { //if element is orphan, probably page of it has already gone, so ignore if (!element.parentNode) { return; } Element.setStyle(element, {"background-color" : element.originalColor}); }, 200); } }); // for use from vs.2003 debugger function objToString(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) { // 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 (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 = Class.create(); Object.extend(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'); } }); function safeScrollIntoView(element) { if (element.scrollIntoView) { element.scrollIntoView(false); return; } // TODO: work out how to scroll browsers that don't support // scrollIntoView (like Konqueror) }