~ideamonk/sahana-eden/latheme

« back to all changes in this revision

Viewing changes to static/selenium/core/scripts/htmlutils.js

  • Committer: Fran Boon
  • Date: 2009-01-21 00:06:49 UTC
  • Revision ID: flavour@partyvibe.com-20090121000649-9ja31auafrrd1051
Added Selenium into static along with some initial Tests (Register/Login)
Unit Tests started via 2 approaches: SeleniumRC & WebTest (Selenium more promising)
Updated code for PyLint recommendations
Remove unused files

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*
 
2
 * Copyright 2004 ThoughtWorks, Inc
 
3
 *
 
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
 
7
 *
 
8
 *      http://www.apache.org/licenses/LICENSE-2.0
 
9
 *
 
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.
 
15
 *
 
16
 */
 
17
 
 
18
// This script contains a badly-organised collection of miscellaneous
 
19
// functions that really better homes.
 
20
 
 
21
function classCreate() {
 
22
    return function() {
 
23
      this.initialize.apply(this, arguments);
 
24
    }
 
25
}
 
26
 
 
27
function objectExtend(destination, source) {
 
28
  for (var property in source) {
 
29
    destination[property] = source[property];
 
30
  }
 
31
  return destination;
 
32
}
 
33
 
 
34
function sel$() {
 
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;
 
41
  }
 
42
  return results.length < 2 ? results[0] : results;
 
43
}
 
44
 
 
45
function sel$A(iterable) {
 
46
  if (!iterable) return [];
 
47
  if (iterable.toArray) {
 
48
    return iterable.toArray();
 
49
  } else {
 
50
    var results = [];
 
51
    for (var i = 0; i < iterable.length; i++)
 
52
      results.push(iterable[i]);
 
53
    return results;
 
54
  }
 
55
}
 
56
 
 
57
function fnBind() {
 
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)));
 
61
  }
 
62
  retval.__method = __method;
 
63
  return retval;
 
64
}
 
65
 
 
66
function fnBindAsEventListener(fn, object) {
 
67
  var __method = fn;
 
68
  return function(event) {
 
69
    return __method.call(object, event || window.event);
 
70
  }
 
71
}
 
72
 
 
73
function removeClassName(element, name) {
 
74
    var re = new RegExp("\\b" + name + "\\b", "g");
 
75
    element.className = element.className.replace(re, "");
 
76
}
 
77
 
 
78
function addClassName(element, name) {
 
79
    element.className = element.className + ' ' + name;
 
80
}
 
81
 
 
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;
 
87
    }
 
88
}
 
89
 
 
90
function elementGetStyle(element, style) {
 
91
    var value = element.style[style];
 
92
    if (!value) {
 
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];
 
98
      }
 
99
    }
 
100
 
 
101
    /** DGF necessary? 
 
102
    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
 
103
      if (Element.getStyle(element, 'position') == 'static') value = 'auto'; */
 
104
 
 
105
    return value == 'auto' ? null : value;
 
106
  }
 
107
 
 
108
String.prototype.trim = function() {
 
109
    var result = this.replace(/^\s+/g, "");
 
110
    // strip leading
 
111
    return result.replace(/\s+$/g, "");
 
112
    // strip trailing
 
113
};
 
114
String.prototype.lcfirst = function() {
 
115
    return this.charAt(0).toLowerCase() + this.substr(1);
 
116
};
 
117
String.prototype.ucfirst = function() {
 
118
    return this.charAt(0).toUpperCase() + this.substr(1);
 
119
};
 
120
String.prototype.startsWith = function(str) {
 
121
    return this.indexOf(str) == 0;
 
122
};
 
123
 
 
124
/**
 
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.
 
128
 */
 
129
String.prototype.quoteForXPath = function()
 
130
{
 
131
    if (/\'/.test(this)) {
 
132
        if (/\"/.test(this)) {
 
133
            // concat scenario
 
134
            var pieces = [];
 
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
 
140
                    if (j < i) {
 
141
                        pieces.push(a + this.substring(j, i) + a);
 
142
                        j = i;
 
143
                    }
 
144
                    c = a;
 
145
                    a = b;
 
146
                    b = c;
 
147
                }
 
148
                else {
 
149
                    ++i;
 
150
                }
 
151
            }
 
152
            pieces.push(a + this.substring(j) + a);
 
153
            return 'concat(' + pieces.join(', ') + ')';
 
154
        }
 
155
        else {
 
156
            // quote with doubles
 
157
            return '"' + this + '"';
 
158
        }
 
159
    }
 
160
    // quote with singles
 
161
    return "'" + this + "'";
 
162
};
 
163
 
 
164
// Returns the text in this element
 
165
function getText(element) {
 
166
    var text = "";
 
167
 
 
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;
 
175
    }
 
176
 
 
177
    text = normalizeNewlines(text);
 
178
    text = normalizeSpaces(text);
 
179
 
 
180
    return text.trim();
 
181
}
 
182
 
 
183
function getTextContent(element, preformatted) {
 
184
    if (element.nodeType == 3 /*Node.TEXT_NODE*/) {
 
185
        var text = element.data;
 
186
        if (!preformatted) {
 
187
            text = text.replace(/\n|\r|\t/g, " ");
 
188
        }
 
189
        return text;
 
190
    }
 
191
    if (element.nodeType == 1 /*Node.ELEMENT_NODE*/) {
 
192
        var childrenPreformatted = preformatted || (element.tagName == "PRE");
 
193
        var text = "";
 
194
        for (var i = 0; i < element.childNodes.length; i++) {
 
195
            var child = element.childNodes.item(i);
 
196
            text += getTextContent(child, childrenPreformatted);
 
197
        }
 
198
        // Handle block elements that introduce newlines
 
199
        // -- From HTML spec:
 
200
        //<!ENTITY % block
 
201
        //     "P | %heading; | %list; | %preformatted; | DL | DIV | NOSCRIPT |
 
202
        //      BLOCKQUOTE | F:wORM | HR | TABLE | FIELDSET | ADDRESS">
 
203
        //
 
204
        // TODO: should potentially introduce multiple newlines to separate blocks
 
205
        if (element.tagName == "P" || element.tagName == "BR" || element.tagName == "HR" || element.tagName == "DIV") {
 
206
            text += "\n";
 
207
        }
 
208
        return text;
 
209
    }
 
210
    return '';
 
211
}
 
