2
* Copyright 2004 ThoughtWorks, Inc
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
* you may not use this file except in compliance with the License.
6
* You may obtain a copy of the License at
8
* http://www.apache.org/licenses/LICENSE-2.0
10
* Unless required by applicable law or agreed to in writing, software
11
* distributed under the License is distributed on an "AS IS" BASIS,
12
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
* See the License for the specific language governing permissions and
14
* limitations under the License.
18
// This script contains a badly-organised collection of miscellaneous
19
// functions that really better homes.
21
function classCreate() {
23
this.initialize.apply(this, arguments);
27
function objectExtend(destination, source) {
28
for (var property in source) {
29
destination[property] = source[property];
35
var results = [], element;
36
for (var i = 0; i < arguments.length; i++) {
37
element = arguments[i];
38
if (typeof element == 'string')
39
element = document.getElementById(element);
40
results[results.length] = element;
42
return results.length < 2 ? results[0] : results;
45
function sel$A(iterable) {
46
if (!iterable) return [];
47
if (iterable.toArray) {
48
return iterable.toArray();
51
for (var i = 0; i < iterable.length; i++)
52
results.push(iterable[i]);
58
var args = sel$A(arguments), __method = args.shift(), object = args.shift();
59
var retval = function() {
60
return __method.apply(object, args.concat(sel$A(arguments)));
62
retval.__method = __method;
66
function fnBindAsEventListener(fn, object) {
68
return function(event) {
69
return __method.call(object, event || window.event);
73
function removeClassName(element, name) {
74
var re = new RegExp("\\b" + name + "\\b", "g");
75
element.className = element.className.replace(re, "");
78
function addClassName(element, name) {
79
element.className = element.className + ' ' + name;
82
function elementSetStyle(element, style) {
83
for (var name in style) {
84
var value = style[name];
85
if (value == null) value = "";
86
element.style[name] = value;
90
function elementGetStyle(element, style) {
91
var value = element.style[style];
93
if (document.defaultView && document.defaultView.getComputedStyle) {
94
var css = document.defaultView.getComputedStyle(element, null);
95
value = css ? css.getPropertyValue(style) : null;
96
} else if (element.currentStyle) {
97
value = element.currentStyle[style];
102
if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
103
if (Element.getStyle(element, 'position') == 'static') value = 'auto'; */
105
return value == 'auto' ? null : value;
108
String.prototype.trim = function() {
109
var result = this.replace(/^\s+/g, "");
111
return result.replace(/\s+$/g, "");
114
String.prototype.lcfirst = function() {
115
return this.charAt(0).toLowerCase() + this.substr(1);
117
String.prototype.ucfirst = function() {
118
return this.charAt(0).toUpperCase() + this.substr(1);
120
String.prototype.startsWith = function(str) {
121
return this.indexOf(str) == 0;
125
* Given a string literal that would appear in an XPath, puts it in quotes and
126
* returns it. Special consideration is given to literals who themselves
127
* contain quotes. It's possible for a concat() expression to be returned.
129
String.prototype.quoteForXPath = function()
131
if (/\'/.test(this)) {
132
if (/\"/.test(this)) {
135
var a = "'", b = '"', c;
136
for (var i = 0, j = 0; i < this.length;) {
137
if (this.charAt(i) == a) {
138
// encountered a quote that cannot be contained in current
139
// quote, so need to flip-flop quoting scheme
141
pieces.push(a + this.substring(j, i) + a);
152
pieces.push(a + this.substring(j) + a);
153
return 'concat(' + pieces.join(', ') + ')';
156
// quote with doubles
157
return '"' + this + '"';
160
// quote with singles
161
return "'" + this + "'";
164
// Returns the text in this element
165
function getText(element) {
168
var isRecentFirefox = (browserVersion.isFirefox && browserVersion.firefoxVersion >= "1.5");
169
if (isRecentFirefox || browserVersion.isKonqueror || browserVersion.isSafari || browserVersion.isOpera) {
170
text = getTextContent(element);
171
} else if (element.textContent) {
172
text = element.textContent;
173
} else if (element.innerText) {
174
text = element.innerText;
177
text = normalizeNewlines(text);
178
text = normalizeSpaces(text);
183
function getTextContent(element, preformatted) {
184
if (element.nodeType == 3 /*Node.TEXT_NODE*/) {
185
var text = element.data;
187
text = text.replace(/\n|\r|\t/g, " ");
191
if (element.nodeType == 1 /*Node.ELEMENT_NODE*/) {
192
var childrenPreformatted = preformatted || (element.tagName == "PRE");
194
for (var i = 0; i < element.childNodes.length; i++) {
195
var child = element.childNodes.item(i);
196
text += getTextContent(child, childrenPreformatted);
198
// Handle block elements that introduce newlines
199
// -- From HTML spec:
201
// "P | %heading; | %list; | %preformatted; | DL | DIV | NOSCRIPT |
202
// BLOCKQUOTE | F:wORM | HR | TABLE | FIELDSET | ADDRESS">
204
// TODO: should potentially introduce multiple newlines to separate blocks
205
if (element.tagName == "P" || element.tagName == "BR" || element.tagName == "HR" || element.tagName == "DIV") {
214
* Convert all newlines to \n
216
function normalizeNewlines(text)
218
return text.replace(/\r\n|\r/g, "\n");
222
* Replace multiple sequential spaces with a single space, and then convert to space.
224
function normalizeSpaces(text)
226
// IE has already done this conversion, so doing it again will remove multiple nbsp
227
if (browserVersion.isIE)
232
// Replace multiple spaces with a single space
233
// TODO - this shouldn't occur inside PRE elements
234
text = text.replace(/\ +/g, " ");
236
// Replace with a space
237
var nbspPattern = new RegExp(String.fromCharCode(160), "g");
238
if (browserVersion.isSafari) {
239
return replaceAll(text, String.fromCharCode(160), " ");
241
return text.replace(nbspPattern, " ");
245
function replaceAll(text, oldText, newText) {
246
while (text.indexOf(oldText) != -1) {
247
text = text.replace(oldText, newText);
253
function xmlDecode(text) {
254
text = text.replace(/"/g, '"');
255
text = text.replace(/'/g, "'");
256
text = text.replace(/</g, "<");
257
text = text.replace(/>/g, ">");
258
text = text.replace(/&/g, "&");
262
// Sets the text in this element
263
function setText(element, text) {
264
if (element.textContent != null) {
265
element.textContent = text;
266
} else if (element.innerText != null) {
267
element.innerText = text;
271
// Get the value of an <input> element
272
function getInputValue(inputElement) {
273
if (inputElement.type) {
274
if (inputElement.type.toUpperCase() == 'CHECKBOX' ||
275
inputElement.type.toUpperCase() == 'RADIO')
277
return (inputElement.checked ? 'on' : 'off');
280
if (inputElement.value == null) {
281
throw new SeleniumError("This element has no value; is it really a form field?");
283
return inputElement.value;
286
/* Fire an event in a browser-compatible manner */
287
function triggerEvent(element, eventType, canBubble, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) {
288
canBubble = (typeof(canBubble) == undefined) ? true : canBubble;
289
if (element.fireEvent && element.ownerDocument && element.ownerDocument.createEventObject) { // IE
290
var evt = createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown);
291
element.fireEvent('on' + eventType, evt);
294
var evt = document.createEvent('HTMLEvents');
297
evt.shiftKey = shiftKeyDown;
298
evt.metaKey = metaKeyDown;
299
evt.altKey = altKeyDown;
300
evt.ctrlKey = controlKeyDown;
302
// On Firefox 1.0, you can only set these during initMouseEvent or initKeyEvent
303
// we'll have to ignore them here
307
evt.initEvent(eventType, canBubble, true);
308
element.dispatchEvent(evt);
312
function getKeyCodeFromKeySequence(keySequence) {
313
var match = /^\\(\d{1,3})$/.exec(keySequence);
317
match = /^.$/.exec(keySequence);
319
return match[0].charCodeAt(0);
321
// this is for backward compatibility with existing tests
322
// 1 digit ascii codes will break however because they are used for the digit chars
323
match = /^\d{2,3}$/.exec(keySequence);
327
throw new SeleniumError("invalid keySequence");
330
function createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) {
331
var evt = element.ownerDocument.createEventObject();
332
evt.shiftKey = shiftKeyDown;
333
evt.metaKey = metaKeyDown;
334
evt.altKey = altKeyDown;
335
evt.ctrlKey = controlKeyDown;
339
function triggerKeyEvent(element, eventType, keySequence, canBubble, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) {
340
var keycode = getKeyCodeFromKeySequence(keySequence);
341
canBubble = (typeof(canBubble) == undefined) ? true : canBubble;
342
if (element.fireEvent && element.ownerDocument && element.ownerDocument.createEventObject) { // IE
343
var keyEvent = createEventObject(element, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown);
344
keyEvent.keyCode = keycode;
345
element.fireEvent('on' + eventType, keyEvent);
349
if (window.KeyEvent) {
350
evt = document.createEvent('KeyEvents');
351
evt.initKeyEvent(eventType, true, true, window, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown, keycode, keycode);
353
evt = document.createEvent('UIEvents');
355
evt.shiftKey = shiftKeyDown;
356
evt.metaKey = metaKeyDown;
357
evt.altKey = altKeyDown;
358
evt.ctrlKey = controlKeyDown;
360
evt.initUIEvent(eventType, true, true, window, 1);
361
evt.keyCode = keycode;
365
element.dispatchEvent(evt);
369
function removeLoadListener(element, command) {
370
LOG.debug('Removing loadListenter for ' + element + ', ' + command);
371
if (window.removeEventListener)
372
element.removeEventListener("load", command, true);
373
else if (window.detachEvent)
374
element.detachEvent("onload", command);
377
function addLoadListener(element, command) {
378
LOG.debug('Adding loadListenter for ' + element + ', ' + command);
379
var augmentedCommand = function() {
380
command.call(this, element);
382
if (window.addEventListener && !browserVersion.isOpera)
383
element.addEventListener("load", augmentedCommand, true);
384
else if (window.attachEvent)
385
element.attachEvent("onload", augmentedCommand);
389
* Override the broken getFunctionName() method from JsUnit
390
* This file must be loaded _after_ the jsunitCore.js
392
function getFunctionName(aFunction) {
393
var regexpResult = aFunction.toString().match(/function (\w*)/);
394
if (regexpResult && regexpResult[1]) {
395
return regexpResult[1];
400
function getDocumentBase(doc) {
401
var bases = document.getElementsByTagName("base");
402
if (bases && bases.length && bases[0].href) {
403
return bases[0].href;
408
function getTagName(element) {
410
if (element && element.tagName && element.tagName.toLowerCase) {
411
tagName = element.tagName.toLowerCase();
416
function selArrayToString(a) {
418
// DGF copying the array, because the array-like object may be a non-modifiable nodelist
420
for (var i = 0; i < a.length; i++) {
422
var replaced = new String(item).replace(/([,\\])/g, '\\$1');
423
retval[i] = replaced;
427
return new String(a);
431
function isArray(x) {
432
return ((typeof x) == "object") && (x["length"] != null);
435
function absolutify(url, baseUrl) {
436
/** returns a relative url in its absolute form, given by baseUrl.
438
* This function is a little odd, because it can take baseUrls that
439
* aren't necessarily directories. It uses the same rules as the HTML
440
* <base> tag; if the baseUrl doesn't end with "/", we'll assume
441
* that it points to a file, and strip the filename off to find its
444
* So absolutify("foo", "http://x/bar") will return "http://x/foo" (stripping off bar),
445
* whereas absolutify("foo", "http://x/bar/") will return "http://x/bar/foo" (preserving bar).
446
* Naturally absolutify("foo", "http://x") will return "http://x/foo", appropriately.
448
* @param url the url to make absolute; if this url is already absolute, we'll just return that, unchanged
449
* @param baseUrl the baseUrl from which we'll absolutify, following the rules above.
450
* @return 'url' if it was already absolute, or the absolutized version of url if it was not absolute.
453
// DGF isn't there some library we could use for this?
455
if (/^\w+:/.test(url)) {
456
// it's already absolute
462
loc = parseUrl(baseUrl);
464
// is it an absolute windows file path? let's play the hero in that case
465
if (/^\w:\\/.test(baseUrl)) {
466
baseUrl = "file:///" + baseUrl.replace(/\\/g, "/");
467
loc = parseUrl(baseUrl);
469
throw new SeleniumError("baseUrl wasn't absolute: " + baseUrl);
475
// if url begins with /, then that's the whole pathname
476
if (/^\//.test(url)) {
478
var result = reassembleLocation(loc);
482
// if pathname is null, then we'll just append "/" + the url
484
loc.pathname = "/" + url;
485
var result = reassembleLocation(loc);
489
// if pathname ends with /, just append url
490
if (/\/$/.test(loc.pathname)) {
492
var result = reassembleLocation(loc);
496
// if we're here, then the baseUrl has a pathname, but it doesn't end with /
497
// in that case, we replace everything after the final / with the relative url
498
loc.pathname = loc.pathname.replace(/[^\/\\]+$/, url);
499
var result = reassembleLocation(loc);
504
var URL_REGEX = /^((\w+):\/\/)(([^:]+):?([^@]+)?@)?([^\/\?:]*):?(\d+)?(\/?[^\?#]+)?\??([^#]+)?#?(.+)?/;
506
function parseUrl(url) {
507
var fields = ['url', null, 'protocol', null, 'username', 'password', 'host', 'port', 'pathname', 'search', 'hash'];
508
var result = URL_REGEX.exec(url);
510
throw new SeleniumError("Invalid URL: " + url);
512
var loc = new Object();
513
for (var i = 0; i < fields.length; i++) {
514
var field = fields[i];
518
loc[field] = result[i];
523
function reassembleLocation(loc) {
525
throw new Error("Not a valid location object: " + o2s(loc));
527
var protocol = loc.protocol;
528
protocol = protocol.replace(/:$/, "");
529
var url = protocol + "://";
533
url += ":" + loc.password;
542
url += ":" + loc.port;
550
url += "?" + loc.search;
554
hash = loc.hash.replace(/^#/, "");
560
function canonicalize(url) {
561
if(url == "about:blank")
565
var tempLink = window.document.createElement("link");
566
tempLink.href = url; // this will canonicalize the href on most browsers
567
var loc = parseUrl(tempLink.href)
568
if (!/\/\.\.\//.test(loc.pathname)) {
569
return tempLink.href;
571
// didn't work... let's try it the hard way
572
var originalParts = loc.pathname.split("/");
574
newParts.push(originalParts.shift());
575
for (var i = 0; i < originalParts.length; i++) {
576
var part = originalParts[i];
583
loc.pathname = newParts.join("/");
584
return reassembleLocation(loc);
587
function extractExceptionMessage(ex) {
588
if (ex == null) return "null exception";
589
if (ex.message != null) return ex.message;
590
if (ex.toString && ex.toString() != null) return ex.toString();
594
function describe(object, delimiter) {
595
var props = new Array();
596
for (var prop in object) {
598
props.push(prop + " -> " + object[prop]);
600
props.push(prop + " -> [htmlutils: ack! couldn't read this property! (Permission Denied?)]");
603
return props.join(delimiter || '\n');
606
var PatternMatcher = function(pattern) {
607
this.selectStrategy(pattern);
609
PatternMatcher.prototype = {
611
selectStrategy: function(pattern) {
612
this.pattern = pattern;
613
var strategyName = 'glob';
615
if (/^([a-z-]+):(.*)/.test(pattern)) {
616
var possibleNewStrategyName = RegExp.$1;
617
var possibleNewPattern = RegExp.$2;
618
if (PatternMatcher.strategies[possibleNewStrategyName]) {
619
strategyName = possibleNewStrategyName;
620
pattern = possibleNewPattern;
623
var matchStrategy = PatternMatcher.strategies[strategyName];
624
if (!matchStrategy) {
625
throw new SeleniumError("cannot find PatternMatcher.strategies." + strategyName);
627
this.strategy = matchStrategy;
628
this.matcher = new matchStrategy(pattern);
631
matches: function(actual) {
632
return this.matcher.matches(actual + '');
633
// Note: appending an empty string avoids a Konqueror bug
639
* A "static" convenience method for easy matching
641
PatternMatcher.matches = function(pattern, actual) {
642
return new PatternMatcher(pattern).matches(actual);
645
PatternMatcher.strategies = {
648
* Exact matching, e.g. "exact:***"
650
exact: function(expected) {
651
this.expected = expected;
652
this.matches = function(actual) {
653
return actual == this.expected;
658
* Match by regular expression, e.g. "regexp:^[0-9]+$"
660
regexp: function(regexpString) {
661
this.regexp = new RegExp(regexpString);
662
this.matches = function(actual) {
663
return this.regexp.test(actual);
667
regex: function(regexpString) {
668
this.regexp = new RegExp(regexpString);
669
this.matches = function(actual) {
670
return this.regexp.test(actual);
674
regexpi: function(regexpString) {
675
this.regexp = new RegExp(regexpString, "i");
676
this.matches = function(actual) {
677
return this.regexp.test(actual);
681
regexi: function(regexpString) {
682
this.regexp = new RegExp(regexpString, "i");
683
this.matches = function(actual) {
684
return this.regexp.test(actual);
689
* "globContains" (aka "wildmat") patterns, e.g. "glob:one,two,*",
690
* but don't require a perfect match; instead succeed if actual
691
* contains something that matches globString.
692
* Making this distinction is motivated by a bug in IE6 which
693
* leads to the browser hanging if we implement *TextPresent tests
694
* by just matching against a regular expression beginning and
695
* ending with ".*". The globcontains strategy allows us to satisfy
696
* the functional needs of the *TextPresent ops more efficiently
697
* and so avoid running into this IE6 freeze.
699
globContains: function(globString) {
700
this.regexp = new RegExp(PatternMatcher.regexpFromGlobContains(globString));
701
this.matches = function(actual) {
702
return this.regexp.test(actual);
708
* "glob" (aka "wildmat") patterns, e.g. "glob:one,two,*"
710
glob: function(globString) {
711
this.regexp = new RegExp(PatternMatcher.regexpFromGlob(globString));
712
this.matches = function(actual) {
713
return this.regexp.test(actual);
719
PatternMatcher.convertGlobMetaCharsToRegexpMetaChars = function(glob) {
721
re = re.replace(/([.^$+(){}\[\]\\|])/g, "\\$1");
722
re = re.replace(/\?/g, "(.|[\r\n])");
723
re = re.replace(/\*/g, "(.|[\r\n])*");
727
PatternMatcher.regexpFromGlobContains = function(globContains) {
728
return PatternMatcher.convertGlobMetaCharsToRegexpMetaChars(globContains);
731
PatternMatcher.regexpFromGlob = function(glob) {
732
return "^" + PatternMatcher.convertGlobMetaCharsToRegexpMetaChars(glob) + "$";
735
if (!this["Assert"]) Assert = {};
738
Assert.fail = function(message) {
739
throw new AssertionFailedError(message);
743
* Assert.equals(comment?, expected, actual)
745
Assert.equals = function() {
746
var args = new AssertionArguments(arguments);
747
if (args.expected === args.actual) {
750
Assert.fail(args.comment +
751
"Expected '" + args.expected +
752
"' but was '" + args.actual + "'");
755
Assert.assertEquals = Assert.equals;
758
* Assert.matches(comment?, pattern, actual)
760
Assert.matches = function() {
761
var args = new AssertionArguments(arguments);
762
if (PatternMatcher.matches(args.expected, args.actual)) {
765
Assert.fail(args.comment +
766
"Actual value '" + args.actual +
767
"' did not match '" + args.expected + "'");
771
* Assert.notMtches(comment?, pattern, actual)
773
Assert.notMatches = function() {
774
var args = new AssertionArguments(arguments);
775
if (!PatternMatcher.matches(args.expected, args.actual)) {
778
Assert.fail(args.comment +
779
"Actual value '" + args.actual +
780
"' did match '" + args.expected + "'");
784
// Preprocess the arguments to allow for an optional comment.
785
function AssertionArguments(args) {
786
if (args.length == 2) {
788
this.expected = args[0];
789
this.actual = args[1];
791
this.comment = args[0] + "; ";
792
this.expected = args[1];
793
this.actual = args[2];
797
function AssertionFailedError(message) {
798
this.isAssertionFailedError = true;
799
this.isSeleniumError = true;
800
this.message = message;
801
this.failureMessage = message;
804
function SeleniumError(message) {
805
var error = new Error(message);
806
if (typeof(arguments.caller) != 'undefined') { // IE, not ECMA
808
for (var a = arguments.caller; a != null; a = a.caller) {
809
result += '> ' + a.callee.toString() + '\n';
815
error.stack = result;
817
error.isSeleniumError = true;
821
function highlight(element) {
822
var highLightColor = "yellow";
823
if (element.originalColor == undefined) { // avoid picking up highlight
824
element.originalColor = elementGetStyle(element, "background-color");
826
elementSetStyle(element, {"backgroundColor" : highLightColor});
827
window.setTimeout(function() {
829
//if element is orphan, probably page of it has already gone, so ignore
830
if (!element.parentNode) {
833
elementSetStyle(element, {"backgroundColor" : element.originalColor});
834
} catch (e) {} // DGF unhighlighting is very dangerous and low priority
840
// for use from vs.2003 debugger
844
var line = key + "->" + obj[key];
845
line.replace("\n", " ");
851
var seenReadyStateWarning = false;
853
function openSeparateApplicationWindow(url, suppressMozillaWarning) {
854
// resize the Selenium window itself
855
window.resizeTo(1200, 500);
856
window.moveTo(window.screenX, 0);
858
var appWindow = window.open(url + '?start=true', 'main');
859
if (appWindow == null) {
860
var errorMessage = "Couldn't open app window; is the pop-up blocker enabled?"
861
LOG.error(errorMessage);
862
throw new Error("Couldn't open app window; is the pop-up blocker enabled?");
865
var windowHeight = 500;
866
if (window.outerHeight) {
867
windowHeight = window.outerHeight;
868
} else if (document.documentElement && document.documentElement.offsetHeight) {
869
windowHeight = document.documentElement.offsetHeight;
872
if (window.screenLeft && !window.screenX) window.screenX = window.screenLeft;
873
if (window.screenTop && !window.screenY) window.screenY = window.screenTop;
875
appWindow.resizeTo(1200, screen.availHeight - windowHeight - 60);
876
appWindow.moveTo(window.screenX, window.screenY + windowHeight + 25);
878
LOG.error("Couldn't resize app window");
883
if (!suppressMozillaWarning && window.document.readyState == null && !seenReadyStateWarning) {
884
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.");
885
seenReadyStateWarning = true;
891
var URLConfiguration = classCreate();
892
objectExtend(URLConfiguration.prototype, {
893
initialize: function() {
895
_isQueryParameterTrue: function (name) {
896
var parameterValue = this._getQueryParameter(name);
897
if (parameterValue == null) return false;
898
if (parameterValue.toLowerCase() == "true") return true;
899
if (parameterValue.toLowerCase() == "on") return true;
903
_getQueryParameter: function(searchKey) {
904
var str = this.queryString
905
if (str == null) return null;
906
var clauses = str.split('&');
907
for (var i = 0; i < clauses.length; i++) {
908
var keyValuePair = clauses[i].split('=', 2);
909
var key = unescape(keyValuePair[0]);
910
if (key == searchKey) {
911
return unescape(keyValuePair[1]);
917
_extractArgs: function() {
918
var str = SeleniumHTARunner.commandLine;
919
if (str == null || str == "") return new Array();
920
var matches = str.match(/(?:\"([^\"]+)\"|(?!\"([^\"]+)\")(\S+))/g);
921
// We either want non quote stuff ([^"]+) surrounded by quotes
922
// or we want to look-ahead, see that the next character isn't
923
// a quoted argument, and then grab all the non-space stuff
924
// this will return for the line: "foo" bar
925
// the results "\"foo\"" and "bar"
927
// So, let's unquote the quoted arguments:
928
var args = new Array;
929
for (var i = 0; i < matches.length; i++) {
930
args[i] = matches[i];
931
args[i] = args[i].replace(/^"(.*)"$/, "$1");
936
isMultiWindowMode:function() {
937
return this._isQueryParameterTrue('multiWindow');
940
getBaseUrl:function() {
941
return this._getQueryParameter('baseUrl');
947
function safeScrollIntoView(element) {
948
if (element.scrollIntoView) {
949
element.scrollIntoView(false);
952
// TODO: work out how to scroll browsers that don't support
953
// scrollIntoView (like Konqueror)
957
* Returns the absolute time represented as an offset of the current time.
958
* Throws a SeleniumException if timeout is invalid.
960
* @param timeout the number of milliseconds from "now" whose absolute time
963
function getTimeoutTime(timeout) {
964
var now = new Date().getTime();
965
var timeoutLength = parseInt(timeout);
967
if (isNaN(timeoutLength)) {
968
throw new SeleniumError("Timeout is not a number: '" + timeout + "'");
971
return now + timeoutLength;
975
* Returns true iff the current environment is the IDE.
979
return (typeof(SeleniumIDE) != 'undefined');
983
* Logs a message if the Logger exists, and does nothing if it doesn't exist.
985
* @param level the level to log at
986
* @param msg the message to log
988
function safe_log(level, msg)
999
* Displays a warning message to the user appropriate to the context under
1000
* which the issue is encountered. This is primarily used to avoid popping up
1001
* alert dialogs that might pause an automated test suite.
1003
* @param msg the warning message to display
1005
function safe_alert(msg)
1013
* Returns true iff the given element represents a link with a javascript
1014
* href attribute, and does not have an onclick attribute defined.
1016
* @param element the element to test
1018
function hasJavascriptHref(element) {
1019
if (getTagName(element) != 'a') {
1022
if (element.onclick) {
1025
if (! element.href) {
1028
if (! /\s*javascript:/i.test(element.href)) {
1035
* Returns the given element, or its nearest ancestor, that satisfies
1036
* hasJavascriptHref(). Returns null if none is found.
1038
* @param element the element whose ancestors to test
1040
function getAncestorOrSelfWithJavascriptHref(element) {
1041
if (hasJavascriptHref(element)) {
1044
if (element.parentNode == null) {
1047
return getAncestorOrSelfWithJavascriptHref(element.parentNode);
1050
//******************************************************************************
1051
// Locator evaluation support
1054
* Parses a Selenium locator, returning its type and the unprefixed locator
1055
* string as an object.
1057
* @param locator the locator to parse
1059
function parse_locator(locator)
1061
var result = locator.match(/^([A-Za-z]+)=(.+)/);
1063
return { type: result[1].toLowerCase(), string: result[2] };
1065
return { type: 'implicit', string: locator };
1069
* Evaluates an xpath on a document, and returns a list containing nodes in the
1070
* resulting nodeset. The browserbot xpath methods are now backed by this
1071
* function. A context node may optionally be provided, and the xpath will be
1072
* evaluated from that context.
1074
* @param xpath the xpath to evaluate
1075
* @param inDocument the document in which to evaluate the xpath.
1076
* @param opts (optional) An object containing various flags that can
1077
* modify how the xpath is evaluated. Here's a listing of
1078
* the meaningful keys:
1081
* the context node from which to evaluate the xpath. If
1082
* unspecified, the context will be the root document
1085
* namespaceResolver:
1086
* the namespace resolver function. Defaults to null.
1089
* the javascript library to use for XPath. "ajaxslt" is
1090
* the default. "javascript-xpath" is newer and faster,
1091
* but needs more testing.
1094
* whether to allow native evaluate(). Defaults to true.
1096
* ignoreAttributesWithoutValue:
1097
* whether it's ok to ignore attributes without value
1098
* when evaluating the xpath. This can greatly improve
1099
* performance in IE; however, if your xpaths depend on
1100
* such attributes, you can't ignore them! Defaults to
1103
* returnOnFirstMatch:
1104
* whether to optimize the XPath evaluation to only
1105
* return the first match. The match, if any, will still
1106
* be returned in a list. Defaults to false.
1108
function eval_xpath(xpath, inDocument, opts)
1113
var contextNode = opts.contextNode
1114
? opts.contextNode : inDocument;
1115
var namespaceResolver = opts.namespaceResolver
1116
? opts.namespaceResolver : null;
1117
var xpathLibrary = opts.xpathLibrary
1118
? opts.xpathLibrary : null;
1119
var allowNativeXpath = (opts.allowNativeXpath != undefined)
1120
? opts.allowNativeXpath : true;
1121
var ignoreAttributesWithoutValue = (opts.ignoreAttributesWithoutValue != undefined)
1122
? opts.ignoreAttributesWithoutValue : true;
1123
var returnOnFirstMatch = (opts.returnOnFirstMatch != undefined)
1124
? opts.returnOnFirstMatch : false;
1126
// Trim any trailing "/": not valid xpath, and remains from attribute
1128
if (xpath.charAt(xpath.length - 1) == '/') {
1129
xpath = xpath.slice(0, -1);
1131
// HUGE hack - remove namespace from xpath for IE
1132
if (browserVersion && browserVersion.isIE) {
1133
xpath = xpath.replace(/x:/g, '')
1136
var nativeXpathAvailable = inDocument.evaluate;
1137
var useNativeXpath = allowNativeXpath && nativeXpathAvailable;
1138
var useDocumentEvaluate = useNativeXpath;
1140
// When using the new and faster javascript-xpath library,
1141
// we'll use the TestRunner's document object, not the App-Under-Test's document.
1142
// The new library only modifies the TestRunner document with the new
1144
if (xpathLibrary == 'javascript-xpath' && !useNativeXpath) {
1145
documentForXpath = document;
1146
useDocumentEvaluate = true;
1148
documentForXpath = inDocument;
1152
// this is either native xpath or javascript-xpath via TestRunner.evaluate
1153
if (useDocumentEvaluate) {
1155
// Regarding use of the second argument to document.evaluate():
1156
// http://groups.google.com/group/comp.lang.javascript/browse_thread/thread/a59ce20639c74ba1/a9d9f53e88e5ebb5
1157
var xpathResult = documentForXpath
1158
.evaluate((contextNode == inDocument ? xpath : '.' + xpath),
1159
contextNode, namespaceResolver, 0, null);
1162
throw new SeleniumError("Invalid xpath: " + extractExceptionMessage(e));
1165
if (xpathResult == null) {
1166
// If the result is null, we should still throw an Error.
1167
throw new SeleniumError("Invalid xpath: " + xpath);
1170
var result = xpathResult.iterateNext();
1172
results.push(result);
1173
result = xpathResult.iterateNext();
1178
// If not, fall back to slower JavaScript implementation
1179
// DGF set xpathdebug = true (using getEval, if you like) to turn on JS XPath debugging
1180
//xpathdebug = true;
1182
if (contextNode == inDocument) {
1183
context = new ExprContext(inDocument);
1186
// provide false values to get the default constructor values
1187
context = new ExprContext(contextNode, false, false,
1188
contextNode.parentNode);
1190
context.setCaseInsensitive(true);
1191
context.setIgnoreAttributesWithoutValue(ignoreAttributesWithoutValue);
1192
context.setReturnOnFirstMatch(returnOnFirstMatch);
1195
xpathObj = xpathParse(xpath);
1198
throw new SeleniumError("Invalid xpath: " + extractExceptionMessage(e));
1200
var xpathResult = xpathObj.evaluate(context);
1201
if (xpathResult && xpathResult.value) {
1202
for (var i = 0; i < xpathResult.value.length; ++i) {
1203
results.push(xpathResult.value[i]);
1210
* Returns the full resultset of a CSS selector evaluation.
1212
function eval_css(locator, inDocument)
1214
return cssQuery(locator, inDocument);
1218
* This function duplicates part of BrowserBot.findElement() to open up locator
1219
* evaluation on arbitrary documents. It returns a plain old array of located
1220
* elements found by using a Selenium locator.
1222
* Multiple results may be generated for xpath and CSS locators. Even though a
1223
* list could potentially be generated for other locator types, such as link,
1224
* we don't try for them, because they aren't very expressive location
1225
* strategies; if you want a list, use xpath or CSS. Furthermore, strategies
1226
* for these locators have been optimized to only return the first result. For
1227
* these types of locators, performance is more important than ideal behavior.
1229
* @param locator a locator string
1230
* @param inDocument the document in which to apply the locator
1231
* @param opt_contextNode the context within which to evaluate the locator
1233
* @return a list of result elements
1235
function eval_locator(locator, inDocument, opt_contextNode)
1237
locator = parse_locator(locator);
1240
if (typeof(selenium) != 'undefined' && selenium != undefined) {
1241
if (typeof(editor) == 'undefined' || editor.state == 'playing') {
1242
safe_log('info', 'Trying [' + locator.type + ']: '
1245
pageBot = selenium.browserbot;
1248
if (!UI_GLOBAL.mozillaBrowserBot) {
1249
// create a browser bot to evaluate the locator. Hand it the IDE
1250
// window as a dummy window, and cache it for future use.
1251
UI_GLOBAL.mozillaBrowserBot = new MozillaBrowserBot(window)
1253
pageBot = UI_GLOBAL.mozillaBrowserBot;
1258
if (locator.type == 'xpath' || (locator.string.charAt(0) == '/' &&
1259
locator.type == 'implicit')) {
1260
results = eval_xpath(locator.string, inDocument,
1261
{ contextNode: opt_contextNode });
1263
else if (locator.type == 'css') {
1264
results = eval_css(locator.string, inDocument);
1267
var element = pageBot
1268
.findElementBy(locator.type, locator.string, inDocument);
1269
if (element != null) {
1270
results.push(element);
1277
//******************************************************************************
1281
* Escapes the special regular expression characters in a string intended to be
1282
* used as a regular expression.
1284
* Based on: http://simonwillison.net/2006/Jan/20/escape/
1286
RegExp.escape = (function() {
1288
'/', '.', '*', '+', '?', '|', '^', '$',
1289
'(', ')', '[', ']', '{', '}', '\\'
1292
var sRE = new RegExp(
1293
'(\\' + specials.join('|\\') + ')', 'g'
1296
return function(text) {
1297
return text.replace(sRE, '\\$1');
1302
* Returns true if two arrays are identical, and false otherwise.
1304
* @param a1 the first array, may only contain simple values (strings or
1306
* @param a2 the second array, same restricts on data as for a1
1307
* @return true if the arrays are equivalent, false otherwise.
1309
function are_equal(a1, a2)
1311
if (typeof(a1) != typeof(a2))
1314
switch(typeof(a1)) {
1318
if (a1.length != a2.length)
1320
for (var i = 0; i < a1.length; ++i) {
1321
if (!are_equal(a1[i], a2[i]))
1325
// associative arrays
1328
for (var key in a1) {
1331
for (var key in a2) {
1334
for (var key in keys) {
1335
if (!are_equal(a1[key], a2[key]))
1348
* Create a clone of an object and return it. This is a deep copy of everything
1349
* but functions, whose references are copied. You shouldn't expect a deep copy
1350
* of functions anyway.
1352
* @param orig the original object to copy
1353
* @return a deep copy of the original object. Any functions attached,
1354
* however, will have their references copied only.
1356
function clone(orig) {
1358
switch(typeof(orig)) {
1360
copy = (orig.length) ? [] : {};
1361
for (var attr in orig) {
1362
copy[attr] = clone(orig[attr]);
1373
* Emulates php's print_r() functionality. Returns a nicely formatted string
1374
* representation of an object. Very useful for debugging.
1376
* @param object the object to dump
1377
* @param maxDepth the maximum depth to recurse into the object. Ellipses will
1378
* be shown for objects whose depth exceeds the maximum.
1379
* @param indent the string to use for indenting progressively deeper levels
1381
* @return a string representing a dump of the object
1383
function print_r(object, maxDepth, indent)
1385
var parentIndent, attr, str = "";
1386
if (arguments.length == 1) {
1387
var maxDepth = Number.MAX_VALUE;
1391
if (arguments.length < 3) {
1395
parentIndent = indent;
1399
switch(typeof(object)) {
1401
if (object.length != undefined) {
1402
if (object.length == 0) {
1403
str += "Array ()\r\n";
1406
str += "Array (\r\n";
1407
for (var i = 0; i < object.length; ++i) {
1408
str += indent + '[' + i + '] => ';
1412
str += print_r(object[i], maxDepth, indent);
1414
str += parentIndent + ")\r\n";
1418
str += "Object (\r\n";
1419
for (attr in object) {
1420
str += indent + "[" + attr + "] => ";
1424
str += print_r(object[attr], maxDepth, indent);
1426
str += parentIndent + ")\r\n";
1430
str += (object ? 'true' : 'false') + "\r\n";
1433
str += "Function\r\n";
1436
str += object + "\r\n";
1444
* Return an array containing all properties of an object. Perl-style.
1446
* @param object the object whose keys to return
1447
* @return array of object keys, as strings
1449
function keys(object)
1452
for (var k in object) {
1459
* Emulates python's range() built-in. Returns an array of integers, counting
1460
* up (or down) from start to end. Note that the range returned is up to, but
1461
* NOT INCLUDING, end.
1463
* @param start integer from which to start counting. If the end parameter is
1464
* not provided, this value is considered the end and start will
1466
* @param end integer to which to count. If omitted, the function will count
1467
* up from zero to the value of the start parameter. Note that
1468
* the array returned will count up to but will not include this
1470
* @return an array of consecutive integers.
1472
function range(start, end)
1474
if (arguments.length == 1) {
1481
while (start != end)
1485
while (start != end)
1492
* Parses a python-style keyword arguments string and returns the pairs in a
1495
* @param kwargs a string representing a set of keyword arguments. It should
1496
* look like <tt>keyword1=value1, keyword2=value2, ...</tt>
1497
* @return an object mapping strings to strings
1499
function parse_kwargs(kwargs)
1501
var args = new Object();
1502
var pairs = kwargs.split(/,/);
1503
for (var i = 0; i < pairs.length;) {
1504
if (i > 0 && pairs[i].indexOf('=') == -1) {
1505
// the value string contained a comma. Glue the parts back together.
1506
pairs[i-1] += ',' + pairs.splice(i, 1)[0];
1512
for (var i = 0; i < pairs.length; ++i) {
1513
var splits = pairs[i].split(/=/);
1514
if (splits.length == 1) {
1517
var key = splits.shift();
1518
var value = splits.join('=');
1519
args[key.trim()] = value.trim();
1525
* Creates a python-style keyword arguments string from an object.
1527
* @param args an associative array mapping strings to strings
1528
* @param sortedKeys (optional) a list of keys of the args parameter that
1529
* specifies the order in which the arguments will appear in
1530
* the returned kwargs string
1532
* @return a kwarg string representation of args
1534
function to_kwargs(args, sortedKeys)
1538
var sortedKeys = keys(args).sort();
1540
for (var i = 0; i < sortedKeys.length; ++i) {
1541
var k = sortedKeys[i];
1542
if (args[k] != undefined) {
1546
s += k + '=' + args[k];
1553
* Returns true if a node is an ancestor node of a target node, and false
1556
* @param node the node being compared to the target node
1557
* @param target the target node
1558
* @return true if node is an ancestor node of target, false otherwise.
1560
function is_ancestor(node, target)
1562
while (target.parentNode) {
1563
target = target.parentNode;
1570
//******************************************************************************
1575
Copyright (c) 2007 Steven Levithan <stevenlevithan.com>
1577
Permission is hereby granted, free of charge, to any person obtaining a copy
1578
of this software and associated documentation files (the "Software"), to deal
1579
in the Software without restriction, including without limitation the rights
1580
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1581
copies of the Software, and to permit persons to whom the Software is
1582
furnished to do so, subject to the following conditions:
1584
The above copyright notice and this permission notice shall be included in
1585
all copies or substantial portions of the Software.
1588
function parseUri (str) {
1589
var o = parseUri.options,
1590
m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
1594
while (i--) uri[o.key[i]] = m[i] || "";
1597
uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
1598
if ($1) uri[o.q.name][$1] = $2;
1604
parseUri.options = {
1606
key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
1609
parser: /(?:^|&)([^&=]*)=?([^&]*)/g
1612
strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
1613
loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/