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