212
 
 
213
/**
 
214
 * Convert all newlines to \n
 
215
 */
 
216
function normalizeNewlines(text)
 
217
{
 
218
    return text.replace(/\r\n|\r/g, "\n");
 
219
}
 
220
 
 
221
/**
 
222
 * Replace multiple sequential spaces with a single space, and then convert &nbsp; to space.
 
223
 */
 
224
function normalizeSpaces(text)
 
225
{
 
226
    // IE has already done this conversion, so doing it again will remove multiple nbsp
 
227
    if (browserVersion.isIE)
 
228
    {
 
229
        return text;
 
230
    }
 
231
 
 
232
    // Replace multiple spaces with a single space
 
233
    // TODO - this shouldn't occur inside PRE elements
 
234
    text = text.replace(/\ +/g, " ");
 
235
 
 
236
    // Replace &nbsp; with a space
 
237
    var nbspPattern = new RegExp(String.fromCharCode(160), "g");
 
238
    if (browserVersion.isSafari) {
 
239
        return replaceAll(text, String.fromCharCode(160), " ");
 
240
    } else {
 
241
        return text.replace(nbspPattern, " ");
 
242
    }
 
243
}
 
244
 
 
245
function replaceAll(text, oldText, newText) {
 
246
    while (text.indexOf(oldText) != -1) {
 
247
        text = text.replace(oldText, newText);
 
248
    }
 
249
    return text;
 
250
}
 
251
 
 
252
 
 
253
function xmlDecode(text) {
 
254
    text = text.replace(/&quot;/g, '"');
 
255
    text = text.replace(/&apos;/g, "'");
 
256
    text = text.replace(/&lt;/g, "<");
 
257
    text = text.replace(/&gt;/g, ">");
 
258
    text = text.replace(/&amp;/g, "&");
 
259
    return text;
 
260
}
 
261
 
 
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;
 
268
    }
 
269
}
 
270
 
 
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')
 
276
        {
 
277
            return (inputElement.checked ? 'on' : 'off');
 
278
        }
 
279
    }
 
280
    if (inputElement.value == null) {
 
281
        throw new SeleniumError("This element has no value; is it really a form field?");
 
282
    }
 
283
    return inputElement.value;
 
284
}
 
285
 
 
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);
 
292
    }
 
293
    else {
 
294
        var evt = document.createEvent('HTMLEvents');
 
295
        
 
296
        try {
 
297
            evt.shiftKey = shiftKeyDown;
 
298
            evt.metaKey = metaKeyDown;
 
299
            evt.altKey = altKeyDown;
 
300
            evt.ctrlKey = controlKeyDown;
 
301
        } catch (e) {
 
302
            // On Firefox 1.0, you can only set these during initMouseEvent or initKeyEvent
 
303
            // we'll have to ignore them here
 
304
            LOG.exception(e);
 
305
        }
 
306
        
 
307
        evt.initEvent(eventType, canBubble, true);
 
308
        element.dispatchEvent(evt);
 
309
    }
 
310
}
 
311
 
 
312
function getKeyCodeFromKeySequence(keySequence) {
 
313
    var match = /^\\(\d{1,3})$/.exec(keySequence);
 
314
    if (match != null) {
 
315
        return match[1];
 
316
    }
 
317
    match = /^.$/.exec(keySequence);
 
318
    if (match != null) {
 
319
        return match[0].charCodeAt(0);
 
320
    }
 
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);
 
324
    if (match != null) {
 
325
        return match[0];
 
326
    }
 
327
    throw new SeleniumError("invalid keySequence");
 
328
}
 
329
 
 
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;
 
336
     return evt;
 
337
}
 
338
 
 
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);
 
346
    }
 
347
    else {
 
348
        var evt;
 
349
        if (window.KeyEvent) {
 
350
            evt = document.createEvent('KeyEvents');
 
351
            evt.initKeyEvent(eventType, true, true, window, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown, keycode, keycode);
 
352
        } else {
 
353
            evt = document.createEvent('UIEvents');
 
354
            
 
355
            evt.shiftKey = shiftKeyDown;
 
356
            evt.metaKey = metaKeyDown;
 
357
            evt.altKey = altKeyDown;
 
358
            evt.ctrlKey = controlKeyDown;
 
359
 
 
360
            evt.initUIEvent(eventType, true, true, window, 1);
 
361
            evt.keyCode = keycode;
 
362
            evt.which = keycode;
 
363
        }
 
364
 
 
365
        element.dispatchEvent(evt);
 
366
    }
 
367
}
 
368
 
 
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);
 
375
}
 
376
 
 
377
function addLoadListener(element, command) {
 
378
    LOG.debug('Adding loadListenter for ' + element + ', ' + command);
 
379
    var augmentedCommand = function() {
 
380
        command.call(this, element);
 
381
    }
 
382
    if (window.addEventListener && !browserVersion.isOpera)
 
383
        element.addEventListener("load", augmentedCommand, true);
 
384
    else if (window.attachEvent)
 
385
        element.attachEvent("onload", augmentedCommand);
 
386
}
 
387
 
 
388
/**
 
389
 * Override the broken getFunctionName() method from JsUnit
 
390
 * This file must be loaded _after_ the jsunitCore.js
 
391
 */
 
392
function getFunctionName(aFunction) {
 
393
    var regexpResult = aFunction.toString().match(/function (\w*)/);
 
394
    if (regexpResult && regexpResult[1]) {
 
395
        return regexpResult[1];
 
396
    }
 
397
    return 'anonymous';
 
398
}
 
399
 
 
400
function getDocumentBase(doc) {
 
401
    var bases = document.getElementsByTagName("base");
 
402
    if (bases && bases.length && bases[0].href) {
 
403
        return bases[0].href;
 
404
    }
 
405
    return "";
 
406
}
 
407
 
 
408
function getTagName(element) {
 
409
    var tagName;
 
410
    if (element && element.tagName && element.tagName.toLowerCase) {
 
411
        tagName = element.tagName.toLowerCase();
 
412
    }
 
413
    return tagName;
 
414
}
 
415
 
 
416
function selArrayToString(a) {
 
417
    if (isArray(a)) {
 
418
        // DGF copying the array, because the array-like object may be a non-modifiable nodelist
 
419
        var retval = [];
 
420
        for (var i = 0; i < a.length; i++) {
 
421
            var item = a[i];
 
422
            var replaced = new String(item).replace(/([,\\])/g, '\\$1');
 
423
            retval[i] = replaced;
 
424
        }
 
425
        return retval;
 
426
    }
 
427
    return new String(a);
 
428
}
 
429
 
 
430
 
 
431
function isArray(x) {
 
432
    return ((typeof x) == "object") && (x["length"] != null);
 
433
}
 
434
 
 
435
function absolutify(url, baseUrl) {
 
436
    /** returns a relative url in its absolute form, given by baseUrl.
 
437
    * 
 
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
    * &lt;base&gt; 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
 
442
    * base directory.
 
443
    *
 
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.
 
447
    * 
 
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.
 
451
    */
 
452
    
 
453
    // DGF isn't there some library we could use for this?
 
454
        
 
455
    if (/^\w+:/.test(url)) {
 
456
        // it's already absolute
 
457
        return url;
 
458
    }
 
459
    
 
460
    var loc;
 
461
    try {
 
462
        loc = parseUrl(baseUrl);
 
463
    } catch (e) {
 
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);
 
468
        } else {
 
469
            throw new SeleniumError("baseUrl wasn't absolute: " + baseUrl);
 
470
        }
 
471
    }
 
472
    loc.search = null;
 
473
    loc.hash = null;
 
474
    
 
475
    // if url begins with /, then that's the whole pathname
 
476
    if (/^\//.test(url)) {
 
477
        loc.pathname = url;
 
478
        var result = reassembleLocation(loc);
 
479
        return result;
 
480
    }
 
481
    
 
482
    // if pathname is null, then we'll just append "/" + the url
 
483
    if (!loc.pathname) {
 
484
        loc.pathname = "/" + url;
 
485
        var result = reassembleLocation(loc);
 
486
        return result;
 
487
    }
 
488
    
 
489
    // if pathname ends with /, just append url
 
490
    if (/\/$/.test(loc.pathname)) {
 
491
        loc.pathname += url;
 
492
        var result = reassembleLocation(loc);
 
493
        return result;
 
494
    }
 
495
    
 
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);
 
500
    return result;
 
501
    
 
502
}
 
503
 
 
504
var URL_REGEX = /^((\w+):\/\/)(([^:]+):?([^@]+)?@)?([^\/\?:]*):?(\d+)?(\/?[^\?#]+)?\??([^#]+)?#?(.+)?/;
 
505
 
 
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);
 
509
    if (!result) {
 
510
        throw new SeleniumError("Invalid URL: " + url);
 
511
    }
 
512
    var loc = new Object();
 
513
    for (var i = 0; i < fields.length; i++) {
 
514
        var field = fields[i];
 
515
        if (field == null) {
 
516
            continue;
 
517
        }
 
518
        loc[field] = result[i];
 
519
    }
 
520
    return loc;
 
521
}
 
522
 
 
523
function reassembleLocation(loc) {
 
524
    if (!loc.protocol) {
 
525
        throw new Error("Not a valid location object: " + o2s(loc));
 
526
    }
 
527
    var protocol = loc.protocol;
 
528
    protocol = protocol.replace(/:$/, "");
 
529
    var url = protocol + "://";
 
530
    if (loc.username) {
 
531
        url += loc.username;
 
532
        if (loc.password) {
 
533
            url += ":" + loc.password;
 
534
        }
 
535
        url += "@";
 
536
    }
 
537
    if (loc.host) {
 
538
        url += loc.host;
 
539
    }
 
540
    
 
541
    if (loc.port) {
 
542
        url += ":" + loc.port;
 
543
    }
 
544
    
 
545
    if (loc.pathname) {
 
546
        url += loc.pathname;
 
547
    }
 
548
    
 
549
    if (loc.search) {
 
550
        url += "?" + loc.search;
 
551
    }
 
552
    if (loc.hash) {
 
553
        var hash = loc.hash;
 
554
        hash = loc.hash.replace(/^#/, "");
 
555
        url += "#" + hash;
 
556
    }
 
557
    return url;
 
558
}
 
559
 
 
560
function canonicalize(url) {
 
561
    if(url == "about:blank")
 
562
    {
 
563
        return url;
 
564
    }
 
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;
 
570
    }
 
571
        // didn't work... let's try it the hard way
 
572
        var originalParts = loc.pathname.split("/");
 
573
        var newParts = [];
 
574
        newParts.push(originalParts.shift());
 
575
        for (var i = 0; i < originalParts.length; i++) {
 
576
                var part = originalParts[i];
 
577
                if (".." == part) {
 
578
                        newParts.pop();
 
579
                        continue;
 
580
                }
 
581
                newParts.push(part);
 
582
        }
 
583
        loc.pathname = newParts.join("/");
 
584
    return reassembleLocation(loc);
 
585
}
 
586
 
 
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();
 
591
}
 
592
    
 
593
 
 
594
function describe(object, delimiter) {
 
595
    var props = new Array();
 
596
    for (var prop in object) {
 
597
        try {
 
598
            props.push(prop + " -> " + object[prop]);
 
599
        } catch (e) {
 
600
            props.push(prop + " -> [htmlutils: ack! couldn't read this property! (Permission Denied?)]");
 
601
        }
 
602
    }
 
603
    return props.join(delimiter || '\n');
 
604
}
 
605
 
 
606
var PatternMatcher = function(pattern) {
 
607
    this.selectStrategy(pattern);
 
608
};
 
609
PatternMatcher.prototype = {
 
610
 
 
611
    selectStrategy: function(pattern) {
 
612
        this.pattern = pattern;
 
613
        var strategyName = 'glob';
 
614
        // by default
 
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;
 
621
            }
 
622
        }
 
623
        var matchStrategy = PatternMatcher.strategies[strategyName];
 
624
        if (!matchStrategy) {
 
625
            throw new SeleniumError("cannot find PatternMatcher.strategies." + strategyName);
 
626
        }
 
627
        this.strategy = matchStrategy;
 
628
        this.matcher = new matchStrategy(pattern);
 
629
    },
 
630
 
 
631
    matches: function(actual) {
 
632
        return this.matcher.matches(actual + '');
 
633
        // Note: appending an empty string avoids a Konqueror bug
 
634
    }
 
635
 
 
636
};
 
637
 
 
638
/**
 
639
 * A "static" convenience method for easy matching
 
640
 */
 
641
PatternMatcher.matches = function(pattern, actual) {
 
642
    return new PatternMatcher(pattern).matches(actual);
 
643
};
 
644
 
 
645
PatternMatcher.strategies = {
 
646
 
 
647
/**
 
648
 * Exact matching, e.g. "exact:***"
 
649
 */
 
650
    exact: function(expected) {
 
651
        this.expected = expected;
 
652
        this.matches = function(actual) {
 
653
            return actual == this.expected;
 
654
        };
 
655
    },
 
656
 
 
657
/**
 
658
 * Match by regular expression, e.g. "regexp:^[0-9]+$"
 
659
 */
 
660
    regexp: function(regexpString) {
 
661
        this.regexp = new RegExp(regexpString);
 
662
        this.matches = function(actual) {
 
663
            return this.regexp.test(actual);
 
664
        };
 
665
    },
 
666
 
 
667
    regex: function(regexpString) {
 
668
        this.regexp = new RegExp(regexpString);
 
669
        this.matches = function(actual) {
 
670
            return this.regexp.test(actual);
 
671
        };
 
672
    },
 
673
    
 
674
    regexpi: function(regexpString) {
 
675
        this.regexp = new RegExp(regexpString, "i");
 
676
        this.matches = function(actual) {
 
677
            return this.regexp.test(actual);
 
678
        };
 
679
    },
 
680
 
 
681
    regexi: function(regexpString) {
 
682
        this.regexp = new RegExp(regexpString, "i");
 
683
        this.matches = function(actual) {
 
684
            return this.regexp.test(actual);
 
685
        };
 
686
    },
 
687
 
 
688
/**
 
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.
 
698
 */
 
699
    globContains: function(globString) {
 
700
        this.regexp = new RegExp(PatternMatcher.regexpFromGlobContains(globString));
 
701
        this.matches = function(actual) {
 
702
            return this.regexp.test(actual);
 
703
        };
 
704
    },
 
705
 
 
706
 
 
707
/**
 
708
 * "glob" (aka "wildmat") patterns, e.g. "glob:one,two,*"
 
709
 */
 
710
    glob: function(globString) {
 
711
        this.regexp = new RegExp(PatternMatcher.regexpFromGlob(globString));
 
712
        this.matches = function(actual) {
 
713
            return this.regexp.test(actual);
 
714
        };
 
715
    }
 
716
 
 
717
};
 
718
 
 
719
PatternMatcher.convertGlobMetaCharsToRegexpMetaChars = function(glob) {
 
720
    var re = glob;
 
721
    re = re.replace(/([.^$+(){}\[\]\\|])/g, "\\$1");
 
722
    re = re.replace(/\?/g, "(.|[\r\n])");
 
723
    re = re.replace(/\*/g, "(.|[\r\n])*");
 
724
    return re;
 
725
};
 
726
 
 
727
PatternMatcher.regexpFromGlobContains = function(globContains) {
 
728
    return PatternMatcher.convertGlobMetaCharsToRegexpMetaChars(globContains);
 
729
};
 
730
 
 
731
PatternMatcher.regexpFromGlob = function(glob) {
 
732
    return "^" + PatternMatcher.convertGlobMetaCharsToRegexpMetaChars(glob) + "$";
 
733
};
 
734
 
 
735
if (!this["Assert"]) Assert = {};
 
736
 
 
737
 
 
738
Assert.fail = function(message) {
 
739
    throw new AssertionFailedError(message);
 
740
};
 
741
 
 
742
/*
 
743
* Assert.equals(comment?, expected, actual)
 
744
*/
 
745
Assert.equals = function() {
 
746
    var args = new AssertionArguments(arguments);
 
747
    if (args.expected === args.actual) {
 
748
        return;
 
749
    }
 
750
    Assert.fail(args.comment +
 
751
                "Expected '" + args.expected +
 
752
                "' but was '" + args.actual + "'");
 
753
};
 
754
 
 
755
Assert.assertEquals = Assert.equals;
 
756
 
 
757
/*
 
758
* Assert.matches(comment?, pattern, actual)
 
759
*/
 
760
Assert.matches = function() {
 
761
    var args = new AssertionArguments(arguments);
 
762
    if (PatternMatcher.matches(args.expected, args.actual)) {
 
763
        return;
 
764
    }
 
765
    Assert.fail(args.comment +
 
766
                "Actual value '" + args.actual +
 
767
                "' did not match '" + args.expected + "'");
 
768
}
 
769
 
 
770
/*
 
771
* Assert.notMtches(comment?, pattern, actual)
 
772
*/
 
773
Assert.notMatches = function() {
 
774
    var args = new AssertionArguments(arguments);
 
775
    if (!PatternMatcher.matches(args.expected, args.actual)) {
 
776
        return;
 
777
    }
 
778
    Assert.fail(args.comment +
 
779
                "Actual value '" + args.actual +
 
780
                "' did match '" + args.expected + "'");
 
781
}
 
782
 
 
783
 
 
784
// Preprocess the arguments to allow for an optional comment.
 
785
function AssertionArguments(args) {
 
786
    if (args.length == 2) {
 
787
        this.comment = "";
 
788
        this.expected = args[0];
 
789
        this.actual = args[1];
 
790
    } else {
 
791
        this.comment = args[0] + "; ";
 
792
        this.expected = args[1];
 
793
        this.actual = args[2];
 
794
    }
 
795
}
 
796
 
 
797
function AssertionFailedError(message) {
 
798
    this.isAssertionFailedError = true;
 
799
    this.isSeleniumError = true;
 
800
    this.message = message;
 
801
    this.failureMessage = message;
 
802
}
 
803
 
 
804
function SeleniumError(message) {
 
805
    var error = new Error(message);
 
806
    if (typeof(arguments.caller) != 'undefined') { // IE, not ECMA
 
807
        var result = '';
 
808
        for (var a = arguments.caller; a != null; a = a.caller) {
 
809
            result += '> ' + a.callee.toString() + '\n';
 
810
            if (a.caller == a) {
 
811
                result += '*';
 
812
                break;
 
813
            }
 
814
        }
 
815
        error.stack = result;
 
816
    }
 
817
    error.isSeleniumError = true;
 
818
    return error;
 
819
}
 
820
 
 
821
function highlight(element) {
 
822
    var highLightColor = "yellow";
 
823
    if (element.originalColor == undefined) { // avoid picking up highlight
 
824
        element.originalColor = elementGetStyle(element, "background-color");
 
825
    }
 
826
    elementSetStyle(element, {"backgroundColor" : highLightColor});
 
827
    window.setTimeout(function() {
 
828
        try {
 
829
            //if element is orphan, probably page of it has already gone, so ignore
 
830
            if (!element.parentNode) {
 
831
                return;
 
832
            }
 
833
            elementSetStyle(element, {"backgroundColor" : element.originalColor});
 
834
        } catch (e) {} // DGF unhighlighting is very dangerous and low priority
 
835
    }, 200);
 
836
}
 
837
 
 
838
 
 
839
 
 
840
// for use from vs.2003 debugger
 
841
function o2s(obj) {
 
842
    var s = "";
 
843
    for (key in obj) {
 
844
        var line = key + "->" + obj[key];
 
845
        line.replace("\n", " ");
 
846
        s += line + "\n";
 
847
    }
 
848
    return s;
 
849
}
 
850
 
 
851
var seenReadyStateWarning = false;
 
852
 
 
853
function openSeparateApplicationWindow(url, suppressMozillaWarning) {
 
854
    // resize the Selenium window itself
 
855
    window.resizeTo(1200, 500);
 
856
    window.moveTo(window.screenX, 0);
 
857
 
 
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?");
 
863
    }
 
864
    try {
 
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;
 
870
        }
 
871
 
 
872
        if (window.screenLeft && !window.screenX) window.screenX = window.screenLeft;
 
873
        if (window.screenTop && !window.screenY) window.screenY = window.screenTop;
 
874
 
 
875
        appWindow.resizeTo(1200, screen.availHeight - windowHeight - 60);
 
876
        appWindow.moveTo(window.screenX, window.screenY + windowHeight + 25);
 
877
    } catch (e) {
 
878
        LOG.error("Couldn't resize app window");
 
879
        LOG.exception(e);
 
880
    }
 
881
 
 
882
 
 
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;
 
886
    }
 
887
 
 
888
    return appWindow;
 
889
}
 
890
 
 
891
var URLConfiguration = classCreate();
 
892
objectExtend(URLConfiguration.prototype, {
 
893
    initialize: function() {
 
894
    },
 
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;
 
900
        return false;
 
901
    },
 
902
 
 
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]);
 
912
            }
 
913
        }
 
914
        return null;
 
915
    },
 
916
 
 
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"
 
926
 
 
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");
 
932
        }
 
933
        return args;
 
934
    },
 
935
 
 
936
    isMultiWindowMode:function() {
 
937
        return this._isQueryParameterTrue('multiWindow');
 
938
    },
 
939
    
 
940
    getBaseUrl:function() {
 
941
        return this._getQueryParameter('baseUrl');
 
942
            
 
943
    }
 
944
});
 
945
 
 
946
 
 
947
function safeScrollIntoView(element) {
 
948
    if (element.scrollIntoView) {
 
949
        element.scrollIntoView(false);
 
950
        return;
 
951
    }
 
952
    // TODO: work out how to scroll browsers that don't support
 
953
    // scrollIntoView (like Konqueror)
 
954
}
 
955
 
 
956
/**
 
957
 * Returns the absolute time represented as an offset of the current time.
 
958
 * Throws a SeleniumException if timeout is invalid.
 
959
 *
 
960
 * @param timeout  the number of milliseconds from "now" whose absolute time
 
961
 *                 to return
 
962
 */
 
963
function getTimeoutTime(timeout) {
 
964
    var now = new Date().getTime();
 
965
    var timeoutLength = parseInt(timeout);
 
966
    
 
967
    if (isNaN(timeoutLength)) {
 
968
        throw new SeleniumError("Timeout is not a number: '" + timeout + "'");
 
969
    }
 
970
    
 
971
    return now + timeoutLength;
 
972
}
 
973
 
 
974
/**
 
975
 * Returns true iff the current environment is the IDE.
 
976
 */
 
977
function is_IDE()
 
978
{
 
979
    return (typeof(SeleniumIDE) != 'undefined');
 
980
}
 
981
 
 
982
/**
 
983
 * Logs a message if the Logger exists, and does nothing if it doesn't exist.
 
984
 *
 
985
 * @param level  the level to log at
 
986
 * @param msg    the message to log
 
987
 */
 
988
function safe_log(level, msg)
 
989
{
 
990
    try {
 
991
        LOG[level](msg);
 
992
    }
 
993
    catch (e) {
 
994
        // couldn't log!
 
995
    }
 
996
}
 
997
 
 
998
/**
 
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.
 
1002
 *
 
1003
 * @param msg  the warning message to display
 
1004
 */
 
1005
function safe_alert(msg)
 
1006
{
 
1007
    if (is_IDE()) {
 
1008
        alert(msg);
 
1009
    }
 
1010
}
 
1011
 
 
1012
/**
 
1013
 * Returns true iff the given element represents a link with a javascript
 
1014
 * href attribute, and does not have an onclick attribute defined.
 
1015
 *
 
1016
 * @param element  the element to test
 
1017
 */
 
1018
function hasJavascriptHref(element) {
 
1019
    if (getTagName(element) != 'a') {
 
1020
        return false;
 
1021
    }
 
1022
    if (element.onclick) {
 
1023
        return false;
 
1024
    }
 
1025
    if (! element.href) {
 
1026
        return false;
 
1027
    }
 
1028
    if (! /\s*javascript:/i.test(element.href)) {
 
1029
        return false;
 
1030
    }
 
1031
    return true;
 
1032
}
 
1033
 
 
1034
/**
 
1035
 * Returns the given element, or its nearest ancestor, that satisfies
 
1036
 * hasJavascriptHref(). Returns null if none is found.
 
1037
 *
 
1038
 * @param element  the element whose ancestors to test
 
1039
 */
 
1040
function getAncestorOrSelfWithJavascriptHref(element) {
 
1041
    if (hasJavascriptHref(element)) {
 
1042
        return element;
 
1043
    }
 
1044
    if (element.parentNode == null) {
 
1045
        return null;
 
1046
    }
 
1047
    return getAncestorOrSelfWithJavascriptHref(element.parentNode);
 
1048
}
 
1049
 
 
1050
//******************************************************************************
 
1051
// Locator evaluation support
 
1052
 
 
1053
/**
 
1054
 * Parses a Selenium locator, returning its type and the unprefixed locator
 
1055
 * string as an object.
 
1056
 *
 
1057
 * @param locator  the locator to parse
 
1058
 */
 
1059
function parse_locator(locator)
 
1060
{
 
1061
    var result = locator.match(/^([A-Za-z]+)=(.+)/);
 
1062
    if (result) {
 
1063
        return { type: result[1].toLowerCase(), string: result[2] };
 
1064
    }
 
1065
    return { type: 'implicit', string: locator };
 
1066
}
 
1067
 
 
1068
/**
 
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.
 
1073
 *
 
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:
 
1079
 *
 
1080
 *                     contextNode: 
 
1081
 *                       the context node from which to evaluate the xpath. If
 
1082
 *                       unspecified, the context will be the root document
 
1083
 *                       element.
 
1084
 *
 
1085
 *                     namespaceResolver:
 
1086
 *                       the namespace resolver function. Defaults to null.
 
1087
 *
 
1088
 *                     xpathLibrary:
 
1089
 *                       the javascript library to use for XPath. "ajaxslt" is
 
1090
 *                       the default. "javascript-xpath" is newer and faster,
 
1091
 *                       but needs more testing.
 
1092
 *
 
1093
 *                     allowNativeXpath:
 
1094
 *                       whether to allow native evaluate(). Defaults to true.
 
1095
 *
 
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
 
1101
 *                       true.
 
1102
 *
 
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.
 
1107
 */
 
1108
function eval_xpath(xpath, inDocument, opts)
 
1109
{
 
1110
    if (!opts) {
 
1111
        var opts = {};
 
1112
    }
 
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;
 
1125
 
 
1126
    // Trim any trailing "/": not valid xpath, and remains from attribute
 
1127
    // locator.
 
1128
    if (xpath.charAt(xpath.length - 1) == '/') {
 
1129
        xpath = xpath.slice(0, -1);
 
1130
    }
 
1131
    // HUGE hack - remove namespace from xpath for IE
 
1132
    if (browserVersion && browserVersion.isIE) {
 
1133
        xpath = xpath.replace(/x:/g, '')
 
1134
    }
 
1135
    
 
1136
    var nativeXpathAvailable = inDocument.evaluate;
 
1137
    var useNativeXpath = allowNativeXpath && nativeXpathAvailable;
 
1138
    var useDocumentEvaluate = useNativeXpath;
 
1139
 
 
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 
 
1143
    // functionality.
 
1144
    if (xpathLibrary == 'javascript-xpath' && !useNativeXpath) {
 
1145
        documentForXpath = document;
 
1146
        useDocumentEvaluate = true;
 
1147
    } else {
 
1148
        documentForXpath = inDocument;
 
1149
    }
 
1150
    var results = [];
 
1151
    
 
1152
    // this is either native xpath or javascript-xpath via TestRunner.evaluate 
 
1153
    if (useDocumentEvaluate) {
 
1154
        try {
 
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);
 
1160
        }
 
1161
        catch (e) {
 
1162
            throw new SeleniumError("Invalid xpath: " + extractExceptionMessage(e));
 
1163
        }
 
1164
        finally{
 
1165
            if (xpathResult == null) {
 
1166
                // If the result is null, we should still throw an Error.
 
1167
                throw new SeleniumError("Invalid xpath: " + xpath); 
 
1168
            }
 
1169
        }
 
1170
        var result = xpathResult.iterateNext();
 
1171
        while (result) {
 
1172
            results.push(result);
 
1173
            result = xpathResult.iterateNext();
 
1174
        }
 
1175
        return results;
 
1176
    }
 
1177
 
 
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;
 
1181
    var context;
 
1182
    if (contextNode == inDocument) {
 
1183
        context = new ExprContext(inDocument);
 
1184
    }
 
1185
    else {
 
1186
        // provide false values to get the default constructor values
 
1187
        context = new ExprContext(contextNode, false, false,
 
1188
            contextNode.parentNode);
 
1189
    }
 
1190
    context.setCaseInsensitive(true);
 
1191
    context.setIgnoreAttributesWithoutValue(ignoreAttributesWithoutValue);
 
1192
    context.setReturnOnFirstMatch(returnOnFirstMatch);
 
1193
    var xpathObj;
 
1194
    try {
 
1195
        xpathObj = xpathParse(xpath);
 
1196
    }
 
1197
    catch (e) {
 
1198
        throw new SeleniumError("Invalid xpath: " + extractExceptionMessage(e));
 
1199
    }
 
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]);
 
1204
        }
 
1205
    }
 
1206
    return results;
 
1207
}
 
1208
 
 
1209
/**
 
1210
 * Returns the full resultset of a CSS selector evaluation.
 
1211
 */
 
1212
function eval_css(locator, inDocument)
 
1213
{
 
1214
    return cssQuery(locator, inDocument);
 
1215
}
 
1216
 
 
1217
/**
 
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.
 
1221
 * 
 
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.
 
1228
 * 
 
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
 
1232
 *
 
1233
 * @return  a list of result elements
 
1234
 */
 
1235
function eval_locator(locator, inDocument, opt_contextNode)
 
1236
{
 
1237
    locator = parse_locator(locator);
 
1238
    
 
1239
    var pageBot;
 
1240
    if (typeof(selenium) != 'undefined' && selenium != undefined) {
 
1241
        if (typeof(editor) == 'undefined' || editor.state == 'playing') {
 
1242
            safe_log('info', 'Trying [' + locator.type + ']: '
 
1243
                + locator.string);
 
1244
        }
 
1245
        pageBot = selenium.browserbot;
 
1246
    }
 
1247
    else {
 
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)
 
1252
        }
 
1253
        pageBot = UI_GLOBAL.mozillaBrowserBot;
 
1254
    }
 
1255
    
 
1256
    var results = [];
 
1257
    
 
1258
    if (locator.type == 'xpath' || (locator.string.charAt(0) == '/' &&
 
1259
        locator.type == 'implicit')) {
 
1260
        results = eval_xpath(locator.string, inDocument,
 
1261
            { contextNode: opt_contextNode });
 
1262
    }
 
1263
    else if (locator.type == 'css') {
 
1264
        results = eval_css(locator.string, inDocument);
 
1265
    }
 
1266
    else {
 
1267
        var element = pageBot
 
1268
            .findElementBy(locator.type, locator.string, inDocument);
 
1269
        if (element != null) {
 
1270
            results.push(element);
 
1271
        }
 
1272
    }
 
1273
    
 
1274
    return results;
 
1275
}
 
1276
 
 
1277
//******************************************************************************
 
1278
// UI-Element
 
1279
 
 
1280
/**
 
1281
 * Escapes the special regular expression characters in a string intended to be
 
1282
 * used as a regular expression.
 
1283
 *
 
1284
 * Based on: http://simonwillison.net/2006/Jan/20/escape/
 
1285
 */
 
1286
RegExp.escape = (function() {
 
1287
    var specials = [
 
1288
        '/', '.', '*', '+', '?', '|', '^', '$',
 
1289
        '(', ')', '[', ']', '{', '}', '\\'
 
1290
    ];
 
1291
    
 
1292
    var sRE = new RegExp(
 
1293
        '(\\' + specials.join('|\\') + ')', 'g'
 
1294
    );
 
1295
  
 
1296
    return function(text) {
 
1297
        return text.replace(sRE, '\\$1');
 
1298
    }
 
1299
})();
 
1300
 
 
1301
/**
 
1302
 * Returns true if two arrays are identical, and false otherwise.
 
1303
 *
 
1304
 * @param a1  the first array, may only contain simple values (strings or
 
1305
 *            numbers)
 
1306
 * @param a2  the second array, same restricts on data as for a1
 
1307
 * @return    true if the arrays are equivalent, false otherwise.
 
1308
 */
 
1309
function are_equal(a1, a2)
 
1310
{
 
1311
    if (typeof(a1) != typeof(a2))
 
1312
        return false;
 
1313
    
 
1314
    switch(typeof(a1)) {
 
1315
        case 'object':
 
1316
            // arrays
 
1317
            if (a1.length) {
 
1318
                if (a1.length != a2.length)
 
1319
                    return false;
 
1320
                for (var i = 0; i < a1.length; ++i) {
 
1321
                    if (!are_equal(a1[i], a2[i]))
 
1322
                        return false
 
1323
                }
 
1324
            }
 
1325
            // associative arrays
 
1326
            else {
 
1327
                var keys = {};
 
1328
                for (var key in a1) {
 
1329
                    keys[key] = true;
 
1330
                }
 
1331
                for (var key in a2) {
 
1332
                    keys[key] = true;
 
1333
                }
 
1334
                for (var key in keys) {
 
1335
                    if (!are_equal(a1[key], a2[key]))
 
1336
                        return false;
 
1337
                }
 
1338
            }
 
1339
            return true;
 
1340
            
 
1341
        default:
 
1342
            return a1 == a2;
 
1343
    }
 
1344
}
 
1345
 
 
1346
 
 
1347
/**
 
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.
 
1351
 *
 
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.
 
1355
 */
 
1356
function clone(orig) {
 
1357
    var copy;
 
1358
    switch(typeof(orig)) {
 
1359
        case 'object':
 
1360
            copy = (orig.length) ? [] : {};
 
1361
            for (var attr in orig) {
 
1362
                copy[attr] = clone(orig[attr]);
 
1363
            }
 
1364
            break;
 
1365
        default:
 
1366
            copy = orig;
 
1367
            break;
 
1368
    }
 
1369
    return copy;
 
1370
}
 
1371
 
 
1372
/**
 
1373
 * Emulates php's print_r() functionality. Returns a nicely formatted string
 
1374
 * representation of an object. Very useful for debugging.
 
1375
 *
 
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
 
1380
 *                  of the dump.
 
1381
 * @return          a string representing a dump of the object
 
1382
 */
 
1383
function print_r(object, maxDepth, indent)
 
1384
{
 
1385
    var parentIndent, attr, str = "";
 
1386
    if (arguments.length == 1) {
 
1387
        var maxDepth = Number.MAX_VALUE;
 
1388
    } else {
 
1389
        maxDepth--;
 
1390
    }
 
1391
    if (arguments.length < 3) {
 
1392
        parentIndent = ''
 
1393
        var indent = '    ';
 
1394
    } else {
 
1395
        parentIndent = indent;
 
1396
        indent += '    ';
 
1397
    }
 
1398
 
 
1399
    switch(typeof(object)) {
 
1400
    case 'object':
 
1401
        if (object.length != undefined) {
 
1402
            if (object.length == 0) {
 
1403
                str += "Array ()\r\n";
 
1404
            }
 
1405
            else {
 
1406
                str += "Array (\r\n";
 
1407
                for (var i = 0; i < object.length; ++i) {
 
1408
                    str += indent + '[' + i + '] => ';
 
1409
                    if (maxDepth == 0)
 
1410
                        str += "...\r\n";
 
1411
                    else
 
1412
                        str += print_r(object[i], maxDepth, indent);
 
1413
                }
 
1414
                str += parentIndent + ")\r\n";
 
1415
            }
 
1416
        }
 
1417
        else {
 
1418
            str += "Object (\r\n";
 
1419
            for (attr in object) {
 
1420
                str += indent + "[" + attr + "] => ";
 
1421
                if (maxDepth == 0)
 
1422
                    str += "...\r\n";
 
1423
                else
 
1424
                    str += print_r(object[attr], maxDepth, indent);
 
1425
            }
 
1426
            str += parentIndent + ")\r\n";
 
1427
        }
 
1428
        break;
 
1429
    case 'boolean':
 
1430
        str += (object ? 'true' : 'false') + "\r\n";
 
1431
        break;
 
1432
    case 'function':
 
1433
        str += "Function\r\n";
 
1434
        break;
 
1435
    default:
 
1436
        str += object + "\r\n";
 
1437
        break;
 
1438
 
 
1439
    }
 
1440
    return str;
 
1441
}
 
1442
 
 
1443
/**
 
1444
 * Return an array containing all properties of an object. Perl-style.
 
1445
 *
 
1446
 * @param object  the object whose keys to return
 
1447
 * @return        array of object keys, as strings
 
1448
 */
 
1449
function keys(object)
 
1450
{
 
1451
    var keys = [];
 
1452
    for (var k in object) {
 
1453
        keys.push(k);
 
1454
    }
 
1455
    return keys;
 
1456
}
 
1457
 
 
1458
/**
 
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.
 
1462
 *.
 
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
 
1465
 *               be zero.
 
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
 
1469
 *               value.
 
1470
 * @return       an array of consecutive integers. 
 
1471
 */
 
1472
function range(start, end)
 
1473
{
 
1474
    if (arguments.length == 1) {
 
1475
        var end = start;
 
1476
        start = 0;
 
1477
    }
 
1478
    
 
1479
    var r = [];
 
1480
    if (start < end) {
 
1481
        while (start != end)
 
1482
            r.push(start++);
 
1483
    }
 
1484
    else {
 
1485
        while (start != end)
 
1486
            r.push(start--);
 
1487
    }
 
1488
    return r;
 
1489
}
 
1490
 
 
1491
/**
 
1492
 * Parses a python-style keyword arguments string and returns the pairs in a
 
1493
 * new object.
 
1494
 *
 
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
 
1498
 */
 
1499
function parse_kwargs(kwargs)
 
1500
{
 
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];
 
1507
        }
 
1508
        else {
 
1509
            ++i;
 
1510
        }
 
1511
    }
 
1512
    for (var i = 0; i < pairs.length; ++i) {
 
1513
        var splits = pairs[i].split(/=/);
 
1514
        if (splits.length == 1) {
 
1515
            continue;
 
1516
        }
 
1517
        var key = splits.shift();
 
1518
        var value = splits.join('=');
 
1519
        args[key.trim()] = value.trim();
 
1520
    }
 
1521
    return args;
 
1522
}
 
1523
 
 
1524
/**
 
1525
 * Creates a python-style keyword arguments string from an object.
 
1526
 *
 
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
 
1531
 *
 
1532
 * @return            a kwarg string representation of args
 
1533
 */
 
1534
function to_kwargs(args, sortedKeys)
 
1535
{
 
1536
    var s = '';
 
1537
    if (!sortedKeys) {
 
1538
        var sortedKeys = keys(args).sort();
 
1539
    }
 
1540
    for (var i = 0; i < sortedKeys.length; ++i) {
 
1541
        var k = sortedKeys[i];
 
1542
        if (args[k] != undefined) {
 
1543
            if (s) {
 
1544
                s += ', ';
 
1545
            }
 
1546
            s += k + '=' + args[k];
 
1547
        }
 
1548
    }
 
1549
    return s;
 
1550
}
 
1551
 
 
1552
/**
 
1553
 * Returns true if a node is an ancestor node of a target node, and false
 
1554
 * otherwise.
 
1555
 *
 
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.
 
1559
 */
 
1560
function is_ancestor(node, target)
 
1561
{
 
1562
    while (target.parentNode) {
 
1563
        target = target.parentNode;
 
1564
        if (node == target)
 
1565
            return true;
 
1566
    }
 
1567
    return false;
 
1568
}
 
1569
 
 
1570
//******************************************************************************
 
1571
// parseUri 1.2.1
 
1572
// MIT License
 
1573
 
 
1574
/*
 
1575
Copyright (c) 2007 Steven Levithan <stevenlevithan.com>
 
1576
 
 
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:
 
1583
 
 
1584
The above copyright notice and this permission notice shall be included in
 
1585
all copies or substantial portions of the Software.
 
1586
*/
 
1587
 
 
1588
function parseUri (str) {
 
1589
    var o   = parseUri.options,
 
1590
        m   = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
 
1591
        uri = {},
 
1592
        i   = 14;
 
1593
 
 
1594
    while (i--) uri[o.key[i]] = m[i] || "";
 
1595
 
 
1596
    uri[o.q.name] = {};
 
1597
    uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
 
1598
        if ($1) uri[o.q.name][$1] = $2;
 
1599
    });
 
1600
 
 
1601
    return uri;
 
1602
};
 
1603
 
 
1604
parseUri.options = {
 
1605
    strictMode: false,
 
1606
    key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
 
1607
    q:   {
 
1608
        name:   "queryKey",
 
1609
        parser: /(?:^|&)([^&=]*)=?([^&]*)/g
 
1610
    },
 
1611
    parser: {
 
1612
        strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
 
1613
        loose:  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
 
1614
    }
 
1615
};
 
1616