diff options
| author | xue <> | 2006-07-07 14:54:15 +0000 | 
|---|---|---|
| committer | xue <> | 2006-07-07 14:54:15 +0000 | 
| commit | 61bb16ee2e5f0a66234e1575242169a10fde47b5 (patch) | |
| tree | 3ee24dcc36ceae2c213130df1ea3d5c9fc110a27 /tests/test_tools/selenium | |
| parent | 7b84938b1b5964f2274d66e28ba17435924ffe35 (diff) | |
Merge from 3.0 branch till 1253.
Diffstat (limited to 'tests/test_tools/selenium')
24 files changed, 11233 insertions, 0 deletions
| diff --git a/tests/test_tools/selenium/core/SeleniumLog.html b/tests/test_tools/selenium/core/SeleniumLog.html new file mode 100644 index 00000000..dfa0080a --- /dev/null +++ b/tests/test_tools/selenium/core/SeleniumLog.html @@ -0,0 +1,78 @@ +<html> + +<head> +<title>Selenium Log Console</title> + +</head> +<body id="logging-console"> + +<script language="JavaScript"> + +var logLevels = { +    debug: 0, +    info: 1, +    warn: 2, +    error: 3 +}; + +var logLevelThreshold = null; + +function getThresholdLevel() { +    var buttons = document.getElementById('logLevelChooser').level; +    for (var i = 0; i < buttons.length; i++) { +        if (buttons[i].checked) { +            return buttons[i].value; +        } +    } +} + +function setThresholdLevel(logLevel) { +    logLevelThreshold = logLevel; +    var buttons = document.getElementById('logLevelChooser').level; +    for (var i = 0; i < buttons.length; i++) { +    	if (buttons[i].value==logLevel) { +            buttons[i].checked = true; +        } +        else { +            buttons[i].checked = false; +        } +    } +} + +function append(message, logLevel) { +    if (logLevelThreshold==null) { +    	logLevelThreshold = getThresholdLevel(); +    } +    if (logLevels[logLevel] < logLevels[logLevelThreshold]) { +        return; +    } +    var log = document.getElementById('log'); +    var newEntry = document.createElement('li'); +    newEntry.className = logLevel; +    newEntry.appendChild(document.createTextNode(message)); +    log.appendChild(newEntry); +    if (newEntry.scrollIntoView) { +        newEntry.scrollIntoView(); +    } +} + +</script> + +<div id="banner"> +  <form id="logLevelChooser"> +      <input id="level-error" type="radio" name="level"  +             value="error" /><label for="level-error">Error</label> +      <input id="level-warn" type="radio" name="level" +             value="warn" /><label for="level-warn">Warn</label> +      <input id="level-info" type="radio" name="level" checked="yes" +             value="info" /><label for="level-info">Info</label> +      <input id="level-debug" type="radio" name="level"  +             value="debug" /><label for="level-debug">Debug</label> +  </form> +  <h1>Selenium Log Console</h1> +</div> + +<ul id="log"></ul> + +</body> +</html> diff --git a/tests/test_tools/selenium/core/TestRunner-splash.html b/tests/test_tools/selenium/core/TestRunner-splash.html new file mode 100644 index 00000000..205bb8ef --- /dev/null +++ b/tests/test_tools/selenium/core/TestRunner-splash.html @@ -0,0 +1,52 @@ +<!-- +Copyright 2005 ThoughtWorks, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + +     http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<html> +<body> +<table width="100%"> + +<tr> +  <th>↑</th> +  <th>↑</th> +  <th>↑</th> +</tr> +<tr> +  <th width="25%">Test Suite</th> +  <th width="50%">Current Test</th> +  <th width="25%">Control Panel</th> +</tr> +<tr><td> </td></tr> + +<tr> +<td></td> +<td class="selenium splash"> + +<h1>Selenium</h1> +<h2>by <a href="http://www.thoughtworks.com">ThoughtWorks</a> and friends</h2> + +<p> +For more information on Selenium, visit + +<pre> +    <a href="http://selenium.openqa.org" target="_blank">http://selenium.openqa.org</a> +</pre> + +</td> +<tr> + +</table> +</body> +</html> diff --git a/tests/test_tools/selenium/core/scripts/find_matching_child.js b/tests/test_tools/selenium/core/scripts/find_matching_child.js new file mode 100644 index 00000000..197d1032 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/find_matching_child.js @@ -0,0 +1,69 @@ +/* + * Copyright 2004 ThoughtWorks, Inc + * + *  Licensed under the Apache License, Version 2.0 (the "License"); + *  you may not use this file except in compliance with the License. + *  You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + *  Unless required by applicable law or agreed to in writing, software + *  distributed under the License is distributed on an "AS IS" BASIS, + *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + *  See the License for the specific language governing permissions and + *  limitations under the License. + * + */ + +Element.findMatchingChildren = function(element, selector) { +  var matches = $A([]); + +  var childCount = element.childNodes.length; +  for (var i=0; i<childCount; i++) { +    var child = element.childNodes[i]; +    if (selector(child)) { +      matches.push(child); +    } else { +      childMatches = Element.findMatchingChildren(child, selector); +      matches.push(childMatches); +    } +  } + +  return matches.flatten(); +} + +ELEMENT_NODE_TYPE = 1; + +Element.findFirstMatchingChild = function(element, selector) { + +  var childCount = element.childNodes.length; +  for (var i=0; i<childCount; i++) { +    var child = element.childNodes[i]; +    if (child.nodeType == ELEMENT_NODE_TYPE) { +      if (selector(child)) { +        return child; +      } +      result = Element.findFirstMatchingChild(child, selector); +      if (result) { +        return result; +      } +    } +  } +  return null; +} + +Element.findFirstMatchingParent = function(element, selector) { +  var current = element.parentNode; +  while (current != null) { +    if (selector(current)) { +      break; +    } +    current = current.parentNode; +  } +  return current; +} + +Element.findMatchingChildById = function(element, id) { +  return Element.findFirstMatchingChild(element, function(element){return element.id==id} ); +} + diff --git a/tests/test_tools/selenium/core/scripts/htmlutils.js b/tests/test_tools/selenium/core/scripts/htmlutils.js new file mode 100644 index 00000000..fcb1ee44 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/htmlutils.js @@ -0,0 +1,463 @@ +/* + * Copyright 2004 ThoughtWorks, Inc + * + *  Licensed under the Apache License, Version 2.0 (the "License"); + *  you may not use this file except in compliance with the License. + *  You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + *  Unless required by applicable law or agreed to in writing, software + *  distributed under the License is distributed on an "AS IS" BASIS, + *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + *  See the License for the specific language governing permissions and + *  limitations under the License. + * + */ +  +// This script contains some HTML utility functions that +// make it possible to handle elements in a way that is  +// compatible with both IE-like and Mozilla-like browsers + +String.prototype.trim = function() { +  var result = this.replace( /^\s+/g, "" );// strip leading +  return result.replace( /\s+$/g, "" );// strip trailing +}; +String.prototype.lcfirst = function() { +   return this.charAt(0).toLowerCase() + this.substr(1); +}; +String.prototype.ucfirst = function() { +   return this.charAt(0).toUpperCase() + this.substr(1); +}; +String.prototype.startsWith = function(str) { +    return this.indexOf(str) == 0; +}; + +// Returns the text in this element +function getText(element) { +    var text = ""; + +    if(browserVersion.isFirefox && browserVersion.firefoxVersion >= "1.5") +    { +        var dummyElement = element.cloneNode(true); +        renderWhitespaceInTextContent(dummyElement); +        text = dummyElement.textContent; +    } else if (browserVersion.isOpera) { +    	var dummyElement = element.cloneNode(true); +        renderWhitespaceInTextContent(dummyElement); +        text = dummyElement.innerText; +        text = xmlDecode(text); +    } +    else if(element.textContent) +    { +        text = element.textContent; +    } +    else if(element.innerText) +    { +        text = element.innerText; +    } + +    text = normalizeNewlines(text); +    text = normalizeSpaces(text); + +    return text.trim(); +} + +function renderWhitespaceInTextContent(element) { +    // Remove non-visible newlines in text nodes +    if (element.nodeType == Node.TEXT_NODE) +    { +        element.data = element.data.replace(/\n|\r|\t/g, " "); +        return; +    } +     +    if (element.nodeType == Node.COMMENT_NODE) +    { +        element.data = ""; +        return; +    } + +    // Don't modify PRE elements +    if (element.tagName == "PRE") +    { +        return; +    } + +    // Handle inline element that force newlines +    if (tagIs(element, ["BR", "HR"])) +    { +        // Replace this element with a newline text element +        element.parentNode.replaceChild(element.ownerDocument.createTextNode("\n"), element) +    } + +    for (var i = 0; i < element.childNodes.length; i++) +    { +        var child = element.childNodes.item(i) +        renderWhitespaceInTextContent(child); +    } + +    // Handle block elements that introduce newlines +// -- From HTML spec: +//<!ENTITY % block +//     "P | %heading; | %list; | %preformatted; | DL | DIV | NOSCRIPT | +//      BLOCKQUOTE | FORM | HR | TABLE | FIELDSET | ADDRESS"> +    if (tagIs(element, ["P", "DIV"])) +    { +        element.appendChild(element.ownerDocument.createTextNode("\n"), element) +    } +     +} + +function tagIs(element, tags) +{ +    var tag = element.tagName; +    for (var i = 0; i < tags.length; i++) +    { +        if (tags[i] == tag) +        { +            return true; +        } +    } +    return false; +} + +/** + * Convert all newlines to \m + */ +function normalizeNewlines(text) +{ +    return text.replace(/\r\n|\r/g, "\n"); +} + +/** + * Replace multiple sequential spaces with a single space, and then convert   to space. + */ +function normalizeSpaces(text) +{ +    // IE has already done this conversion, so doing it again will remove multiple nbsp +    if (browserVersion.isIE) +    { +        return text; +    } + +    // Replace multiple spaces with a single space +    // TODO - this shouldn't occur inside PRE elements +    text = text.replace(/\ +/g, " "); + +    // Replace   with a space +    var pat = String.fromCharCode(160); // Opera doesn't like /\240/g +   	var re = new RegExp(pat, "g"); +    return text.replace(re, " "); +} + +function xmlDecode(text) { +	text = text.replace(/"/g, '"'); +	text = text.replace(/'/g, "'"); +	text = text.replace(/</g, "<"); +	text = text.replace(/>/g, ">"); +	text = text.replace(/&/g, "&"); +	return text; +} + +// Sets the text in this element +function setText(element, text) { +    if(element.textContent) { +        element.textContent = text; +    } else if(element.innerText) { +        element.innerText = text; +    } +} + +// Get the value of an <input> element +function getInputValue(inputElement) { +    if (inputElement.type.toUpperCase() == 'CHECKBOX' || +        inputElement.type.toUpperCase() == 'RADIO') +    { +        return (inputElement.checked ? 'on' : 'off'); +    } +    return inputElement.value; +} + +/* Fire an event in a browser-compatible manner */ +function triggerEvent(element, eventType, canBubble) { +    canBubble = (typeof(canBubble) == undefined) ? true : canBubble; +    if (element.fireEvent) { +        element.fireEvent('on' + eventType); +    } +    else { +        var evt = document.createEvent('HTMLEvents'); +        evt.initEvent(eventType, canBubble, true); +        element.dispatchEvent(evt); +    } +} + +function triggerKeyEvent(element, eventType, keycode, canBubble) { +    canBubble = (typeof(canBubble) == undefined) ? true : canBubble; +    if (element.fireEvent) { +		keyEvent = parent.frames['myiframe'].document.createEventObject(); +		keyEvent.keyCode=keycode; +		element.fireEvent('on' + eventType, keyEvent); +    } +    else { +    	var evt; +    	if( window.KeyEvent ) { +			evt = document.createEvent('KeyEvents'); +			evt.initKeyEvent(eventType, true, true, window, false, false, false, false, keycode, keycode); +		} else { +			evt = document.createEvent('UIEvents'); +			evt.initUIEvent( eventType, true, true, window, 1 ); +			evt.keyCode = keycode; +		} +         +        element.dispatchEvent(evt); +    } +} + +/* Fire a mouse event in a browser-compatible manner */ +function triggerMouseEvent(element, eventType, canBubble) { +    canBubble = (typeof(canBubble) == undefined) ? true : canBubble; +    if (element.fireEvent) { +        element.fireEvent('on' + eventType); +    } +    else { +        var evt = document.createEvent('MouseEvents'); +        if (evt.initMouseEvent) +        { +            evt.initMouseEvent(eventType, canBubble, true, document.defaultView, 1, 0, 0, 0, 0, false, false, false, false, 0, null) +        } +        else +        { +            // Safari +            // TODO we should be initialising other mouse-event related attributes here +            evt.initEvent(eventType, canBubble, true); +        } +        element.dispatchEvent(evt); +    } +} + +function removeLoadListener(element, command) { +    if (window.removeEventListener) +        element.removeEventListener("load", command, true); +    else if (window.detachEvent) +        element.detachEvent("onload", command); +} + +function addLoadListener(element, command) { +    if (window.addEventListener && !browserVersion.isOpera) +        element.addEventListener("load",command, true); +    else if (window.attachEvent) +        element.attachEvent("onload",command); +} + +function addUnloadListener(element, command) { +    if (window.addEventListener) +        element.addEventListener("unload",command, true); +    else if (window.attachEvent) +        element.attachEvent("onunload",command); +} + +/** + * Override the broken getFunctionName() method from JsUnit + * This file must be loaded _after_ the jsunitCore.js + */ +function getFunctionName(aFunction) { +  var regexpResult = aFunction.toString().match(/function (\w*)/); +  if (regexpResult && regexpResult[1]) { +      return regexpResult[1]; +  } +  return 'anonymous'; +} + +function getDocumentBase(doc) { +	var bases = document.getElementsByTagName("base"); +	if (bases && bases.length && bases[0].href) { +		return bases[0].href; +	} +	return ""; +} + +function describe(object, delimiter) { +    var props = new Array(); +    for (var prop in object) { +        props.push(prop + " -> " + object[prop]); +    } +    return props.join(delimiter || '\n'); +} + +var PatternMatcher = function(pattern) { +    this.selectStrategy(pattern); +}; +PatternMatcher.prototype = { + +    selectStrategy: function(pattern) { +        this.pattern = pattern; +        var strategyName = 'glob'; // by default +        if (/^([a-z-]+):(.*)/.test(pattern)) { +            strategyName = RegExp.$1; +            pattern = RegExp.$2; +        } +        var matchStrategy = PatternMatcher.strategies[strategyName]; +        if (!matchStrategy) { +            throw new SeleniumError("cannot find PatternMatcher.strategies." + strategyName); +        } +        this.strategy = matchStrategy; +        this.matcher = new matchStrategy(pattern); +    }, + +    matches: function(actual) { +        return this.matcher.matches(actual + ''); +        // Note: appending an empty string avoids a Konqueror bug +    } + +}; + +/** + * A "static" convenience method for easy matching + */ +PatternMatcher.matches = function(pattern, actual) { +    return new PatternMatcher(pattern).matches(actual); +}; + +PatternMatcher.strategies = { + +    /** +     * Exact matching, e.g. "exact:***" +     */ +    exact: function(expected) { +        this.expected = expected; +        this.matches = function(actual) { +            return actual == this.expected; +        }; +    }, + +    /** +     * Match by regular expression, e.g. "regexp:^[0-9]+$" +     */ +    regexp: function(regexpString) { +        this.regexp = new RegExp(regexpString); +        this.matches = function(actual) { +            return this.regexp.test(actual); +        }; +    }, + +    /** +     * "globContains" (aka "wildmat") patterns, e.g. "glob:one,two,*", +     * but don't require a perfect match; instead succeed if actual +     * contains something that matches globString. +     * Making this distinction is motivated by a bug in IE6 which +     * leads to the browser hanging if we implement *TextPresent tests +     * by just matching against a regular expression beginning and +     * ending with ".*".  The globcontains strategy allows us to satisfy +     * the functional needs of the *TextPresent ops more efficiently +     * and so avoid running into this IE6 freeze. +     */ +    globContains: function(globString) { +        this.regexp = new RegExp(PatternMatcher.regexpFromGlobContains(globString)); +        this.matches = function(actual) { +            return this.regexp.test(actual); +        }; +    }, + + +    /** +     * "glob" (aka "wildmat") patterns, e.g. "glob:one,two,*" +     */ +    glob: function(globString) { +        this.regexp = new RegExp(PatternMatcher.regexpFromGlob(globString)); +        this.matches = function(actual) { +            return this.regexp.test(actual); +        }; +    } + +}; + +PatternMatcher.convertGlobMetaCharsToRegexpMetaChars = function(glob) { +    var re = glob; +    re = re.replace(/([.^$+(){}\[\]\\|])/g, "\\$1"); +    re = re.replace(/\?/g, "(.|[\r\n])"); +    re = re.replace(/\*/g, "(.|[\r\n])*"); +    return re; +}; + +PatternMatcher.regexpFromGlobContains = function(globContains) { +    return PatternMatcher.convertGlobMetaCharsToRegexpMetaChars(globContains); +}; + +PatternMatcher.regexpFromGlob = function(glob) { +    return "^" + PatternMatcher.convertGlobMetaCharsToRegexpMetaChars(glob) + "$"; +}; + +var Assert = { + +    fail: function(message) { +        throw new AssertionFailedError(message); +    }, + +    /* +     * Assert.equals(comment?, expected, actual) +     */ +    equals: function() { +        var args = new AssertionArguments(arguments); +        if (args.expected === args.actual) { +            return; +        } +        Assert.fail(args.comment + +                    "Expected '" + args.expected + +                    "' but was '" + args.actual + "'"); +    }, + +    /* +     * Assert.matches(comment?, pattern, actual) +     */ +    matches: function() { +        var args = new AssertionArguments(arguments); +        if (PatternMatcher.matches(args.expected, args.actual)) { +            return; +        } +        Assert.fail(args.comment + +                    "Actual value '" + args.actual + +                    "' did not match '" + args.expected + "'"); +    }, + +    /* +     * Assert.notMtches(comment?, pattern, actual) +     */ +    notMatches: function() { +        var args = new AssertionArguments(arguments); +        if (!PatternMatcher.matches(args.expected, args.actual)) { +            return; +        } +        Assert.fail(args.comment + +                    "Actual value '" + args.actual + +                    "' did match '" + args.expected + "'"); +    } + +}; + +// Preprocess the arguments to allow for an optional comment. +function AssertionArguments(args) { +    if (args.length == 2) { +        this.comment = ""; +        this.expected = args[0]; +        this.actual = args[1]; +    } else { +        this.comment = args[0] + "; "; +        this.expected = args[1]; +        this.actual = args[2]; +    } +} + + + +function AssertionFailedError(message) { +    this.isAssertionFailedError = true; +    this.isSeleniumError = true; +    this.message = message; +    this.failureMessage = message; +} + +function SeleniumError(message) { +    var error = new Error(message); +    error.isSeleniumError = true; +    return error; +}; diff --git a/tests/test_tools/selenium/core/scripts/prototype-1.4.0.js b/tests/test_tools/selenium/core/scripts/prototype-1.4.0.js new file mode 100644 index 00000000..0e85338b --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/prototype-1.4.0.js @@ -0,0 +1,1781 @@ +/*  Prototype JavaScript framework, version 1.4.0 + *  (c) 2005 Sam Stephenson <sam@conio.net> + * + *  Prototype is freely distributable under the terms of an MIT-style license. + *  For details, see the Prototype web site: http://prototype.conio.net/ + * +/*--------------------------------------------------------------------------*/ + +var Prototype = { +  Version: '1.4.0', +  ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)', + +  emptyFunction: function() {}, +  K: function(x) {return x} +} + +var Class = { +  create: function() { +    return function() { +      this.initialize.apply(this, arguments); +    } +  } +} + +var Abstract = new Object(); + +Object.extend = function(destination, source) { +  for (property in source) { +    destination[property] = source[property]; +  } +  return destination; +} + +Object.inspect = function(object) { +  try { +    if (object == undefined) return 'undefined'; +    if (object == null) return 'null'; +    return object.inspect ? object.inspect() : object.toString(); +  } catch (e) { +    if (e instanceof RangeError) return '...'; +    throw e; +  } +} + +Function.prototype.bind = function() { +  var __method = this, args = $A(arguments), object = args.shift(); +  return function() { +    return __method.apply(object, args.concat($A(arguments))); +  } +} + +Function.prototype.bindAsEventListener = function(object) { +  var __method = this; +  return function(event) { +    return __method.call(object, event || window.event); +  } +} + +Object.extend(Number.prototype, { +  toColorPart: function() { +    var digits = this.toString(16); +    if (this < 16) return '0' + digits; +    return digits; +  }, + +  succ: function() { +    return this + 1; +  }, + +  times: function(iterator) { +    $R(0, this, true).each(iterator); +    return this; +  } +}); + +var Try = { +  these: function() { +    var returnValue; + +    for (var i = 0; i < arguments.length; i++) { +      var lambda = arguments[i]; +      try { +        returnValue = lambda(); +        break; +      } catch (e) {} +    } + +    return returnValue; +  } +} + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create(); +PeriodicalExecuter.prototype = { +  initialize: function(callback, frequency) { +    this.callback = callback; +    this.frequency = frequency; +    this.currentlyExecuting = false; + +    this.registerCallback(); +  }, + +  registerCallback: function() { +    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); +  }, + +  onTimerEvent: function() { +    if (!this.currentlyExecuting) { +      try { +        this.currentlyExecuting = true; +        this.callback(); +      } finally { +        this.currentlyExecuting = false; +      } +    } +  } +} + +/*--------------------------------------------------------------------------*/ + +function $() { +  var elements = new Array(); + +  for (var i = 0; i < arguments.length; i++) { +    var element = arguments[i]; +    if (typeof element == 'string') +      element = document.getElementById(element); + +    if (arguments.length == 1) +      return element; + +    elements.push(element); +  } + +  return elements; +} +Object.extend(String.prototype, { +  stripTags: function() { +    return this.replace(/<\/?[^>]+>/gi, ''); +  }, + +  stripScripts: function() { +    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); +  }, + +  extractScripts: function() { +    var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); +    var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); +    return (this.match(matchAll) || []).map(function(scriptTag) { +      return (scriptTag.match(matchOne) || ['', ''])[1]; +    }); +  }, + +  evalScripts: function() { +    return this.extractScripts().map(eval); +  }, + +  escapeHTML: function() { +    var div = document.createElement('div'); +    var text = document.createTextNode(this); +    div.appendChild(text); +    return div.innerHTML; +  }, + +  unescapeHTML: function() { +    var div = document.createElement('div'); +    div.innerHTML = this.stripTags(); +    return div.childNodes[0] ? div.childNodes[0].nodeValue : ''; +  }, + +  toQueryParams: function() { +    var pairs = this.match(/^\??(.*)$/)[1].split('&'); +    return pairs.inject({}, function(params, pairString) { +      var pair = pairString.split('='); +      params[pair[0]] = pair[1]; +      return params; +    }); +  }, + +  toArray: function() { +    return this.split(''); +  }, + +  camelize: function() { +    var oStringList = this.split('-'); +    if (oStringList.length == 1) return oStringList[0]; + +    var camelizedString = this.indexOf('-') == 0 +      ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) +      : oStringList[0]; + +    for (var i = 1, len = oStringList.length; i < len; i++) { +      var s = oStringList[i]; +      camelizedString += s.charAt(0).toUpperCase() + s.substring(1); +    } + +    return camelizedString; +  }, + +  inspect: function() { +    return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'"; +  } +}); + +String.prototype.parseQuery = String.prototype.toQueryParams; + +var $break    = new Object(); +var $continue = new Object(); + +var Enumerable = { +  each: function(iterator) { +    var index = 0; +    try { +      this._each(function(value) { +        try { +          iterator(value, index++); +        } catch (e) { +          if (e != $continue) throw e; +        } +      }); +    } catch (e) { +      if (e != $break) throw e; +    } +  }, + +  all: function(iterator) { +    var result = true; +    this.each(function(value, index) { +      result = result && !!(iterator || Prototype.K)(value, index); +      if (!result) throw $break; +    }); +    return result; +  }, + +  any: function(iterator) { +    var result = true; +    this.each(function(value, index) { +      if (result = !!(iterator || Prototype.K)(value, index)) +        throw $break; +    }); +    return result; +  }, + +  collect: function(iterator) { +    var results = []; +    this.each(function(value, index) { +      results.push(iterator(value, index)); +    }); +    return results; +  }, + +  detect: function (iterator) { +    var result; +    this.each(function(value, index) { +      if (iterator(value, index)) { +        result = value; +        throw $break; +      } +    }); +    return result; +  }, + +  findAll: function(iterator) { +    var results = []; +    this.each(function(value, index) { +      if (iterator(value, index)) +        results.push(value); +    }); +    return results; +  }, + +  grep: function(pattern, iterator) { +    var results = []; +    this.each(function(value, index) { +      var stringValue = value.toString(); +      if (stringValue.match(pattern)) +        results.push((iterator || Prototype.K)(value, index)); +    }) +    return results; +  }, + +  include: function(object) { +    var found = false; +    this.each(function(value) { +      if (value == object) { +        found = true; +        throw $break; +      } +    }); +    return found; +  }, + +  inject: function(memo, iterator) { +    this.each(function(value, index) { +      memo = iterator(memo, value, index); +    }); +    return memo; +  }, + +  invoke: function(method) { +    var args = $A(arguments).slice(1); +    return this.collect(function(value) { +      return value[method].apply(value, args); +    }); +  }, + +  max: function(iterator) { +    var result; +    this.each(function(value, index) { +      value = (iterator || Prototype.K)(value, index); +      if (value >= (result || value)) +        result = value; +    }); +    return result; +  }, + +  min: function(iterator) { +    var result; +    this.each(function(value, index) { +      value = (iterator || Prototype.K)(value, index); +      if (value <= (result || value)) +        result = value; +    }); +    return result; +  }, + +  partition: function(iterator) { +    var trues = [], falses = []; +    this.each(function(value, index) { +      ((iterator || Prototype.K)(value, index) ? +        trues : falses).push(value); +    }); +    return [trues, falses]; +  }, + +  pluck: function(property) { +    var results = []; +    this.each(function(value, index) { +      results.push(value[property]); +    }); +    return results; +  }, + +  reject: function(iterator) { +    var results = []; +    this.each(function(value, index) { +      if (!iterator(value, index)) +        results.push(value); +    }); +    return results; +  }, + +  sortBy: function(iterator) { +    return this.collect(function(value, index) { +      return {value: value, criteria: iterator(value, index)}; +    }).sort(function(left, right) { +      var a = left.criteria, b = right.criteria; +      return a < b ? -1 : a > b ? 1 : 0; +    }).pluck('value'); +  }, + +  toArray: function() { +    return this.collect(Prototype.K); +  }, + +  zip: function() { +    var iterator = Prototype.K, args = $A(arguments); +    if (typeof args.last() == 'function') +      iterator = args.pop(); + +    var collections = [this].concat(args).map($A); +    return this.map(function(value, index) { +      iterator(value = collections.pluck(index)); +      return value; +    }); +  }, + +  inspect: function() { +    return '#<Enumerable:' + this.toArray().inspect() + '>'; +  } +} + +Object.extend(Enumerable, { +  map:     Enumerable.collect, +  find:    Enumerable.detect, +  select:  Enumerable.findAll, +  member:  Enumerable.include, +  entries: Enumerable.toArray +}); +var $A = Array.from = function(iterable) { +  if (!iterable) return []; +  if (iterable.toArray) { +    return iterable.toArray(); +  } else { +    var results = []; +    for (var i = 0; i < iterable.length; i++) +      results.push(iterable[i]); +    return results; +  } +} + +Object.extend(Array.prototype, Enumerable); + +Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { +  _each: function(iterator) { +    for (var i = 0; i < this.length; i++) +      iterator(this[i]); +  }, + +  clear: function() { +    this.length = 0; +    return this; +  }, + +  first: function() { +    return this[0]; +  }, + +  last: function() { +    return this[this.length - 1]; +  }, + +  compact: function() { +    return this.select(function(value) { +      return value != undefined || value != null; +    }); +  }, + +  flatten: function() { +    return this.inject([], function(array, value) { +      return array.concat(value.constructor == Array ? +        value.flatten() : [value]); +    }); +  }, + +  without: function() { +    var values = $A(arguments); +    return this.select(function(value) { +      return !values.include(value); +    }); +  }, + +  indexOf: function(object) { +    for (var i = 0; i < this.length; i++) +      if (this[i] == object) return i; +    return -1; +  }, + +  reverse: function(inline) { +    return (inline !== false ? this : this.toArray())._reverse(); +  }, + +  shift: function() { +    var result = this[0]; +    for (var i = 0; i < this.length - 1; i++) +      this[i] = this[i + 1]; +    this.length--; +    return result; +  }, + +  inspect: function() { +    return '[' + this.map(Object.inspect).join(', ') + ']'; +  } +}); +var Hash = { +  _each: function(iterator) { +    for (key in this) { +      var value = this[key]; +      if (typeof value == 'function') continue; + +      var pair = [key, value]; +      pair.key = key; +      pair.value = value; +      iterator(pair); +    } +  }, + +  keys: function() { +    return this.pluck('key'); +  }, + +  values: function() { +    return this.pluck('value'); +  }, + +  merge: function(hash) { +    return $H(hash).inject($H(this), function(mergedHash, pair) { +      mergedHash[pair.key] = pair.value; +      return mergedHash; +    }); +  }, + +  toQueryString: function() { +    return this.map(function(pair) { +      return pair.map(encodeURIComponent).join('='); +    }).join('&'); +  }, + +  inspect: function() { +    return '#<Hash:{' + this.map(function(pair) { +      return pair.map(Object.inspect).join(': '); +    }).join(', ') + '}>'; +  } +} + +function $H(object) { +  var hash = Object.extend({}, object || {}); +  Object.extend(hash, Enumerable); +  Object.extend(hash, Hash); +  return hash; +} +ObjectRange = Class.create(); +Object.extend(ObjectRange.prototype, Enumerable); +Object.extend(ObjectRange.prototype, { +  initialize: function(start, end, exclusive) { +    this.start = start; +    this.end = end; +    this.exclusive = exclusive; +  }, + +  _each: function(iterator) { +    var value = this.start; +    do { +      iterator(value); +      value = value.succ(); +    } while (this.include(value)); +  }, + +  include: function(value) { +    if (value < this.start) +      return false; +    if (this.exclusive) +      return value < this.end; +    return value <= this.end; +  } +}); + +var $R = function(start, end, exclusive) { +  return new ObjectRange(start, end, exclusive); +} + +var Ajax = { +  getTransport: function() { +    return Try.these( +      function() {return new ActiveXObject('Msxml2.XMLHTTP')}, +      function() {return new ActiveXObject('Microsoft.XMLHTTP')}, +      function() {return new XMLHttpRequest()} +    ) || false; +  }, + +  activeRequestCount: 0 +} + +Ajax.Responders = { +  responders: [], + +  _each: function(iterator) { +    this.responders._each(iterator); +  }, + +  register: function(responderToAdd) { +    if (!this.include(responderToAdd)) +      this.responders.push(responderToAdd); +  }, + +  unregister: function(responderToRemove) { +    this.responders = this.responders.without(responderToRemove); +  }, + +  dispatch: function(callback, request, transport, json) { +    this.each(function(responder) { +      if (responder[callback] && typeof responder[callback] == 'function') { +        try { +          responder[callback].apply(responder, [request, transport, json]); +        } catch (e) {} +      } +    }); +  } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ +  onCreate: function() { +    Ajax.activeRequestCount++; +  }, + +  onComplete: function() { +    Ajax.activeRequestCount--; +  } +}); + +Ajax.Base = function() {}; +Ajax.Base.prototype = { +  setOptions: function(options) { +    this.options = { +      method:       'post', +      asynchronous: true, +      parameters:   '' +    } +    Object.extend(this.options, options || {}); +  }, + +  responseIsSuccess: function() { +    return this.transport.status == undefined +        || this.transport.status == 0 +        || (this.transport.status >= 200 && this.transport.status < 300); +  }, + +  responseIsFailure: function() { +    return !this.responseIsSuccess(); +  } +} + +Ajax.Request = Class.create(); +Ajax.Request.Events = +  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { +  initialize: function(url, options) { +    this.transport = Ajax.getTransport(); +    this.setOptions(options); +    this.request(url); +  }, + +  request: function(url) { +    var parameters = this.options.parameters || ''; +    if (parameters.length > 0) parameters += '&_='; + +    try { +      this.url = url; +      if (this.options.method == 'get' && parameters.length > 0) +        this.url += (this.url.match(/\?/) ? '&' : '?') + parameters; + +      Ajax.Responders.dispatch('onCreate', this, this.transport); + +      this.transport.open(this.options.method, this.url, +        this.options.asynchronous); + +      if (this.options.asynchronous) { +        this.transport.onreadystatechange = this.onStateChange.bind(this); +        setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); +      } + +      this.setRequestHeaders(); + +      var body = this.options.postBody ? this.options.postBody : parameters; +      this.transport.send(this.options.method == 'post' ? body : null); + +    } catch (e) { +      this.dispatchException(e); +    } +  }, + +  setRequestHeaders: function() { +    var requestHeaders = +      ['X-Requested-With', 'XMLHttpRequest', +       'X-Prototype-Version', Prototype.Version]; + +    if (this.options.method == 'post') { +      requestHeaders.push('Content-type', +        'application/x-www-form-urlencoded'); + +      /* Force "Connection: close" for Mozilla browsers to work around +       * a bug where XMLHttpReqeuest sends an incorrect Content-length +       * header. See Mozilla Bugzilla #246651. +       */ +      if (this.transport.overrideMimeType) +        requestHeaders.push('Connection', 'close'); +    } + +    if (this.options.requestHeaders) +      requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); + +    for (var i = 0; i < requestHeaders.length; i += 2) +      this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); +  }, + +  onStateChange: function() { +    var readyState = this.transport.readyState; +    if (readyState != 1) +      this.respondToReadyState(this.transport.readyState); +  }, + +  header: function(name) { +    try { +      return this.transport.getResponseHeader(name); +    } catch (e) {} +  }, + +  evalJSON: function() { +    try { +      return eval(this.header('X-JSON')); +    } catch (e) {} +  }, + +  evalResponse: function() { +    try { +      return eval(this.transport.responseText); +    } catch (e) { +      this.dispatchException(e); +    } +  }, + +  respondToReadyState: function(readyState) { +    var event = Ajax.Request.Events[readyState]; +    var transport = this.transport, json = this.evalJSON(); + +    if (event == 'Complete') { +      try { +        (this.options['on' + this.transport.status] +         || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] +         || Prototype.emptyFunction)(transport, json); +      } catch (e) { +        this.dispatchException(e); +      } + +      if ((this.header('Content-type') || '').match(/^text\/javascript/i)) +        this.evalResponse(); +    } + +    try { +      (this.options['on' + event] || Prototype.emptyFunction)(transport, json); +      Ajax.Responders.dispatch('on' + event, this, transport, json); +    } catch (e) { +      this.dispatchException(e); +    } + +    /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ +    if (event == 'Complete') +      this.transport.onreadystatechange = Prototype.emptyFunction; +  }, + +  dispatchException: function(exception) { +    (this.options.onException || Prototype.emptyFunction)(this, exception); +    Ajax.Responders.dispatch('onException', this, exception); +  } +}); + +Ajax.Updater = Class.create(); + +Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { +  initialize: function(container, url, options) { +    this.containers = { +      success: container.success ? $(container.success) : $(container), +      failure: container.failure ? $(container.failure) : +        (container.success ? null : $(container)) +    } + +    this.transport = Ajax.getTransport(); +    this.setOptions(options); + +    var onComplete = this.options.onComplete || Prototype.emptyFunction; +    this.options.onComplete = (function(transport, object) { +      this.updateContent(); +      onComplete(transport, object); +    }).bind(this); + +    this.request(url); +  }, + +  updateContent: function() { +    var receiver = this.responseIsSuccess() ? +      this.containers.success : this.containers.failure; +    var response = this.transport.responseText; + +    if (!this.options.evalScripts) +      response = response.stripScripts(); + +    if (receiver) { +      if (this.options.insertion) { +        new this.options.insertion(receiver, response); +      } else { +        Element.update(receiver, response); +      } +    } + +    if (this.responseIsSuccess()) { +      if (this.onComplete) +        setTimeout(this.onComplete.bind(this), 10); +    } +  } +}); + +Ajax.PeriodicalUpdater = Class.create(); +Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { +  initialize: function(container, url, options) { +    this.setOptions(options); +    this.onComplete = this.options.onComplete; + +    this.frequency = (this.options.frequency || 2); +    this.decay = (this.options.decay || 1); + +    this.updater = {}; +    this.container = container; +    this.url = url; + +    this.start(); +  }, + +  start: function() { +    this.options.onComplete = this.updateComplete.bind(this); +    this.onTimerEvent(); +  }, + +  stop: function() { +    this.updater.onComplete = undefined; +    clearTimeout(this.timer); +    (this.onComplete || Prototype.emptyFunction).apply(this, arguments); +  }, + +  updateComplete: function(request) { +    if (this.options.decay) { +      this.decay = (request.responseText == this.lastText ? +        this.decay * this.options.decay : 1); + +      this.lastText = request.responseText; +    } +    this.timer = setTimeout(this.onTimerEvent.bind(this), +      this.decay * this.frequency * 1000); +  }, + +  onTimerEvent: function() { +    this.updater = new Ajax.Updater(this.container, this.url, this.options); +  } +}); +document.getElementsByClassName = function(className, parentElement) { +  var children = ($(parentElement) || document.body).getElementsByTagName('*'); +  return $A(children).inject([], function(elements, child) { +    if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) +      elements.push(child); +    return elements; +  }); +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Element) { +  var Element = new Object(); +} + +Object.extend(Element, { +  visible: function(element) { +    return $(element).style.display != 'none'; +  }, + +  toggle: function() { +    for (var i = 0; i < arguments.length; i++) { +      var element = $(arguments[i]); +      Element[Element.visible(element) ? 'hide' : 'show'](element); +    } +  }, + +  hide: function() { +    for (var i = 0; i < arguments.length; i++) { +      var element = $(arguments[i]); +      element.style.display = 'none'; +    } +  }, + +  show: function() { +    for (var i = 0; i < arguments.length; i++) { +      var element = $(arguments[i]); +      element.style.display = ''; +    } +  }, + +  remove: function(element) { +    element = $(element); +    element.parentNode.removeChild(element); +  }, + +  update: function(element, html) { +    $(element).innerHTML = html.stripScripts(); +    setTimeout(function() {html.evalScripts()}, 10); +  }, + +  getHeight: function(element) { +    element = $(element); +    return element.offsetHeight; +  }, + +  classNames: function(element) { +    return new Element.ClassNames(element); +  }, + +  hasClassName: function(element, className) { +    if (!(element = $(element))) return; +    return Element.classNames(element).include(className); +  }, + +  addClassName: function(element, className) { +    if (!(element = $(element))) return; +    return Element.classNames(element).add(className); +  }, + +  removeClassName: function(element, className) { +    if (!(element = $(element))) return; +    return Element.classNames(element).remove(className); +  }, + +  // removes whitespace-only text node children +  cleanWhitespace: function(element) { +    element = $(element); +    for (var i = 0; i < element.childNodes.length; i++) { +      var node = element.childNodes[i]; +      if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) +        Element.remove(node); +    } +  }, + +  empty: function(element) { +    return $(element).innerHTML.match(/^\s*$/); +  }, + +  scrollTo: function(element) { +    element = $(element); +    var x = element.x ? element.x : element.offsetLeft, +        y = element.y ? element.y : element.offsetTop; +    window.scrollTo(x, y); +  }, + +  getStyle: function(element, style) { +    element = $(element); +    var value = element.style[style.camelize()]; +    if (!value) { +      if (document.defaultView && document.defaultView.getComputedStyle) { +        var css = document.defaultView.getComputedStyle(element, null); +        value = css ? css.getPropertyValue(style) : null; +      } else if (element.currentStyle) { +        value = element.currentStyle[style.camelize()]; +      } +    } + +    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) +      if (Element.getStyle(element, 'position') == 'static') value = 'auto'; + +    return value == 'auto' ? null : value; +  }, + +  setStyle: function(element, style) { +    element = $(element); +    for (name in style) +      element.style[name.camelize()] = style[name]; +  }, + +  getDimensions: function(element) { +    element = $(element); +    if (Element.getStyle(element, 'display') != 'none') +      return {width: element.offsetWidth, height: element.offsetHeight}; + +    // All *Width and *Height properties give 0 on elements with display none, +    // so enable the element temporarily +    var els = element.style; +    var originalVisibility = els.visibility; +    var originalPosition = els.position; +    els.visibility = 'hidden'; +    els.position = 'absolute'; +    els.display = ''; +    var originalWidth = element.clientWidth; +    var originalHeight = element.clientHeight; +    els.display = 'none'; +    els.position = originalPosition; +    els.visibility = originalVisibility; +    return {width: originalWidth, height: originalHeight}; +  }, + +  makePositioned: function(element) { +    element = $(element); +    var pos = Element.getStyle(element, 'position'); +    if (pos == 'static' || !pos) { +      element._madePositioned = true; +      element.style.position = 'relative'; +      // Opera returns the offset relative to the positioning context, when an +      // element is position relative but top and left have not been defined +      if (window.opera) { +        element.style.top = 0; +        element.style.left = 0; +      } +    } +  }, + +  undoPositioned: function(element) { +    element = $(element); +    if (element._madePositioned) { +      element._madePositioned = undefined; +      element.style.position = +        element.style.top = +        element.style.left = +        element.style.bottom = +        element.style.right = ''; +    } +  }, + +  makeClipping: function(element) { +    element = $(element); +    if (element._overflow) return; +    element._overflow = element.style.overflow; +    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') +      element.style.overflow = 'hidden'; +  }, + +  undoClipping: function(element) { +    element = $(element); +    if (element._overflow) return; +    element.style.overflow = element._overflow; +    element._overflow = undefined; +  } +}); + +var Toggle = new Object(); +Toggle.display = Element.toggle; + +/*--------------------------------------------------------------------------*/ + +Abstract.Insertion = function(adjacency) { +  this.adjacency = adjacency; +} + +Abstract.Insertion.prototype = { +  initialize: function(element, content) { +    this.element = $(element); +    this.content = content.stripScripts(); + +    if (this.adjacency && this.element.insertAdjacentHTML) { +      try { +        this.element.insertAdjacentHTML(this.adjacency, this.content); +      } catch (e) { +        if (this.element.tagName.toLowerCase() == 'tbody') { +          this.insertContent(this.contentFromAnonymousTable()); +        } else { +          throw e; +        } +      } +    } else { +      this.range = this.element.ownerDocument.createRange(); +      if (this.initializeRange) this.initializeRange(); +      this.insertContent([this.range.createContextualFragment(this.content)]); +    } + +    setTimeout(function() {content.evalScripts()}, 10); +  }, + +  contentFromAnonymousTable: function() { +    var div = document.createElement('div'); +    div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>'; +    return $A(div.childNodes[0].childNodes[0].childNodes); +  } +} + +var Insertion = new Object(); + +Insertion.Before = Class.create(); +Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { +  initializeRange: function() { +    this.range.setStartBefore(this.element); +  }, + +  insertContent: function(fragments) { +    fragments.each((function(fragment) { +      this.element.parentNode.insertBefore(fragment, this.element); +    }).bind(this)); +  } +}); + +Insertion.Top = Class.create(); +Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { +  initializeRange: function() { +    this.range.selectNodeContents(this.element); +    this.range.collapse(true); +  }, + +  insertContent: function(fragments) { +    fragments.reverse(false).each((function(fragment) { +      this.element.insertBefore(fragment, this.element.firstChild); +    }).bind(this)); +  } +}); + +Insertion.Bottom = Class.create(); +Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { +  initializeRange: function() { +    this.range.selectNodeContents(this.element); +    this.range.collapse(this.element); +  }, + +  insertContent: function(fragments) { +    fragments.each((function(fragment) { +      this.element.appendChild(fragment); +    }).bind(this)); +  } +}); + +Insertion.After = Class.create(); +Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { +  initializeRange: function() { +    this.range.setStartAfter(this.element); +  }, + +  insertContent: function(fragments) { +    fragments.each((function(fragment) { +      this.element.parentNode.insertBefore(fragment, +        this.element.nextSibling); +    }).bind(this)); +  } +}); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { +  initialize: function(element) { +    this.element = $(element); +  }, + +  _each: function(iterator) { +    this.element.className.split(/\s+/).select(function(name) { +      return name.length > 0; +    })._each(iterator); +  }, + +  set: function(className) { +    this.element.className = className; +  }, + +  add: function(classNameToAdd) { +    if (this.include(classNameToAdd)) return; +    this.set(this.toArray().concat(classNameToAdd).join(' ')); +  }, + +  remove: function(classNameToRemove) { +    if (!this.include(classNameToRemove)) return; +    this.set(this.select(function(className) { +      return className != classNameToRemove; +    }).join(' ')); +  }, + +  toString: function() { +    return this.toArray().join(' '); +  } +} + +Object.extend(Element.ClassNames.prototype, Enumerable); +var Field = { +  clear: function() { +    for (var i = 0; i < arguments.length; i++) +      $(arguments[i]).value = ''; +  }, + +  focus: function(element) { +    $(element).focus(); +  }, + +  present: function() { +    for (var i = 0; i < arguments.length; i++) +      if ($(arguments[i]).value == '') return false; +    return true; +  }, + +  select: function(element) { +    $(element).select(); +  }, + +  activate: function(element) { +    element = $(element); +    element.focus(); +    if (element.select) +      element.select(); +  } +} + +/*--------------------------------------------------------------------------*/ + +var Form = { +  serialize: function(form) { +    var elements = Form.getElements($(form)); +    var queryComponents = new Array(); + +    for (var i = 0; i < elements.length; i++) { +      var queryComponent = Form.Element.serialize(elements[i]); +      if (queryComponent) +        queryComponents.push(queryComponent); +    } + +    return queryComponents.join('&'); +  }, + +  getElements: function(form) { +    form = $(form); +    var elements = new Array(); + +    for (tagName in Form.Element.Serializers) { +      var tagElements = form.getElementsByTagName(tagName); +      for (var j = 0; j < tagElements.length; j++) +        elements.push(tagElements[j]); +    } +    return elements; +  }, + +  getInputs: function(form, typeName, name) { +    form = $(form); +    var inputs = form.getElementsByTagName('input'); + +    if (!typeName && !name) +      return inputs; + +    var matchingInputs = new Array(); +    for (var i = 0; i < inputs.length; i++) { +      var input = inputs[i]; +      if ((typeName && input.type != typeName) || +          (name && input.name != name)) +        continue; +      matchingInputs.push(input); +    } + +    return matchingInputs; +  }, + +  disable: function(form) { +    var elements = Form.getElements(form); +    for (var i = 0; i < elements.length; i++) { +      var element = elements[i]; +      element.blur(); +      element.disabled = 'true'; +    } +  }, + +  enable: function(form) { +    var elements = Form.getElements(form); +    for (var i = 0; i < elements.length; i++) { +      var element = elements[i]; +      element.disabled = ''; +    } +  }, + +  findFirstElement: function(form) { +    return Form.getElements(form).find(function(element) { +      return element.type != 'hidden' && !element.disabled && +        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); +    }); +  }, + +  focusFirstElement: function(form) { +    Field.activate(Form.findFirstElement(form)); +  }, + +  reset: function(form) { +    $(form).reset(); +  } +} + +Form.Element = { +  serialize: function(element) { +    element = $(element); +    var method = element.tagName.toLowerCase(); +    var parameter = Form.Element.Serializers[method](element); + +    if (parameter) { +      var key = encodeURIComponent(parameter[0]); +      if (key.length == 0) return; + +      if (parameter[1].constructor != Array) +        parameter[1] = [parameter[1]]; + +      return parameter[1].map(function(value) { +        return key + '=' + encodeURIComponent(value); +      }).join('&'); +    } +  }, + +  getValue: function(element) { +    element = $(element); +    var method = element.tagName.toLowerCase(); +    var parameter = Form.Element.Serializers[method](element); + +    if (parameter) +      return parameter[1]; +  } +} + +Form.Element.Serializers = { +  input: function(element) { +    switch (element.type.toLowerCase()) { +      case 'submit': +      case 'hidden': +      case 'password': +      case 'text': +        return Form.Element.Serializers.textarea(element); +      case 'checkbox': +      case 'radio': +        return Form.Element.Serializers.inputSelector(element); +    } +    return false; +  }, + +  inputSelector: function(element) { +    if (element.checked) +      return [element.name, element.value]; +  }, + +  textarea: function(element) { +    return [element.name, element.value]; +  }, + +  select: function(element) { +    return Form.Element.Serializers[element.type == 'select-one' ? +      'selectOne' : 'selectMany'](element); +  }, + +  selectOne: function(element) { +    var value = '', opt, index = element.selectedIndex; +    if (index >= 0) { +      opt = element.options[index]; +      value = opt.value; +      if (!value && !('value' in opt)) +        value = opt.text; +    } +    return [element.name, value]; +  }, + +  selectMany: function(element) { +    var value = new Array(); +    for (var i = 0; i < element.length; i++) { +      var opt = element.options[i]; +      if (opt.selected) { +        var optValue = opt.value; +        if (!optValue && !('value' in opt)) +          optValue = opt.text; +        value.push(optValue); +      } +    } +    return [element.name, value]; +  } +} + +/*--------------------------------------------------------------------------*/ + +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = function() {} +Abstract.TimedObserver.prototype = { +  initialize: function(element, frequency, callback) { +    this.frequency = frequency; +    this.element   = $(element); +    this.callback  = callback; + +    this.lastValue = this.getValue(); +    this.registerCallback(); +  }, + +  registerCallback: function() { +    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); +  }, + +  onTimerEvent: function() { +    var value = this.getValue(); +    if (this.lastValue != value) { +      this.callback(this.element, value); +      this.lastValue = value; +    } +  } +} + +Form.Element.Observer = Class.create(); +Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { +  getValue: function() { +    return Form.Element.getValue(this.element); +  } +}); + +Form.Observer = Class.create(); +Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { +  getValue: function() { +    return Form.serialize(this.element); +  } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = function() {} +Abstract.EventObserver.prototype = { +  initialize: function(element, callback) { +    this.element  = $(element); +    this.callback = callback; + +    this.lastValue = this.getValue(); +    if (this.element.tagName.toLowerCase() == 'form') +      this.registerFormCallbacks(); +    else +      this.registerCallback(this.element); +  }, + +  onElementEvent: function() { +    var value = this.getValue(); +    if (this.lastValue != value) { +      this.callback(this.element, value); +      this.lastValue = value; +    } +  }, + +  registerFormCallbacks: function() { +    var elements = Form.getElements(this.element); +    for (var i = 0; i < elements.length; i++) +      this.registerCallback(elements[i]); +  }, + +  registerCallback: function(element) { +    if (element.type) { +      switch (element.type.toLowerCase()) { +        case 'checkbox': +        case 'radio': +          Event.observe(element, 'click', this.onElementEvent.bind(this)); +          break; +        case 'password': +        case 'text': +        case 'textarea': +        case 'select-one': +        case 'select-multiple': +          Event.observe(element, 'change', this.onElementEvent.bind(this)); +          break; +      } +    } +  } +} + +Form.Element.EventObserver = Class.create(); +Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { +  getValue: function() { +    return Form.Element.getValue(this.element); +  } +}); + +Form.EventObserver = Class.create(); +Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { +  getValue: function() { +    return Form.serialize(this.element); +  } +}); +if (!window.Event) { +  var Event = new Object(); +} + +Object.extend(Event, { +  KEY_BACKSPACE: 8, +  KEY_TAB:       9, +  KEY_RETURN:   13, +  KEY_ESC:      27, +  KEY_LEFT:     37, +  KEY_UP:       38, +  KEY_RIGHT:    39, +  KEY_DOWN:     40, +  KEY_DELETE:   46, + +  element: function(event) { +    return event.target || event.srcElement; +  }, + +  isLeftClick: function(event) { +    return (((event.which) && (event.which == 1)) || +            ((event.button) && (event.button == 1))); +  }, + +  pointerX: function(event) { +    return event.pageX || (event.clientX + +      (document.documentElement.scrollLeft || document.body.scrollLeft)); +  }, + +  pointerY: function(event) { +    return event.pageY || (event.clientY + +      (document.documentElement.scrollTop || document.body.scrollTop)); +  }, + +  stop: function(event) { +    if (event.preventDefault) { +      event.preventDefault(); +      event.stopPropagation(); +    } else { +      event.returnValue = false; +      event.cancelBubble = true; +    } +  }, + +  // find the first node with the given tagName, starting from the +  // node the event was triggered on; traverses the DOM upwards +  findElement: function(event, tagName) { +    var element = Event.element(event); +    while (element.parentNode && (!element.tagName || +        (element.tagName.toUpperCase() != tagName.toUpperCase()))) +      element = element.parentNode; +    return element; +  }, + +  observers: false, + +  _observeAndCache: function(element, name, observer, useCapture) { +    if (!this.observers) this.observers = []; +    if (element.addEventListener) { +      this.observers.push([element, name, observer, useCapture]); +      element.addEventListener(name, observer, useCapture); +    } else if (element.attachEvent) { +      this.observers.push([element, name, observer, useCapture]); +      element.attachEvent('on' + name, observer); +    } +  }, + +  unloadCache: function() { +    if (!Event.observers) return; +    for (var i = 0; i < Event.observers.length; i++) { +      Event.stopObserving.apply(this, Event.observers[i]); +      Event.observers[i][0] = null; +    } +    Event.observers = false; +  }, + +  observe: function(element, name, observer, useCapture) { +    var element = $(element); +    useCapture = useCapture || false; + +    if (name == 'keypress' && +        (navigator.appVersion.match(/Konqueror|Safari|KHTML/) +        || element.attachEvent)) +      name = 'keydown'; + +    this._observeAndCache(element, name, observer, useCapture); +  }, + +  stopObserving: function(element, name, observer, useCapture) { +    var element = $(element); +    useCapture = useCapture || false; + +    if (name == 'keypress' && +        (navigator.appVersion.match(/Konqueror|Safari|KHTML/) +        || element.detachEvent)) +      name = 'keydown'; + +    if (element.removeEventListener) { +      element.removeEventListener(name, observer, useCapture); +    } else if (element.detachEvent) { +      element.detachEvent('on' + name, observer); +    } +  } +}); + +/* prevent memory leaks in IE */ +Event.observe(window, 'unload', Event.unloadCache, false); +var Position = { +  // set to true if needed, warning: firefox performance problems +  // NOT neeeded for page scrolling, only if draggable contained in +  // scrollable elements +  includeScrollOffsets: false, + +  // must be called before calling withinIncludingScrolloffset, every time the +  // page is scrolled +  prepare: function() { +    this.deltaX =  window.pageXOffset +                || document.documentElement.scrollLeft +                || document.body.scrollLeft +                || 0; +    this.deltaY =  window.pageYOffset +                || document.documentElement.scrollTop +                || document.body.scrollTop +                || 0; +  }, + +  realOffset: function(element) { +    var valueT = 0, valueL = 0; +    do { +      valueT += element.scrollTop  || 0; +      valueL += element.scrollLeft || 0; +      element = element.parentNode; +    } while (element); +    return [valueL, valueT]; +  }, + +  cumulativeOffset: function(element) { +    var valueT = 0, valueL = 0; +    do { +      valueT += element.offsetTop  || 0; +      valueL += element.offsetLeft || 0; +      element = element.offsetParent; +    } while (element); +    return [valueL, valueT]; +  }, + +  positionedOffset: function(element) { +    var valueT = 0, valueL = 0; +    do { +      valueT += element.offsetTop  || 0; +      valueL += element.offsetLeft || 0; +      element = element.offsetParent; +      if (element) { +        p = Element.getStyle(element, 'position'); +        if (p == 'relative' || p == 'absolute') break; +      } +    } while (element); +    return [valueL, valueT]; +  }, + +  offsetParent: function(element) { +    if (element.offsetParent) return element.offsetParent; +    if (element == document.body) return element; + +    while ((element = element.parentNode) && element != document.body) +      if (Element.getStyle(element, 'position') != 'static') +        return element; + +    return document.body; +  }, + +  // caches x/y coordinate pair to use with overlap +  within: function(element, x, y) { +    if (this.includeScrollOffsets) +      return this.withinIncludingScrolloffsets(element, x, y); +    this.xcomp = x; +    this.ycomp = y; +    this.offset = this.cumulativeOffset(element); + +    return (y >= this.offset[1] && +            y <  this.offset[1] + element.offsetHeight && +            x >= this.offset[0] && +            x <  this.offset[0] + element.offsetWidth); +  }, + +  withinIncludingScrolloffsets: function(element, x, y) { +    var offsetcache = this.realOffset(element); + +    this.xcomp = x + offsetcache[0] - this.deltaX; +    this.ycomp = y + offsetcache[1] - this.deltaY; +    this.offset = this.cumulativeOffset(element); + +    return (this.ycomp >= this.offset[1] && +            this.ycomp <  this.offset[1] + element.offsetHeight && +            this.xcomp >= this.offset[0] && +            this.xcomp <  this.offset[0] + element.offsetWidth); +  }, + +  // within must be called directly before +  overlap: function(mode, element) { +    if (!mode) return 0; +    if (mode == 'vertical') +      return ((this.offset[1] + element.offsetHeight) - this.ycomp) / +        element.offsetHeight; +    if (mode == 'horizontal') +      return ((this.offset[0] + element.offsetWidth) - this.xcomp) / +        element.offsetWidth; +  }, + +  clone: function(source, target) { +    source = $(source); +    target = $(target); +    target.style.position = 'absolute'; +    var offsets = this.cumulativeOffset(source); +    target.style.top    = offsets[1] + 'px'; +    target.style.left   = offsets[0] + 'px'; +    target.style.width  = source.offsetWidth + 'px'; +    target.style.height = source.offsetHeight + 'px'; +  }, + +  page: function(forElement) { +    var valueT = 0, valueL = 0; + +    var element = forElement; +    do { +      valueT += element.offsetTop  || 0; +      valueL += element.offsetLeft || 0; + +      // Safari fix +      if (element.offsetParent==document.body) +        if (Element.getStyle(element,'position')=='absolute') break; + +    } while (element = element.offsetParent); + +    element = forElement; +    do { +      valueT -= element.scrollTop  || 0; +      valueL -= element.scrollLeft || 0; +    } while (element = element.parentNode); + +    return [valueL, valueT]; +  }, + +  clone: function(source, target) { +    var options = Object.extend({ +      setLeft:    true, +      setTop:     true, +      setWidth:   true, +      setHeight:  true, +      offsetTop:  0, +      offsetLeft: 0 +    }, arguments[2] || {}) + +    // find page position of source +    source = $(source); +    var p = Position.page(source); + +    // find coordinate system to use +    target = $(target); +    var delta = [0, 0]; +    var parent = null; +    // delta [0,0] will do fine with position: fixed elements, +    // position:absolute needs offsetParent deltas +    if (Element.getStyle(target,'position') == 'absolute') { +      parent = Position.offsetParent(target); +      delta = Position.page(parent); +    } + +    // correct by body offsets (fixes Safari) +    if (parent == document.body) { +      delta[0] -= document.body.offsetLeft; +      delta[1] -= document.body.offsetTop; +    } + +    // set position +    if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px'; +    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px'; +    if(options.setWidth)  target.style.width = source.offsetWidth + 'px'; +    if(options.setHeight) target.style.height = source.offsetHeight + 'px'; +  }, + +  absolutize: function(element) { +    element = $(element); +    if (element.style.position == 'absolute') return; +    Position.prepare(); + +    var offsets = Position.positionedOffset(element); +    var top     = offsets[1]; +    var left    = offsets[0]; +    var width   = element.clientWidth; +    var height  = element.clientHeight; + +    element._originalLeft   = left - parseFloat(element.style.left  || 0); +    element._originalTop    = top  - parseFloat(element.style.top || 0); +    element._originalWidth  = element.style.width; +    element._originalHeight = element.style.height; + +    element.style.position = 'absolute'; +    element.style.top    = top + 'px';; +    element.style.left   = left + 'px';; +    element.style.width  = width + 'px';; +    element.style.height = height + 'px';; +  }, + +  relativize: function(element) { +    element = $(element); +    if (element.style.position == 'relative') return; +    Position.prepare(); + +    element.style.position = 'relative'; +    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0); +    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + +    element.style.top    = top + 'px'; +    element.style.left   = left + 'px'; +    element.style.height = element._originalHeight; +    element.style.width  = element._originalWidth; +  } +} + +// Safari returns margins on body which is incorrect if the child is absolutely +// positioned.  For performance reasons, redefine Position.cumulativeOffset for +// KHTML/WebKit only. +if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { +  Position.cumulativeOffset = function(element) { +    var valueT = 0, valueL = 0; +    do { +      valueT += element.offsetTop  || 0; +      valueL += element.offsetLeft || 0; +      if (element.offsetParent == document.body) +        if (Element.getStyle(element, 'position') == 'absolute') break; + +      element = element.offsetParent; +    } while (element); + +    return [valueL, valueT]; +  } +}
\ No newline at end of file diff --git a/tests/test_tools/selenium/core/scripts/selenium-api.js b/tests/test_tools/selenium/core/scripts/selenium-api.js new file mode 100644 index 00000000..ad0509ee --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/selenium-api.js @@ -0,0 +1,1402 @@ +/* + * Copyright 2004 ThoughtWorks, Inc + * + *  Licensed under the Apache License, Version 2.0 (the "License"); + *  you may not use this file except in compliance with the License. + *  You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + *  Unless required by applicable law or agreed to in writing, software + *  distributed under the License is distributed on an "AS IS" BASIS, + *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + *  See the License for the specific language governing permissions and + *  limitations under the License. + * + */ + +var storedVars = new Object(); + +function Selenium(browserbot) { +	/** +	 * Defines an object that runs Selenium commands. +	 *  +	 * <h3><a name="locators"></a>Element Locators</h3> +	 * <p> +	 * Element Locators tell Selenium which HTML element a command refers to. +	 * The format of a locator is:</p> +	 * <blockquote> +	 * <em>locatorType</em><strong>=</strong><em>argument</em> +	 * </blockquote> +	 *  +	 * <p> +	 * We support the following strategies for locating elements: +	 * </p> +	 * <blockquote> +	 * <dl> +	 * <dt><strong>identifier</strong>=<em>id</em></dt> +	 * <dd>Select the element with the specified @id attribute. If no match is +	 * found, select the first element whose @name attribute is <em>id</em>. +	 * (This is normally the default; see below.)</dd> +	 * <dt><strong>id</strong>=<em>id</em></dt> +	 * <dd>Select the element with the specified @id attribute.</dd> +	 *  +	 * <dt><strong>name</strong>=<em>name</em></dt> +	 * <dd>Select the first element with the specified @name attribute.</dd> +	 * <dd><ul class="first last simple"> +	 * <li>username</li> +	 * <li>name=username</li> +	 * </ul> +	 * </dd> +	 * <dd>The name may optionally be followed by one or more <em>element-filters</em>, separated from the name by whitespace.  If the <em>filterType</em> is not specified, <strong>value</strong> is assumed.</dd> +	 *  +	 * <dd><ul class="first last simple"> +	 * <li>name=flavour value=chocolate</li> +	 * </ul> +	 * </dd> +	 * <dt><strong>dom</strong>=<em>javascriptExpression</em></dt> +	 *  +	 * <dd> +	 *  +	 * <dd>Find an element using JavaScript traversal of the HTML Document Object +	 * Model. DOM locators <em>must</em> begin with "document.". +	 * <ul class="first last simple"> +	 * <li>dom=document.forms['myForm'].myDropdown</li> +	 * <li>dom=document.images[56]</li> +	 * </ul> +	 * </dd> +	 *  +	 * </dd> +	 *  +	 * <dt><strong>xpath</strong>=<em>xpathExpression</em></dt> +	 * <dd>Locate an element using an XPath expression. +	 * <ul class="first last simple"> +	 * <li>xpath=//img[@alt='The image alt text']</li> +	 * <li>xpath=//table[@id='table1']//tr[4]/td[2]</li> +	 *  +	 * </ul> +	 * </dd> +	 * <dt><strong>link</strong>=<em>textPattern</em></dt> +	 * <dd>Select the link (anchor) element which contains text matching the +	 * specified <em>pattern</em>. +	 * <ul class="first last simple"> +	 * <li>link=The link text</li> +	 * </ul> +	 *  +	 * </dd> +	 * </dl> +	 * </blockquote> +	 * <p> +	 * Without an explicit locator prefix, Selenium uses the following default +	 * strategies: +	 * </p> +	 *  +	 * <ul class="simple"> +	 * <li><strong>dom</strong>, for locators starting with "document."</li> +	 * <li><strong>xpath</strong>, for locators starting with "//"</li> +	 * <li><strong>identifier</strong>, otherwise</li> +	 * </ul> +	 * +	 * <h3><a name="element-filters">Element Filters</a></h3> +	 * <blockquote> +	 * <p>Element filters can be used with a locator to refine a list of candidate elements.  They are currently used only in the 'name' element-locator.</p> +	 * <p>Filters look much like locators, ie.</p> +	 * <blockquote> +	 * <em>filterType</em><strong>=</strong><em>argument</em></blockquote> +	 *  +	 * <p>Supported element-filters are:</p> +	 * <p><strong>value=</strong><em>valuePattern</em></p> +	 * <blockquote> +	 * Matches elements based on their values.  This is particularly useful for refining a list of similarly-named toggle-buttons.</blockquote> +	 * <p><strong>index=</strong><em>index</em></p> +	 * <blockquote> +	 * Selects a single element based on its position in the list (offset from zero).</blockquote> +	 * </blockquote> +	 * +	 * <h3><a name="patterns"></a>String-match Patterns</h3> +	 *  +	 * <p> +	 * Various Pattern syntaxes are available for matching string values: +	 * </p> +	 * <blockquote> +	 * <dl> +	 * <dt><strong>glob:</strong><em>pattern</em></dt> +	 * <dd>Match a string against a "glob" (aka "wildmat") pattern. "Glob" is a +	 * kind of limited regular-expression syntax typically used in command-line +	 * shells. In a glob pattern, "*" represents any sequence of characters, and "?" +	 * represents any single character. Glob patterns match against the entire +	 * string.</dd> +	 * <dt><strong>regexp:</strong><em>regexp</em></dt> +	 * <dd>Match a string using a regular-expression. The full power of JavaScript +	 * regular-expressions is available.</dd> +	 * <dt><strong>exact:</strong><em>string</em></dt> +	 *  +	 * <dd>Match a string exactly, verbatim, without any of that fancy wildcard +	 * stuff.</dd> +	 * </dl> +	 * </blockquote> +	 * <p> +	 * If no pattern prefix is specified, Selenium assumes that it's a "glob" +	 * pattern. +	 * </p> +	 */ +    this.browserbot = browserbot; +    this.optionLocatorFactory = new OptionLocatorFactory(); +    this.page = function() { +        return browserbot.getCurrentPage(); +    }; +} + +Selenium.createForFrame = function(frame) { +    return new Selenium(BrowserBot.createForFrame(frame)); +}; + +Selenium.prototype.reset = function() { +	/** +   * Clear out all stored variables and select the null (starting) window +   */ +    storedVars = new Object(); +    this.browserbot.selectWindow("null"); +}; + +Selenium.prototype.doClick = function(locator) { +	/** +   * Clicks on a link, button, checkbox or radio button. If the click action +   * causes a new page to load (like a link usually does), call +   * waitForPageToLoad. +   *  +   * @param locator an element locator +   *  +   */ +    var element = this.page().findElement(locator); +    this.page().clickElement(element); +}; + +Selenium.prototype.doFireEvent = function(locator, eventName) { +	/** +   * Explicitly simulate an event, to trigger the corresponding "on<em>event</em>" +   * handler. +   * +   * @param locator an <a href="#locators">element locator</a> +   * @param eventName the event name, e.g. "focus" or "blur" +   */ +    var element = this.page().findElement(locator); +    triggerEvent(element, eventName, false); +}; + +Selenium.prototype.doKeyPress = function(locator, keycode) { +	/** +   * Simulates a user pressing and releasing a key. +   *  +   * @param locator an <a href="#locators">element locator</a> +   * @param keycode the numeric keycode of the key to be pressed, normally the +   *            ASCII value of that key. +   */ +    var element = this.page().findElement(locator); +    triggerKeyEvent(element, 'keypress', keycode, true); +}; + +Selenium.prototype.doKeyDown = function(locator, keycode) { +	/** +   * Simulates a user pressing a key (without releasing it yet). +   *  +   * @param locator an <a href="#locators">element locator</a> +   * @param keycode the numeric keycode of the key to be pressed, normally the +   *            ASCII value of that key. +   */ +    var element = this.page().findElement(locator); +    triggerKeyEvent(element, 'keydown', keycode, true); +}; + +Selenium.prototype.doKeyUp = function(locator, keycode) { +	/** +   * Simulates a user releasing a key. +   *  +   * @param locator an <a href="#locators">element locator</a> +   * @param keycode the numeric keycode of the key to be released, normally the +   *            ASCII value of that key. +   */ +    var element = this.page().findElement(locator); +    triggerKeyEvent(element, 'keyup', keycode, true); +}; + +Selenium.prototype.doMouseOver = function(locator) { +	/** +   * Simulates a user hovering a mouse over the specified element. +   *  +   * @param locator an <a href="#locators">element locator</a> +   */ +    var element = this.page().findElement(locator); +    triggerMouseEvent(element, 'mouseover', true); +}; + +Selenium.prototype.doMouseDown = function(locator) { +	/** +   * Simulates a user pressing the mouse button (without releasing it yet) on +   * the specified element. +   *  +   * @param locator an <a href="#locators">element locator</a> +   */ +    var element = this.page().findElement(locator); +    triggerMouseEvent(element, 'mousedown', true); +}; + +Selenium.prototype.doType = function(locator, value) { +	/** +   * Sets the value of an input field, as though you typed it in. +   *  +   * <p>Can also be used to set the value of combo boxes, check boxes, etc. In these cases, +   * value should be the value of the option selected, not the visible text.</p> +   *  +   * @param locator an <a href="#locators">element locator</a> +   * @param value the value to type +   */ +		// TODO fail if it can't be typed into. +    var element = this.page().findElement(locator); +    this.page().replaceText(element, value); +}; + +Selenium.prototype.findToggleButton = function(locator) { +    var element = this.page().findElement(locator); +    if (element.checked == null) { +        Assert.fail("Element " + locator + " is not a toggle-button."); +    } +    return element; +} + +Selenium.prototype.doCheck = function(locator) { +	/** +   * Check a toggle-button (checkbox/radio) +   *  +   * @param locator an <a href="#locators">element locator</a> +   */ +    this.findToggleButton(locator).checked = true; +}; + +Selenium.prototype.doUncheck = function(locator) { +	/** +   * Uncheck a toggle-button (checkbox/radio) +   *  +   * @param locator an <a href="#locators">element locator</a> +   */ +    this.findToggleButton(locator).checked = false; +}; + +Selenium.prototype.doSelect = function(selectLocator, optionLocator) { +	/** +   * Select an option from a drop-down using an option locator. +   *  +   * <p> +   * Option locators provide different ways of specifying options of an HTML +   * Select element (e.g. for selecting a specific option, or for asserting +   * that the selected option satisfies a specification). There are several +   * forms of Select Option Locator. +   * </p> +   * <dl> +   * <dt><strong>label</strong>=<em>labelPattern</em></dt> +   * <dd>matches options based on their labels, i.e. the visible text. (This +   * is the default.) +   * <ul class="first last simple"> +   * <li>label=regexp:^[Oo]ther</li> +   * </ul> +   * </dd> +   * <dt><strong>value</strong>=<em>valuePattern</em></dt> +   * <dd>matches options based on their values. +   * <ul class="first last simple"> +   * <li>value=other</li> +   * </ul> +   *  +   *  +   * </dd> +   * <dt><strong>id</strong>=<em>id</em></dt> +   *  +   * <dd>matches options based on their ids. +   * <ul class="first last simple"> +   * <li>id=option1</li> +   * </ul> +   * </dd> +   * <dt><strong>index</strong>=<em>index</em></dt> +   * <dd>matches an option based on its index (offset from zero). +   * <ul class="first last simple"> +   *  +   * <li>index=2</li> +   * </ul> +   * </dd> +   * </dl> +   * <p> +   * If no option locator prefix is provided, the default behaviour is to match on <strong>label</strong>. +   * </p> +   *  +   *  +   * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu +   * @param optionLocator an option locator (a label by default) +   */ +    var element = this.page().findElement(selectLocator); +    if (!("options" in element)) { +        throw new SeleniumError("Specified element is not a Select (has no options)"); +    } +    var locator = this.optionLocatorFactory.fromLocatorString(optionLocator); +    var option = locator.findOption(element); +    this.page().selectOption(element, option); +}; + +Selenium.prototype.doAddSelection = function(locator, optionLocator) { +    /** +   * Add a selection to the set of selected options in a multi-select element using an option locator. +   * +   * @see #doSelect for details of option locators +   * +   * @param locator an <a href="#locators">element locator</a> identifying a multi-select box +   * @param optionLocator an option locator (a label by default) +   */ +    var element = this.page().findElement(locator); +    if (!("options" in element)) { +        throw new SeleniumError("Specified element is not a Select (has no options)"); +    } +    var locator = this.optionLocatorFactory.fromLocatorString(optionLocator); +    var option = locator.findOption(element); +    this.page().addSelection(element, option); +}; + +Selenium.prototype.doRemoveSelection = function(locator, optionLocator) { +    /** +   * Remove a selection from the set of selected options in a multi-select element using an option locator. +   * +   * @see #doSelect for details of option locators +   * +   * @param locator an <a href="#locators">element locator</a> identifying a multi-select box +   * @param optionLocator an option locator (a label by default) +   */ + +    var element = this.page().findElement(locator); +    if (!("options" in element)) { +        throw new SeleniumError("Specified element is not a Select (has no options)"); +    } +    var locator = this.optionLocatorFactory.fromLocatorString(optionLocator); +    var option = locator.findOption(element); +    this.page().removeSelection(element, option); +}; + +Selenium.prototype.doSubmit = function(formLocator) { +	/** +   * Submit the specified form. This is particularly useful for forms without +   * submit buttons, e.g. single-input "Search" forms. +   *  +   * @param formLocator an <a href="#locators">element locator</a> for the form you want to submit +   */ +    var form = this.page().findElement(formLocator); +    var actuallySubmit = true; +    if (form.onsubmit) { +    	// apply this to the correct window so alerts are properly handled, even in IE HTA mode +    	actuallySubmit = form.onsubmit.apply(this.browserbot.getContentWindow()); +    } +    if (actuallySubmit) { +        form.submit(); +    } +     +}; + +Selenium.prototype.doOpen = function(url) { +	/** +   * Opens an URL in the test frame. This accepts both relative and absolute +   * URLs. +   *  +   * The "open" command waits for the page to load before proceeding, +   * ie. the "AndWait" suffix is implicit. +   * +   * <em>Note</em>: The URL must be on the same domain as the runner HTML +   * due to security restrictions in the browser (Same Origin Policy). If you +   * need to open an URL on another domain, use the Selenium Server to start a +   * new browser session on that domain. +   *  +   * @param url the URL to open; may be relative or absolute +   */ +    this.browserbot.openLocation(url); +    return SELENIUM_PROCESS_WAIT; +}; + +Selenium.prototype.doSelectWindow = function(windowID) { +	/** +   * Selects a popup window; once a popup window has been selected, all +   * commands go to that window. To select the main window again, use "null" +   * as the target. +   *  +   * @param windowID the JavaScript window ID of the window to select +   */ +    this.browserbot.selectWindow(windowID); +}; + +Selenium.prototype.doWaitForPopUp = function(windowID, timeout) { +	/** +	* Waits for a popup window to appear and load up. +	* +	* @param windowID the JavaScript window ID of the window that will appear +	* @param timeout a timeout in milliseconds, after which the action will return with an error +	*/ +	if (isNaN(timeout)) { +    	throw new SeleniumError("Timeout is not a number: " + timeout); +    } +     +    testLoop.waitForCondition = function () { +        var targetWindow = selenium.browserbot.getTargetWindow(windowID); +        if (!targetWindow) return false; +        if (!targetWindow.location) return false; +        if ("about:blank" == targetWindow.location) return false; +        if (!targetWindow.document) return false; +        if (!targetWindow.document.readyState) return true; +        if ('complete' != targetWindow.document.readyState) return false; +        return true; +    }; +     +    testLoop.waitForConditionStart = new Date().getTime(); +    testLoop.waitForConditionTimeout = timeout; +	 +} + +Selenium.prototype.doWaitForPopUp.dontCheckAlertsAndConfirms = true; + +Selenium.prototype.doChooseCancelOnNextConfirmation = function() { +	/** +   * By default, Selenium's overridden window.confirm() function will +   * return true, as if the user had manually clicked OK.  After running +   * this command, the next call to confirm() will return false, as if +   * the user had clicked Cancel. +   *  +   */ +    this.browserbot.cancelNextConfirmation(); +}; + + +Selenium.prototype.doAnswerOnNextPrompt = function(answer) { +	/** +   * Instructs Selenium to return the specified answer string in response to +   * the next JavaScript prompt [window.prompt()]. +   *  +   *  +   * @param answer the answer to give in response to the prompt pop-up +   */ +    this.browserbot.setNextPromptResult(answer); +}; + +Selenium.prototype.doGoBack = function() { +    /** +     * Simulates the user clicking the "back" button on their browser. +     *  +     */ +    this.page().goBack(); +}; + +Selenium.prototype.doRefresh = function() { +    /** +     * Simulates the user clicking the "Refresh" button on their browser. +     *  +     */ +    this.page().refresh(); +}; + +Selenium.prototype.doClose = function() { +	/** +   * Simulates the user clicking the "close" button in the titlebar of a popup +   * window or tab. +   */ +    this.page().close(); +}; + +Selenium.prototype.isAlertPresent = function() { +   /** +   * Has an alert occurred? +   *  +   * <p> +   * This function never throws an exception +   * </p> +   * @return boolean true if there is an alert +   */ +    return this.browserbot.hasAlerts(); +}; +Selenium.prototype.isPromptPresent = function() { +   /** +   * Has a prompt occurred? +   *  +   * <p> +   * This function never throws an exception +   * </p> +   * @return boolean true if there is a pending prompt +   */ +    return this.browserbot.hasPrompts(); +}; +Selenium.prototype.isConfirmationPresent = function() { +   /** +   * Has confirm() been called? +   *  +   * <p> +   * This function never throws an exception +   * </p> +   * @return boolean true if there is a pending confirmation +   */ +    return this.browserbot.hasConfirmations(); +}; +Selenium.prototype.getAlert = function() { +	/** +   * Retrieves the message of a JavaScript alert generated during the previous action, or fail if there were no alerts. +   *  +   * <p>Getting an alert has the same effect as manually clicking OK. If an +   * alert is generated but you do not get/verify it, the next Selenium action +   * will fail.</p> +   *  +   * <p>NOTE: under Selenium, JavaScript alerts will NOT pop up a visible alert +   * dialog.</p> +   *  +   * <p>NOTE: Selenium does NOT support JavaScript alerts that are generated in a +   * page's onload() event handler. In this case a visible dialog WILL be +   * generated and Selenium will hang until someone manually clicks OK.</p> +   * @return string The message of the most recent JavaScript alert +   */ +    if (!this.browserbot.hasAlerts()) { +        Assert.fail("There were no alerts"); +    } +    return this.browserbot.getNextAlert(); +}; +Selenium.prototype.getAlert.dontCheckAlertsAndConfirms = true; + +Selenium.prototype.getConfirmation = function() { +	/** +   * Retrieves the message of a JavaScript confirmation dialog generated during +   * the previous action. +   *  +   * <p> +   * By default, the confirm function will return true, having the same effect +   * as manually clicking OK. This can be changed by prior execution of the +   * chooseCancelOnNextConfirmation command. If an confirmation is generated +   * but you do not get/verify it, the next Selenium action will fail. +   * </p> +   *  +   * <p> +   * NOTE: under Selenium, JavaScript confirmations will NOT pop up a visible +   * dialog. +   * </p> +   *  +   * <p> +   * NOTE: Selenium does NOT support JavaScript confirmations that are +   * generated in a page's onload() event handler. In this case a visible +   * dialog WILL be generated and Selenium will hang until you manually click +   * OK. +   * </p> +   *  +   * @return string the message of the most recent JavaScript confirmation dialog +   */ +    if (!this.browserbot.hasConfirmations()) { +        Assert.fail("There were no confirmations"); +    } +    return this.browserbot.getNextConfirmation(); +}; +Selenium.prototype.getConfirmation.dontCheckAlertsAndConfirms = true; +  +Selenium.prototype.getPrompt = function() { +	/** +   * Retrieves the message of a JavaScript question prompt dialog generated during +   * the previous action. +   *  +   * <p>Successful handling of the prompt requires prior execution of the +   * answerOnNextPrompt command. If a prompt is generated but you +   * do not get/verify it, the next Selenium action will fail.</p> +   *  +   * <p>NOTE: under Selenium, JavaScript prompts will NOT pop up a visible +   * dialog.</p> +   *  +   * <p>NOTE: Selenium does NOT support JavaScript prompts that are generated in a +   * page's onload() event handler. In this case a visible dialog WILL be +   * generated and Selenium will hang until someone manually clicks OK.</p> +   * @return string the message of the most recent JavaScript question prompt +   */ +    if (! this.browserbot.hasPrompts()) { +        Assert.fail("There were no prompts"); +    } +    return this.browserbot.getNextPrompt(); +}; + +Selenium.prototype.getLocation = function() { +	/** Gets the absolute URL of the current page. +   *  +   * @return string the absolute URL of the current page +   */ +    return this.page().location; +}; + +Selenium.prototype.getTitle = function() { +	/** Gets the title of the current page. +   *  +   * @return string the title of the current page +   */ +    return this.page().title(); +}; + + +Selenium.prototype.getBodyText = function() { +	/** +	 * Gets the entire text of the page. +	 * @return string the entire text of the page +	 */ +    return this.page().bodyText(); +}; + + +Selenium.prototype.getValue = function(locator) { +  /** +   * Gets the (whitespace-trimmed) value of an input field (or anything else with a value parameter). +   * For checkbox/radio elements, the value will be "on" or "off" depending on +   * whether the element is checked or not. +   *  +   * @param locator an <a href="#locators">element locator</a> +   * @return string the element value, or "on/off" for checkbox/radio elements +   */ +    var element = this.page().findElement(locator) +    return getInputValue(element).trim(); +} + +Selenium.prototype.getText = function(locator) { +	/** +   * Gets the text of an element. This works for any element that contains +   * text. This command uses either the textContent (Mozilla-like browsers) or +   * the innerText (IE-like browsers) of the element, which is the rendered +   * text shown to the user. +   *  +   * @param locator an <a href="#locators">element locator</a> +   * @return string the text of the element +   */ +    var element = this.page().findElement(locator); +    return getText(element).trim(); +}; + +Selenium.prototype.getEval = function(script) { +	/** Gets the result of evaluating the specified JavaScript snippet.  The snippet may  +   * have multiple lines, but only the result of the last line will be returned. +   *  +   * <p>Note that, by default, the snippet will run in the context of the "selenium" +   * object itself, so <code>this</code> will refer to the Selenium object, and <code>window</code> will +   * refer to the top-level runner test window, not the window of your application.</p> +   * +   * <p>If you need a reference to the window of your application, you can refer +   * to <code>this.browserbot.getCurrentWindow()</code> and if you need to use +   * a locator to refer to a single element in your application page, you can +   * use <code>this.page().findElement("foo")</code> where "foo" is your locator.</p> +   *  +   * @param script the JavaScript snippet to run +   * @return string the results of evaluating the snippet +   */ +    try { +    	var result = eval(script); +    	// Selenium RC doesn't allow returning null +    	if (null == result) return "null"; +    	return result; +    } catch (e) { +    	throw new SeleniumError("Threw an exception: " + e.message); +    } +}; + +Selenium.prototype.isChecked = function(locator) { +	/** +   * Gets whether a toggle-button (checkbox/radio) is checked.  Fails if the specified element doesn't exist or isn't a toggle-button. +   * @param locator an <a href="#locators">element locator</a> pointing to a checkbox or radio button +   * @return string either "true" or "false" depending on whether the checkbox is checked +   */ +    var element = this.page().findElement(locator); +    if (element.checked == null) { +        throw new SeleniumError("Element " + locator + " is not a toggle-button."); +    } +    return element.checked; +}; + +Selenium.prototype.getTable = function(tableCellAddress) { +	/** +   * Gets the text from a cell of a table. The cellAddress syntax +   * tableLocator.row.column, where row and column start at 0. +   *  +   * @param tableCellAddress a cell address, e.g. "foo.1.4" +   * @return string the text from the specified cell +   */ +    // This regular expression matches "tableName.row.column" +    // For example, "mytable.3.4" +    pattern = /(.*)\.(\d+)\.(\d+)/; + +    if(!pattern.test(tableCellAddress)) { +        throw new SeleniumError("Invalid target format. Correct format is tableName.rowNum.columnNum"); +    } + +    pieces = tableCellAddress.match(pattern); + +    tableName = pieces[1]; +    row = pieces[2]; +    col = pieces[3]; + +    var table = this.page().findElement(tableName); +    if (row > table.rows.length) { +        Assert.fail("Cannot access row " + row + " - table has " + table.rows.length + " rows"); +    } +    else if (col > table.rows[row].cells.length) { +        Assert.fail("Cannot access column " + col + " - table row has " + table.rows[row].cells.length + " columns"); +    } +    else { +        actualContent = getText(table.rows[row].cells[col]); +        return actualContent.trim(); +    } +	return null; +}; + +Selenium.prototype.assertSelected = function(selectLocator, optionLocator) { +	/** +   * Verifies that the selected option of a drop-down satisfies the optionSpecifier. +   *  +   * <p>See the select command for more information about option locators.</p> +   *  +   * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu +   * @param optionLocator an option locator, typically just an option label (e.g. "John Smith") +   */ +    var element = this.page().findElement(selectLocator); +    var locator = this.optionLocatorFactory.fromLocatorString(optionLocator); +    if (element.selectedIndex == -1) +    { +        Assert.fail("No option selected"); +    } +    locator.assertSelected(element); +}; + +Selenium.prototype.getSelectedLabels = function(selectLocator) { +    /** Gets all option labels (visible text) for selected options in the specified select or multi-select element. +   * +   * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu +   * @return string[] an array of all selected option labels in the specified select drop-down +   */ +    return this.findSelectedOptionProperties(selectLocator, "text").join(","); +} + +Selenium.prototype.getSelectedLabel = function(selectLocator) { +    /** Gets option label (visible text) for selected option in the specified select element. +   * +   * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu +   * @return string the selected option label in the specified select drop-down +   */ +    return this.findSelectedOptionProperty(selectLocator, "text"); +} + +Selenium.prototype.getSelectedValues = function(selectLocator) { +    /** Gets all option values (value attributes) for selected options in the specified select or multi-select element. +   * +   * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu +   * @return string[] an array of all selected option values in the specified select drop-down +   */ +    return this.findSelectedOptionProperties(selectLocator, "value").join(","); +} + +Selenium.prototype.getSelectedValue = function(selectLocator) { +    /** Gets option value (value attribute) for selected option in the specified select element. +   * +   * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu +   * @return string the selected option value in the specified select drop-down +   */ +    return this.findSelectedOptionProperty(selectLocator, "value"); +} + +Selenium.prototype.getSelectedIndexes = function(selectLocator) { +    /** Gets all option indexes (option number, starting at 0) for selected options in the specified select or multi-select element. +   * +   * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu +   * @return string[] an array of all selected option indexes in the specified select drop-down +   */ +    return this.findSelectedOptionProperties(selectLocator, "index").join(","); +} + +Selenium.prototype.getSelectedIndex = function(selectLocator) { +    /** Gets option index (option number, starting at 0) for selected option in the specified select element. +   * +   * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu +   * @return string the selected option index in the specified select drop-down +   */ +    return this.findSelectedOptionProperty(selectLocator, "index"); +} + +Selenium.prototype.getSelectedIds = function(selectLocator) { +    /** Gets all option element IDs for selected options in the specified select or multi-select element. +   * +   * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu +   * @return string[] an array of all selected option IDs in the specified select drop-down +   */ +    return this.findSelectedOptionProperties(selectLocator, "id").join(","); +} + +Selenium.prototype.getSelectedId = function(selectLocator) { +    /** Gets option element ID for selected option in the specified select element. +   * +   * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu +   * @return string the selected option ID in the specified select drop-down +   */ +    return this.findSelectedOptionProperty(selectLocator, "id"); +} + +Selenium.prototype.isSomethingSelected = function(selectLocator) { +    /** Determines whether some option in a drop-down menu is selected. +   * +   * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu +   * @return boolean true if some option has been selected, false otherwise +   */ +    var element = this.page().findElement(selectLocator); +    if (!("options" in element)) { +        throw new SeleniumError("Specified element is not a Select (has no options)"); +    } +     +    var selectedOptions = []; +     +    for (var i = 0; i < element.options.length; i++) { +        if (element.options[i].selected) +        { +            return true; +        } +    } +    return false; +} + +Selenium.prototype.findSelectedOptionProperties = function(locator, property) { +   var element = this.page().findElement(locator); +   if (!("options" in element)) { +        throw new SeleniumError("Specified element is not a Select (has no options)"); +    } + +	var selectedOptions = []; + +    for (var i = 0; i < element.options.length; i++) { +        if (element.options[i].selected) +        { +            var propVal = element.options[i][property]; +            if (propVal.replace) { +                propVal.replace(/,/g, "\\,"); +            } +            selectedOptions.push(propVal); +        } +    } +    if (selectedOptions.length == 0) Assert.fail("No option selected"); +    return selectedOptions; +} + +Selenium.prototype.findSelectedOptionProperty = function(locator, property) { +    var selectedOptions = this.findSelectedOptionProperties(locator, property); +    if (selectedOptions.length > 1) { +        Assert.fail("More than one selected option!"); +    } +    return selectedOptions[0]; +} + +Selenium.prototype.getSelectOptions = function(selectLocator) { +	/** Gets all option labels in the specified select drop-down. +   *  +   * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu +   * @return string[] an array of all option labels in the specified select drop-down +   */ +    var element = this.page().findElement(selectLocator); + +    var selectOptions = []; + +    for (var i = 0; i < element.options.length; i++) { +    	var option = element.options[i].text.replace(/,/g, "\\,"); +        selectOptions.push(option); +    } +     +    return selectOptions.join(","); +}; + + +Selenium.prototype.getAttribute = function(attributeLocator) { +	/** +   * Gets the value of an element attribute. +   * @param attributeLocator an element locator followed by an @ sign and then the name of the attribute, e.g. "foo@bar" +   * @return string the value of the specified attribute +   */ +   var result = this.page().findAttribute(attributeLocator); +   if (result == null) { +   		throw new SeleniumError("Could not find element attribute: " + attributeLocator); +	} +    return result; +}; + +Selenium.prototype.isTextPresent = function(pattern) { +	/** +   * Verifies that the specified text pattern appears somewhere on the rendered page shown to the user. +   * @param pattern a <a href="#patterns">pattern</a> to match with the text of the page +   * @return boolean true if the pattern matches the text, false otherwise +   */ +    var allText = this.page().bodyText(); + +    if(allText == "") { +        Assert.fail("Page text not found"); +    } else { +    	var patternMatcher = new PatternMatcher(pattern); +        if (patternMatcher.strategy == PatternMatcher.strategies.glob) { +    		patternMatcher.matcher = new PatternMatcher.strategies.globContains(pattern); +    	} +    	return patternMatcher.matches(allText); +    } +}; + +Selenium.prototype.isElementPresent = function(locator) { +	/** +   * Verifies that the specified element is somewhere on the page. +   * @param locator an <a href="#locators">element locator</a> +   * @return boolean true if the element is present, false otherwise +   */ +    try { +        this.page().findElement(locator); +    } catch (e) { +        return false; +    } +    return true; +}; + +Selenium.prototype.isVisible = function(locator) { +	/** +   * Determines if the specified element is visible. An +   * element can be rendered invisible by setting the CSS "visibility" +   * property to "hidden", or the "display" property to "none", either for the +   * element itself or one if its ancestors.  This method will fail if +   * the element is not present. +   *  +   * @param locator an <a href="#locators">element locator</a> +   * @return boolean true if the specified element is visible, false otherwise +   */ +    var element; +    element = this.page().findElement(locator);  + +	if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) +		var visibility = element.style["visibility"]; +	else +    	var visibility = this.findEffectiveStyleProperty(element, "visibility"); + +   	var _isDisplayed = this._isDisplayed(element); +    return (visibility != "hidden" && _isDisplayed); +}; + +Selenium.prototype.findEffectiveStyleProperty = function(element, property) { +    var effectiveStyle = this.findEffectiveStyle(element); +    var propertyValue = effectiveStyle[property]; +    if (propertyValue == 'inherit' && element.parentNode.style) { +        return this.findEffectiveStyleProperty(element.parentNode, property); +    } +    return propertyValue; +}; + +Selenium.prototype._isDisplayed = function(element) { +    if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) +		var display = element.style["display"]; +	else +		var display = this.findEffectiveStyleProperty(element, "display"); +    if (display == "none") return false; +    if (element.parentNode.style) { +        return this._isDisplayed(element.parentNode); +    } +    return true; +}; + +Selenium.prototype.findEffectiveStyle = function(element) { +    if (element.style == undefined) { +        return undefined; // not a styled element +    } +    var window = this.browserbot.getContentWindow(); +    if (window.getComputedStyle) {  +        // DOM-Level-2-CSS +        return window.getComputedStyle(element, null); +    } +    if (element.currentStyle) { +        // non-standard IE alternative +        return element.currentStyle; +        // TODO: this won't really work in a general sense, as +        //   currentStyle is not identical to getComputedStyle() +        //   ... but it's good enough for "visibility" +    } +    throw new SeleniumError("cannot determine effective stylesheet in this browser"); +}; + +Selenium.prototype.isEditable = function(locator) { +	/** +   * Determines whether the specified input element is editable, ie hasn't been disabled. +   * This method will fail if the specified element isn't an input element. +   *  +   * @param locator an <a href="#locators">element locator</a> +   * @return boolean true if the input element is editable, false otherwise +   */ +    var element = this.page().findElement(locator); +    if (element.value == undefined) { +        Assert.fail("Element " + locator + " is not an input."); +    } +    return !element.disabled; +}; + +Selenium.prototype.getAllButtons = function() { +	/** Returns the IDs of all buttons on the page. +   *  +   * <p>If a given button has no ID, it will appear as "" in this array.</p> +   *  +   * @return string[] the IDs of all buttons on the page +   */ +   return this.page().getAllButtons(); +}; + +Selenium.prototype.getAllLinks = function() { +	/** Returns the IDs of all links on the page. +   *  +   * <p>If a given link has no ID, it will appear as "" in this array.</p> +   *  +   * @return string[] the IDs of all links on the page +   */ +   return this.page().getAllLinks(); +}; + +Selenium.prototype.getAllFields = function() { +	/** Returns the IDs of all input fields on the page. +   *  +   * <p>If a given field has no ID, it will appear as "" in this array.</p> +   *  +   * @return string[] the IDs of all field on the page +   */ +   return this.page().getAllFields(); +}; + +Selenium.prototype.getHtmlSource = function() { +	/** Returns the entire HTML source between the opening and +   * closing "html" tags. +   * +   * @return string the entire HTML source +   */ +	return this.page().currentDocument.getElementsByTagName("html")[0].innerHTML; +}; + +Selenium.prototype.doSetCursorPosition = function(locator, position) { +	/** +   * Moves the text cursor to the specified position in the given input element or textarea. +   * This method will fail if the specified element isn't an input element or textarea. +   *  +   * @param locator an <a href="#locators">element locator</a> pointing to an input element or textarea +   * @param position the numerical position of the cursor in the field; position should be 0 to move the position to the beginning of the field.  You can also set the cursor to -1 to move it to the end of the field. +   */ +   var element = this.page().findElement(locator); +    if (element.value == undefined) { +        Assert.fail("Element " + locator + " is not an input."); +    } +    if (position == -1) { +    	position = element.value.length; +    } +     +   if( element.setSelectionRange && !browserVersion.isOpera) { +   	element.focus(); +     element.setSelectionRange(/*start*/position,/*end*/position); +   }  +   else if( element.createTextRange ) { +   		triggerEvent(element, 'focus', false); +      var range = element.createTextRange(); +      range.collapse(true); +      range.moveEnd('character',position); +      range.moveStart('character',position); +      range.select(); +   } +} + +Selenium.prototype.getCursorPosition = function(locator) { +	/** +   * Retrieves the text cursor position in the given input element or textarea; beware, this may not work perfectly on all browsers. +   *  +   * <p>Specifically, if the cursor/selection has been cleared by JavaScript, this command will tend to +   * return the position of the last location of the cursor, even though the cursor is now gone from the page.  This is filed as <a href="http://jira.openqa.org/browse/SEL-243">SEL-243</a>.</p> +   * This method will fail if the specified element isn't an input element or textarea, or there is no cursor in the element. +   *  +   * @param locator an <a href="#locators">element locator</a> pointing to an input element or textarea +   * @return number the numerical position of the cursor in the field +   */ +   var element = this.page().findElement(locator); +   var doc = this.page().currentDocument; +   var win = this.browserbot.getCurrentWindow(); +	if( doc.selection && !browserVersion.isOpera){ +		 +		var selectRange = doc.selection.createRange().duplicate(); +		var elementRange = element.createTextRange(); +		selectRange.move("character",0); +		elementRange.move("character",0); +		var inRange1 = selectRange.inRange(elementRange); +		var inRange2 = elementRange.inRange(selectRange); +		try { +			elementRange.setEndPoint("EndToEnd", selectRange); +		} catch (e) { +			Assert.fail("There is no cursor on this page!"); +		} +		var answer = String(elementRange.text).replace(/\r/g,"").length; +		return answer; +	} else { +		if (typeof(element.selectionStart) != undefined) { +			if (win.getSelection && typeof(win.getSelection().rangeCount) != undefined && win.getSelection().rangeCount == 0) { +				Assert.fail("There is no cursor on this page!"); +			} +			return element.selectionStart; +		}  +	} +	throw new Error("Couldn't detect cursor position on this browser!"); +} +    + +Selenium.prototype.doSetContext = function(context, logLevelThreshold) { +	/** +   * Writes a message to the status bar and adds a note to the browser-side +   * log. +   *  +   * <p>If logLevelThreshold is specified, set the threshold for logging +   * to that level (debug, info, warn, error).</p> +   *  +   * <p>(Note that the browser-side logs will <i>not</i> be sent back to the +   * server, and are invisible to the Client Driver.)</p> +   *  +   * @param context +   *            the message to be sent to the browser +   * @param logLevelThreshold one of "debug", "info", "warn", "error", sets the threshold for browser-side logging +   */ +    if  (logLevelThreshold==null || logLevelThreshold=="") { +    	return this.page().setContext(context); +    } +    return this.page().setContext(context, logLevelThreshold); +}; + +Selenium.prototype.getExpression = function(expression) { +	/** +	 * Returns the specified expression. +	 * +	 * <p>This is useful because of JavaScript preprocessing. +	 * It is used to generate commands like assertExpression and storeExpression.</p> +	 *  +	 * @param expression the value to return +	 * @return string the value passed in +	 */ +	return expression; +} + +Selenium.prototype.doWaitForCondition = function(script, timeout) { +	/** +   * Runs the specified JavaScript snippet repeatedly until it evaluates to "true". +   * The snippet may have multiple lines, but only the result of the last line +   * will be considered. +   *  +   * <p>Note that, by default, the snippet will be run in the runner's test window, not in the window +   * of your application.  To get the window of your application, you can use +   * the JavaScript snippet <code>selenium.browserbot.getCurrentWindow()</code>, and then +   * run your JavaScript in there</p> +   * @param script the JavaScript snippet to run +   * @param timeout a timeout in milliseconds, after which this command will return with an error +   */ +    if (isNaN(timeout)) { +    	throw new SeleniumError("Timeout is not a number: " + timeout); +    } +     +    testLoop.waitForCondition = function () { +        return eval(script); +    }; +     +    testLoop.waitForConditionStart = new Date().getTime(); +    testLoop.waitForConditionTimeout = timeout; +}; + +Selenium.prototype.doWaitForCondition.dontCheckAlertsAndConfirms = true; + +Selenium.prototype.doSetTimeout = function(timeout) { +	/** +	 * Specifies the amount of time that Selenium will wait for actions to complete. +	 * +	 * <p>Actions that require waiting include "open" and the "waitFor*" actions.</p> +	 * The default timeout is 30 seconds. +	 * @param timeout a timeout in milliseconds, after which the action will return with an error +	 */ +	testLoop.waitForConditionTimeout = timeout; +} + +Selenium.prototype.doWaitForPageToLoad = function(timeout) { +	/** +   * Waits for a new page to load. +   *  +   * <p>You can use this command instead of the "AndWait" suffixes, "clickAndWait", "selectAndWait", "typeAndWait" etc. +   * (which are only available in the JS API).</p> +   *  +   * <p>Selenium constantly keeps track of new pages loading, and sets a "newPageLoaded" +   * flag when it first notices a page load.  Running any other Selenium command after +   * turns the flag to false.  Hence, if you want to wait for a page to load, you must +   * wait immediately after a Selenium command that caused a page-load.</p> +   * @param timeout a timeout in milliseconds, after which this command will return with an error +   */ +    this.doWaitForCondition("selenium.browserbot.isNewPageLoaded()", timeout); +}; + +Selenium.prototype.doWaitForPageToLoad.dontCheckAlertsAndConfirms = true; + +/** + * Evaluate a parameter, performing JavaScript evaluation and variable substitution. + * If the string matches the pattern "javascript{ ... }", evaluate the string between the braces. + */ +Selenium.prototype.preprocessParameter = function(value) { +    var match = value.match(/^javascript\{((.|\r?\n)+)\}$/); +    if (match && match[1]) { +        return eval(match[1]).toString(); +    } +    return this.replaceVariables(value); +}; + +/* + * Search through str and replace all variable references ${varName} with their + * value in storedVars. + */ +Selenium.prototype.replaceVariables = function(str) { +    var stringResult = str; + +    // Find all of the matching variable references +    var match = stringResult.match(/\$\{\w+\}/g); +    if (!match) { +        return stringResult; +    } + +    // For each match, lookup the variable value, and replace if found +    for (var i = 0; match && i < match.length; i++) { +        var variable = match[i]; // The replacement variable, with ${} +        var name = variable.substring(2, variable.length - 1); // The replacement variable without ${} +        var replacement = storedVars[name]; +        if (replacement != undefined) { +            stringResult = stringResult.replace(variable, replacement); +        } +    } +    return stringResult; +}; + + +/** + *  Factory for creating "Option Locators". + *  An OptionLocator is an object for dealing with Select options (e.g. for + *  finding a specified option, or asserting that the selected option of  + *  Select element matches some condition. + *  The type of locator returned by the factory depends on the locator string: + *     label=<exp>  (OptionLocatorByLabel) + *     value=<exp>  (OptionLocatorByValue) + *     index=<exp>  (OptionLocatorByIndex) + *     id=<exp>     (OptionLocatorById) + *     <exp> (default is OptionLocatorByLabel). + */ +function OptionLocatorFactory() { +} + +OptionLocatorFactory.prototype.fromLocatorString = function(locatorString) { +    var locatorType = 'label'; +    var locatorValue = locatorString; +    // If there is a locator prefix, use the specified strategy +    var result = locatorString.match(/^([a-zA-Z]+)=(.*)/); +    if (result) { +        locatorType = result[1]; +        locatorValue = result[2]; +    } +    if (this.optionLocators == undefined) { +        this.registerOptionLocators(); +    } +    if (this.optionLocators[locatorType]) { +        return new this.optionLocators[locatorType](locatorValue); +    } +    throw new SeleniumError("Unkown option locator type: " + locatorType); +}; + +/** + * To allow for easy extension, all of the option locators are found by + * searching for all methods of OptionLocatorFactory.prototype that start + * with "OptionLocatorBy". + * TODO: Consider using the term "Option Specifier" instead of "Option Locator". + */ +OptionLocatorFactory.prototype.registerOptionLocators = function() { +    this.optionLocators={}; +    for (var functionName in this) { +      var result = /OptionLocatorBy([A-Z].+)$/.exec(functionName); +      if (result != null) { +          var locatorName = result[1].lcfirst(); +          this.optionLocators[locatorName] = this[functionName]; +      } +    } +}; + +/** + *  OptionLocator for options identified by their labels. + */ +OptionLocatorFactory.prototype.OptionLocatorByLabel = function(label) { +    this.label = label; +    this.labelMatcher = new PatternMatcher(this.label); +    this.findOption = function(element) { +        for (var i = 0; i < element.options.length; i++) { +            if (this.labelMatcher.matches(element.options[i].text)) { +                return element.options[i]; +            } +        } +        throw new SeleniumError("Option with label '" + this.label + "' not found"); +    }; + +    this.assertSelected = function(element) { +        var selectedLabel = element.options[element.selectedIndex].text; +        Assert.matches(this.label, selectedLabel) +    }; +}; + +/** + *  OptionLocator for options identified by their values. + */ +OptionLocatorFactory.prototype.OptionLocatorByValue = function(value) { +    this.value = value; +    this.valueMatcher = new PatternMatcher(this.value); +    this.findOption = function(element) { +        for (var i = 0; i < element.options.length; i++) { +            if (this.valueMatcher.matches(element.options[i].value)) { +                return element.options[i]; +            } +        } +        throw new SeleniumError("Option with value '" + this.value + "' not found"); +    }; + +    this.assertSelected = function(element) { +        var selectedValue = element.options[element.selectedIndex].value; +        Assert.matches(this.value, selectedValue) +    }; +}; + +/** + *  OptionLocator for options identified by their index. + */ +OptionLocatorFactory.prototype.OptionLocatorByIndex = function(index) { +    this.index = Number(index); +    if (isNaN(this.index) || this.index < 0) { +        throw new SeleniumError("Illegal Index: " + index); +    } + +    this.findOption = function(element) { +        if (element.options.length <= this.index) { +            throw new SeleniumError("Index out of range.  Only " + element.options.length + " options available"); +        } +        return element.options[this.index]; +    }; + +    this.assertSelected = function(element) { +    	Assert.equals(this.index, element.selectedIndex); +    }; +}; + +/** + *  OptionLocator for options identified by their id. + */ +OptionLocatorFactory.prototype.OptionLocatorById = function(id) { +    this.id = id; +    this.idMatcher = new PatternMatcher(this.id); +    this.findOption = function(element) { +        for (var i = 0; i < element.options.length; i++) { +            if (this.idMatcher.matches(element.options[i].id)) { +                return element.options[i]; +            } +        } +        throw new SeleniumError("Option with id '" + this.id + "' not found"); +    }; + +    this.assertSelected = function(element) { +        var selectedId = element.options[element.selectedIndex].id; +        Assert.matches(this.id, selectedId) +    }; +}; + + diff --git a/tests/test_tools/selenium/core/scripts/selenium-browserbot.js b/tests/test_tools/selenium/core/scripts/selenium-browserbot.js new file mode 100644 index 00000000..8df46865 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/selenium-browserbot.js @@ -0,0 +1,1114 @@ +/* +* Copyright 2004 ThoughtWorks, Inc +* +*  Licensed under the Apache License, Version 2.0 (the "License"); +*  you may not use this file except in compliance with the License. +*  You may obtain a copy of the License at +* +*      http://www.apache.org/licenses/LICENSE-2.0 +* +*  Unless required by applicable law or agreed to in writing, software +*  distributed under the License is distributed on an "AS IS" BASIS, +*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +*  See the License for the specific language governing permissions and +*  limitations under the License. +* +*/ + +/* +* This script provides the Javascript API to drive the test application contained within +* a Browser Window. +* TODO: +*    Add support for more events (keyboard and mouse) +*    Allow to switch "user-entry" mode from mouse-based to keyboard-based, firing different +*          events in different modes. +*/ + +// The window to which the commands will be sent.  For example, to click on a +// popup window, first select that window, and then do a normal click command. + +var BrowserBot = function(frame) { +    this.frame = frame; +    this.currentPage = null; +    this.currentWindowName = null; + +    this.modalDialogTest = null; +    this.recordedAlerts = new Array(); +    this.recordedConfirmations = new Array(); +    this.recordedPrompts = new Array(); +    this.openedWindows = {}; +    this.nextConfirmResult = true; +    this.nextPromptResult = ''; +    this.newPageLoaded = false; +    this.pageLoadError = null; + +    var self = this; +    this.recordPageLoad = function() { +    	LOG.debug("Page load detected"); +        try { +        	LOG.debug("Page load location=" + self.getCurrentWindow().location); +        } catch (e) { +        	self.pageLoadError = e; +        	return; +        } +        self.currentPage = null; +        self.newPageLoaded = true; +    }; + +    this.isNewPageLoaded = function() { +    	if (this.pageLoadError) throw this.pageLoadError; +        return self.newPageLoaded; +    }; +}; + +BrowserBot.createForFrame = function(frame) { +    var browserbot; +    LOG.debug("browserName: " + browserVersion.name); +    LOG.debug("userAgent: " + navigator.userAgent); +    if (browserVersion.isIE) { +        browserbot = new IEBrowserBot(frame); +    } +    else if (browserVersion.isKonqueror) { +        browserbot = new KonquerorBrowserBot(frame); +    } +    else if (browserVersion.isSafari) { +        browserbot = new SafariBrowserBot(frame); +    } +    else { +        LOG.info("Using MozillaBrowserBot") +        // Use mozilla by default +        browserbot = new MozillaBrowserBot(frame); +    } + +    // Modify the test IFrame so that page loads are detected. +    addLoadListener(browserbot.getFrame(), browserbot.recordPageLoad); +    return browserbot; +}; + +BrowserBot.prototype.doModalDialogTest = function(test) { +    this.modalDialogTest = test; +}; + +BrowserBot.prototype.cancelNextConfirmation = function() { +    this.nextConfirmResult = false; +}; + +BrowserBot.prototype.setNextPromptResult = function(result) { +    this.nextPromptResult = result; +}; + +BrowserBot.prototype.hasAlerts = function() { +    return (this.recordedAlerts.length > 0) ; +}; + +BrowserBot.prototype.getNextAlert = function() { +    return this.recordedAlerts.shift(); +}; + +BrowserBot.prototype.hasConfirmations = function() { +    return (this.recordedConfirmations.length > 0) ; +}; + +BrowserBot.prototype.getNextConfirmation = function() { +    return this.recordedConfirmations.shift(); +}; + +BrowserBot.prototype.hasPrompts = function() { +    return (this.recordedPrompts.length > 0) ; +}; + +BrowserBot.prototype.getNextPrompt = function() { +    return this.recordedPrompts.shift(); +}; + +BrowserBot.prototype.getFrame = function() { +    return this.frame; +}; + +BrowserBot.prototype.selectWindow = function(target) { +    // we've moved to a new page - clear the current one +    this.currentPage = null; +    this.currentWindowName = null; +    if (target && target != "null") { +        // If window exists +        if (this.getTargetWindow(target)) { +            this.currentWindowName = target; +        } +    } +}; + +BrowserBot.prototype.openLocation = function(target) { +    // We're moving to a new page - clear the current one +    this.currentPage = null; +    this.newPageLoaded = false; + +    this.setOpenLocation(target); +}; + +BrowserBot.prototype.setIFrameLocation = function(iframe, location) { +    iframe.src = location; +}; + +BrowserBot.prototype.setOpenLocation = function(location) { +    this.getCurrentWindow().location.href = location; +}; + +BrowserBot.prototype.getCurrentPage = function() { +    if (this.currentPage == null) { +        var testWindow = this.getCurrentWindow(); +        this.modifyWindowToRecordPopUpDialogs(testWindow, this); +        this.modifySeparateTestWindowToDetectPageLoads(testWindow); +        this.currentPage = PageBot.createForWindow(testWindow); +        this.newPageLoaded = false; +    } + +    return this.currentPage; +}; + +BrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModify, browserBot) { +    windowToModify.alert = function(alert) { +        browserBot.recordedAlerts.push(alert); +    }; + +    windowToModify.confirm = function(message) { +        browserBot.recordedConfirmations.push(message); +        var result = browserBot.nextConfirmResult; +        browserBot.nextConfirmResult = true; +        return result; +    }; + +    windowToModify.prompt = function(message) { +        browserBot.recordedPrompts.push(message); +        var result = !browserBot.nextConfirmResult ? null : browserBot.nextPromptResult; +        browserBot.nextConfirmResult = true; +        browserBot.nextPromptResult = ''; +        return result; +    }; + +    // Keep a reference to all popup windows by name +    // note that in IE the "windowName" argument must be a valid javascript identifier, it seems. +    var originalOpen = windowToModify.open; +    windowToModify.open = function(url, windowName, windowFeatures, replaceFlag) { +        var openedWindow = originalOpen(url, windowName, windowFeatures, replaceFlag); +        selenium.browserbot.openedWindows[windowName] = openedWindow; +        return openedWindow; +    }; +}; + +/** + * The main IFrame has a single, long-lived onload handler that clears + * Browserbot.currentPage and sets the "newPageLoaded" flag. For separate + * windows, we need to attach a handler each time. This uses the + * "callOnWindowPageTransition" mechanism, which is implemented differently + * for different browsers. + */ +BrowserBot.prototype.modifySeparateTestWindowToDetectPageLoads = function(windowToModify) { +    if (this.currentWindowName != null) { +        this.callOnWindowPageTransition(this.recordPageLoad, windowToModify); +    } +}; + +/** + * Call the supplied function when a the current page unloads and a new one loads. + * This is done by polling continuously until the document changes and is fully loaded. + */ +BrowserBot.prototype.callOnWindowPageTransition = function(loadFunction, windowObject) { +    // Since the unload event doesn't fire in Safari 1.3, we start polling immediately +    if (windowObject && !windowObject.closed) { +        LOG.debug("Starting pollForLoad: " + windowObject.document.location); +        this.pollingForLoad = true; +        this.pollForLoad(loadFunction, windowObject, windowObject.location, windowObject.location.href); +    } +}; + +/** + * Set up a polling timer that will keep checking the readyState of the document until it's complete. + * Since we might call this before the original page is unloaded, we first check to see that the current location + * or href is different from the original one. + */ +BrowserBot.prototype.pollForLoad = function(loadFunction, windowObject, originalLocation, originalHref) { +    var windowClosed = true; +    try { +    	windowClosed = windowObject.closed; +    } catch (e) { +    	LOG.debug("exception detecting closed window (I guess it must be closed)"); +    	LOG.exception(e); +    	// swallow exceptions which may occur in HTA mode when the window is closed +    } +    if (null == windowClosed) windowClosed = true; +    if (windowClosed) { +    	this.pollingForLoad = false; +        return; +    } + +    LOG.debug("pollForLoad original: " + originalHref); +    try { + +	    var currentLocation = windowObject.location; +	    var currentHref = currentLocation.href + +	    var sameLoc = (originalLocation === currentLocation); +	    var sameHref = (originalHref === currentHref); +	    var rs = windowObject.document.readyState; + +		if (rs == null) rs = 'complete'; + +	    if (!(sameLoc && sameHref) && rs == 'complete') { +	        LOG.debug("pollForLoad complete: " + rs + " (" + currentHref + ")"); +	        loadFunction(); +	        this.pollingForLoad = false; +	        return; +	    } +	    var self = this; +	    LOG.debug("pollForLoad continue: " + currentHref); +	    window.setTimeout(function() {self.pollForLoad(loadFunction, windowObject, originalLocation, originalHref);}, 500); +	} catch (e) { +		LOG.error("Exception during pollForLoad; this should get noticed soon!"); +		LOG.exception(e); +		this.pageLoadError = e; +	} +}; + + +BrowserBot.prototype.getContentWindow = function() { +    return this.getFrame().contentWindow || frames[this.getFrame().id]; +}; + +BrowserBot.prototype.getTargetWindow = function(windowName) { +    LOG.debug("getTargetWindow(" + windowName + ")"); +    // First look in the map of opened windows +    var targetWindow = this.openedWindows[windowName]; +    if (!targetWindow) { +        var evalString = "this.getContentWindow().window." + windowName; +        targetWindow = eval(evalString); +    } +    if (!targetWindow) { +        throw new SeleniumError("Window does not exist"); +    } +    return targetWindow; +}; + +BrowserBot.prototype.getCurrentWindow = function() { +    var testWindow = this.getContentWindow().window; +    if (this.currentWindowName != null) { +        testWindow = this.getTargetWindow(this.currentWindowName); +    } +    return testWindow; +}; + +function MozillaBrowserBot(frame) { +    BrowserBot.call(this, frame); +} +MozillaBrowserBot.prototype = new BrowserBot; + +function KonquerorBrowserBot(frame) { +    BrowserBot.call(this, frame); +} +KonquerorBrowserBot.prototype = new BrowserBot; + +KonquerorBrowserBot.prototype.setIFrameLocation = function(iframe, location) { +    // Window doesn't fire onload event when setting src to the current value, +    // so we set it to blank first. +    iframe.src = "about:blank"; +    iframe.src = location; +}; + +KonquerorBrowserBot.prototype.setOpenLocation = function(location) { +    // Window doesn't fire onload event when setting src to the current value, +    // so we set it to blank first. +    this.getCurrentWindow().location.href = "about:blank"; +    this.getCurrentWindow().location.href = location; +}; + +function SafariBrowserBot(frame) { +    BrowserBot.call(this, frame); +} +SafariBrowserBot.prototype = new BrowserBot; + +SafariBrowserBot.prototype.setIFrameLocation = KonquerorBrowserBot.prototype.setIFrameLocation; + +function IEBrowserBot(frame) { +    BrowserBot.call(this, frame); +} +IEBrowserBot.prototype = new BrowserBot; + +IEBrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModify, browserBot) { +    BrowserBot.prototype.modifyWindowToRecordPopUpDialogs(windowToModify, browserBot); + +    // we will call the previous version of this method from within our own interception +    oldShowModalDialog = windowToModify.showModalDialog; + +    windowToModify.showModalDialog = function(url, args, features) { +        // Get relative directory to where TestRunner.html lives +        // A risky assumption is that the user's TestRunner is named TestRunner.html +        var doc_location = document.location.toString(); +        var end_of_base_ref = doc_location.indexOf('TestRunner.html'); +        var base_ref = doc_location.substring(0, end_of_base_ref); + +        var fullURL = base_ref + "TestRunner.html?singletest=" + escape(browserBot.modalDialogTest) + "&autoURL=" + escape(url) + "&runInterval=" + runInterval; +        browserBot.modalDialogTest = null; + +        var returnValue = oldShowModalDialog(fullURL, args, features); +        return returnValue; +    }; +}; + +SafariBrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModify, browserBot) { +    BrowserBot.prototype.modifyWindowToRecordPopUpDialogs(windowToModify, browserBot); + +    var originalOpen = windowToModify.open; +    /* +     * Safari seems to be broken, so that when we manually trigger the onclick method +     * of a button/href, any window.open calls aren't resolved relative to the app location. +     * So here we replace the open() method with one that does resolve the url correctly. +     */ +    windowToModify.open = function(url, windowName, windowFeatures, replaceFlag) { + +        if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")) { +            return originalOpen(url, windowName, windowFeatures, replaceFlag); +        } + +        // Reduce the current path to the directory +        var currentPath = windowToModify.location.pathname || "/"; +        currentPath = currentPath.replace(/\/[^\/]*$/, "/"); + +        // Remove any leading "./" from the new url. +        url = url.replace(/^\.\//, ""); + +        newUrl = currentPath + url; + +        return originalOpen(newUrl, windowName, windowFeatures, replaceFlag); +    }; +}; + +var PageBot = function(pageWindow) { +    if (pageWindow) { +        this.currentWindow = pageWindow; +        this.currentDocument = pageWindow.document; +        this.location = pageWindow.location; +        this.title = function() {return this.currentDocument.title;}; +    } + +    // Register all locateElementBy* functions +    // TODO - don't do this in the constructor - only needed once ever +    this.locationStrategies = {}; +    for (var functionName in this) { +        var result = /^locateElementBy([A-Z].+)$/.exec(functionName); +        if (result != null) { +            var locatorFunction = this[functionName]; +            if (typeof(locatorFunction) != 'function') { +                continue; +            } +            // Use a specified prefix in preference to one generated from +            // the function name +            var locatorPrefix = locatorFunction.prefix || result[1].toLowerCase(); +            this.locationStrategies[locatorPrefix] = locatorFunction; +        } +    } + +    /** +     * Find a locator based on a prefix. +     */ +    this.findElementBy = function(locatorType, locator, inDocument) { +        var locatorFunction = this.locationStrategies[locatorType];         +        if (! locatorFunction) { +            throw new SeleniumError("Unrecognised locator type: '" + locatorType + "'"); +        } +        return locatorFunction.call(this, locator, inDocument); +    }; + +    /** +     * The implicit locator, that is used when no prefix is supplied. +     */ +    this.locationStrategies['implicit'] = function(locator, inDocument) { +        if (locator.startsWith('//')) { +            return this.locateElementByXPath(locator, inDocument); +        } +        if (locator.startsWith('document.')) { +            return this.locateElementByDomTraversal(locator, inDocument); +        } +        return this.locateElementByIdentifier(locator, inDocument); +    }; + +}; + +PageBot.createForWindow = function(windowObject) { +    if (browserVersion.isIE) { +        return new IEPageBot(windowObject); +    } +    else if (browserVersion.isKonqueror) { +        return new KonquerorPageBot(windowObject); +    } +    else if (browserVersion.isSafari) { +        return new SafariPageBot(windowObject); +    } +    else if (browserVersion.isOpera) { +        return new OperaPageBot(windowObject); +    } +    else { +        LOG.info("Using MozillaPageBot") +        // Use mozilla by default +        return new MozillaPageBot(windowObject); +    } +}; + +var MozillaPageBot = function(pageWindow) { +    PageBot.call(this, pageWindow); +}; +MozillaPageBot.prototype = new PageBot(); + +var KonquerorPageBot = function(pageWindow) { +    PageBot.call(this, pageWindow); +}; +KonquerorPageBot.prototype = new PageBot(); + +var SafariPageBot = function(pageWindow) { +    PageBot.call(this, pageWindow); +}; +SafariPageBot.prototype = new PageBot(); + +var IEPageBot = function(pageWindow) { +    PageBot.call(this, pageWindow); +}; +IEPageBot.prototype = new PageBot(); + +OperaPageBot = function(pageWindow) { +    PageBot.call(this, pageWindow); +}; +OperaPageBot.prototype = new PageBot(); + +/* +* Finds an element on the current page, using various lookup protocols +*/ +PageBot.prototype.findElement = function(locator) { +    var locatorType = 'implicit'; +    var locatorString = locator; + +    // If there is a locator prefix, use the specified strategy +    var result = locator.match(/^([A-Za-z]+)=(.+)/); +    if (result) { +        locatorType = result[1].toLowerCase(); +        locatorString = result[2]; +    } + +    var element = this.findElementBy(locatorType, locatorString, this.currentDocument); +    if (element != null) { +        return element; +    } +    for (var i = 0; i < this.currentWindow.frames.length; i++) { +        element = this.findElementBy(locatorType, locatorString, this.currentWindow.frames[i].document); +        if (element != null) { +            return element; +        } +    } + +    // Element was not found by any locator function. +    throw new SeleniumError("Element " + locator + " not found"); +}; + +/** + * In non-IE browsers, getElementById() does not search by name.  Instead, we + * we search separately by id and name. + */ +PageBot.prototype.locateElementByIdentifier = function(identifier, inDocument) { +    return PageBot.prototype.locateElementById(identifier, inDocument) +            || PageBot.prototype.locateElementByName(identifier, inDocument) +            || null; +}; + +/** + * In IE, getElementById() also searches by name - this is an optimisation for IE. + */ +IEPageBot.prototype.locateElementByIdentifer = function(identifier, inDocument) { +    return inDocument.getElementById(identifier); +}; + +/** + * Find the element with id - can't rely on getElementById, coz it returns by name as well in IE.. + */ +PageBot.prototype.locateElementById = function(identifier, inDocument) { +    var element = inDocument.getElementById(identifier); +    if (element && element.id === identifier) { +        return element; +    } +    else { +        return null; +    } +}; + +/** + * Find an element by name, refined by (optional) element-filter + * expressions. + */ +PageBot.prototype.locateElementByName = function(locator, document) { +    var elements = document.getElementsByTagName("*"); + +    var filters = locator.split(' '); +    filters[0] = 'name=' + filters[0]; + +    while (filters.length) { +        var filter = filters.shift(); +        elements = this.selectElements(filter, elements, 'value'); +    } + +    if (elements.length > 0) { +        return elements[0]; +    } +    return null; +}; + +/** +* Finds an element using by evaluating the "document.*" string against the +* current document object. Dom expressions must begin with "document." +*/ +PageBot.prototype.locateElementByDomTraversal = function(domTraversal, inDocument) { +    if (domTraversal.indexOf("document.") != 0) { +        return null; +    } + +    // Trim the leading 'document' +    domTraversal = domTraversal.substr(9); +    var locatorScript = "inDocument." + domTraversal; +    var element = eval(locatorScript); + +    if (!element) { +        return null; +    } + +    return element; +}; +PageBot.prototype.locateElementByDomTraversal.prefix = "dom"; + +/** +* Finds an element identified by the xpath expression. Expressions _must_ +* begin with "//". +*/ +PageBot.prototype.locateElementByXPath = function(xpath, inDocument) { + +    // Trim any trailing "/": not valid xpath, and remains from attribute +    // locator. +    if (xpath.charAt(xpath.length - 1) == '/') { +        xpath = xpath.slice(0, -1); +    } + +    // Handle //tag +    var match = xpath.match(/^\/\/(\w+|\*)$/); +    if (match) { +        var elements = inDocument.getElementsByTagName(match[1].toUpperCase()); +        if (elements == null) return null; +        return elements[0]; +    } + +    // Handle //tag[@attr='value'] +    var match = xpath.match(/^\/\/(\w+|\*)\[@(\w+)=('([^\']+)'|"([^\"]+)")\]$/); +    if (match) { +        return this.findElementByTagNameAndAttributeValue( +            inDocument, +            match[1].toUpperCase(), +            match[2].toLowerCase(), +            match[3].slice(1, -1) +        ); +    } + +    // Handle //tag[text()='value'] +    var match = xpath.match(/^\/\/(\w+|\*)\[text\(\)=('([^\']+)'|"([^\"]+)")\]$/); +    if (match) { +        return this.findElementByTagNameAndText( +            inDocument, +            match[1].toUpperCase(), +            match[2].slice(1, -1) +        ); +    } + +    return this.findElementUsingFullXPath(xpath, inDocument); +}; + +PageBot.prototype.findElementByTagNameAndAttributeValue = function( +    inDocument, tagName, attributeName, attributeValue +) { +    if (browserVersion.isIE && attributeName == "class") { +        attributeName = "className"; +    } +    var elements = inDocument.getElementsByTagName(tagName); +    for (var i = 0; i < elements.length; i++) { +        var elementAttr = elements[i].getAttribute(attributeName); +        if (elementAttr == attributeValue) { +            return elements[i]; +        } +    } +    return null; +}; + +PageBot.prototype.findElementByTagNameAndText = function( +    inDocument, tagName, text +) { +    var elements = inDocument.getElementsByTagName(tagName); +    for (var i = 0; i < elements.length; i++) { +        if (getText(elements[i]) == text) { +            return elements[i]; +        } +    } +    return null; +}; + +PageBot.prototype.findElementUsingFullXPath = function(xpath, inDocument) { +    // Use document.evaluate() if it's available +    if (inDocument.evaluate) { +        return inDocument.evaluate(xpath, inDocument, null, 0, null).iterateNext(); +    } + +    // If not, fall back to slower JavaScript implementation +    var context = new ExprContext(inDocument); +    var xpathObj = xpathParse(xpath); +    var xpathResult = xpathObj.evaluate(context); +    if (xpathResult && xpathResult.value) { +        return xpathResult.value[0]; +    } +    return null; +}; + +/** +* Finds a link element with text matching the expression supplied. Expressions must +* begin with "link:". +*/ +PageBot.prototype.locateElementByLinkText = function(linkText, inDocument) { +    var links = inDocument.getElementsByTagName('a'); +    for (var i = 0; i < links.length; i++) { +        var element = links[i]; +        if (PatternMatcher.matches(linkText, getText(element))) { +            return element; +        } +    } +    return null; +}; +PageBot.prototype.locateElementByLinkText.prefix = "link"; + +/** +* Returns an attribute based on an attribute locator. This is made up of an element locator +* suffixed with @attribute-name. +*/ +PageBot.prototype.findAttribute = function(locator) { +    // Split into locator + attributeName +    var attributePos = locator.lastIndexOf("@"); +    var elementLocator = locator.slice(0, attributePos); +    var attributeName = locator.slice(attributePos + 1); + +    // Find the element. +    var element = this.findElement(elementLocator); + +    // Handle missing "class" attribute in IE. +    if (browserVersion.isIE && attributeName == "class") { +        attributeName = "className"; +    } + +    // Get the attribute value. +    var attributeValue = element.getAttribute(attributeName); + +    return attributeValue ? attributeValue.toString() : null; +}; + +/* +* Select the specified option and trigger the relevant events of the element. +*/ +PageBot.prototype.selectOption = function(element, optionToSelect) { +    triggerEvent(element, 'focus', false); +    var changed = false; +    for (var i = 0; i < element.options.length; i++) { +        var option = element.options[i]; +        if (option.selected && option != optionToSelect) { +            option.selected = false; +            changed = true; +        } +        else if (!option.selected && option == optionToSelect) { +            option.selected = true; +            changed = true; +        } +    } + +    if (changed) { +        triggerEvent(element, 'change', true); +    } +    triggerEvent(element, 'blur', false); +}; + +/* +* Select the specified option and trigger the relevant events of the element. +*/ +PageBot.prototype.addSelection = function(element, option) { +    this.checkMultiselect(element); +    triggerEvent(element, 'focus', false); +    if (!option.selected) { +        option.selected = true; +        triggerEvent(element, 'change', true); +    } +    triggerEvent(element, 'blur', false); +}; + +/* +* Select the specified option and trigger the relevant events of the element. +*/ +PageBot.prototype.removeSelection = function(element, option) { +    this.checkMultiselect(element); +    triggerEvent(element, 'focus', false); +    if (option.selected) { +        option.selected = false; +        triggerEvent(element, 'change', true); +    } +    triggerEvent(element, 'blur', false); +}; + +PageBot.prototype.checkMultiselect = function(element) { +    if (!element.multiple) +    { +        throw new SeleniumError("Not a multi-select"); +    } + +}; + +PageBot.prototype.replaceText = function(element, stringValue) { +    triggerEvent(element, 'focus', false); +    triggerEvent(element, 'select', true); +    element.value=stringValue; +    if (!browserVersion.isChrome) { +        // In chrome URL, The change event is already fired by setting the value. +        triggerEvent(element, 'change', true); +    } +    triggerEvent(element, 'blur', false); +}; + +MozillaPageBot.prototype.clickElement = function(element) { + +    triggerEvent(element, 'focus', false); + +    // Add an event listener that detects if the default action has been prevented. +    // (This is caused by a javascript onclick handler returning false) +    var preventDefault = false; +     +    element.addEventListener("click", function(evt) {preventDefault = evt.getPreventDefault();}, false); +     +    // Trigger the click event. +    triggerMouseEvent(element, 'click', true); + +    // Perform the link action if preventDefault was set. +    // In chrome URL, the link action is already executed by triggerMouseEvent. +    if (!browserVersion.isChrome && !preventDefault) { +        // Try the element itself, as well as it's parent - this handles clicking images inside links. +        if (element.href) { +            this.currentWindow.location.href = element.href; +        } +        else if (element.parentNode && element.parentNode.href) { +            this.currentWindow.location.href = element.parentNode.href; +        } +    } + +    if (this.windowClosed()) { +        return; +    } + +    triggerEvent(element, 'blur', false); +}; + +OperaPageBot.prototype.clickElement = function(element) { + +    triggerEvent(element, 'focus', false); + +    // Trigger the click event. +    triggerMouseEvent(element, 'click', true); +     +    if (isDefined(element.checked)) { +    	// In Opera, clicking won't check/uncheck +    	if (element.type == "checkbox") { +    		element.checked = !element.checked; +    	} else { +    		element.checked = true; +    	} +    } +     +    if (this.windowClosed()) { +        return; +    } + +    triggerEvent(element, 'blur', false); +}; + + +KonquerorPageBot.prototype.clickElement = function(element) { + +    triggerEvent(element, 'focus', false); + +    if (element.click) { +        element.click(); +    } +    else { +        triggerMouseEvent(element, 'click', true); +    } + +    if (this.windowClosed()) { +        return; +    } + +    triggerEvent(element, 'blur', false); +}; + +SafariPageBot.prototype.clickElement = function(element) { + +    triggerEvent(element, 'focus', false); + +    var wasChecked = element.checked; + +    // For form element it is simple. +    if (element.click) { +        element.click(); +    } +    // For links and other elements, event emulation is required. +    else { +        triggerMouseEvent(element, 'click', true); + +        // Unfortunately, triggering the event doesn't seem to activate onclick handlers. +        // We currently call onclick for the link, but I'm guessing that the onclick for containing +        // elements is not being called. +        var success = true; +        if (element.onclick) { +            var evt = document.createEvent('HTMLEvents'); +            evt.initEvent('click', true, true); +            var onclickResult = element.onclick(evt); +            if (onclickResult === false) { +                success = false; +            } +        } + +        if (success) { +            // Try the element itself, as well as it's parent - this handles clicking images inside links. +            if (element.href) { +                this.currentWindow.location.href = element.href; +            } +            else if (element.parentNode.href) { +                this.currentWindow.location.href = element.parentNode.href; +            } else { +                // This is true for buttons outside of forms, and maybe others. +                LOG.warn("Ignoring 'click' call for button outside form, or link without href." +                        + "Using buttons without an enclosing form can cause wierd problems with URL resolution in Safari." ); +                // I implemented special handling for window.open, but unfortunately this behaviour is also displayed +                // when we have a button without an enclosing form that sets document.location in the onclick handler. +                // The solution is to always use an enclosing form for a button. +            } +        } +    } + +    if (this.windowClosed()) { +        return; +    } + +    triggerEvent(element, 'blur', false); +}; + +IEPageBot.prototype.clickElement = function(element) { + +    triggerEvent(element, 'focus', false); + +    var wasChecked = element.checked; + +    // Set a flag that records if the page will unload - this isn't always accurate, because +    // <a href="javascript:alert('foo'):"> triggers the onbeforeunload event, even thought the page won't unload +    var pageUnloading = false; +    var pageUnloadDetector = function() {pageUnloading = true;}; +    this.currentWindow.attachEvent("onbeforeunload", pageUnloadDetector); + +    element.click(); + +    // If the page is going to unload - still attempt to fire any subsequent events. +    // However, we can't guarantee that the page won't unload half way through, so we need to handle exceptions. +    try { +        this.currentWindow.detachEvent("onbeforeunload", pageUnloadDetector); + +        if (this.windowClosed()) { +            return; +        } + +        // Onchange event is not triggered automatically in IE. +        if (isDefined(element.checked) && wasChecked != element.checked) { +            triggerEvent(element, 'change', true); +        } + +        triggerEvent(element, 'blur', false); +    } +    catch (e) { +        // If the page is unloading, we may get a "Permission denied" or "Unspecified error". +        // Just ignore it, because the document may have unloaded. +        if (pageUnloading) { +            LOG.warn("Caught exception when firing events on unloading page: " + e.message); +            return; +        } +        throw e; +    } +}; + +PageBot.prototype.windowClosed = function(element) { +    return this.currentWindow.closed; +}; + +PageBot.prototype.bodyText = function() { +    return getText(this.currentDocument.body); +}; + +PageBot.prototype.getAllButtons = function() { +    var elements = this.currentDocument.getElementsByTagName('input'); +    var result = ''; + +    for (var i = 0; i < elements.length; i++) { +        if (elements[i].type == 'button' || elements[i].type == 'submit' || elements[i].type == 'reset') { +            result += elements[i].id; + +            result += ','; +        } +    } + +    return result; +}; + + +PageBot.prototype.getAllFields = function() { +    var elements = this.currentDocument.getElementsByTagName('input'); +    var result = ''; + +    for (var i = 0; i < elements.length; i++) { +        if (elements[i].type == 'text') { +            result += elements[i].id; + +            result += ','; +        } +    } + +    return result; +}; + +PageBot.prototype.getAllLinks = function() { +    var elements = this.currentDocument.getElementsByTagName('a'); +    var result = ''; + +    for (var i = 0; i < elements.length; i++) { +        result += elements[i].id; + +        result += ','; +    } + +    return result; +}; + +PageBot.prototype.setContext = function(strContext, logLevel) { +     //set the current test title +    document.getElementById("context").innerHTML=strContext; +    if (logLevel!=null) { +    	LOG.setLogLevelThreshold(logLevel); +    } +}; + +function isDefined(value) { +    return typeof(value) != undefined; +} + +PageBot.prototype.goBack = function() { +    this.currentWindow.history.back(); +    if (browserVersion.isOpera && !selenium.browserbot.pollingForLoad) { +    	// DGF On Opera, goBack doesn't re-trigger a load event, so we have to poll for it +        selenium.browserbot.callOnWindowPageTransition(selenium.browserbot.recordPageLoad, this.currentWindow); +    } +}; + +PageBot.prototype.goForward = function() { +    this.currentWindow.history.forward(); +}; + +PageBot.prototype.close = function() { +	if (browserVersion.isChrome) { +		this.currentWindow.close(); +	} else { +	    this.currentWindow.eval("window.close();"); +	} +}; + +PageBot.prototype.refresh = function() { +    this.currentWindow.location.reload(true); +}; + +/** + * Refine a list of elements using a filter. + */ +PageBot.prototype.selectElementsBy = function(filterType, filter, elements) { +    var filterFunction = PageBot.filterFunctions[filterType]; +    if (! filterFunction) { +        throw new SeleniumError("Unrecognised element-filter type: '" + filterType + "'"); +    } + +    return filterFunction(filter, elements); +}; + +PageBot.filterFunctions = {};  + +PageBot.filterFunctions.name = function(name, elements) { +    var selectedElements = []; +    for (var i = 0; i < elements.length; i++) { +        if (elements[i].name === name) { +            selectedElements.push(elements[i]); +        } +    } +    return selectedElements; +}; + +PageBot.filterFunctions.value = function(value, elements) { +    var selectedElements = []; +    for (var i = 0; i < elements.length; i++) { +        if (elements[i].value === value) { +            selectedElements.push(elements[i]); +        } +    } +    return selectedElements; +}; + +PageBot.filterFunctions.index = function(index, elements) { +    index = Number(index); +    if (isNaN(index) || index < 0) { +        throw new SeleniumError("Illegal Index: " + index); +    } +    if (elements.length <= index) { +        throw new SeleniumError("Index out of range: " + index); +    } +    return [elements[index]]; +}; + +PageBot.prototype.selectElements = function(filterExpr, elements, defaultFilterType) {     + +    var filterType = (defaultFilterType || 'value'); +     +    // If there is a filter prefix, use the specified strategy +    var result = filterExpr.match(/^([A-Za-z]+)=(.+)/); +    if (result) { +        filterType = result[1].toLowerCase(); +        filterExpr = result[2]; +    } + +    return this.selectElementsBy(filterType, filterExpr, elements); +}; + +/** + * Find an element by class + */ +PageBot.prototype.locateElementByClass = function(locator, document) { +    return Element.findFirstMatchingChild(document,  +	    function(element) {  +		    return element.className == locator +		} +    ); +} + +/** + * Find an element by alt + */ +PageBot.prototype.locateElementByAlt = function(locator, document) { +    return Element.findFirstMatchingChild(document, +	    function(element) { +		    return element.alt == locator +		} +    ); +} + diff --git a/tests/test_tools/selenium/core/scripts/selenium-browserdetect.js b/tests/test_tools/selenium/core/scripts/selenium-browserdetect.js new file mode 100644 index 00000000..137a1518 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/selenium-browserdetect.js @@ -0,0 +1,115 @@ +/* +* Copyright 2004 ThoughtWorks, Inc +* +*  Licensed under the Apache License, Version 2.0 (the "License"); +*  you may not use this file except in compliance with the License. +*  You may obtain a copy of the License at +* +*      http://www.apache.org/licenses/LICENSE-2.0 +* +*  Unless required by applicable law or agreed to in writing, software +*  distributed under the License is distributed on an "AS IS" BASIS, +*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +*  See the License for the specific language governing permissions and +*  limitations under the License. +* +*/ + +// Although it's generally better web development practice not to use browser-detection +// (feature detection is better), the subtle browser differences that Selenium has to +// work around seem to make it necessary. Maybe as we learn more about what we need, +// we can do this in a more "feature-centric" rather than "browser-centric" way. + +BrowserVersion = function() { +    this.name = navigator.appName; + +    if (window.opera != null) +    { +        this.browser = BrowserVersion.OPERA; +        this.isOpera = true; +        return; +    } +     +    var self = this; +     +    var checkChrome = function() { +    	var loc = window.document.location.href; +    	try { +    		loc = window.top.document.location.href; +    	} catch (e) { +    		// can't see the top (that means we might be chrome, but it's impossible to be sure) +    		self.isChromeDetectable = "no, top location couldn't be read in this window"; +    	} +    	 +    	if (/^chrome:\/\//.test(loc)) { +    		self.isChrome = true; +    	} else { +    		self.isChrome = false; +    	} +    } + +    if (this.name == "Microsoft Internet Explorer") +    { +        this.browser = BrowserVersion.IE; +        this.isIE = true; +        if (window.top.SeleniumHTARunner && window.top.document.location.pathname.match(/.hta$/i)) { +        	this.isHTA = true; +        } +        if ("0" == navigator.appMinorVersion) { +        	this.preSV1 = true; +        } +        return; +    } + +    if (navigator.userAgent.indexOf('Safari') != -1) +    { +        this.browser = BrowserVersion.SAFARI; +        this.isSafari = true; +        this.khtml = true; +        return; +    } + +    if (navigator.userAgent.indexOf('Konqueror') != -1) +    { +        this.browser = BrowserVersion.KONQUEROR; +        this.isKonqueror = true; +        this.khtml = true; +        return; +    } + +    if (navigator.userAgent.indexOf('Firefox') != -1) +    { +        this.browser = BrowserVersion.FIREFOX; +        this.isFirefox = true; +        this.isGecko = true; +        var result = /.*Firefox\/([\d\.]+).*/.exec(navigator.userAgent); +        if (result) +        { +            this.firefoxVersion = result[1]; +        } +        checkChrome(); +        return; +    } + +    if (navigator.userAgent.indexOf('Gecko') != -1) +    { +        this.browser = BrowserVersion.MOZILLA; +        this.isMozilla = true; +        this.isGecko = true; +        checkChrome(); +        return; +    } + +    this.browser = BrowserVersion.UNKNOWN; +} + +BrowserVersion.OPERA = "Opera"; +BrowserVersion.IE = "IE"; +BrowserVersion.KONQUEROR = "Konqueror"; +BrowserVersion.SAFARI = "Safari"; +BrowserVersion.FIREFOX = "Firefox"; +BrowserVersion.MOZILLA = "Mozilla"; +BrowserVersion.UNKNOWN = "Unknown"; + +browserVersion = new BrowserVersion(); + diff --git a/tests/test_tools/selenium/core/scripts/selenium-commandhandlers.js b/tests/test_tools/selenium/core/scripts/selenium-commandhandlers.js new file mode 100644 index 00000000..ee01ea76 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/selenium-commandhandlers.js @@ -0,0 +1,371 @@ +/* +* Copyright 2004 ThoughtWorks, Inc +* +*  Licensed under the Apache License, Version 2.0 (the "License"); +*  you may not use this file except in compliance with the License. +*  You may obtain a copy of the License at +* +*      http://www.apache.org/licenses/LICENSE-2.0 +* +*  Unless required by applicable law or agreed to in writing, software +*  distributed under the License is distributed on an "AS IS" BASIS, +*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +*  See the License for the specific language governing permissions and +*  limitations under the License. +* +*/ +function CommandHandlerFactory() { +    this.actions = {}; +    this.asserts = {}; +    this.accessors = {}; + +    var self = this; + +    this.registerAction = function(name, action, wait, dontCheckAlertsAndConfirms) { +        var handler = new ActionHandler(action, wait, dontCheckAlertsAndConfirms); +        this.actions[name] = handler; +    }; + +    this.registerAccessor = function(name, accessor) { +        var handler = new AccessorHandler(accessor); +        this.accessors[name] = handler; +    }; + +    this.registerAssert = function(name, assertion, haltOnFailure) { +        var handler = new AssertHandler(assertion, haltOnFailure); +        this.asserts[name] = handler; +    }; +     +    this.getCommandHandler = function(name) { +        return this.actions[name] || this.accessors[name] || this.asserts[name] || null; +    }; + +    this.registerAll = function(commandObject) { +        registerAllAccessors(commandObject); +        registerAllActions(commandObject); +        registerAllAsserts(commandObject); +    }; + +    var registerAllActions = function(commandObject) { +        for (var functionName in commandObject) { +            var result = /^do([A-Z].+)$/.exec(functionName); +            if (result != null) { +                var actionName = result[1].lcfirst(); + +                // Register the action without the wait flag. +                var action = commandObject[functionName]; +                self.registerAction(actionName, action, false, action.dontCheckAlertsAndConfirms); + +                // Register actionName + "AndWait" with the wait flag; +                var waitActionName = actionName + "AndWait"; +                self.registerAction(waitActionName, action, true, action.dontCheckAlertsAndConfirms); +            } +        } +    }; + +  +    var registerAllAsserts = function(commandObject) { +        for (var functionName in commandObject) { +            var result = /^assert([A-Z].+)$/.exec(functionName); +            if (result != null) { +                var assert = commandObject[functionName]; + +                // Register the assert with the "assert" prefix, and halt on failure. +                var assertName = functionName; +                self.registerAssert(assertName, assert, true); + +                // Register the assert with the "verify" prefix, and do not halt on failure. +                var verifyName = "verify" + result[1]; +                self.registerAssert(verifyName, assert, false); +            } +        } +    }; + +     +    // Given an accessor function getBlah(target), +    // return a "predicate" equivalient to isBlah(target, value) that +    // is true when the value returned by the accessor matches the specified value. +    this.createPredicateFromSingleArgAccessor = function(accessor) { +        return function(target, value) { +            var accessorResult = accessor.call(this, target); +            if (PatternMatcher.matches(value, accessorResult)) { +                return new PredicateResult(true, "Actual value '" + accessorResult + "' did match '" + value + "'"); +            } else { +                return new PredicateResult(false, "Actual value '" + accessorResult + "' did not match '" + value + "'"); +            } +        }; +    }; +     +    // Given a (no-arg) accessor function getBlah(), +    // return a "predicate" equivalient to isBlah(value) that +    // is true when the value returned by the accessor matches the specified value. +    this.createPredicateFromNoArgAccessor = function(accessor) { +        return function(value) { +            var accessorResult = accessor.call(this); +            if (PatternMatcher.matches(value, accessorResult)) { +                return new PredicateResult(true, "Actual value '" + accessorResult + "' did match '" + value + "'"); +            } else { +                return new PredicateResult(false, "Actual value '" + accessorResult + "' did not match '" + value + "'"); +            } +        }; +    }; +         +    // Given a boolean accessor function isBlah(), +    // return a "predicate" equivalient to isBlah() that +    // returns an appropriate PredicateResult value. +    this.createPredicateFromBooleanAccessor = function(accessor) { +        return function() { +        	var accessorResult; +        	if (arguments.length > 2) throw new SeleniumError("Too many arguments! " + arguments.length); +        	if (arguments.length == 2) { +            	accessorResult = accessor.call(this, arguments[0], arguments[1]); +            } else if (arguments.length == 1) { +            	accessorResult = accessor.call(this, arguments[0]); +            } else { +            	accessorResult = accessor.call(this); +            } +            if (accessorResult) { +                return new PredicateResult(true, "true"); +            } else { +                return new PredicateResult(false, "false"); +            } +        }; +    }; +     +    // Given an accessor fuction getBlah([target])  (target is optional) +    // return a predicate equivalent to isBlah([target,] value) that +    // is true when the value returned by the accessor matches the specified value. +    this.createPredicateFromAccessor = function(accessor) { +		if (accessor.length == 0) { +            return self.createPredicateFromNoArgAccessor(accessor); +        } +        return self.createPredicateFromSingleArgAccessor(accessor); +    }; +     +    // Given a predicate, return the negation of that predicate. +    // Leaves the message unchanged. +    // Used to create assertNot, verifyNot, and waitForNot commands. +    this.invertPredicate = function(predicate) { +        return function(target, value) { +            var result = predicate.call(this, target, value); +            result.isTrue = ! result.isTrue; +            return result; +        }; +    }; +     +    // Convert an isBlahBlah(target, value) function into an assertBlahBlah(target, value) function. +    this.createAssertionFromPredicate = function(predicate) { +    	return function(target, value) { +    		var result = predicate.call(this, target, value); +    		if (!result.isTrue) { +    			Assert.fail(result.message); +    		} +    	}; +    }; +     +    // Register an assertion, a verification, a negative assertion, +    // and a negative verification based on the specified accessor. +    this.registerAssertionsBasedOnAccessor = function(accessor, baseName, predicate) { +        if (predicate==null) { +            predicate = self.createPredicateFromAccessor(accessor); +        } +        var assertion = self.createAssertionFromPredicate(predicate); +        // Register an assert with the "assert" prefix, and halt on failure. +        self.registerAssert("assert" + baseName, assertion, true); +        // Register a verify with the "verify" prefix, and do not halt on failure. +        self.registerAssert("verify" + baseName, assertion, false); +         +        var invertedPredicate = self.invertPredicate(predicate); +        var negativeAssertion = self.createAssertionFromPredicate(invertedPredicate); +         +        var result = /^(.*)Present$/.exec(baseName); +        if (result==null) { +            // Register an assertNot with the "assertNot" prefix, and halt on failure. +            self.registerAssert("assertNot"+baseName, negativeAssertion, true); +            // Register a verifyNot with the "verifyNot" prefix, and do not halt on failure. +            self.registerAssert("verifyNot"+baseName, negativeAssertion, false); +        } +        else { +            var invertedBaseName = result[1] + "NotPresent"; +         +            // Register an assertNot ending w/ "NotPresent", and halt on failure. +            self.registerAssert("assert"+invertedBaseName, negativeAssertion, true); +            // Register an assertNot ending w/ "NotPresent", and do not halt on failure. +            self.registerAssert("verify"+invertedBaseName, negativeAssertion, false); +        } +    }; +     +     +    // Convert an isBlahBlah(target, value) function into a waitForBlahBlah(target, value) function. +    this.createWaitForActionFromPredicate = function(predicate) { +    	var action = function(target, value) { +    		var seleniumApi = this; +		    testLoop.waitForCondition = function () { +		    	try { +				    return predicate.call(seleniumApi, target, value).isTrue; +				} catch (e) { +					// Treat exceptions as meaning the condition is not yet met. +					// Useful, for example, for waitForValue when the element has +					// not even been created yet. +					// TODO: possibly should rethrow some types of exception. +					return false; +				} +		    }; +    	}; +    	return action; +    }; +     +    // Register a waitForBlahBlah and waitForNotBlahBlah based on the specified accessor. +    this.registerWaitForCommandsBasedOnAccessor = function(accessor, baseName, predicate) { +        if (predicate==null) { +            predicate = self.createPredicateFromAccessor(accessor); +        } +        var waitForAction = self.createWaitForActionFromPredicate(predicate); +    	self.registerAction("waitFor"+baseName, waitForAction, false, true); +        var invertedPredicate = self.invertPredicate(predicate); +        var waitForNotAction = self.createWaitForActionFromPredicate(invertedPredicate); +	   	self.registerAction("waitForNot"+baseName, waitForNotAction, false, true); +	} +	 +	// Register a storeBlahBlah based on the specified accessor. +    this.registerStoreCommandBasedOnAccessor = function(accessor, baseName) { +        var action; +        if (accessor.length == 1) { +	    	action = function(target, varName) { +			    storedVars[varName] = accessor.call(this, target); +	    	}; +	    } else { +	    	action = function(varName) { +			    storedVars[varName] = accessor.call(this); +	    	}; +	    } +    	self.registerAction("store"+baseName, action, false, accessor.dontCheckAlertsAndConfirms); +    }; +         +    // Methods of the form getFoo(target) result in commands: +    // getFoo, assertFoo, verifyFoo, assertNotFoo, verifyNotFoo +    // storeFoo, waitForFoo, and waitForNotFoo. +    var registerAllAccessors = function(commandObject) { +        for (var functionName in commandObject) { +            var match = /^get([A-Z].+)$/.exec(functionName); +            if (match != null) { +                var accessor = commandObject[functionName]; +                var baseName = match[1]; +                self.registerAccessor(functionName, accessor); +                self.registerAssertionsBasedOnAccessor(accessor, baseName); +                self.registerStoreCommandBasedOnAccessor(accessor, baseName); +                self.registerWaitForCommandsBasedOnAccessor(accessor, baseName); +            } +            var match = /^is([A-Z].+)$/.exec(functionName); +            if (match != null) { +                var accessor = commandObject[functionName]; +                var baseName = match[1]; +                var predicate = self.createPredicateFromBooleanAccessor(accessor); +                self.registerAccessor(functionName, accessor); +                self.registerAssertionsBasedOnAccessor(accessor, baseName, predicate); +                self.registerStoreCommandBasedOnAccessor(accessor, baseName); +                self.registerWaitForCommandsBasedOnAccessor(accessor, baseName, predicate); +            } +        } +    }; +     +     +} + +function PredicateResult(isTrue, message) { +    this.isTrue = isTrue; +    this.message = message; +} + +// NOTE: The CommandHandler is effectively an abstract base for +// various handlers including ActionHandler, AccessorHandler and AssertHandler. +// Subclasses need to implement an execute(seleniumApi, command) function, +// where seleniumApi is the Selenium object, and command a SeleniumCommand object. +function CommandHandler(type, haltOnFailure, executor) { +    this.type = type; +    this.haltOnFailure = haltOnFailure; +    this.executor = executor; +} + +// An ActionHandler is a command handler that executes the sepcified action, +// possibly checking for alerts and confirmations (if checkAlerts is set), and +// possibly waiting for a page load if wait is set. +function ActionHandler(action, wait, dontCheckAlerts) { +    CommandHandler.call(this, "action", true, action); +    if (wait) { +        this.wait = true; +    } +    // note that dontCheckAlerts could be undefined!!! +    this.checkAlerts = (dontCheckAlerts) ? false : true; +} +ActionHandler.prototype = new CommandHandler; +ActionHandler.prototype.execute = function(seleniumApi, command) { +    if (this.checkAlerts && (null==/(Alert|Confirmation)(Not)?Present/.exec(command.command))) { +	this.checkForAlerts(seleniumApi); +    } +    var processState = this.executor.call(seleniumApi, command.target, command.value); +    // If the handler didn't return a wait flag, check to see if the +    // handler was registered with the wait flag. +    if (processState == undefined && this.wait) { +        processState = SELENIUM_PROCESS_WAIT; +    } +    return new CommandResult(processState); +}; +ActionHandler.prototype.checkForAlerts = function(seleniumApi) { +    if ( seleniumApi.browserbot.hasAlerts() ) { +        throw new SeleniumError("There was an unexpected Alert! [" + seleniumApi.browserbot.getNextAlert() + "]"); +    } +    if ( seleniumApi.browserbot.hasConfirmations() ) { +        throw new SeleniumError("There was an unexpected Confirmation! [" + seleniumApi.browserbot.getNextConfirmation() + "]"); +    } +}; + +function AccessorHandler(accessor) { +    CommandHandler.call(this, "accessor", true, accessor); +} +AccessorHandler.prototype = new CommandHandler; +AccessorHandler.prototype.execute = function(seleniumApi, command) { +    var returnValue = this.executor.call(seleniumApi, command.target, command.value); +    var result = new CommandResult(); +    result.result = returnValue; +    return result; +}; + +/** + * Handler for assertions and verifications. + */ +function AssertHandler(assertion, haltOnFailure) { +    CommandHandler.call(this, "assert", haltOnFailure || false, assertion); +} +AssertHandler.prototype = new CommandHandler; +AssertHandler.prototype.execute = function(seleniumApi, command) { +    var result = new CommandResult(); +    try { +        this.executor.call(seleniumApi, command.target, command.value); +        result.passed = true; +    } catch (e) { +        // If this is not a AssertionFailedError, or we should haltOnFailure, rethrow. +        if (!e.isAssertionFailedError) { +            throw e; +        } +        if (this.haltOnFailure) { +            var error = new SeleniumError(e.failureMessage); +            throw error; +        } +        result.failed = true; +        result.failureMessage = e.failureMessage; +    } +    return result; +}; + + +function CommandResult(processState) { +    this.processState = processState; +    this.result = null; +} + +function SeleniumCommand(command, target, value) { +    this.command = command; +    this.target = target; +    this.value = value; +} diff --git a/tests/test_tools/selenium/core/scripts/selenium-executionloop.js b/tests/test_tools/selenium/core/scripts/selenium-executionloop.js new file mode 100644 index 00000000..14c1a07a --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/selenium-executionloop.js @@ -0,0 +1,266 @@ +/* +* Copyright 2004 ThoughtWorks, Inc +* +*  Licensed under the Apache License, Version 2.0 (the "License"); +*  you may not use this file except in compliance with the License. +*  You may obtain a copy of the License at +* +*      http://www.apache.org/licenses/LICENSE-2.0 +* +*  Unless required by applicable law or agreed to in writing, software +*  distributed under the License is distributed on an "AS IS" BASIS, +*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +*  See the License for the specific language governing permissions and +*  limitations under the License. +*/ + +SELENIUM_PROCESS_WAIT = "wait"; + +function TestLoop(commandFactory) { + +    this.commandFactory = commandFactory; +    this.waitForConditionTimeout = 30 * 1000; // 30 seconds + +    this.start = function() { +        selenium.reset(); +        LOG.debug("testLoop.start()"); +        this.continueTest(); +    }; + +    /** +     * Select the next command and continue the test. +     */ +    this.continueTest = function() { +    	LOG.debug("testLoop.continueTest() - acquire the next command"); +        if (! this.aborted) { +            this.currentCommand = this.nextCommand(); +        } +        if (! this.requiresCallBack) { +        	this.beginNextTest(); +        } // otherwise, just finish and let the callback invoke beginNextTest() +    }; +     +    this.beginNextTest = function() { +    	LOG.debug("testLoop.beginNextTest()"); +    	if (this.currentCommand) { +            // TODO: rename commandStarted to commandSelected, OR roll it into nextCommand +            this.commandStarted(this.currentCommand); +            this.resumeAfterDelay(); +        } else { +            this.testComplete(); +        } +    } +     +    /** +     * Pause, then execute the current command. +     */ +    this.resumeAfterDelay = function() { + +        // Get the command delay. If a pauseInterval is set, use it once +        // and reset it.  Otherwise, use the defined command-interval. +        var delay = this.pauseInterval || this.getCommandInterval(); +        this.pauseInterval = undefined; + +        if (delay < 0) { +            // Pause: enable the "next/continue" button +            this.pause(); +        } else { +            window.setTimeout("testLoop.resume()", delay); +        } +    }; + +    /** +     * Select the next command and continue the test. +     */ +    this.resume = function() { +    	LOG.debug("testLoop.resume() - actually execute"); +        try { +            this.executeCurrentCommand(); +            this.waitForConditionStart = new Date().getTime(); +            this.continueTestWhenConditionIsTrue(); +        } catch (e) { +            this.handleCommandError(e); +            this.testComplete(); +            return; +        } +    }; + +    /** +     * Execute the current command.   +     *  +     * The return value, if not null, should be a function which will be +     * used to determine when execution can continue. +     */ +    this.executeCurrentCommand = function() { + +        var command = this.currentCommand; +        LOG.info("Executing: |" + command.command + " | " + command.target + " | " + command.value + " |"); +         +        var handler = this.commandFactory.getCommandHandler(command.command); +        if (handler == null) { +            throw new SeleniumError("Unknown command: '" + command.command + "'"); +        } +         +        command.target = selenium.preprocessParameter(command.target); +        command.value = selenium.preprocessParameter(command.value); +        LOG.debug("Command found, going to execute " + command.command); +        var result = handler.execute(selenium, command); +		LOG.debug("Command complete"); +        this.commandComplete(result); + +        if (result.processState == SELENIUM_PROCESS_WAIT) { +            this.waitForCondition = function() { +            	LOG.debug("Checking condition: isNewPageLoaded?"); +                return selenium.browserbot.isNewPageLoaded(); +            }; +        } +    }; +     +    this.handleCommandError = function(e) { +       if (!e.isSeleniumError) { +            LOG.exception(e); +            var msg = "Selenium failure. Please report to selenium-dev@openqa.org, with error details from the log window."; +            if (e.message) { +               msg += "  The error message is: " + e.message; +            } +            this.commandError(msg); +        } else { +            LOG.error(e.message); +            this.commandError(e.message); +        } +    }; + +    /** +     * Busy wait for waitForCondition() to become true, and then carry on +     * with test.  Fail the current test if there's a timeout or an exception. +     */ +    this.continueTestWhenConditionIsTrue = function () { +    	LOG.debug("testLoop.continueTestWhenConditionIsTrue()"); +        try { +            if (this.waitForCondition == null || this.waitForCondition()) { +            	LOG.debug("condition satisfied; let's continueTest()"); +	            this.waitForCondition = null; +	            this.waitForConditionStart = null; +	            this.continueTest(); +	        } else { +	        	LOG.debug("waitForCondition was false; keep waiting!"); +	        	if (this.waitForConditionTimeout != null) { +		        	var now = new Date(); +		        	if ((now - this.waitForConditionStart) > this.waitForConditionTimeout) { +		        		throw new SeleniumError("Timed out after " + this.waitForConditionTimeout + "ms"); +		        	} +		        } +	            window.setTimeout("testLoop.continueTestWhenConditionIsTrue()", 10); +	        } +	    } catch (e) { +	    	var lastResult = new CommandResult(); +    		lastResult.failed = true; +    		lastResult.failureMessage = e.message; +	    	this.commandComplete(lastResult); +	    	this.testComplete(); +	    } +    }; + +} + +/** The default is not to have any interval between commands. */ +TestLoop.prototype.getCommandInterval = function() { +    return 0; +}; + +TestLoop.prototype.nextCommand = noop; + +TestLoop.prototype.commandStarted = noop; + +TestLoop.prototype.commandError = noop; + +TestLoop.prototype.commandComplete = noop; + +TestLoop.prototype.testComplete = noop; + +TestLoop.prototype.pause = noop; + +function noop() { + +}; + +/** + * Tell Selenium to expect a failure on the next command execution. This + * command temporarily installs a CommandFactory that generates + * CommandHandlers that expect a failure. + */ +Selenium.prototype.assertFailureOnNext = function(message) { +    if (!message) { +        throw new Error("Message must be provided"); +    } + +    var expectFailureCommandFactory = +        new ExpectFailureCommandFactory(testLoop.commandFactory, message, "failure"); +    expectFailureCommandFactory.baseExecutor = executeCommandAndReturnFailureMessage; +    testLoop.commandFactory = expectFailureCommandFactory; +}; + +/** + * Tell Selenium to expect an error on the next command execution. This + * command temporarily installs a CommandFactory that generates + * CommandHandlers that expect a failure. + */ +Selenium.prototype.assertErrorOnNext = function(message) { +    if (!message) { +        throw new Error("Message must be provided"); +    } + +    var expectFailureCommandFactory = +        new ExpectFailureCommandFactory(testLoop.commandFactory, message, "error"); +    expectFailureCommandFactory.baseExecutor = executeCommandAndReturnErrorMessage; +    testLoop.commandFactory = expectFailureCommandFactory; +}; + +function ExpectFailureCommandFactory(originalCommandFactory, expectedErrorMessage, errorType) { +    this.getCommandHandler = function(name) { +        var baseHandler = originalCommandFactory.getCommandHandler(name); +        var baseExecutor = this.baseExecutor; +        var expectFailureCommand = {}; +        expectFailureCommand.execute = function() { +            var baseFailureMessage = baseExecutor(baseHandler, arguments); +            var result = new CommandResult(); +            if (!baseFailureMessage) { +                result.failed = true; +                result.failureMessage = "Expected " + errorType + " did not occur."; +            } +            else { +                if (! PatternMatcher.matches(expectedErrorMessage, baseFailureMessage)) { +                    result.failed = true; +                    result.failureMessage = "Expected " + errorType + " message '" + expectedErrorMessage +                                            + "' but was '" + baseFailureMessage + "'"; +                } +                else { +                    result.passed = true; +                    result.result = baseFailureMessage; +                } +            } +            testLoop.commandFactory = originalCommandFactory; +            return result; +        }; +        return expectFailureCommand; +    }; +}; + +function executeCommandAndReturnFailureMessage(baseHandler, originalArguments) { +    var baseResult = baseHandler.execute.apply(baseHandler, originalArguments); +    if (baseResult.passed) { +        return null; +    } +    return baseResult.failureMessage; +}; + +function executeCommandAndReturnErrorMessage(baseHandler, originalArguments) { +    try { +        baseHandler.execute.apply(baseHandler, originalArguments); +        return null; +    } +    catch (expected) { +        return expected.message; +    } +}; + diff --git a/tests/test_tools/selenium/core/scripts/selenium-logging.js b/tests/test_tools/selenium/core/scripts/selenium-logging.js new file mode 100644 index 00000000..b0fc67e4 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/selenium-logging.js @@ -0,0 +1,112 @@ +/* +* Copyright 2004 ThoughtWorks, Inc +* +*  Licensed under the Apache License, Version 2.0 (the "License"); +*  you may not use this file except in compliance with the License. +*  You may obtain a copy of the License at +* +*      http://www.apache.org/licenses/LICENSE-2.0 +* +*  Unless required by applicable law or agreed to in writing, software +*  distributed under the License is distributed on an "AS IS" BASIS, +*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +*  See the License for the specific language governing permissions and +*  limitations under the License. +*/ + +var Logger = function() { +    this.logWindow = null; +} +Logger.prototype = { + +    setLogLevelThreshold: function(logLevel) { +    	this.pendingLogLevelThreshold = logLevel; +        this.show(); +        // +        // The following message does not show up in the log -- _unless_ I step along w/ the debugger +        // down to the append call.  I believe this is because the new log window has not yet loaded, +        // and therefore the log msg is discarded; but if I step through the debugger, this changes +        // the scheduling so as to load that window and make it ready. +        // this.info("Log level programmatically set to " + logLevel + " (presumably by driven-mode test code)"); +    }, + +    getLogWindow: function() { +        if (this.logWindow && this.logWindow.closed) { +            this.logWindow = null; +        } +        if (this.logWindow && this.pendingLogLevelThreshold && this.logWindow.setThresholdLevel) { +            this.logWindow.setThresholdLevel(this.pendingLogLevelThreshold); +             +            // can't just directly log because that action would loop back to this code infinitely +            this.pendingInfoMessage = "Log level programmatically set to " + this.pendingLogLevelThreshold + " (presumably by driven-mode test code)"; +             +            this.pendingLogLevelThreshold = null;	// let's only go this way one time +        } + +        return this.logWindow; +    }, +     +    openLogWindow: function() { +        this.logWindow = window.open( +            getDocumentBase(document) + "SeleniumLog.html", "SeleniumLog", +            "width=600,height=250,bottom=0,right=0,status,scrollbars,resizable" +        ); +        return this.logWindow; +    }, +     +    show: function() { +        if (! this.getLogWindow()) { +            this.openLogWindow(); +        } +    }, + +    log: function(message, className) { +        var logWindow = this.getLogWindow(); +        if (logWindow) { +            if (logWindow.append) { +            	if (this.pendingInfoMessage) { + 		    logWindow.append("info: " + this.pendingInfoMessage, "info"); +                    this.pendingInfoMessage = null; +                } +                logWindow.append(className + ": " + message, className); +            } +        } +    }, + +    close: function(message) { +    	if (this.logWindow != null) { +        	try { +        		this.logWindow.close(); +        	} catch (e) { +        		// swallow exception +        		// the window is probably closed if we get an exception here +        	} +        	this.logWindow = null; +        } +    }, + +    debug: function(message) { +        this.log(message, "debug"); +    }, + +    info: function(message) { +        this.log(message, "info"); +    }, + +    warn: function(message) { +        this.log(message, "warn"); +    }, + +    error: function(message) { +        this.log(message, "error"); +    }, + +    exception: function(exception) { +        var msg = "Unexpected Exception: " + describe(exception, ', '); +        this.error(msg); +    } + +}; + +var LOG = new Logger(); + diff --git a/tests/test_tools/selenium/core/scripts/selenium-seleneserunner.js b/tests/test_tools/selenium/core/scripts/selenium-seleneserunner.js new file mode 100644 index 00000000..041b3bf9 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/selenium-seleneserunner.js @@ -0,0 +1,300 @@ +/* +* Copyright 2005 ThoughtWorks, Inc +* +*  Licensed under the Apache License, Version 2.0 (the "License"); +*  you may not use this file except in compliance with the License. +*  You may obtain a copy of the License at +* +*      http://www.apache.org/licenses/LICENSE-2.0 +* +*  Unless required by applicable law or agreed to in writing, software +*  distributed under the License is distributed on an "AS IS" BASIS, +*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +*  See the License for the specific language governing permissions and +*  limitations under the License. +* +*/ + +passColor = "#cfffcf"; +failColor = "#ffcfcf"; +errorColor = "#ffffff"; +workingColor = "#DEE7EC"; +doneColor = "#FFFFCC"; + +slowMode = false; + +var cmd1 = document.createElement("div"); +var cmd2 = document.createElement("div"); +var cmd3 = document.createElement("div"); +var cmd4 = document.createElement("div"); + +var postResult = "START"; + +queryString = null; + +function runTest() { +    var testAppFrame = document.getElementById('myiframe'); +    selenium = Selenium.createForFrame(testAppFrame); + +    commandFactory = new CommandHandlerFactory(); +    commandFactory.registerAll(selenium); + +    testLoop = new TestLoop(commandFactory); + +    testLoop.nextCommand = nextCommand; +    testLoop.commandStarted = commandStarted; +    testLoop.commandComplete = commandComplete; +    testLoop.commandError = commandError; +    testLoop.requiresCallBack = true; +    testLoop.testComplete = function() { +    	window.status = "Selenium Tests Complete, for this Test" +    	// Continue checking for new results +    	testLoop.continueTest(); +    	postResult = "START"; +    }; + +    document.getElementById("commandList").appendChild(cmd4); +    document.getElementById("commandList").appendChild(cmd3); +    document.getElementById("commandList").appendChild(cmd2); +    document.getElementById("commandList").appendChild(cmd1); +     +    var doContinue = getQueryVariable("continue"); +	if (doContinue != null) postResult = "OK"; + +    testLoop.start(); +} + +function getQueryString() { +	if (queryString != null) return queryString; +	if (browserVersion.isHTA) { +		var args = extractArgs(); +		if (args.length < 2) return null; +		queryString = args[1]; +		return queryString; +	} else { +		return location.search.substr(1); +	} +} + +function extractArgs() { +	var str = SeleniumHTARunner.commandLine; +	if (str == null || str == "") return new Array(); +    var matches = str.match(/(?:"([^"]+)"|(?!"([^"]+)")(\S+))/g); +    // We either want non quote stuff ([^"]+) surrounded by quotes +    // or we want to look-ahead, see that the next character isn't +    // a quoted argument, and then grab all the non-space stuff +    // this will return for the line: "foo" bar +    // the results "\"foo\"" and "bar" + +    // So, let's unquote the quoted arguments: +    var args = new Array; +    for (var i = 0; i < matches.length; i++) { +        args[i] = matches[i]; +        args[i] = args[i].replace(/^"(.*)"$/, "$1"); +    } +    return args; +} + +function getQueryVariable(variable) { +    var query = getQueryString(); +    if (query == null) return null; +    var vars = query.split("&"); +    for (var i=0;i<vars.length;i++) { +        var pair = vars[i].split("="); +        if (pair[0] == variable) { +            return pair[1]; +        } +    } +} + +function buildBaseUrl() { +	var baseUrl = getQueryVariable("baseUrl"); +	if (baseUrl != null) return baseUrl; +	var lastSlash = window.location.href.lastIndexOf('/'); +	baseUrl = window.location.href.substring(0, lastSlash+1); +	return baseUrl; +} + +function buildDriverParams() { +    var params = ""; + +    var host = getQueryVariable("driverhost"); +    var port = getQueryVariable("driverport"); +    if (host != undefined && port != undefined) { +        params = params + "&driverhost=" + host + "&driverport=" + port; +    } + +    var sessionId = getQueryVariable("sessionId"); +    if (sessionId != undefined) { +        params = params + "&sessionId=" + sessionId; +    } + +    return params; +} + +function preventBrowserCaching() { +    var t = (new Date()).getTime(); +    return "&counterToMakeURsUniqueAndSoStopPageCachingInTheBrowser=" + t; +}    + +function nextCommand() { +    xmlHttp = XmlHttp.create(); +    try { +    	 +    	var url = buildBaseUrl(); +        if (postResult == "START") { +        	url = url + "driver/?seleniumStart=true" + buildDriverParams() + preventBrowserCaching(); +        } else { +        	url = url + "driver/?" + buildDriverParams() + preventBrowserCaching(); +        } +        LOG.debug("XMLHTTPRequesting " + url); +        xmlHttp.open("POST", url, true); +        xmlHttp.onreadystatechange=handleHttpResponse; +        xmlHttp.send(postResult); +    } catch(e) { +       	var s = 'xmlHttp returned:\n' +        for (key in e) { +            s += "\t" + key + " -> " + e[key] + "\n" +        } +        LOG.error(s); +        return null; +    } +    return null; +} + + function handleHttpResponse() { + 	if (xmlHttp.readyState == 4) { + 		if (xmlHttp.status == 200) { + 			var command = extractCommand(xmlHttp); + 			testLoop.currentCommand = command; + 			testLoop.beginNextTest(); + 		} else { + 			var s = 'xmlHttp returned: ' + xmlHttp.status + ": " + xmlHttp.statusText; + 			LOG.error(s); + 			testLoop.currentCommand = null; + 			setTimeout("testLoop.beginNextTest();", 2000); + 		} + 		 + 	} + } + + +function extractCommand(xmlHttp) { +    if (slowMode) { +        delay(2000); +    } + +    var command; +    try { +        command = xmlHttp.responseText; +    } catch (e) { +        alert('could not get responseText: ' + e.message); +    } +    if (command.substr(0,'|testComplete'.length)=='|testComplete') { +        return null; +    } + +    return createCommandFromRequest(command); +} + +function commandStarted(command) { +    commandNode = document.createElement("div"); +    innerHTML = command.command + '('; +    if (command.target != null && command.target != "") { +        innerHTML += command.target; +        if (command.value != null && command.value != "") { +            innerHTML += ', ' + command.value; +        } +    } +    innerHTML += ")"; +    commandNode.innerHTML = innerHTML; +    commandNode.style.backgroundColor = workingColor; +    document.getElementById("commandList").removeChild(cmd1); +    document.getElementById("commandList").removeChild(cmd2); +    document.getElementById("commandList").removeChild(cmd3); +    document.getElementById("commandList").removeChild(cmd4); +    cmd4 = cmd3; +    cmd3 = cmd2; +    cmd2 = cmd1; +    cmd1 = commandNode; +    document.getElementById("commandList").appendChild(cmd4); +    document.getElementById("commandList").appendChild(cmd3); +    document.getElementById("commandList").appendChild(cmd2); +    document.getElementById("commandList").appendChild(cmd1); +} + +function commandComplete(result) { +    if (result.failed) { +    	if (postResult == "CONTINUATION") { +    		testLoop.aborted = true; +    	} +        postResult = result.failureMessage; +        commandNode.title = result.failureMessage; +        commandNode.style.backgroundColor = failColor; +    } else if (result.passed) { +        postResult = "OK"; +        commandNode.style.backgroundColor = passColor; +    } else { +    	if (result.result == null) { +    		postResult = "OK"; +    	} else { +    		postResult = "OK," + result.result; +    	} +        commandNode.style.backgroundColor = doneColor; +    } +} + +function commandError(message) { +    postResult = "ERROR: " + message; +    commandNode.style.backgroundColor = errorColor; +    commandNode.title = message; +} + +function slowClicked() { +    slowMode = !slowMode; +} + +function delay(millis) { +    startMillis = new Date(); +    while (true) { +        milli = new Date(); +        if (milli-startMillis > millis) { +            break; +        } +    } +} + +function getIframeDocument(iframe) { +    if (iframe.contentDocument) { +        return iframe.contentDocument; +    } +    else { +        return iframe.contentWindow.document; +    } +} + +// Parses a URI query string into a SeleniumCommand object +function createCommandFromRequest(commandRequest) { +	//decodeURIComponent doesn't strip plus signs +	var processed = commandRequest.replace(/\+/g, "%20"); +	// strip trailing spaces +	var processed = processed.replace(/\s+$/, ""); +    var vars = processed.split("&"); +    var cmdArgs = new Object(); +    for (var i=0;i<vars.length;i++) { +        var pair = vars[i].split("="); +        cmdArgs[pair[0]] = pair[1]; +    } +    var cmd = cmdArgs['cmd']; +    var arg1 = cmdArgs['1']; +    if (null == arg1) arg1 = ""; +    arg1 = decodeURIComponent(arg1); +    var arg2 = cmdArgs['2']; +    if (null == arg2) arg2 = ""; +    arg2 = decodeURIComponent(arg2); +    if (cmd == null) { +    	throw new Error("Bad command request: " + commandRequest); +    } +    return new SeleniumCommand(cmd, arg1, arg2); +} + diff --git a/tests/test_tools/selenium/core/scripts/selenium-testrunner.js b/tests/test_tools/selenium/core/scripts/selenium-testrunner.js new file mode 100644 index 00000000..1ced0a11 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/selenium-testrunner.js @@ -0,0 +1,748 @@ +/* +* Copyright 2004 ThoughtWorks, Inc +* +*  Licensed under the Apache License, Version 2.0 (the "License"); +*  you may not use this file except in compliance with the License. +*  You may obtain a copy of the License at +* +*      http://www.apache.org/licenses/LICENSE-2.0 +* +*  Unless required by applicable law or agreed to in writing, software +*  distributed under the License is distributed on an "AS IS" BASIS, +*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +*  See the License for the specific language governing permissions and +*  limitations under the License. +* +*/ + +// The current row in the list of tests (test suite) +currentRowInSuite = 0; + +// An object representing the current test +currentTest = null; + +// Whether or not the jsFT should run all tests in the suite +runAllTests = false; + +// Whether or not the current test has any errors; +testFailed = false; +suiteFailed = false; + +// Colors used to provide feedback +passColor = "#ccffcc"; +doneColor = "#eeffee"; +failColor = "#ffcccc"; +workingColor = "#ffffcc"; + +// Holds the handlers for each command. +commandHandlers = null; + +// The number of tests run +numTestPasses = 0; + +// The number of tests that have failed +numTestFailures = 0; + +// The number of commands which have passed +numCommandPasses = 0; + +// The number of commands which have failed +numCommandFailures = 0; + +// The number of commands which have caused errors (element not found) +numCommandErrors = 0; + +// The time that the test was started. +startTime = null; + +// The current time. +currentTime = null; + +// An simple enum for failureType +ERROR = 0; +FAILURE = 1; + +runInterval = 0; + +queryString = null; + +function setRunInterval() { +    // Get the value of the checked runMode option. +    // There should be a way of getting the value of the "group", but I don't know how. +    var runModeOptions = document.forms['controlPanel'].runMode; +    for (var i = 0; i < runModeOptions.length; i++) { +        if (runModeOptions[i].checked) { +            runInterval = runModeOptions[i].value; +            break; +        } +    } +} + +function continueCurrentTest() { +    document.getElementById('continueTest').disabled = true; +    testLoop.resume(); +} + +function getApplicationFrame() { +    return document.getElementById('myiframe'); +} + +function getSuiteFrame() { +    return document.getElementById('testSuiteFrame'); +} + +function getTestFrame(){ +    return document.getElementById('testFrame'); +} + +function loadAndRunIfAuto() { +    loadSuiteFrame(); +} + +function start() { +	queryString = null; +    setRunInterval(); +    loadSuiteFrame(); +} + +function loadSuiteFrame() { +    var testAppFrame = document.getElementById('myiframe'); +    selenium = Selenium.createForFrame(testAppFrame); +    registerCommandHandlers(); + +    //set the runInterval if there is a queryParameter for it +    var tempRunInterval = getQueryParameter("runInterval"); +    if (tempRunInterval) { +        runInterval = tempRunInterval; +    } + +    document.getElementById("modeRun").onclick = setRunInterval; +    document.getElementById('modeWalk').onclick = setRunInterval; +    document.getElementById('modeStep').onclick = setRunInterval; +    document.getElementById('continueTest').onclick = continueCurrentTest; + +    var testSuiteName = getQueryParameter("test"); + +    if (testSuiteName) { +        addLoadListener(getSuiteFrame(), onloadTestSuite); +        getSuiteFrame().src = testSuiteName; +    } else { +        onloadTestSuite(); +    } +} + +function startSingleTest() { +    removeLoadListener(getApplicationFrame(), startSingleTest); +    var singleTestName = getQueryParameter("singletest"); +    addLoadListener(getTestFrame(), startTest); +    getTestFrame().src = singleTestName; +} + +function getIframeDocument(iframe) +{ +    if (iframe.contentDocument) { +        return iframe.contentDocument; +    } +    else { +        return iframe.contentWindow.document; +    } +} + +function onloadTestSuite() { +    removeLoadListener(getSuiteFrame(), onloadTestSuite); +     +    // Add an onclick function to each link in all suite tables +    var allTables = getIframeDocument(getSuiteFrame()).getElementsByTagName("table"); +    for (var tableNum = 0; tableNum < allTables.length; tableNum++) +    { +        var skippedTable = allTables[tableNum]; +        for(rowNum = 1;rowNum < skippedTable.rows.length; rowNum++) { +            addOnclick(skippedTable, rowNum); +        } +    } + +    suiteTable = getIframeDocument(getSuiteFrame()).getElementsByTagName("table")[0]; +    if (suiteTable!=null) { + +        if (isAutomatedRun()) { +	    startTestSuite(); +        } else if (getQueryParameter("autoURL")) { + +            addLoadListener(getApplicationFrame(), startSingleTest); + +	    getApplicationFrame().src = getQueryParameter("autoURL"); + +	} else { +            testLink = suiteTable.rows[currentRowInSuite+1].cells[0].getElementsByTagName("a")[0]; +            getTestFrame().src = testLink.href; +        } +    } +} + +// Adds an onclick function to the link in the given row in suite table. +// This function checks whether the test has already been run and the data is +// stored. If the data is stored, it sets the test frame to be the stored data. +// Otherwise, it loads the fresh page. +function addOnclick(suiteTable, rowNum) { +    aLink = suiteTable.rows[rowNum].cells[0].getElementsByTagName("a")[0]; +    aLink.onclick = function(eventObj) { +        srcObj = null; + +        // For mozilla-like browsers +        if(eventObj) +                srcObj = eventObj.target; + +        // For IE-like browsers +        else if (getSuiteFrame().contentWindow.event) +                srcObj = getSuiteFrame().contentWindow.event.srcElement; + +        // The target row (the event source is not consistently reported by browsers) +        row = srcObj.parentNode.parentNode.rowIndex || srcObj.parentNode.parentNode.parentNode.rowIndex; + +        // If the row has a stored results table, use that +        if(suiteTable.rows[row].cells[1]) { +            var bodyElement = getIframeDocument(getTestFrame()).body; + +            // Create a div element to hold the results table. +            var tableNode = getIframeDocument(getTestFrame()).createElement("div"); +            var resultsCell = suiteTable.rows[row].cells[1]; +            tableNode.innerHTML = resultsCell.innerHTML; + +            // Append this text node, and remove all the preceding nodes. +            bodyElement.appendChild(tableNode); +            while (bodyElement.firstChild != bodyElement.lastChild) { +                bodyElement.removeChild(bodyElement.firstChild); +            } +        } +        // Otherwise, just open up the fresh page. +        else { +            getTestFrame().src = suiteTable.rows[row].cells[0].getElementsByTagName("a")[0].href; +        } + +        return false; +    }; +} + +function isQueryParameterTrue(name) { +    parameterValue = getQueryParameter(name); +    return (parameterValue != null && parameterValue.toLowerCase() == "true"); +} + +function getQueryString() { +	if (queryString != null) return queryString; +	if (browserVersion.isHTA) { +		var args = extractArgs(); +		if (args.length < 2) return null; +		queryString = args[1]; +		return queryString; +	} else { +		return location.search.substr(1); +	} +} + +function extractArgs() { +	var str = SeleniumHTARunner.commandLine; +	if (str == null || str == "") return new Array(); +    var matches = str.match(/(?:"([^"]+)"|(?!"([^"]+)")(\S+))/g); +    // We either want non quote stuff ([^"]+) surrounded by quotes +    // or we want to look-ahead, see that the next character isn't +    // a quoted argument, and then grab all the non-space stuff +    // this will return for the line: "foo" bar +    // the results "\"foo\"" and "bar" + +    // So, let's unquote the quoted arguments: +    var args = new Array; +    for (var i = 0; i < matches.length; i++) { +        args[i] = matches[i]; +        args[i] = args[i].replace(/^"(.*)"$/, "$1"); +    } +    return args; +} + +function getQueryParameter(searchKey) { +	var str = getQueryString(); +	if (str == null) return null; +	var clauses = str.split('&'); +    for (var i = 0; i < clauses.length; i++) { +        var keyValuePair = clauses[i].split('=',2); +        var key = unescape(keyValuePair[0]); +        if (key == searchKey) { +            return unescape(keyValuePair[1]); +        } +    } +    return null; +} + +function isNewWindow() { +    return isQueryParameterTrue("newWindow"); +} + +function isAutomatedRun() { +    return isQueryParameterTrue("auto"); +} + +function resetMetrics() { +    numTestPasses = 0; +    numTestFailures = 0; +    numCommandPasses = 0; +    numCommandFailures = 0; +    numCommandErrors = 0; +    startTime = new Date().getTime(); +} + +function runSingleTest() { +    runAllTests = false; +    resetMetrics(); +    startTest(); +} + +function startTest() { +    removeLoadListener(getTestFrame(), startTest); + +    // Scroll to the top of the test frame +    if (getTestFrame().contentWindow) { +        getTestFrame().contentWindow.scrollTo(0,0); +    } +    else { +        frames['testFrame'].scrollTo(0,0); +    } + +    currentTest = new HtmlTest(getIframeDocument(getTestFrame())); + +    testFailed = false; +    storedVars = new Object(); + +    testLoop = initialiseTestLoop(); +    testLoop.start(); +} + +function HtmlTest(testDocument) { +    this.init(testDocument); +} + +HtmlTest.prototype = { + +    init: function(testDocument) { +        this.document = testDocument; +        this.document.bgColor = ""; +        this.currentRow = null; +        this.commandRows = new Array(); +        this.headerRow = null; +        var tables = this.document.getElementsByTagName("table"); +        for (var i = 0; i < tables.length; i++) { +            var candidateRows = tables[i].rows; +            for (var j = 0; j < candidateRows.length; j++) { +                if (!this.headerRow) { +                    this.headerRow = candidateRows[j]; +                } +                if (candidateRows[j].cells.length >= 3) { +                    this.addCommandRow(candidateRows[j]); +                } +            } +        } +    }, + +    addCommandRow: function(row) { +        if (row.cells[2] && row.cells[2].originalHTML) { +            row.cells[2].innerHTML = row.cells[2].originalHTML; +        } +        row.bgColor = ""; +        this.commandRows.push(row); +    }, + +    nextCommand: function() { +        if (this.commandRows.length > 0) { +            this.currentRow = this.commandRows.shift(); +        } else { +            this.currentRow = null; +        } +        return this.currentRow; +    } + +}; + +function startTestSuite() { +    resetMetrics(); +    currentRowInSuite = 0; +    runAllTests = true; +    suiteFailed = false; + +    runNextTest(); +} + +function runNextTest() { +    if (!runAllTests) +            return; + +    suiteTable = getIframeDocument(getSuiteFrame()).getElementsByTagName("table")[0]; + +    // Do not change the row color of the first row +    if (currentRowInSuite > 0) { +        // Provide test-status feedback +        if (testFailed) { +            setCellColor(suiteTable.rows, currentRowInSuite, 0, failColor); +        } else { +            setCellColor(suiteTable.rows, currentRowInSuite, 0, passColor); +        } + +        // Set the results from the previous test run +        setResultsData(suiteTable, currentRowInSuite); +    } + +    currentRowInSuite++; + +    // If we are done with all of the tests, set the title bar as pass or fail +    if (currentRowInSuite >= suiteTable.rows.length) { +        if (suiteFailed) { +            setCellColor(suiteTable.rows, 0, 0, failColor); +        } else { +            setCellColor(suiteTable.rows, 0, 0, passColor); +        } + +        // If this is an automated run (i.e., build script), then submit +        // the test results by posting to a form +        if (isAutomatedRun()) +                postTestResults(suiteFailed, suiteTable); +    } + +    else { +        // Make the current row blue +        setCellColor(suiteTable.rows, currentRowInSuite, 0, workingColor); + +        testLink = suiteTable.rows[currentRowInSuite].cells[0].getElementsByTagName("a")[0]; +        testLink.focus(); + +        var testFrame = getTestFrame(); +        addLoadListener(testFrame, startTest); + +        selenium.browserbot.setIFrameLocation(testFrame, testLink.href); +    } +} + +function setCellColor(tableRows, row, col, colorStr) { +    tableRows[row].cells[col].bgColor = colorStr; +} + +// Sets the results from a test into a hidden column on the suite table.  So, +// for each tests, the second column is set to the HTML from the test table. +function setResultsData(suiteTable, row) { +    // Create a text node of the test table +    var resultTable = getIframeDocument(getTestFrame()).body.innerHTML; +    if (!resultTable) return; + +    var tableNode = suiteTable.ownerDocument.createElement("div"); +    tableNode.innerHTML = resultTable; + +    var new_column = suiteTable.ownerDocument.createElement("td"); +    new_column.appendChild(tableNode); + +    // Set the column to be invisible +    new_column.style.display = "none"; + +    // Add the invisible column +    suiteTable.rows[row].appendChild(new_column); +} + +// Post the results to a servlet, CGI-script, etc.  The URL of the +// results-handler defaults to "/postResults", but an alternative location +// can be specified by providing a "resultsUrl" query parameter. +// +// Parameters passed to the results-handler are: +//      result:         passed/failed depending on whether the suite passed or failed +//      totalTime:      the total running time in seconds for the suite. +// +//      numTestPasses:  the total number of tests which passed. +//      numTestFailures: the total number of tests which failed. +// +//      numCommandPasses: the total number of commands which passed. +//      numCommandFailures: the total number of commands which failed. +//      numCommandErrors: the total number of commands which errored. +// +//      suite:      the suite table, including the hidden column of test results +//      testTable.1 to testTable.N: the individual test tables +// +function postTestResults(suiteFailed, suiteTable) { + +    form = document.createElement("form"); +    document.body.appendChild(form); + +    form.id = "resultsForm"; +    form.method="post"; +    form.target="myiframe"; + +    var resultsUrl = getQueryParameter("resultsUrl"); +    if (!resultsUrl) { +        resultsUrl = "./postResults"; +    } + +    var actionAndParameters = resultsUrl.split('?',2); +    form.action = actionAndParameters[0]; +    var resultsUrlQueryString = actionAndParameters[1]; + +    form.createHiddenField = function(name, value) { +        input = document.createElement("input"); +        input.type = "hidden"; +        input.name = name; +        input.value = value; +        this.appendChild(input); +    }; + +    if (resultsUrlQueryString) { +        var clauses = resultsUrlQueryString.split('&'); +        for (var i = 0; i < clauses.length; i++) { +            var keyValuePair = clauses[i].split('=',2); +            var key = unescape(keyValuePair[0]); +            var value = unescape(keyValuePair[1]); +            form.createHiddenField(key, value); +        } +    } + +    form.createHiddenField("selenium.version", Selenium.version); +    form.createHiddenField("selenium.revision", Selenium.revision); +     +    form.createHiddenField("result", suiteFailed == true ? "failed" : "passed"); + +    form.createHiddenField("totalTime", Math.floor((currentTime - startTime) / 1000)); +    form.createHiddenField("numTestPasses", numTestPasses); +    form.createHiddenField("numTestFailures", numTestFailures); +    form.createHiddenField("numCommandPasses", numCommandPasses); +    form.createHiddenField("numCommandFailures", numCommandFailures); +    form.createHiddenField("numCommandErrors", numCommandErrors); + +    // Create an input for each test table.  The inputs are named +    // testTable.1, testTable.2, etc. +    for (rowNum = 1; rowNum < suiteTable.rows.length;rowNum++) { +        // If there is a second column, then add a new input +        if (suiteTable.rows[rowNum].cells.length > 1) { +            var resultCell = suiteTable.rows[rowNum].cells[1]; +            form.createHiddenField("testTable." + rowNum, resultCell.innerHTML); +            // remove the resultCell, so it's not included in the suite HTML +            resultCell.parentNode.removeChild(resultCell);  +        } +    } +     +    form.createHiddenField("numTestTotal", rowNum); + +    // Add HTML for the suite itself +    form.createHiddenField("suite", suiteTable.parentNode.innerHTML); + +	if (isQueryParameterTrue("save")) { +		saveToFile(resultsUrl, form); +	} else { +    	form.submit(); +    } +    document.body.removeChild(form); +	if (isQueryParameterTrue("close")) { +    	window.top.close(); +    } +} + +function saveToFile(fileName, form) { +	// This only works when run as an IE HTA +	var inputs = new Object(); +	for (var i = 0; i < form.elements.length; i++) { +		inputs[form.elements[i].name] = form.elements[i].value; +	} +	var objFSO = new ActiveXObject("Scripting.FileSystemObject") +	var scriptFile = objFSO.CreateTextFile(fileName); +	scriptFile.WriteLine("<html><body>\n<h1>Test suite results </h1>" + +            "\n\n<table>\n<tr>\n<td>result:</td>\n<td>" + inputs["result"] + "</td>\n" + +            "</tr>\n<tr>\n<td>totalTime:</td>\n<td>" + inputs["totalTime"] + "</td>\n</tr>\n" + +            "<tr>\n<td>numTestPasses:</td>\n<td>" + inputs["numTestPasses"] + "</td>\n</tr>\n" + +            "<tr>\n<td>numTestFailures:</td>\n<td>" + inputs["numTestFailures"] + "</td>\n</tr>\n" + +            "<tr>\n<td>numCommandPasses:</td>\n<td>" + inputs["numCommandPasses"] + "</td>\n</tr>\n" + +            "<tr>\n<td>numCommandFailures:</td>\n<td>" + inputs["numCommandFailures"] + "</td>\n</tr>\n" + +            "<tr>\n<td>numCommandErrors:</td>\n<td>" + inputs["numCommandErrors"] + "</td>\n</tr>\n" + +            "<tr>\n<td>" + inputs["suite"] + "</td>\n<td> </td>\n</tr>"); +    var testNum = inputs["numTestTotal"]; +    for (var rowNum = 1; rowNum < testNum; rowNum++) { +    	scriptFile.WriteLine("<tr>\n<td>" + inputs["testTable." + rowNum] + "</td>\n<td> </td>\n</tr>"); +    } +    scriptFile.WriteLine("</table></body></html>"); +    scriptFile.Close(); +} + +function printMetrics() { +    setText(document.getElementById("commandPasses"), numCommandPasses); +    setText(document.getElementById("commandFailures"), numCommandFailures); +    setText(document.getElementById("commandErrors"), numCommandErrors); +    setText(document.getElementById("testRuns"), numTestPasses + numTestFailures); +    setText(document.getElementById("testFailures"), numTestFailures); + +    currentTime = new Date().getTime(); + +    timeDiff = currentTime - startTime; +    totalSecs = Math.floor(timeDiff / 1000); + +    minutes = Math.floor(totalSecs / 60); +    seconds = totalSecs % 60; + +    setText(document.getElementById("elapsedTime"), pad(minutes)+":"+pad(seconds)); +} + +// Puts a leading 0 on num if it is less than 10 +function pad (num) { +    return (num > 9) ? num : "0" + num; +} + +/* + * Register all of the built-in command handlers with the CommandHandlerFactory. + * TODO work out an easy way for people to register handlers without modifying the Selenium sources. + */ +function registerCommandHandlers() { +    commandFactory = new CommandHandlerFactory(); +    commandFactory.registerAll(selenium); + +} + +function initialiseTestLoop() { +    testLoop = new TestLoop(commandFactory); + +    testLoop.getCommandInterval = function() { return runInterval; }; +    testLoop.nextCommand = nextCommand; +    testLoop.commandStarted = commandStarted; +    testLoop.commandComplete = commandComplete; +    testLoop.commandError = commandError; +    testLoop.testComplete = testComplete; +    testLoop.pause = function() { +        document.getElementById('continueTest').disabled = false; +    }; +    return testLoop; +} + +function nextCommand() { +    var row = currentTest.nextCommand(); +    if (row == null) { +        return null; +    } +    row.cells[2].originalHTML = row.cells[2].innerHTML; +    return new SeleniumCommand(getText(row.cells[0]),  +                               getText(row.cells[1]), +                               getText(row.cells[2])); +} + +function removeNbsp(value) { +    return value.replace(/\240/g, ""); +} + +function scrollIntoView(element) { +    if (element.scrollIntoView) { +        element.scrollIntoView(false); +        return; +    } +    // TODO: work out how to scroll browsers that don't support +    // scrollIntoView (like Konqueror) +} + +function commandStarted() { +    currentTest.currentRow.bgColor = workingColor; +    scrollIntoView(currentTest.currentRow.cells[0]); +    printMetrics(); +} + +function commandComplete(result) { +    if (result.failed) { +        numCommandFailures += 1; +        recordFailure(result.failureMessage); +    } else if (result.passed) { +        numCommandPasses += 1; +        currentTest.currentRow.bgColor = passColor; +    } else { +        currentTest.currentRow.bgColor = doneColor; +    } +} + +function commandError(errorMessage) { +    numCommandErrors += 1; +    recordFailure(errorMessage); +} + +function recordFailure(errorMsg) { +    LOG.warn("recordFailure: " + errorMsg); +    // Set cell background to red +    currentTest.currentRow.bgColor = failColor; + +    // Set error message +    currentTest.currentRow.cells[2].innerHTML = errorMsg; +    currentTest.currentRow.title = errorMsg; +    testFailed = true; +    suiteFailed = true; +} + +function testComplete() { +    var resultColor = passColor; +    if (testFailed) { +        resultColor = failColor; +        numTestFailures += 1; +    } else { +        numTestPasses += 1; +    } + +    if (currentTest.headerRow) { +        currentTest.headerRow.bgColor = resultColor; +    } +     +    printMetrics(); + +    window.setTimeout("runNextTest()", 1); +} + +Selenium.prototype.doPause = function(waitTime) { +    testLoop.pauseInterval = waitTime; +}; + +Selenium.prototype.doPause.dontCheckAlertsAndConfirms = true; + +Selenium.prototype.doBreak = function() { +    document.getElementById('modeStep').checked = true; +    runInterval = -1; +}; + +/* + * Click on the located element, and attach a callback to notify + * when the page is reloaded. + */ +Selenium.prototype.doModalDialogTest = function(returnValue) { +    this.browserbot.doModalDialogTest(returnValue); +}; + +/* + * Store the value of a form input in a variable + */ +Selenium.prototype.doStoreValue = function(target, varName) { +    if (!varName) { +        // Backward compatibility mode: read the ENTIRE text of the page +        // and stores it in a variable with the name of the target +        value = this.page().bodyText(); +        storedVars[target] = value; +        return; +    } +    var element = this.page().findElement(target); +    storedVars[varName] = getInputValue(element); +}; + +/* + * Store the text of an element in a variable + */ +Selenium.prototype.doStoreText = function(target, varName) { +    var element = this.page().findElement(target); +    storedVars[varName] = getText(element); +}; + +/* + * Store the value of an element attribute in a variable + */ +Selenium.prototype.doStoreAttribute = function(target, varName) { +    storedVars[varName] = this.page().findAttribute(target); +}; + +/* + * Store the result of a literal value + */ +Selenium.prototype.doStore = function(value, varName) { +    storedVars[varName] = value; +}; + +Selenium.prototype.doEcho = function(msg) { +	currentTest.currentRow.cells[2].innerHTML = msg; +} diff --git a/tests/test_tools/selenium/core/scripts/selenium-version.js b/tests/test_tools/selenium/core/scripts/selenium-version.js new file mode 100644 index 00000000..1fee837b --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/selenium-version.js @@ -0,0 +1,5 @@ +Selenium.version = "@VERSION@"; +Selenium.revision = "@REVISION@"; + +window.top.document.title += " v" + Selenium.version + " [" + Selenium.revision + "]"; + diff --git a/tests/test_tools/selenium/core/scripts/user-extensions.js.sample b/tests/test_tools/selenium/core/scripts/user-extensions.js.sample new file mode 100644 index 00000000..0f0ca840 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/user-extensions.js.sample @@ -0,0 +1,75 @@ +/* + * By default, Selenium looks for a file called "user-extensions.js", and loads and javascript + * code found in that file. This file is a sample of what that file could look like. + * + * user-extensions.js provides a convenient location for adding extensions to Selenium, like + * new actions, checks and locator-strategies. + * By default, this file does not exist. Users can create this file and place their extension code + * in this common location, removing the need to modify the Selenium sources, and hopefully assisting + * with the upgrade process. + * + * You can find contributed extensions at http://wiki.openqa.org/display/SEL/Contributed%20User-Extensions + */ + +// The following examples try to give an indication of how Selenium can be extended with javascript. + +// All do* methods on the Selenium prototype are added as actions. +// Eg add a typeRepeated action to Selenium, which types the text twice into a text box. +// The typeTwiceAndWait command will be available automatically +Selenium.prototype.doTypeRepeated = function(locator, text) { +    // All locator-strategies are automatically handled by "findElement" +    var element = this.page().findElement(locator); + +    // Create the text to type +    var valueToType = text + text; + +    // Replace the element text with the new text +    this.page().replaceText(element, valueToType); +}; + +// All assert* methods on the Selenium prototype are added as checks. +// Eg add a assertValueRepeated check, that makes sure that the element value +// consists of the supplied text repeated. +// The verify version will be available automatically. +Selenium.prototype.assertValueRepeated = function(locator, text) { +    // All locator-strategies are automatically handled by "findElement" +    var element = this.page().findElement(locator); + +    // Create the text to verify +    var expectedValue = text + text; + +    // Get the actual element value +    var actualValue = element.value; + +    // Make sure the actual value matches the expected +    Assert.matches(expectedValue, actualValue); +}; + +// All get* methods on the Selenium prototype result in +// store, assert, assertNot, verify, verifyNot, waitFor, and waitForNot commands. +// E.g. add a getTextLength method that returns the length of the text +// of a specified element. +// Will result in support for storeTextLength, assertTextLength, etc. +Selenium.prototype.getTextLength = function(locator) { +	return this.getText(locator).length; +}; + +// All locateElementBy* methods are added as locator-strategies. +// Eg add a "valuerepeated=" locator, that finds the first element with the supplied value, repeated. +// The "inDocument" is a the document you are searching. +PageBot.prototype.locateElementByValueRepeated = function(text, inDocument) { +    // Create the text to search for +    var expectedValue = text + text; + +    // Loop through all elements, looking for ones that have a value === our expected value +    var allElements = inDocument.getElementsByTagName("*"); +    for (var i = 0; i < allElements.length; i++) { +        var testElement = allElements[i]; +        if (testElement.value && testElement.value === expectedValue) { +            return testElement; +        } +    } +    return null; +}; + + diff --git a/tests/test_tools/selenium/core/scripts/xmlextras.js b/tests/test_tools/selenium/core/scripts/xmlextras.js new file mode 100644 index 00000000..267aa058 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/xmlextras.js @@ -0,0 +1,153 @@ +// This is a third party JavaScript library from +// http://webfx.eae.net/dhtml/xmlextras/xmlextras.html +// i.e. This has not been written by ThoughtWorks. + +//<script> +////////////////// +// Helper Stuff // +////////////////// + +// used to find the Automation server name +function getDomDocumentPrefix() { +	if (getDomDocumentPrefix.prefix) +		return getDomDocumentPrefix.prefix; +	 +	var prefixes = ["MSXML2", "Microsoft", "MSXML", "MSXML3"]; +	var o; +	for (var i = 0; i < prefixes.length; i++) { +		try { +			// try to create the objects +			o = new ActiveXObject(prefixes[i] + ".DomDocument"); +			return getDomDocumentPrefix.prefix = prefixes[i]; +		} +		catch (ex) {}; +	} +	 +	throw new Error("Could not find an installed XML parser"); +} + +function getXmlHttpPrefix() { +	if (getXmlHttpPrefix.prefix) +		return getXmlHttpPrefix.prefix; +	 +	var prefixes = ["MSXML2", "Microsoft", "MSXML", "MSXML3"]; +	var o; +	for (var i = 0; i < prefixes.length; i++) { +		try { +			// try to create the objects +			o = new ActiveXObject(prefixes[i] + ".XmlHttp"); +			return getXmlHttpPrefix.prefix = prefixes[i]; +		} +		catch (ex) {}; +	} +	 +	throw new Error("Could not find an installed XML parser"); +} + +////////////////////////// +// Start the Real stuff // +////////////////////////// + + +// XmlHttp factory +function XmlHttp() {} + +XmlHttp.create = function () { +	try { +		if (window.XMLHttpRequest) { +			var req = new XMLHttpRequest(); +			 +			// some versions of Moz do not support the readyState property +			// and the onreadystate event so we patch it! +			if (req.readyState == null) { +				req.readyState = 1; +				req.addEventListener("load", function () { +					req.readyState = 4; +					if (typeof req.onreadystatechange == "function") +						req.onreadystatechange(); +				}, false); +			} +			 +			return req; +		} +		if (window.ActiveXObject) { +			return new ActiveXObject(getXmlHttpPrefix() + ".XmlHttp"); +		} +	} +	catch (ex) {} +	// fell through +	throw new Error("Your browser does not support XmlHttp objects"); +}; + +// XmlDocument factory +function XmlDocument() {} + +XmlDocument.create = function () { +	try { +		// DOM2 +		if (document.implementation && document.implementation.createDocument) { +			var doc = document.implementation.createDocument("", "", null); +			 +			// some versions of Moz do not support the readyState property +			// and the onreadystate event so we patch it! +			if (doc.readyState == null) { +				doc.readyState = 1; +				doc.addEventListener("load", function () { +					doc.readyState = 4; +					if (typeof doc.onreadystatechange == "function") +						doc.onreadystatechange(); +				}, false); +			} +			 +			return doc; +		} +		if (window.ActiveXObject) +			return new ActiveXObject(getDomDocumentPrefix() + ".DomDocument"); +	} +	catch (ex) {} +	throw new Error("Your browser does not support XmlDocument objects"); +}; + +// Create the loadXML method and xml getter for Mozilla +if (window.DOMParser && +	window.XMLSerializer && +	window.Node && Node.prototype && Node.prototype.__defineGetter__) { + +	// XMLDocument did not extend the Document interface in some versions +	// of Mozilla. Extend both! +	//XMLDocument.prototype.loadXML =  +	Document.prototype.loadXML = function (s) { +		 +		// parse the string to a new doc	 +		var doc2 = (new DOMParser()).parseFromString(s, "text/xml"); +		 +		// remove all initial children +		while (this.hasChildNodes()) +			this.removeChild(this.lastChild); +			 +		// insert and import nodes +		for (var i = 0; i < doc2.childNodes.length; i++) { +			this.appendChild(this.importNode(doc2.childNodes[i], true)); +		} +	}; +	 +	 +	/* +	 * xml getter +	 * +	 * This serializes the DOM tree to an XML String +	 * +	 * Usage: var sXml = oNode.xml +	 * +	 */ +	// XMLDocument did not extend the Document interface in some versions +	// of Mozilla. Extend both! +	/* +	XMLDocument.prototype.__defineGetter__("xml", function () { +		return (new XMLSerializer()).serializeToString(this); +	}); +	*/ +	Document.prototype.__defineGetter__("xml", function () { +		return (new XMLSerializer()).serializeToString(this); +	}); +}
\ No newline at end of file diff --git a/tests/test_tools/selenium/core/selenium.css b/tests/test_tools/selenium/core/selenium.css new file mode 100644 index 00000000..6e9f3f30 --- /dev/null +++ b/tests/test_tools/selenium/core/selenium.css @@ -0,0 +1,211 @@ +/* + * Copyright 2005 ThoughtWorks, Inc + *  + *  Licensed under the Apache License, Version 2.0 (the "License"); + *  you may not use this file except in compliance with the License. + *  You may obtain a copy of the License at + *   + *      http://www.apache.org/licenses/LICENSE-2.0 + *   + *  Unless required by applicable law or agreed to in writing, software + *  distributed under the License is distributed on an "AS IS" BASIS, + *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + *  See the License for the specific language governing permissions and + *  limitations under the License. + */ + +/*---( Layout )---*/ + +body { +    margin: 0; +    padding: 0; +    overflow: auto; +} + +td { +    position: static; +} + +tr { +    vertical-align: top; +} + +.layout { +    width: 100%; +    height: 100%; +    border-collapse: collapse; +} + +.layout td { +    margin: 0; +    padding: 0; +    border: 0; +} + +iframe { +    width: 100%; +    height: 100%; +    border: 0; +    background: white; +    overflow: auto; +} + +/*---( Style )---*/ + +body, html { +    font-family: Verdana, Arial, sans-serif; +} + +.selenium th, .selenium td { +    border: 1px solid #999; +} + +.header { +    background: #ccc; +    padding: 0; +    font-size: 90%; +} + +#controlPanel { +    padding: 0.5ex; +    background: #eee; +    overflow: auto; +    font-size: 75%; +    text-align: center; +} + +#controlPanel fieldset { +    margin: 0.3ex; +    padding: 0.3ex; +} + +#controlPanel fieldset legend { +    color: black; +} + +#controlPanel button { +    margin: 0.5ex; +} + +#controlPanel table { +    font-size: 100%; +} + +#controlPanel th, #controlPanel td { +    border: 0; +} + +h1 { +    margin: 0.2ex; +    font-size: 130%; +    font-weight: bold; +} + +h2 { +    margin: 0.2ex; +    font-size: 80%; +    font-weight: normal; +} + +.selenium a { +    color: black; +    text-decoration: none; +} + +.selenium a:hover { +    text-decoration: underline; +} + +button, label { +    cursor: pointer; +} + +#stats { +    margin: 0.5em auto 0.5em auto; +} + +#stats th, #stats td { +    text-align: left; +    padding-left: 2px; +} + +#stats th { +    text-decoration: underline; +} + +#stats td.count { +    font-weight: bold; +    text-align: right; +} + +#testRuns { +    color: green; +} + +#testFailures { +    color: red; +} + +#commandPasses { +    color: green; +} + +#commandFailures { +    color: red; +} + +#commandErrors { +    color: #f90; +} + +.splash { +    border: 1px solid black; +    padding: 20px; +    background: #ccc; +} + +/*---( Logging Console )---*/ + +#logging-console { +    background: #fff; +    font-size: 75%; +} + +#logging-console #banner { +    display: block; +    width: 100%; +    position: fixed; +    top: 0; +    background: #ddd; +    border-bottom: 1px solid #666; +} + +#logging-console #logLevelChooser { +    float: right; +    margin: 3px; +} + +#logging-console ul { +    list-style-type: none; +    margin: 0px; +    margin-top: 3em; +    padding-left: 5px; +} + +#logging-console li { +    margin: 2px; +    border-top: 1px solid #ccc; +} + +#logging-console li.error { +    font-weight: bold; +    color: red; +} + +#logging-console li.warn { +    color: red; +} + +#logging-console li.debug { +    color: green; +} diff --git a/tests/test_tools/selenium/core/xpath/dom.js b/tests/test_tools/selenium/core/xpath/dom.js new file mode 100644 index 00000000..85e0ab08 --- /dev/null +++ b/tests/test_tools/selenium/core/xpath/dom.js @@ -0,0 +1,428 @@ +// Copyright 2005 Google Inc. +// All Rights Reserved +// +// An XML parse and a minimal DOM implementation that just supportes +// the subset of the W3C DOM that is used in the XSLT implementation. +// +// References:  +// +// [DOM] W3C DOM Level 3 Core Specification +//       <http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/>. +// +//  +// Author: Steffen Meschkat <mesch@google.com> + +// NOTE: The split() method in IE omits empty result strings. This is +// utterly annoying. So we don't use it here. + +// Resolve entities in XML text fragments. According to the DOM +// specification, the DOM is supposed to resolve entity references at +// the API level. I.e. no entity references are passed through the +// API. See "Entities and the DOM core", p.12, DOM 2 Core +// Spec. However, different browsers actually pass very different +// values at the API. +// +function xmlResolveEntities(s) { + +  var parts = stringSplit(s, '&'); + +  var ret = parts[0]; +  for (var i = 1; i < parts.length; ++i) { +    var rp = stringSplit(parts[i], ';'); +    if (rp.length == 1) { +      // no entity reference: just a & but no ; +      ret += parts[i]; +      continue; +    } +     +    var ch; +    switch (rp[0]) { +      case 'lt':  +        ch = '<'; +        break; +      case 'gt':  +        ch = '>'; +        break; +      case 'amp':  +        ch = '&'; +        break; +      case 'quot':  +        ch = '"'; +        break; +      case 'apos':  +        ch = '\''; +        break; +      case 'nbsp':  +        ch = String.fromCharCode(160); +        break; +      default: +        // Cool trick: let the DOM do the entity decoding. We assign +        // the entity text through non-W3C DOM properties and read it +        // through the W3C DOM. W3C DOM access is specified to resolve +        // entities.  +        var span = window.document.createElement('span'); +        span.innerHTML = '&' + rp[0] + '; '; +        ch = span.childNodes[0].nodeValue.charAt(0); +    } +    ret += ch + rp[1]; +  } + +  return ret; +} + + +// Parses the given XML string with our custom, JavaScript XML parser. Written +// by Steffen Meschkat (mesch@google.com). +function xmlParse(xml) { +  Timer.start('xmlparse'); +  var regex_empty = /\/$/; + +  // See also <http://www.w3.org/TR/REC-xml/#sec-common-syn> for +  // allowed chars in a tag and attribute name. TODO(mesch): the +  // following is still not completely correct. + +  var regex_tagname = /^([\w:-]*)/; +  var regex_attribute = /([\w:-]+)\s?=\s?('([^\']*)'|"([^\"]*)")/g; + +  var xmldoc = new XDocument(); +  var root = xmldoc; + +  // For the record: in Safari, we would create native DOM nodes, but +  // in Opera that is not possible, because the DOM only allows HTML +  // element nodes to be created, so we have to do our own DOM nodes. + +  // xmldoc = document.implementation.createDocument('','',null); +  // root = xmldoc; // .createDocumentFragment(); +  // NOTE(mesch): using the DocumentFragment instead of the Document +  // crashes my Safari 1.2.4 (v125.12). +  var stack = []; + +  var parent = root; +  stack.push(parent); + +  var x = stringSplit(xml, '<'); +  for (var i = 1; i < x.length; ++i) { +    var xx = stringSplit(x[i], '>'); +    var tag = xx[0]; +    var text = xmlResolveEntities(xx[1] || ''); + +    if (tag.charAt(0) == '/') { +      stack.pop(); +      parent = stack[stack.length-1]; + +    } else if (tag.charAt(0) == '?') { +      // Ignore XML declaration and processing instructions +    } else if (tag.charAt(0) == '!') { +      // Ignore notation and comments +    } else { +      var empty = tag.match(regex_empty); +      var tagname = regex_tagname.exec(tag)[1]; +      var node = xmldoc.createElement(tagname); + +      var att; +      while (att = regex_attribute.exec(tag)) { +        var val = xmlResolveEntities(att[3] || att[4] || ''); +        node.setAttribute(att[1], val); +      } +       +      if (empty) { +        parent.appendChild(node); +      } else { +        parent.appendChild(node); +        parent = node; +        stack.push(node); +      } +    } + +    if (text && parent != root) { +      parent.appendChild(xmldoc.createTextNode(text)); +    } +  } + +  Timer.end('xmlparse'); +  return root; +} + + +// Our W3C DOM Node implementation. Note we call it XNode because we +// can't define the identifier Node. We do this mostly for Opera, +// where we can't reuse the HTML DOM for parsing our own XML, and for +// Safari, where it is too expensive to have the template processor +// operate on native DOM nodes. +function XNode(type, name, value, owner) { +  this.attributes = []; +  this.childNodes = []; + +  XNode.init.call(this, type, name, value, owner); +} + +// Don't call as method, use apply() or call(). +XNode.init = function(type, name, value, owner) { +  this.nodeType = type - 0; +  this.nodeName = '' + name; +  this.nodeValue = '' + value; +  this.ownerDocument = owner; + +  this.firstChild = null; +  this.lastChild = null; +  this.nextSibling = null; +  this.previousSibling = null; +  this.parentNode = null; +} + +XNode.unused_ = []; + +XNode.recycle = function(node) { +  if (!node) { +    return; +  } + +  if (node.constructor == XDocument) { +    XNode.recycle(node.documentElement); +    return; +  } + +  if (node.constructor != this) { +    return; +  } + +  XNode.unused_.push(node); +  for (var a = 0; a < node.attributes.length; ++a) { +    XNode.recycle(node.attributes[a]); +  } +  for (var c = 0; c < node.childNodes.length; ++c) { +    XNode.recycle(node.childNodes[c]); +  } +  node.attributes.length = 0; +  node.childNodes.length = 0; +  XNode.init.call(node, 0, '', '', null); +} + +XNode.create = function(type, name, value, owner) { +  if (XNode.unused_.length > 0) { +    var node = XNode.unused_.pop(); +    XNode.init.call(node, type, name, value, owner); +    return node; +  } else { +    return new XNode(type, name, value, owner); +  } +} + +XNode.prototype.appendChild = function(node) { +  // firstChild +  if (this.childNodes.length == 0) { +    this.firstChild = node; +  } + +  // previousSibling +  node.previousSibling = this.lastChild; + +  // nextSibling +  node.nextSibling = null; +  if (this.lastChild) { +    this.lastChild.nextSibling = node; +  } + +  // parentNode +  node.parentNode = this; + +  // lastChild +  this.lastChild = node; + +  // childNodes +  this.childNodes.push(node); +} + + +XNode.prototype.replaceChild = function(newNode, oldNode) { +  if (oldNode == newNode) { +    return; +  } + +  for (var i = 0; i < this.childNodes.length; ++i) { +    if (this.childNodes[i] == oldNode) { +      this.childNodes[i] = newNode; +       +      var p = oldNode.parentNode; +      oldNode.parentNode = null; +      newNode.parentNode = p; +       +      p = oldNode.previousSibling; +      oldNode.previousSibling = null; +      newNode.previousSibling = p; +      if (newNode.previousSibling) { +        newNode.previousSibling.nextSibling = newNode; +      } +       +      p = oldNode.nextSibling; +      oldNode.nextSibling = null; +      newNode.nextSibling = p; +      if (newNode.nextSibling) { +        newNode.nextSibling.previousSibling = newNode; +      } + +      if (this.firstChild == oldNode) { +        this.firstChild = newNode; +      } + +      if (this.lastChild == oldNode) { +        this.lastChild = newNode; +      } + +      break; +    } +  } +} + +XNode.prototype.insertBefore = function(newNode, oldNode) { +  if (oldNode == newNode) { +    return; +  } + +  if (oldNode.parentNode != this) { +    return; +  } + +  if (newNode.parentNode) { +    newNode.parentNode.removeChild(newNode); +  } + +  var newChildren = []; +  for (var i = 0; i < this.childNodes.length; ++i) { +    var c = this.childNodes[i]; +    if (c == oldNode) { +      newChildren.push(newNode); + +      newNode.parentNode = this; + +      newNode.previousSibling = oldNode.previousSibling; +      oldNode.previousSibling = newNode; +      if (newNode.previousSibling) { +        newNode.previousSibling.nextSibling = newNode; +      } +       +      newNode.nextSibling = oldNode; + +      if (this.firstChild == oldNode) { +        this.firstChild = newNode; +      } +    } +    newChildren.push(c); +  } +  this.childNodes = newChildren; +} + +XNode.prototype.removeChild = function(node) { +  var newChildren = []; +  for (var i = 0; i < this.childNodes.length; ++i) { +    var c = this.childNodes[i]; +    if (c != node) { +      newChildren.push(c); +    } else { +      if (c.previousSibling) { +        c.previousSibling.nextSibling = c.nextSibling; +      } +      if (c.nextSibling) { +        c.nextSibling.previousSibling = c.previousSibling; +      } +      if (this.firstChild == c) { +        this.firstChild = c.nextSibling; +      } +      if (this.lastChild == c) { +        this.lastChild = c.previousSibling; +      } +    } +  } +  this.childNodes = newChildren; +} + + +XNode.prototype.hasAttributes = function() { +  return this.attributes.length > 0; +} + + +XNode.prototype.setAttribute = function(name, value) { +  for (var i = 0; i < this.attributes.length; ++i) { +    if (this.attributes[i].nodeName == name) { +      this.attributes[i].nodeValue = '' + value; +      return; +    } +  } +  this.attributes.push(new XNode(DOM_ATTRIBUTE_NODE, name, value)); +} + + +XNode.prototype.getAttribute = function(name) { +  for (var i = 0; i < this.attributes.length; ++i) { +    if (this.attributes[i].nodeName == name) { +      return this.attributes[i].nodeValue; +    } +  } +  return null; +} + +XNode.prototype.removeAttribute = function(name) { +  var a = []; +  for (var i = 0; i < this.attributes.length; ++i) { +    if (this.attributes[i].nodeName != name) { +      a.push(this.attributes[i]); +    } +  } +  this.attributes = a; +} + + +function XDocument() { +  XNode.call(this, DOM_DOCUMENT_NODE, '#document', null, this); +  this.documentElement = null; +} + +XDocument.prototype = new XNode(DOM_DOCUMENT_NODE, '#document'); + +XDocument.prototype.clear = function() { +  XNode.recycle(this.documentElement); +  this.documentElement = null; +} + +XDocument.prototype.appendChild = function(node) { +  XNode.prototype.appendChild.call(this, node); +  this.documentElement = this.childNodes[0]; +} + +XDocument.prototype.createElement = function(name) { +  return XNode.create(DOM_ELEMENT_NODE, name, null, this); +} + +XDocument.prototype.createDocumentFragment = function() { +  return XNode.create(DOM_DOCUMENT_FRAGMENT_NODE, '#document-fragment', +                    null, this); +} + +XDocument.prototype.createTextNode = function(value) { +  return XNode.create(DOM_TEXT_NODE, '#text', value, this); +} + +XDocument.prototype.createAttribute = function(name) { +  return XNode.create(DOM_ATTRIBUTE_NODE, name, null, this); +} + +XDocument.prototype.createComment = function(data) { +  return XNode.create(DOM_COMMENT_NODE, '#comment', data, this); +} + +XNode.prototype.getElementsByTagName = function(name, list) { +  if (!list) { +    list = []; +  } + +  if (this.nodeName == name) { +    list.push(this); +  } + +  for (var i = 0; i < this.childNodes.length; ++i) { +    this.childNodes[i].getElementsByTagName(name, list); +  } + +  return list; +} diff --git a/tests/test_tools/selenium/core/xpath/misc.js b/tests/test_tools/selenium/core/xpath/misc.js new file mode 100644 index 00000000..b9573a22 --- /dev/null +++ b/tests/test_tools/selenium/core/xpath/misc.js @@ -0,0 +1,255 @@ +// Copyright 2005 Google Inc. +// All Rights Reserved +// +// Miscellania that support the ajaxslt implementation. +// +// Author: Steffen Meschkat <mesch@google.com> +// + +function el(i) { +  return document.getElementById(i); +} + +function px(x) { +  return x + 'px'; +} + +// Split a string s at all occurrences of character c. This is like +// the split() method of the string object, but IE omits empty +// strings, which violates the invariant (s.split(x).join(x) == s). +function stringSplit(s, c) { +  var a = s.indexOf(c); +  if (a == -1) { +    return [ s ]; +  } +   +  var parts = []; +  parts.push(s.substr(0,a)); +  while (a != -1) { +    var a1 = s.indexOf(c, a + 1); +    if (a1 != -1) { +      parts.push(s.substr(a + 1, a1 - a - 1)); +    } else { +      parts.push(s.substr(a + 1)); +    }  +    a = a1; +  } + +  return parts; +} + +// Returns the text value if a node; for nodes without children this +// is the nodeValue, for nodes with children this is the concatenation +// of the value of all children. +function xmlValue(node) { +  if (!node) { +    return ''; +  } + +  var ret = ''; +  if (node.nodeType == DOM_TEXT_NODE || +      node.nodeType == DOM_CDATA_SECTION_NODE || +      node.nodeType == DOM_ATTRIBUTE_NODE) { +    ret += node.nodeValue; + +  } else if (node.nodeType == DOM_ELEMENT_NODE || +             node.nodeType == DOM_DOCUMENT_NODE || +             node.nodeType == DOM_DOCUMENT_FRAGMENT_NODE) { +    for (var i = 0; i < node.childNodes.length; ++i) { +      ret += arguments.callee(node.childNodes[i]); +    } +  } +  return ret; +} + +// Returns the representation of a node as XML text. +function xmlText(node) { +  var ret = ''; +  if (node.nodeType == DOM_TEXT_NODE) { +    ret += xmlEscapeText(node.nodeValue); +     +  } else if (node.nodeType == DOM_ELEMENT_NODE) { +    ret += '<' + node.nodeName; +    for (var i = 0; i < node.attributes.length; ++i) { +      var a = node.attributes[i]; +      if (a && a.nodeName && a.nodeValue) { +        ret += ' ' + a.nodeName; +        ret += '="' + xmlEscapeAttr(a.nodeValue) + '"'; +      } +    } + +    if (node.childNodes.length == 0) { +      ret += '/>'; + +    } else { +      ret += '>'; +      for (var i = 0; i < node.childNodes.length; ++i) { +        ret += arguments.callee(node.childNodes[i]); +      } +      ret += '</' + node.nodeName + '>'; +    } +     +  } else if (node.nodeType == DOM_DOCUMENT_NODE ||  +             node.nodeType == DOM_DOCUMENT_FRAGMENT_NODE) { +    for (var i = 0; i < node.childNodes.length; ++i) { +      ret += arguments.callee(node.childNodes[i]); +    } +  } +   +  return ret; +} + +// Applies the given function to each element of the array. +function mapExec(array, func) { +  for (var i = 0; i < array.length; ++i) { +    func(array[i]); +  } +} + +// Returns an array that contains the return value of the given +// function applied to every element of the input array. +function mapExpr(array, func) { +  var ret = []; +  for (var i = 0; i < array.length; ++i) { +    ret.push(func(array[i])); +  } +  return ret; +}; + +// Reverses the given array in place. +function reverseInplace(array) { +  for (var i = 0; i < array.length / 2; ++i) { +    var h = array[i]; +    var ii = array.length - i - 1; +    array[i] = array[ii]; +    array[ii] = h; +  } +} + +// Shallow-copies an array. +function copyArray(dst, src) {  +  for (var i = 0; i < src.length; ++i) { +    dst.push(src[i]); +  } +} + +function assert(b) { +  if (!b) { +    throw 'assertion failed'; +  } +} + +// Based on +// <http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247> +var DOM_ELEMENT_NODE = 1; +var DOM_ATTRIBUTE_NODE = 2; +var DOM_TEXT_NODE = 3; +var DOM_CDATA_SECTION_NODE = 4; +var DOM_ENTITY_REFERENCE_NODE = 5; +var DOM_ENTITY_NODE = 6; +var DOM_PROCESSING_INSTRUCTION_NODE = 7; +var DOM_COMMENT_NODE = 8; +var DOM_DOCUMENT_NODE = 9; +var DOM_DOCUMENT_TYPE_NODE = 10; +var DOM_DOCUMENT_FRAGMENT_NODE = 11; +var DOM_NOTATION_NODE = 12; + + +var xpathdebug = false; // trace xpath parsing +var xsltdebug = false; // trace xslt processing + + +// Escape XML special markup chracters: tag delimiter < > and entity +// reference start delimiter &. The escaped string can be used in XML +// text portions (i.e. between tags). +function xmlEscapeText(s) { +  return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); +} + +// Escape XML special markup characters: tag delimiter < > entity +// reference start delimiter & and quotes ". The escaped string can be +// used in double quoted XML attribute value portions (i.e. in +// attributes within start tags). +function xmlEscapeAttr(s) { +  return xmlEscapeText(s).replace(/\"/g, '"'); +} + +// Escape markup in XML text, but don't touch entity references. The +// escaped string can be used as XML text (i.e. between tags). +function xmlEscapeTags(s) { +  return s.replace(/</g, '<').replace(/>/g, '>'); +} + +// An implementation of the debug log.  + +var logging__ = false; + +function Log() {}; + +Log.lines = []; + +Log.write = function(s) { +  if (logging__) { +    this.lines.push(xmlEscapeText(s)); +    this.show(); +  } +}; + +// Writes the given XML with every tag on a new line. +Log.writeXML = function(xml) { +  if (logging__) { +    var s0 = xml.replace(/</g, '\n<'); +    var s1 = xmlEscapeText(s0); +    var s2 = s1.replace(/\s*\n(\s|\n)*/g, '<br/>'); +    this.lines.push(s2); +    this.show(); +  } +} + +// Writes without any escaping +Log.writeRaw = function(s) { +  if (logging__) { +    this.lines.push(s); +    this.show(); +  } +} + +Log.clear = function() { +  if (logging__) { +    var l = this.div(); +    l.innerHTML = ''; +    this.lines = []; +  } +} + +Log.show = function() { +  var l = this.div(); +  l.innerHTML += this.lines.join('<br/>') + '<br/>'; +  this.lines = []; +  l.scrollTop = l.scrollHeight; +} + +Log.div = function() { +  var l = document.getElementById('log'); +  if (!l) { +    l = document.createElement('div'); +    l.id = 'log'; +    l.style.position = 'absolute'; +    l.style.right = '5px'; +    l.style.top = '5px'; +    l.style.width = '250px'; +    l.style.height = '150px'; +    l.style.overflow = 'auto'; +    l.style.backgroundColor = '#f0f0f0'; +    l.style.border = '1px solid gray'; +    l.style.fontSize = '10px'; +    l.style.padding = '5px'; +    document.body.appendChild(l); +  } +  return l; +} + + +function Timer() {} +Timer.start = function() {} +Timer.end = function() {} diff --git a/tests/test_tools/selenium/core/xpath/xpath.js b/tests/test_tools/selenium/core/xpath/xpath.js new file mode 100644 index 00000000..b93469f2 --- /dev/null +++ b/tests/test_tools/selenium/core/xpath/xpath.js @@ -0,0 +1,2182 @@ +// Copyright 2005 Google Inc. +// All Rights Reserved +// +// An XPath parser and evaluator written in JavaScript. The +// implementation is complete except for functions handling +// namespaces. +// +// Reference: [XPATH] XPath Specification +// <http://www.w3.org/TR/1999/REC-xpath-19991116>. +// +// +// The API of the parser has several parts: +// +// 1. The parser function xpathParse() that takes a string and returns +// an expession object. +// +// 2. The expression object that has an evaluate() method to evaluate the +// XPath expression it represents. (It is actually a hierarchy of +// objects that resembles the parse tree, but an application will call +// evaluate() only on the top node of this hierarchy.) +// +// 3. The context object that is passed as an argument to the evaluate() +// method, which represents the DOM context in which the expression is +// evaluated. +// +// 4. The value object that is returned from evaluate() and represents +// values of the different types that are defined by XPath (number, +// string, boolean, and node-set), and allows to convert between them. +// +// These parts are near the top of the file, the functions and data +// that are used internally follow after them. +// +// +// TODO(mesch): add jsdoc comments. Use more coherent naming. +// +// +// Author: Steffen Meschkat <mesch@google.com> + + +// The entry point for the parser. +// +// @param expr a string that contains an XPath expression. +// @return an expression object that can be evaluated with an +// expression context. + +function xpathParse(expr) { +  if (xpathdebug) { +    Log.write('XPath parse ' + expr); +  } +  xpathParseInit(); + +  var cached = xpathCacheLookup(expr); +  if (cached) { +    if (xpathdebug) { +      Log.write(' ... cached'); +    } +    return cached; +  } + +  // Optimize for a few common cases: simple attribute node tests +  // (@id), simple element node tests (page), variable references +  // ($address), numbers (4), multi-step path expressions where each +  // step is a plain element node test +  // (page/overlay/locations/location). +   +  if (expr.match(/^(\$|@)?\w+$/i)) { +    var ret = makeSimpleExpr(expr); +    xpathParseCache[expr] = ret; +    if (xpathdebug) { +      Log.write(' ... simple'); +    } +    return ret; +  } + +  if (expr.match(/^\w+(\/\w+)*$/i)) { +    var ret = makeSimpleExpr2(expr); +    xpathParseCache[expr] = ret; +    if (xpathdebug) { +      Log.write(' ... simple 2'); +    } +    return ret; +  } + +  var cachekey = expr; // expr is modified during parse +  if (xpathdebug) { +    Timer.start('XPath parse', cachekey); +  } + +  var stack = []; +  var ahead = null; +  var previous = null; +  var done = false; + +  var parse_count = 0; +  var lexer_count = 0; +  var reduce_count = 0; +   +  while (!done) { +    parse_count++; +    expr = expr.replace(/^\s*/, ''); +    previous = ahead; +    ahead = null; + +    var rule = null; +    var match = ''; +    for (var i = 0; i < xpathTokenRules.length; ++i) { +      var result = xpathTokenRules[i].re.exec(expr); +      lexer_count++; +      if (result && result.length > 0 && result[0].length > match.length) { +        rule = xpathTokenRules[i]; +        match = result[0]; +        break; +      } +    } + +    // Special case: allow operator keywords to be element and +    // variable names. + +    // NOTE(mesch): The parser resolves conflicts by looking ahead, +    // and this is the only case where we look back to +    // disambiguate. So this is indeed something different, and +    // looking back is usually done in the lexer (via states in the +    // general case, called "start conditions" in flex(1)). Also,the +    // conflict resolution in the parser is not as robust as it could +    // be, so I'd like to keep as much off the parser as possible (all +    // these precedence values should be computed from the grammar +    // rules and possibly associativity declarations, as in bison(1), +    // and not explicitly set. + +    if (rule && +        (rule == TOK_DIV ||  +         rule == TOK_MOD || +         rule == TOK_AND ||  +         rule == TOK_OR) && +        (!previous ||  +         previous.tag == TOK_AT ||  +         previous.tag == TOK_DSLASH ||  +         previous.tag == TOK_SLASH || +         previous.tag == TOK_AXIS ||  +         previous.tag == TOK_DOLLAR)) { +      rule = TOK_QNAME; +    } + +    if (rule) { +      expr = expr.substr(match.length); +      if (xpathdebug) { +        Log.write('token: ' + match + ' -- ' + rule.label); +      } +      ahead = { +        tag: rule, +        match: match, +        prec: rule.prec ?  rule.prec : 0, // || 0 is removed by the compiler +        expr: makeTokenExpr(match) +      }; + +    } else { +      if (xpathdebug) { +        Log.write('DONE'); +      } +      done = true; +    } + +    while (xpathReduce(stack, ahead)) { +      reduce_count++; +      if (xpathdebug) { +        Log.write('stack: ' + stackToString(stack)); +      } +    } +  } + +  if (xpathdebug) { +    Log.write(stackToString(stack)); +  } + +  if (stack.length != 1) { +    throw 'XPath parse error ' + cachekey + ':\n' + stackToString(stack); +  } + +  var result = stack[0].expr; +  xpathParseCache[cachekey] = result; + +  if (xpathdebug) { +    Timer.end('XPath parse', cachekey); +  } + +  if (xpathdebug) { +    Log.write('XPath parse: ' + parse_count + ' / ' +  +              lexer_count + ' / ' + reduce_count); +  } + +  return result; +} + +var xpathParseCache = {}; + +function xpathCacheLookup(expr) { +  return xpathParseCache[expr]; +} + +function xpathReduce(stack, ahead) { +  var cand = null; + +  if (stack.length > 0) { +    var top = stack[stack.length-1]; +    var ruleset = xpathRules[top.tag.key]; + +    if (ruleset) { +      for (var i = 0; i < ruleset.length; ++i) { +        var rule = ruleset[i]; +        var match = xpathMatchStack(stack, rule[1]); +        if (match.length) { +          cand = { +            tag: rule[0], +            rule: rule, +            match: match +          }; +          cand.prec = xpathGrammarPrecedence(cand); +          break; +        } +      } +    } +  } + +  var ret; +  if (cand && (!ahead || cand.prec > ahead.prec ||  +               (ahead.tag.left && cand.prec >= ahead.prec))) { +    for (var i = 0; i < cand.match.matchlength; ++i) { +      stack.pop(); +    } + +    if (xpathdebug) { +      Log.write('reduce ' + cand.tag.label + ' ' + cand.prec + +                ' ahead ' + (ahead ? ahead.tag.label + ' ' + ahead.prec +  +                             (ahead.tag.left ? ' left' : '') +                             : ' none ')); +    } + +    var matchexpr = mapExpr(cand.match, function(m) { return m.expr; }); +    cand.expr = cand.rule[3].apply(null, matchexpr); + +    stack.push(cand); +    ret = true; + +  } else { +    if (ahead) { +      if (xpathdebug) { +        Log.write('shift ' + ahead.tag.label + ' ' + ahead.prec +  +                  (ahead.tag.left ? ' left' : '') + +                  ' over ' + (cand ? cand.tag.label + ' ' +  +                              cand.prec : ' none')); +      } +      stack.push(ahead); +    } +    ret = false; +  } +  return ret; +} + +function xpathMatchStack(stack, pattern) { + +  // NOTE(mesch): The stack matches for variable cardinality are +  // greedy but don't do backtracking. This would be an issue only +  // with rules of the form A* A, i.e. with an element with variable +  // cardinality followed by the same element. Since that doesn't +  // occur in the grammar at hand, all matches on the stack are +  // unambiguous. + +  var S = stack.length; +  var P = pattern.length; +  var p, s; +  var match = []; +  match.matchlength = 0; +  var ds = 0; +  for (p = P - 1, s = S - 1; p >= 0 && s >= 0; --p, s -= ds) { +    ds = 0; +    var qmatch = []; +    if (pattern[p] == Q_MM) { +      p -= 1; +      match.push(qmatch); +      while (s - ds >= 0 && stack[s - ds].tag == pattern[p]) { +        qmatch.push(stack[s - ds]); +        ds += 1; +        match.matchlength += 1; +      } + +    } else if (pattern[p] == Q_01) { +      p -= 1; +      match.push(qmatch); +      while (s - ds >= 0 && ds < 2 && stack[s - ds].tag == pattern[p]) { +        qmatch.push(stack[s - ds]); +        ds += 1; +        match.matchlength += 1; +      } + +    } else if (pattern[p] == Q_1M) { +      p -= 1; +      match.push(qmatch); +      if (stack[s].tag == pattern[p]) { +        while (s - ds >= 0 && stack[s - ds].tag == pattern[p]) { +          qmatch.push(stack[s - ds]); +          ds += 1; +          match.matchlength += 1; +        } +      } else { +        return []; +      } + +    } else if (stack[s].tag == pattern[p]) { +      match.push(stack[s]); +      ds += 1; +      match.matchlength += 1; + +    } else { +      return []; +    } + +    reverseInplace(qmatch); +    qmatch.expr = mapExpr(qmatch, function(m) { return m.expr; }); +  } + +  reverseInplace(match); + +  if (p == -1) { +    return match; + +  } else { +    return []; +  } +} + +function xpathTokenPrecedence(tag) { +  return tag.prec || 2; +} + +function xpathGrammarPrecedence(frame) { +  var ret = 0; + +  if (frame.rule) { /* normal reduce */ +    if (frame.rule.length >= 3 && frame.rule[2] >= 0) { +      ret = frame.rule[2]; + +    } else { +      for (var i = 0; i < frame.rule[1].length; ++i) { +        var p = xpathTokenPrecedence(frame.rule[1][i]); +        ret = Math.max(ret, p); +      } +    } +  } else if (frame.tag) { /* TOKEN match */ +    ret = xpathTokenPrecedence(frame.tag); + +  } else if (frame.length) { /* Q_ match */ +    for (var j = 0; j < frame.length; ++j) { +      var p = xpathGrammarPrecedence(frame[j]); +      ret = Math.max(ret, p); +    } +  } + +  return ret; +} + +function stackToString(stack) { +  var ret = ''; +  for (var i = 0; i < stack.length; ++i) { +    if (ret) { +      ret += '\n'; +    } +    ret += stack[i].tag.label; +  } +  return ret; +} + + +// XPath expression evaluation context. An XPath context consists of a +// DOM node, a list of DOM nodes that contains this node, a number +// that represents the position of the single node in the list, and a +// current set of variable bindings. (See XPath spec.) +// +// The interface of the expression context: +// +//   Constructor -- gets the node, its position, the node set it +//   belongs to, and a parent context as arguments. The parent context +//   is used to implement scoping rules for variables: if a variable +//   is not found in the current context, it is looked for in the +//   parent context, recursively. Except for node, all arguments have +//   default values: default position is 0, default node set is the +//   set that contains only the node, and the default parent is null. +// +//     Notice that position starts at 0 at the outside interface; +//     inside XPath expressions this shows up as position()=1. +// +//   clone() -- creates a new context with the current context as +//   parent. If passed as argument to clone(), the new context has a +//   different node, position, or node set. What is not passed is +//   inherited from the cloned context. +// +//   setVariable(name, expr) -- binds given XPath expression to the +//   name. +// +//   getVariable(name) -- what the name says. +// +//   setNode(node, position) -- sets the context to the new node and +//   its corresponding position. Needed to implement scoping rules for +//   variables in XPath. (A variable is visible to all subsequent +//   siblings, not only to its children.) + +function ExprContext(node, position, nodelist, parent) { +  this.node = node; +  this.position = position || 0; +  this.nodelist = nodelist || [ node ]; +  this.variables = {}; +  this.parent = parent || null; +  this.root = parent ? parent.root : node.ownerDocument; +} + +ExprContext.prototype.clone = function(node, position, nodelist) { +  return new +  ExprContext(node || this.node, +              typeof position != 'undefined' ? position : this.position, +              nodelist || this.nodelist, this); +}; + +ExprContext.prototype.setVariable = function(name, value) { +  this.variables[name] = value; +}; + +ExprContext.prototype.getVariable = function(name) { +  if (typeof this.variables[name] != 'undefined') { +    return this.variables[name]; + +  } else if (this.parent) { +    return this.parent.getVariable(name); + +  } else { +    return null; +  } +} + +ExprContext.prototype.setNode = function(node, position) { +  this.node = node; +  this.position = position; +} + + +// XPath expression values. They are what XPath expressions evaluate +// to. Strangely, the different value types are not specified in the +// XPath syntax, but only in the semantics, so they don't show up as +// nonterminals in the grammar. Yet, some expressions are required to +// evaluate to particular types, and not every type can be coerced +// into every other type. Although the types of XPath values are +// similar to the types present in JavaScript, the type coercion rules +// are a bit peculiar, so we explicitly model XPath types instead of +// mapping them onto JavaScript types. (See XPath spec.) +// +// The four types are: +// +//   StringValue +// +//   NumberValue +// +//   BooleanValue +// +//   NodeSetValue +// +// The common interface of the value classes consists of methods that +// implement the XPath type coercion rules: +// +//   stringValue() -- returns the value as a JavaScript String, +// +//   numberValue() -- returns the value as a JavaScript Number, +// +//   booleanValue() -- returns the value as a JavaScript Boolean, +// +//   nodeSetValue() -- returns the value as a JavaScript Array of DOM +//   Node objects. +// + +function StringValue(value) { +  this.value = value; +  this.type = 'string'; +} + +StringValue.prototype.stringValue = function() { +  return this.value; +} + +StringValue.prototype.booleanValue = function() { +  return this.value.length > 0; +} + +StringValue.prototype.numberValue = function() { +  return this.value - 0; +} + +StringValue.prototype.nodeSetValue = function() { +  throw this + ' ' + Error().stack; +} + +function BooleanValue(value) { +  this.value = value; +  this.type = 'boolean'; +} + +BooleanValue.prototype.stringValue = function() { +  return '' + this.value; +} + +BooleanValue.prototype.booleanValue = function() { +  return this.value; +} + +BooleanValue.prototype.numberValue = function() { +  return this.value ? 1 : 0; +} + +BooleanValue.prototype.nodeSetValue = function() { +  throw this + ' ' + Error().stack; +} + +function NumberValue(value) { +  this.value = value; +  this.type = 'number'; +} + +NumberValue.prototype.stringValue = function() { +  return '' + this.value; +} + +NumberValue.prototype.booleanValue = function() { +  return !!this.value; +} + +NumberValue.prototype.numberValue = function() { +  return this.value - 0; +} + +NumberValue.prototype.nodeSetValue = function() { +  throw this + ' ' + Error().stack; +} + +function NodeSetValue(value) { +  this.value = value; +  this.type = 'node-set'; +} + +NodeSetValue.prototype.stringValue = function() { +  if (this.value.length == 0) { +    return ''; +  } else { +    return xmlValue(this.value[0]); +  } +} + +NodeSetValue.prototype.booleanValue = function() { +  return this.value.length > 0; +} + +NodeSetValue.prototype.numberValue = function() { +  return this.stringValue() - 0; +} + +NodeSetValue.prototype.nodeSetValue = function() { +  return this.value; +}; + +// XPath expressions. They are used as nodes in the parse tree and +// possess an evaluate() method to compute an XPath value given an XPath +// context. Expressions are returned from the parser. Teh set of +// expression classes closely mirrors the set of non terminal symbols +// in the grammar. Every non trivial nonterminal symbol has a +// corresponding expression class. +// +// The common expression interface consists of the following methods: +// +// evaluate(context) -- evaluates the expression, returns a value. +// +// toString() -- returns the XPath text representation of the +// expression (defined in xsltdebug.js). +// +// parseTree(indent) -- returns a parse tree representation of the +// expression (defined in xsltdebug.js). + +function TokenExpr(m) { +  this.value = m; +} + +TokenExpr.prototype.evaluate = function() { +  return new StringValue(this.value); +}; + +function LocationExpr() { +  this.absolute = false; +  this.steps = []; +} + +LocationExpr.prototype.appendStep = function(s) { +  this.steps.push(s); +} + +LocationExpr.prototype.prependStep = function(s) { +  var steps0 = this.steps; +  this.steps = [ s ]; +  for (var i = 0; i < steps0.length; ++i) { +    this.steps.push(steps0[i]); +  } +}; + +LocationExpr.prototype.evaluate = function(ctx) { +  var start; +  if (this.absolute) { +    start = ctx.root; + +  } else { +    start = ctx.node; +  } + +  var nodes = []; +  xPathStep(nodes, this.steps, 0, start, ctx); +  return new NodeSetValue(nodes); +}; + +function xPathStep(nodes, steps, step, input, ctx) { +  var s = steps[step]; +  var ctx2 = ctx.clone(input); +  var nodelist = s.evaluate(ctx2).nodeSetValue(); + +  for (var i = 0; i < nodelist.length; ++i) { +    if (step == steps.length - 1) { +      nodes.push(nodelist[i]); +    } else { +      xPathStep(nodes, steps, step + 1, nodelist[i], ctx); +    } +  } +} + +function StepExpr(axis, nodetest, predicate) { +  this.axis = axis; +  this.nodetest = nodetest; +  this.predicate = predicate || []; +} + +StepExpr.prototype.appendPredicate = function(p) { +  this.predicate.push(p); +} + +StepExpr.prototype.evaluate = function(ctx) { +  var input = ctx.node; +  var nodelist = []; + +  // NOTE(mesch): When this was a switch() statement, it didn't work +  // in Safari/2.0. Not sure why though; it resulted in the JavaScript +  // console output "undefined" (without any line number or so). + +  if (this.axis ==  xpathAxis.ANCESTOR_OR_SELF) { +    nodelist.push(input); +    for (var n = input.parentNode; n; n = input.parentNode) { +      nodelist.push(n); +    } + +  } else if (this.axis == xpathAxis.ANCESTOR) { +    for (var n = input.parentNode; n; n = input.parentNode) { +      nodelist.push(n); +    } + +  } else if (this.axis == xpathAxis.ATTRIBUTE) { +    copyArray(nodelist, input.attributes); + +  } else if (this.axis == xpathAxis.CHILD) { +    copyArray(nodelist, input.childNodes); + +  } else if (this.axis == xpathAxis.DESCENDANT_OR_SELF) { +    nodelist.push(input); +    xpathCollectDescendants(nodelist, input); + +  } else if (this.axis == xpathAxis.DESCENDANT) { +    xpathCollectDescendants(nodelist, input); + +  } else if (this.axis == xpathAxis.FOLLOWING) { +    for (var n = input.parentNode; n; n = n.parentNode) { +      for (var nn = n.nextSibling; nn; nn = nn.nextSibling) { +        nodelist.push(nn); +        xpathCollectDescendants(nodelist, nn); +      } +    } + +  } else if (this.axis == xpathAxis.FOLLOWING_SIBLING) { +    for (var n = input.nextSibling; n; n = input.nextSibling) { +      nodelist.push(n); +    } + +  } else if (this.axis == xpathAxis.NAMESPACE) { +    alert('not implemented: axis namespace'); + +  } else if (this.axis == xpathAxis.PARENT) { +    if (input.parentNode) { +      nodelist.push(input.parentNode); +    } + +  } else if (this.axis == xpathAxis.PRECEDING) { +    for (var n = input.parentNode; n; n = n.parentNode) { +      for (var nn = n.previousSibling; nn; nn = nn.previousSibling) { +        nodelist.push(nn); +        xpathCollectDescendantsReverse(nodelist, nn); +      } +    } + +  } else if (this.axis == xpathAxis.PRECEDING_SIBLING) { +    for (var n = input.previousSibling; n; n = input.previousSibling) { +      nodelist.push(n); +    } + +  } else if (this.axis == xpathAxis.SELF) { +    nodelist.push(input); + +  } else { +    throw 'ERROR -- NO SUCH AXIS: ' + this.axis; +  } + +  // process node test +  var nodelist0 = nodelist; +  nodelist = []; +  for (var i = 0; i < nodelist0.length; ++i) { +    var n = nodelist0[i]; +    if (this.nodetest.evaluate(ctx.clone(n, i, nodelist0)).booleanValue()) { +      nodelist.push(n); +    } +  } + +  // process predicates +  for (var i = 0; i < this.predicate.length; ++i) { +    var nodelist0 = nodelist; +    nodelist = []; +    for (var ii = 0; ii < nodelist0.length; ++ii) { +      var n = nodelist0[ii]; +      if (this.predicate[i].evaluate(ctx.clone(n, ii, nodelist0)).booleanValue()) { +        nodelist.push(n); +      } +    } +  } + +  return new NodeSetValue(nodelist); +}; + +function NodeTestAny() { +  this.value = new BooleanValue(true); +} + +NodeTestAny.prototype.evaluate = function(ctx) { +  return this.value; +}; + +function NodeTestElement() {} + +NodeTestElement.prototype.evaluate = function(ctx) { +  return new BooleanValue(ctx.node.nodeType == DOM_ELEMENT_NODE); +} + +function NodeTestText() {} + +NodeTestText.prototype.evaluate = function(ctx) { +  return new BooleanValue(ctx.node.nodeType == DOM_TEXT_NODE); +} + +function NodeTestComment() {} + +NodeTestComment.prototype.evaluate = function(ctx) { +  return new BooleanValue(ctx.node.nodeType == DOM_COMMENT_NODE); +} + +function NodeTestPI(target) { +  this.target = target; +} + +NodeTestPI.prototype.evaluate = function(ctx) { +  return new +  BooleanValue(ctx.node.nodeType == DOM_PROCESSING_INSTRUCTION_NODE && +               (!this.target || ctx.node.nodeName == this.target)); +} + +function NodeTestNC(nsprefix) { +  this.regex = new RegExp("^" + nsprefix + ":"); +  this.nsprefix = nsprefix; +} + +NodeTestNC.prototype.evaluate = function(ctx) { +  var n = ctx.node; +  return new BooleanValue(this.regex.match(n.nodeName)); +} + +function NodeTestName(name) { +  this.name = name; +} + +NodeTestName.prototype.evaluate = function(ctx) { +  var n = ctx.node; +  // NOTE (Patrick Lightbody): this change allows node selection to be case-insensitive +  return new BooleanValue(n.nodeName.toUpperCase() == this.name.toUpperCase()); +} + +function PredicateExpr(expr) { +  this.expr = expr; +} + +PredicateExpr.prototype.evaluate = function(ctx) { +  var v = this.expr.evaluate(ctx); +  if (v.type == 'number') { +    // NOTE(mesch): Internally, position is represented starting with +    // 0, however in XPath position starts with 1. See functions +    // position() and last(). +    return new BooleanValue(ctx.position == v.numberValue() - 1); +  } else { +    return new BooleanValue(v.booleanValue()); +  } +}; + +function FunctionCallExpr(name) { +  this.name = name; +  this.args = []; +} + +FunctionCallExpr.prototype.appendArg = function(arg) { +  this.args.push(arg); +}; + +FunctionCallExpr.prototype.evaluate = function(ctx) { +  var fn = '' + this.name.value; +  var f = this.xpathfunctions[fn]; +  if (f) { +    return f.call(this, ctx); +  } else { +    Log.write('XPath NO SUCH FUNCTION ' + fn); +    return new BooleanValue(false); +  } +}; + +FunctionCallExpr.prototype.xpathfunctions = { +  'last': function(ctx) { +    assert(this.args.length == 0); +    // NOTE(mesch): XPath position starts at 1. +    return new NumberValue(ctx.nodelist.length); +  }, + +  'position': function(ctx) { +    assert(this.args.length == 0); +    // NOTE(mesch): XPath position starts at 1. +    return new NumberValue(ctx.position + 1); +  }, + +  'count': function(ctx) { +    assert(this.args.length == 1); +    var v = this.args[0].evaluate(ctx); +    return new NumberValue(v.nodeSetValue().length); +  }, + +  'id': function(ctx) { +    assert(this.args.length == 1); +    var e = this.args.evaluate(ctx); +    var ret = []; +    var ids; +    if (e.type == 'node-set') { +      ids = []; +      for (var i = 0; i < e.length; ++i) { +        var v = xmlValue(e[i]).split(/\s+/); +        for (var ii = 0; ii < v.length; ++ii) { +          ids.push(v[ii]); +        } +      } +    } else { +      ids = e.split(/\s+/); +    } +    var d = ctx.node.ownerDocument; +    for (var i = 0; i < ids.length; ++i) { +      var n = d.getElementById(ids[i]); +      if (n) { +        ret.push(n); +      } +    } +    return new NodeSetValue(ret); +  }, + +  'local-name': function(ctx) { +    alert('not implmented yet: XPath function local-name()'); +  }, + +  'namespace-uri': function(ctx) { +    alert('not implmented yet: XPath function namespace-uri()'); +  }, + +  'name': function(ctx) { +    assert(this.args.length == 1 || this.args.length == 0); +    var n; +    if (this.args.length == 0) { +      n = [ ctx.node ]; +    } else { +      n = this.args[0].evaluate(ctx).nodeSetValue(); +    } + +    if (n.length == 0) { +      return new StringValue(''); +    } else { +      return new StringValue(n[0].nodeName); +    } +  }, + +  'string':  function(ctx) { +    assert(this.args.length == 1 || this.args.length == 0); +    if (this.args.length == 0) { +      return new StringValue(new NodeSetValue([ ctx.node ]).stringValue()); +    } else { +      return new StringValue(this.args[0].evaluate(ctx).stringValue()); +    } +  }, + +  'concat': function(ctx) { +    var ret = ''; +    for (var i = 0; i < this.args.length; ++i) { +      ret += this.args[i].evaluate(ctx).stringValue(); +    } +    return new StringValue(ret); +  }, + +  'starts-with': function(ctx) { +    assert(this.args.length == 2); +    var s0 = this.args[0].evaluate(ctx).stringValue(); +    var s1 = this.args[1].evaluate(ctx).stringValue(); +    return new BooleanValue(s0.indexOf(s1) == 0); +  }, + +  'contains': function(ctx) { +    assert(this.args.length == 2); +    var s0 = this.args[0].evaluate(ctx).stringValue(); +    var s1 = this.args[1].evaluate(ctx).stringValue(); +    return new BooleanValue(s0.indexOf(s1) != -1); +  }, + +  'substring-before': function(ctx) { +    assert(this.args.length == 2); +    var s0 = this.args[0].evaluate(ctx).stringValue(); +    var s1 = this.args[1].evaluate(ctx).stringValue(); +    var i = s0.indexOf(s1); +    var ret; +    if (i == -1) { +      ret = ''; +    } else { +      ret = s0.substr(0,i); +    } +    return new StringValue(ret); +  }, + +  'substring-after': function(ctx) { +    assert(this.args.length == 2); +    var s0 = this.args[0].evaluate(ctx).stringValue(); +    var s1 = this.args[1].evaluate(ctx).stringValue(); +    var i = s0.indexOf(s1); +    var ret; +    if (i == -1) { +      ret = ''; +    } else { +      ret = s0.substr(i + s1.length); +    } +    return new StringValue(ret); +  }, + +  'substring': function(ctx) { +    // NOTE: XPath defines the position of the first character in a +    // string to be 1, in JavaScript this is 0 ([XPATH] Section 4.2). +    assert(this.args.length == 2 || this.args.length == 3); +    var s0 = this.args[0].evaluate(ctx).stringValue(); +    var s1 = this.args[1].evaluate(ctx).numberValue(); +    var ret; +    if (this.args.length == 2) { +      var i1 = Math.max(0, Math.round(s1) - 1); +      ret = s0.substr(i1); + +    } else { +      var s2 = this.args[2].evaluate(ctx).numberValue(); +      var i0 = Math.round(s1) - 1; +      var i1 = Math.max(0, i0); +      var i2 = Math.round(s2) - Math.max(0, -i0); +      ret = s0.substr(i1, i2); +    } +    return new StringValue(ret); +  }, + +  'string-length': function(ctx) { +    var s; +    if (this.args.length > 0) { +      s = this.args[0].evaluate(ctx).stringValue(); +    } else { +      s = new NodeSetValue([ ctx.node ]).stringValue(); +    } +    return new NumberValue(s.length); +  }, + +  'normalize-space': function(ctx) { +    var s; +    if (this.args.length > 0) { +      s = this.args[0].evaluate(ctx).stringValue(); +    } else { +      s = new NodeSetValue([ ctx.node ]).stringValue(); +    } +    s = s.replace(/^\s*/,'').replace(/\s*$/,'').replace(/\s+/g, ' '); +    return new StringValue(s); +  }, + +  'translate': function(ctx) { +    assert(this.args.length == 3); +    var s0 = this.args[0].evaluate(ctx).stringValue(); +    var s1 = this.args[1].evaluate(ctx).stringValue(); +    var s2 = this.args[2].evaluate(ctx).stringValue(); + +    for (var i = 0; i < s1.length; ++i) { +      s0 = s0.replace(new RegExp(s1.charAt(i), 'g'), s2.charAt(i)); +    } +    return new StringValue(s0); +  }, + +  'boolean': function(ctx) { +    assert(this.args.length == 1); +    return new BooleanValue(this.args[0].evaluate(ctx).booleanValue()); +  }, + +  'not': function(ctx) { +    assert(this.args.length == 1); +    var ret = !this.args[0].evaluate(ctx).booleanValue(); +    return new BooleanValue(ret); +  }, + +  'true': function(ctx) { +    assert(this.args.length == 0); +    return new BooleanValue(true); +  }, + +  'false': function(ctx) { +    assert(this.args.length == 0); +    return new BooleanValue(false); +  }, + +  'lang': function(ctx) { +    assert(this.args.length == 1); +    var lang = this.args[0].evaluate(ctx).stringValue(); +    var xmllang; +    var n = ctx.node; +    while (n && n != n.parentNode /* just in case ... */) { +      xmllang = n.getAttribute('xml:lang'); +      if (xmllang) { +        break; +      } +      n = n.parentNode; +    } +    if (!xmllang) { +      return new BooleanValue(false); +    } else { +      var re = new RegExp('^' + lang + '$', 'i'); +      return new BooleanValue(xmllang.match(re) || +                              xmllang.replace(/_.*$/,'').match(re)); +    } +  }, + +  'number': function(ctx) { +    assert(this.args.length == 1 || this.args.length == 0); + +    if (this.args.length == 1) { +      return new NumberValue(this.args[0].evaluate(ctx).numberValue()); +    } else { +      return new NumberValue(new NodeSetValue([ ctx.node ]).numberValue()); +    } +  }, + +  'sum': function(ctx) { +    assert(this.args.length == 1); +    var n = this.args[0].evaluate(ctx).nodeSetValue(); +    var sum = 0; +    for (var i = 0; i < n.length; ++i) { +      sum += xmlValue(n[i]) - 0; +    } +    return new NumberValue(sum); +  }, + +  'floor': function(ctx) { +    assert(this.args.length == 1); +    var num = this.args[0].evaluate(ctx).numberValue(); +    return new NumberValue(Math.floor(num)); +  }, + +  'ceiling': function(ctx) { +    assert(this.args.length == 1); +    var num = this.args[0].evaluate(ctx).numberValue(); +    return new NumberValue(Math.ceil(num)); +  }, + +  'round': function(ctx) { +    assert(this.args.length == 1); +    var num = this.args[0].evaluate(ctx).numberValue(); +    return new NumberValue(Math.round(num)); +  }, + +  // TODO(mesch): The following functions are custom. There is a +  // standard that defines how to add functions, which should be +  // applied here. + +  'ext-join': function(ctx) { +    assert(this.args.length == 2); +    var nodes = this.args[0].evaluate(ctx).nodeSetValue(); +    var delim = this.args[1].evaluate(ctx).stringValue(); +    var ret = ''; +    for (var i = 0; i < nodes.length; ++i) { +      if (ret) { +        ret += delim; +      } +      ret += xmlValue(nodes[i]); +    } +    return new StringValue(ret); +  }, + +  // ext-if() evaluates and returns its second argument, if the +  // boolean value of its first argument is true, otherwise it +  // evaluates and returns its third argument. + +  'ext-if': function(ctx) { +    assert(this.args.length == 3); +    if (this.args[0].evaluate(ctx).booleanValue()) { +      return this.args[1].evaluate(ctx); +    } else { +      return this.args[2].evaluate(ctx); +    } +  }, + +  'ext-sprintf': function(ctx) { +    assert(this.args.length >= 1); +    var args = []; +    for (var i = 0; i < this.args.length; ++i) { +      args.push(this.args[i].evaluate(ctx).stringValue()); +    } +    return new StringValue(sprintf.apply(null, args)); +  }, + +  // ext-cardinal() evaluates its single argument as a number, and +  // returns the current node that many times. It can be used in the +  // select attribute to iterate over an integer range. +   +  'ext-cardinal': function(ctx) { +    assert(this.args.length >= 1); +    var c = this.args[0].evaluate(ctx).numberValue(); +    var ret = []; +    for (var i = 0; i < c; ++i) { +      ret.push(ctx.node); +    } +    return new NodeSetValue(ret); +  } +}; + +function UnionExpr(expr1, expr2) { +  this.expr1 = expr1; +  this.expr2 = expr2; +} + +UnionExpr.prototype.evaluate = function(ctx) { +  var nodes1 = this.expr1.evaluate(ctx).nodeSetValue(); +  var nodes2 = this.expr2.evaluate(ctx).nodeSetValue(); +  var I1 = nodes1.length; +  for (var i2 = 0; i2 < nodes2.length; ++i2) { +    for (var i1 = 0; i1 < I1; ++i1) { +      if (nodes1[i1] == nodes2[i2]) { +        // break inner loop and continue outer loop, labels confuse +        // the js compiler, so we don't use them here. +        i1 = I1; +      } +    } +    nodes1.push(nodes2[i2]); +  } +  return new NodeSetValue(nodes2); +}; + +function PathExpr(filter, rel) { +  this.filter = filter; +  this.rel = rel; +} + +PathExpr.prototype.evaluate = function(ctx) { +  var nodes = this.filter.evaluate(ctx).nodeSetValue(); +  var nodes1 = []; +  for (var i = 0; i < nodes.length; ++i) { +    var nodes0 = this.rel.evaluate(ctx.clone(nodes[i], i, nodes)).nodeSetValue(); +    for (var ii = 0; ii < nodes0.length; ++ii) { +      nodes1.push(nodes0[ii]); +    } +  } +  return new NodeSetValue(nodes1); +}; + +function FilterExpr(expr, predicate) { +  this.expr = expr; +  this.predicate = predicate; +} + +FilterExpr.prototype.evaluate = function(ctx) { +  var nodes = this.expr.evaluate(ctx).nodeSetValue(); +  for (var i = 0; i < this.predicate.length; ++i) { +    var nodes0 = nodes; +    nodes = []; +    for (var j = 0; j < nodes0.length; ++j) { +      var n = nodes0[j]; +      if (this.predicate[i].evaluate(ctx.clone(n, j, nodes0)).booleanValue()) { +        nodes.push(n); +      } +    } +  } + +  return new NodeSetValue(nodes); +} + +function UnaryMinusExpr(expr) { +  this.expr = expr; +} + +UnaryMinusExpr.prototype.evaluate = function(ctx) { +  return new NumberValue(-this.expr.evaluate(ctx).numberValue()); +}; + +function BinaryExpr(expr1, op, expr2) { +  this.expr1 = expr1; +  this.expr2 = expr2; +  this.op = op; +} + +BinaryExpr.prototype.evaluate = function(ctx) { +  var ret; +  switch (this.op.value) { +    case 'or': +      ret = new BooleanValue(this.expr1.evaluate(ctx).booleanValue() || +                             this.expr2.evaluate(ctx).booleanValue()); +      break; + +    case 'and': +      ret = new BooleanValue(this.expr1.evaluate(ctx).booleanValue() && +                             this.expr2.evaluate(ctx).booleanValue()); +      break; + +    case '+': +      ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() + +                            this.expr2.evaluate(ctx).numberValue()); +      break; + +    case '-': +      ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() - +                            this.expr2.evaluate(ctx).numberValue()); +      break; + +    case '*': +      ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() * +                            this.expr2.evaluate(ctx).numberValue()); +      break; + +    case 'mod': +      ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() % +                            this.expr2.evaluate(ctx).numberValue()); +      break; + +    case 'div': +      ret = new NumberValue(this.expr1.evaluate(ctx).numberValue() / +                            this.expr2.evaluate(ctx).numberValue()); +      break; + +    case '=': +      ret = this.compare(ctx, function(x1, x2) { return x1 == x2; }); +      break; + +    case '!=': +      ret = this.compare(ctx, function(x1, x2) { return x1 != x2; }); +      break; + +    case '<': +      ret = this.compare(ctx, function(x1, x2) { return x1 < x2; }); +      break; + +    case '<=': +      ret = this.compare(ctx, function(x1, x2) { return x1 <= x2; }); +      break; + +    case '>': +      ret = this.compare(ctx, function(x1, x2) { return x1 > x2; }); +      break; + +    case '>=': +      ret = this.compare(ctx, function(x1, x2) { return x1 >= x2; }); +      break; + +    default: +      alert('BinaryExpr.evaluate: ' + this.op.value); +  } +  return ret; +}; + +BinaryExpr.prototype.compare = function(ctx, cmp) { +  var v1 = this.expr1.evaluate(ctx); +  var v2 = this.expr2.evaluate(ctx); + +  var ret; +  if (v1.type == 'node-set' && v2.type == 'node-set') { +    var n1 = v1.nodeSetValue(); +    var n2 = v2.nodeSetValue(); +    ret = false; +    for (var i1 = 0; i1 < n1.length; ++i1) { +      for (var i2 = 0; i2 < n2.length; ++i2) { +        if (cmp(xmlValue(n1[i1]), xmlValue(n2[i2]))) { +          ret = true; +          // Break outer loop. Labels confuse the jscompiler and we +          // don't use them. +          i2 = n2.length; +          i1 = n1.length; +        } +      } +    } + +  } else if (v1.type == 'node-set' || v2.type == 'node-set') { + +    if (v1.type == 'number') { +      var s = v1.numberValue(); +      var n = v2.nodeSetValue(); + +      ret = false; +      for (var i = 0;  i < n.length; ++i) { +        var nn = xmlValue(n[i]) - 0; +        if (cmp(s, nn)) { +          ret = true; +          break; +        } +      } + +    } else if (v2.type == 'number') { +      var n = v1.nodeSetValue(); +      var s = v2.numberValue(); + +      ret = false; +      for (var i = 0;  i < n.length; ++i) { +        var nn = xmlValue(n[i]) - 0; +        if (cmp(nn, s)) { +          ret = true; +          break; +        } +      } + +    } else if (v1.type == 'string') { +      var s = v1.stringValue(); +      var n = v2.nodeSetValue(); + +      ret = false; +      for (var i = 0;  i < n.length; ++i) { +        var nn = xmlValue(n[i]); +        if (cmp(s, nn)) { +          ret = true; +          break; +        } +      } + +    } else if (v2.type == 'string') { +      var n = v1.nodeSetValue(); +      var s = v2.stringValue(); + +      ret = false; +      for (var i = 0;  i < n.length; ++i) { +        var nn = xmlValue(n[i]); +        if (cmp(nn, s)) { +          ret = true; +          break; +        } +      } + +    } else { +      ret = cmp(v1.booleanValue(), v2.booleanValue()); +    } + +  } else if (v1.type == 'boolean' || v2.type == 'boolean') { +    ret = cmp(v1.booleanValue(), v2.booleanValue()); + +  } else if (v1.type == 'number' || v2.type == 'number') { +    ret = cmp(v1.numberValue(), v2.numberValue()); + +  } else { +    ret = cmp(v1.stringValue(), v2.stringValue()); +  } + +  return new BooleanValue(ret); +} + +function LiteralExpr(value) { +  this.value = value; +} + +LiteralExpr.prototype.evaluate = function(ctx) { +  return new StringValue(this.value); +}; + +function NumberExpr(value) { +  this.value = value; +} + +NumberExpr.prototype.evaluate = function(ctx) { +  return new NumberValue(this.value); +}; + +function VariableExpr(name) { +  this.name = name; +} + +VariableExpr.prototype.evaluate = function(ctx) { +  return ctx.getVariable(this.name); +} + +// Factory functions for semantic values (i.e. Expressions) of the +// productions in the grammar. When a production is matched to reduce +// the current parse state stack, the function is called with the +// semantic values of the matched elements as arguments, and returns +// another semantic value. The semantic value is a node of the parse +// tree, an expression object with an evaluate() method that evaluates the +// expression in an actual context. These factory functions are used +// in the specification of the grammar rules, below. + +function makeTokenExpr(m) { +  return new TokenExpr(m); +} + +function passExpr(e) { +  return e; +} + +function makeLocationExpr1(slash, rel) { +  rel.absolute = true; +  return rel; +} + +function makeLocationExpr2(dslash, rel) { +  rel.absolute = true; +  rel.prependStep(makeAbbrevStep(dslash.value)); +  return rel; +} + +function makeLocationExpr3(slash) { +  var ret = new LocationExpr(); +  ret.appendStep(makeAbbrevStep('.')); +  ret.absolute = true; +  return ret; +} + +function makeLocationExpr4(dslash) { +  var ret = new LocationExpr(); +  ret.absolute = true; +  ret.appendStep(makeAbbrevStep(dslash.value)); +  return ret; +} + +function makeLocationExpr5(step) { +  var ret = new LocationExpr(); +  ret.appendStep(step); +  return ret; +} + +function makeLocationExpr6(rel, slash, step) { +  rel.appendStep(step); +  return rel; +} + +function makeLocationExpr7(rel, dslash, step) { +  rel.appendStep(makeAbbrevStep(dslash.value)); +  return rel; +} + +function makeStepExpr1(dot) { +  return makeAbbrevStep(dot.value); +} + +function makeStepExpr2(ddot) { +  return makeAbbrevStep(ddot.value); +} + +function makeStepExpr3(axisname, axis, nodetest) { +  return new StepExpr(axisname.value, nodetest); +} + +function makeStepExpr4(at, nodetest) { +  return new StepExpr('attribute', nodetest); +} + +function makeStepExpr5(nodetest) { +  return new StepExpr('child', nodetest); +} + +function makeStepExpr6(step, predicate) { +  step.appendPredicate(predicate); +  return step; +} + +function makeAbbrevStep(abbrev) { +  switch (abbrev) { +  case '//': +    return new StepExpr('descendant-or-self', new NodeTestAny); + +  case '.': +    return new StepExpr('self', new NodeTestAny); + +  case '..': +    return new StepExpr('parent', new NodeTestAny); +  } +} + +function makeNodeTestExpr1(asterisk) { +  return new NodeTestElement; +} + +function makeNodeTestExpr2(ncname, colon, asterisk) { +  return new NodeTestNC(ncname.value); +} + +function makeNodeTestExpr3(qname) { +  return new NodeTestName(qname.value); +} + +function makeNodeTestExpr4(typeo, parenc) { +  var type = typeo.value.replace(/\s*\($/, ''); +  switch(type) { +  case 'node': +    return new NodeTestAny; + +  case 'text': +    return new NodeTestText; + +  case 'comment': +    return new NodeTestComment; + +  case 'processing-instruction': +    return new NodeTestPI; +  } +} + +function makeNodeTestExpr5(typeo, target, parenc) { +  var type = typeo.replace(/\s*\($/, ''); +  if (type != 'processing-instruction') { +    throw type + ' ' + Error().stack; +  } +  return new NodeTestPI(target.value); +} + +function makePredicateExpr(pareno, expr, parenc) { +  return new PredicateExpr(expr); +} + +function makePrimaryExpr(pareno, expr, parenc) { +  return expr; +} + +function makeFunctionCallExpr1(name, pareno, parenc) { +  return new FunctionCallExpr(name); +} + +function makeFunctionCallExpr2(name, pareno, arg1, args, parenc) { +  var ret = new FunctionCallExpr(name); +  ret.appendArg(arg1); +  for (var i = 0; i < args.length; ++i) { +    ret.appendArg(args[i]); +  } +  return ret; +} + +function makeArgumentExpr(comma, expr) { +  return expr; +} + +function makeUnionExpr(expr1, pipe, expr2) { +  return new UnionExpr(expr1, expr2); +} + +function makePathExpr1(filter, slash, rel) { +  return new PathExpr(filter, rel); +} + +function makePathExpr2(filter, dslash, rel) { +  rel.prependStep(makeAbbrevStep(dslash.value)); +  return new PathExpr(filter, rel); +} + +function makeFilterExpr(expr, predicates) { +  if (predicates.length > 0) { +    return new FilterExpr(expr, predicates); +  } else { +    return expr; +  } +} + +function makeUnaryMinusExpr(minus, expr) { +  return new UnaryMinusExpr(expr); +} + +function makeBinaryExpr(expr1, op, expr2) { +  return new BinaryExpr(expr1, op, expr2); +} + +function makeLiteralExpr(token) { +  // remove quotes from the parsed value: +  var value = token.value.substring(1, token.value.length - 1); +  return new LiteralExpr(value); +} + +function makeNumberExpr(token) { +  return new NumberExpr(token.value); +} + +function makeVariableReference(dollar, name) { +  return new VariableExpr(name.value); +} + +// Used before parsing for optimization of common simple cases. See +// the begin of xpathParse() for which they are. +function makeSimpleExpr(expr) { +  if (expr.charAt(0) == '$') { +    return new VariableExpr(expr.substr(1)); +  } else if (expr.charAt(0) == '@') { +    var a = new NodeTestName(expr.substr(1)); +    var b = new StepExpr('attribute', a); +    var c = new LocationExpr(); +    c.appendStep(b); +    return c; +  } else if (expr.match(/^[0-9]+$/)) { +    return new NumberExpr(expr); +  } else { +    var a = new NodeTestName(expr); +    var b = new StepExpr('child', a); +    var c = new LocationExpr(); +    c.appendStep(b); +    return c; +  } +} + +function makeSimpleExpr2(expr) { +  var steps = expr.split('/'); +  var c = new LocationExpr(); +  for (var i in steps) { +    var a = new NodeTestName(steps[i]); +    var b = new StepExpr('child', a); +    c.appendStep(b); +  } +  return c; +} + +// The axes of XPath expressions. + +var xpathAxis = { +  ANCESTOR_OR_SELF: 'ancestor-or-self', +  ANCESTOR: 'ancestor', +  ATTRIBUTE: 'attribute', +  CHILD: 'child', +  DESCENDANT_OR_SELF: 'descendant-or-self', +  DESCENDANT: 'descendant', +  FOLLOWING_SIBLING: 'following-sibling', +  FOLLOWING: 'following', +  NAMESPACE: 'namespace', +  PARENT: 'parent', +  PRECEDING_SIBLING: 'preceding-sibling', +  PRECEDING: 'preceding', +  SELF: 'self' +}; + +var xpathAxesRe = [ +    xpathAxis.ANCESTOR_OR_SELF, +    xpathAxis.ANCESTOR, +    xpathAxis.ATTRIBUTE, +    xpathAxis.CHILD, +    xpathAxis.DESCENDANT_OR_SELF, +    xpathAxis.DESCENDANT, +    xpathAxis.FOLLOWING_SIBLING, +    xpathAxis.FOLLOWING, +    xpathAxis.NAMESPACE, +    xpathAxis.PARENT, +    xpathAxis.PRECEDING_SIBLING, +    xpathAxis.PRECEDING, +    xpathAxis.SELF +].join('|'); + + +// The tokens of the language. The label property is just used for +// generating debug output. The prec property is the precedence used +// for shift/reduce resolution. Default precedence is 0 as a lookahead +// token and 2 on the stack. TODO(mesch): this is certainly not +// necessary and too complicated. Simplify this! + +// NOTE: tabular formatting is the big exception, but here it should +// be OK. + +var TOK_PIPE =   { label: "|",   prec:   17, re: new RegExp("^\\|") }; +var TOK_DSLASH = { label: "//",  prec:   19, re: new RegExp("^//")  }; +var TOK_SLASH =  { label: "/",   prec:   30, re: new RegExp("^/")   }; +var TOK_AXIS =   { label: "::",  prec:   20, re: new RegExp("^::")  }; +var TOK_COLON =  { label: ":",   prec: 1000, re: new RegExp("^:")  }; +var TOK_AXISNAME = { label: "[axis]", re: new RegExp('^(' + xpathAxesRe + ')') }; +var TOK_PARENO = { label: "(",   prec:   34, re: new RegExp("^\\(") }; +var TOK_PARENC = { label: ")",               re: new RegExp("^\\)") }; +var TOK_DDOT =   { label: "..",  prec:   34, re: new RegExp("^\\.\\.") }; +var TOK_DOT =    { label: ".",   prec:   34, re: new RegExp("^\\.") }; +var TOK_AT =     { label: "@",   prec:   34, re: new RegExp("^@")   }; + +var TOK_COMMA =  { label: ",",               re: new RegExp("^,") }; + +var TOK_OR =     { label: "or",  prec:   10, re: new RegExp("^or\\b") }; +var TOK_AND =    { label: "and", prec:   11, re: new RegExp("^and\\b") }; +var TOK_EQ =     { label: "=",   prec:   12, re: new RegExp("^=")   }; +var TOK_NEQ =    { label: "!=",  prec:   12, re: new RegExp("^!=")  }; +var TOK_GE =     { label: ">=",  prec:   13, re: new RegExp("^>=")  }; +var TOK_GT =     { label: ">",   prec:   13, re: new RegExp("^>")   }; +var TOK_LE =     { label: "<=",  prec:   13, re: new RegExp("^<=")  }; +var TOK_LT =     { label: "<",   prec:   13, re: new RegExp("^<")   }; +var TOK_PLUS =   { label: "+",   prec:   14, re: new RegExp("^\\+"), left: true }; +var TOK_MINUS =  { label: "-",   prec:   14, re: new RegExp("^\\-"), left: true }; +var TOK_DIV =    { label: "div", prec:   15, re: new RegExp("^div\\b"), left: true }; +var TOK_MOD =    { label: "mod", prec:   15, re: new RegExp("^mod\\b"), left: true }; + +var TOK_BRACKO = { label: "[",   prec:   32, re: new RegExp("^\\[") }; +var TOK_BRACKC = { label: "]",               re: new RegExp("^\\]") }; +var TOK_DOLLAR = { label: "$",               re: new RegExp("^\\$") }; + +var TOK_NCNAME = { label: "[ncname]", re: new RegExp('^[a-z][-\\w]*','i') }; + +var TOK_ASTERISK = { label: "*", prec: 15, re: new RegExp("^\\*"), left: true }; +var TOK_LITERALQ = { label: "[litq]", prec: 20, re: new RegExp("^'[^\\']*'") }; +var TOK_LITERALQQ = { +  label: "[litqq]", +  prec: 20, +  re: new RegExp('^"[^\\"]*"') +}; + +var TOK_NUMBER  = { +  label: "[number]", +  prec: 35, +  re: new RegExp('^\\d+(\\.\\d*)?') }; + +var TOK_QNAME = { +  label: "[qname]", +  re: new RegExp('^([a-z][-\\w]*:)?[a-z][-\\w]*','i') +}; + +var TOK_NODEO = { +  label: "[nodetest-start]", +  re: new RegExp('^(processing-instruction|comment|text|node)\\(') +}; + +// The table of the tokens of our grammar, used by the lexer: first +// column the tag, second column a regexp to recognize it in the +// input, third column the precedence of the token, fourth column a +// factory function for the semantic value of the token. +// +// NOTE: order of this list is important, because the first match +// counts. Cf. DDOT and DOT, and AXIS and COLON. + +var xpathTokenRules = [ +    TOK_DSLASH, +    TOK_SLASH, +    TOK_DDOT, +    TOK_DOT, +    TOK_AXIS, +    TOK_COLON, +    TOK_AXISNAME, +    TOK_NODEO, +    TOK_PARENO, +    TOK_PARENC, +    TOK_BRACKO, +    TOK_BRACKC, +    TOK_AT, +    TOK_COMMA, +    TOK_OR, +    TOK_AND, +    TOK_NEQ, +    TOK_EQ, +    TOK_GE, +    TOK_GT, +    TOK_LE, +    TOK_LT, +    TOK_PLUS, +    TOK_MINUS, +    TOK_ASTERISK, +    TOK_PIPE, +    TOK_MOD, +    TOK_DIV, +    TOK_LITERALQ, +    TOK_LITERALQQ, +    TOK_NUMBER, +    TOK_QNAME, +    TOK_NCNAME, +    TOK_DOLLAR +]; + +// All the nonterminals of the grammar. The nonterminal objects are +// identified by object identity; the labels are used in the debug +// output only. +var XPathLocationPath = { label: "LocationPath" }; +var XPathRelativeLocationPath = { label: "RelativeLocationPath" }; +var XPathAbsoluteLocationPath = { label: "AbsoluteLocationPath" }; +var XPathStep = { label: "Step" }; +var XPathNodeTest = { label: "NodeTest" }; +var XPathPredicate = { label: "Predicate" }; +var XPathLiteral = { label: "Literal" }; +var XPathExpr = { label: "Expr" }; +var XPathPrimaryExpr = { label: "PrimaryExpr" }; +var XPathVariableReference = { label: "Variablereference" }; +var XPathNumber = { label: "Number" }; +var XPathFunctionCall = { label: "FunctionCall" }; +var XPathArgumentRemainder = { label: "ArgumentRemainder" }; +var XPathPathExpr = { label: "PathExpr" }; +var XPathUnionExpr = { label: "UnionExpr" }; +var XPathFilterExpr = { label: "FilterExpr" }; +var XPathDigits = { label: "Digits" }; + +var xpathNonTerminals = [ +    XPathLocationPath, +    XPathRelativeLocationPath, +    XPathAbsoluteLocationPath, +    XPathStep, +    XPathNodeTest, +    XPathPredicate, +    XPathLiteral, +    XPathExpr, +    XPathPrimaryExpr, +    XPathVariableReference, +    XPathNumber, +    XPathFunctionCall, +    XPathArgumentRemainder, +    XPathPathExpr, +    XPathUnionExpr, +    XPathFilterExpr, +    XPathDigits +]; + +// Quantifiers that are used in the productions of the grammar. +var Q_01 = { label: "?" }; +var Q_MM = { label: "*" }; +var Q_1M = { label: "+" }; + +// Tag for left associativity (right assoc is implied by undefined). +var ASSOC_LEFT = true; + +// The productions of the grammar. Columns of the table: +// +// - target nonterminal, +// - pattern, +// - precedence, +// - semantic value factory +// +// The semantic value factory is a function that receives parse tree +// nodes from the stack frames of the matched symbols as arguments and +// returns an a node of the parse tree. The node is stored in the top +// stack frame along with the target object of the rule. The node in +// the parse tree is an expression object that has an evaluate() method +// and thus evaluates XPath expressions. +// +// The precedence is used to decide between reducing and shifting by +// comparing the precendence of the rule that is candidate for +// reducing with the precedence of the look ahead token. Precedence of +// -1 means that the precedence of the tokens in the pattern is used +// instead. TODO: It shouldn't be necessary to explicitly assign +// precedences to rules. + +var xpathGrammarRules = +  [ +   [ XPathLocationPath, [ XPathRelativeLocationPath ], 18, +     passExpr ], +   [ XPathLocationPath, [ XPathAbsoluteLocationPath ], 18, +     passExpr ], + +   [ XPathAbsoluteLocationPath, [ TOK_SLASH, XPathRelativeLocationPath ], 18,  +     makeLocationExpr1 ], +   [ XPathAbsoluteLocationPath, [ TOK_DSLASH, XPathRelativeLocationPath ], 18, +     makeLocationExpr2 ], + +   [ XPathAbsoluteLocationPath, [ TOK_SLASH ], 0, +     makeLocationExpr3 ], +   [ XPathAbsoluteLocationPath, [ TOK_DSLASH ], 0, +     makeLocationExpr4 ], + +   [ XPathRelativeLocationPath, [ XPathStep ], 31, +     makeLocationExpr5 ], +   [ XPathRelativeLocationPath, +     [ XPathRelativeLocationPath, TOK_SLASH, XPathStep ], 31, +     makeLocationExpr6 ], +   [ XPathRelativeLocationPath, +     [ XPathRelativeLocationPath, TOK_DSLASH, XPathStep ], 31, +     makeLocationExpr7 ], + +   [ XPathStep, [ TOK_DOT ], 33, +     makeStepExpr1 ], +   [ XPathStep, [ TOK_DDOT ], 33, +     makeStepExpr2 ], +   [ XPathStep, +     [ TOK_AXISNAME, TOK_AXIS, XPathNodeTest ], 33, +     makeStepExpr3 ], +   [ XPathStep, [ TOK_AT, XPathNodeTest ], 33, +     makeStepExpr4 ], +   [ XPathStep, [ XPathNodeTest ], 33, +     makeStepExpr5 ], +   [ XPathStep, [ XPathStep, XPathPredicate ], 33, +     makeStepExpr6 ], + +   [ XPathNodeTest, [ TOK_ASTERISK ], 33, +     makeNodeTestExpr1 ], +   [ XPathNodeTest, [ TOK_NCNAME, TOK_COLON, TOK_ASTERISK ], 33, +     makeNodeTestExpr2 ], +   [ XPathNodeTest, [ TOK_QNAME ], 33, +     makeNodeTestExpr3 ], +   [ XPathNodeTest, [ TOK_NODEO, TOK_PARENC ], 33, +     makeNodeTestExpr4 ], +   [ XPathNodeTest, [ TOK_NODEO, XPathLiteral, TOK_PARENC ], 33, +     makeNodeTestExpr5 ], + +   [ XPathPredicate, [ TOK_BRACKO, XPathExpr, TOK_BRACKC ], 33, +     makePredicateExpr ], + +   [ XPathPrimaryExpr, [ XPathVariableReference ], 33, +     passExpr ], +   [ XPathPrimaryExpr, [ TOK_PARENO, XPathExpr, TOK_PARENC ], 33, +     makePrimaryExpr ], +   [ XPathPrimaryExpr, [ XPathLiteral ], 30, +     passExpr ], +   [ XPathPrimaryExpr, [ XPathNumber ], 30, +     passExpr ], +   [ XPathPrimaryExpr, [ XPathFunctionCall ], 30, +     passExpr ], + +   [ XPathFunctionCall, [ TOK_QNAME, TOK_PARENO, TOK_PARENC ], -1, +     makeFunctionCallExpr1 ], +   [ XPathFunctionCall, +     [ TOK_QNAME, TOK_PARENO, XPathExpr, XPathArgumentRemainder, Q_MM, +       TOK_PARENC ], -1, +     makeFunctionCallExpr2 ], +   [ XPathArgumentRemainder, [ TOK_COMMA, XPathExpr ], -1, +     makeArgumentExpr ], + +   [ XPathUnionExpr, [ XPathPathExpr ], 20, +     passExpr ], +   [ XPathUnionExpr, [ XPathUnionExpr, TOK_PIPE, XPathPathExpr ], 20, +     makeUnionExpr ], + +   [ XPathPathExpr, [ XPathLocationPath ], 20,  +     passExpr ],  +   [ XPathPathExpr, [ XPathFilterExpr ], 19,  +     passExpr ],  +   [ XPathPathExpr,  +     [ XPathFilterExpr, TOK_SLASH, XPathRelativeLocationPath ], 20, +     makePathExpr1 ], +   [ XPathPathExpr, +     [ XPathFilterExpr, TOK_DSLASH, XPathRelativeLocationPath ], 20, +     makePathExpr2 ], + +   [ XPathFilterExpr, [ XPathPrimaryExpr, XPathPredicate, Q_MM ], 20, +     makeFilterExpr ],  + +   [ XPathExpr, [ XPathPrimaryExpr ], 16, +     passExpr ], +   [ XPathExpr, [ XPathUnionExpr ], 16, +     passExpr ], + +   [ XPathExpr, [ TOK_MINUS, XPathExpr ], -1, +     makeUnaryMinusExpr ], + +   [ XPathExpr, [ XPathExpr, TOK_OR, XPathExpr ], -1, +     makeBinaryExpr ], +   [ XPathExpr, [ XPathExpr, TOK_AND, XPathExpr ], -1, +     makeBinaryExpr ], + +   [ XPathExpr, [ XPathExpr, TOK_EQ, XPathExpr ], -1, +     makeBinaryExpr ], +   [ XPathExpr, [ XPathExpr, TOK_NEQ, XPathExpr ], -1, +     makeBinaryExpr ], + +   [ XPathExpr, [ XPathExpr, TOK_LT, XPathExpr ], -1, +     makeBinaryExpr ], +   [ XPathExpr, [ XPathExpr, TOK_LE, XPathExpr ], -1, +     makeBinaryExpr ], +   [ XPathExpr, [ XPathExpr, TOK_GT, XPathExpr ], -1, +     makeBinaryExpr ], +   [ XPathExpr, [ XPathExpr, TOK_GE, XPathExpr ], -1, +     makeBinaryExpr ], + +   [ XPathExpr, [ XPathExpr, TOK_PLUS, XPathExpr ], -1, +     makeBinaryExpr, ASSOC_LEFT ], +   [ XPathExpr, [ XPathExpr, TOK_MINUS, XPathExpr ], -1, +     makeBinaryExpr, ASSOC_LEFT ], + +   [ XPathExpr, [ XPathExpr, TOK_ASTERISK, XPathExpr ], -1, +     makeBinaryExpr, ASSOC_LEFT ], +   [ XPathExpr, [ XPathExpr, TOK_DIV, XPathExpr ], -1, +     makeBinaryExpr, ASSOC_LEFT ], +   [ XPathExpr, [ XPathExpr, TOK_MOD, XPathExpr ], -1, +     makeBinaryExpr, ASSOC_LEFT ], + +   [ XPathLiteral, [ TOK_LITERALQ ], -1, +     makeLiteralExpr ], +   [ XPathLiteral, [ TOK_LITERALQQ ], -1, +     makeLiteralExpr ], + +   [ XPathNumber, [ TOK_NUMBER ], -1, +     makeNumberExpr ], + +   [ XPathVariableReference, [ TOK_DOLLAR, TOK_QNAME ], 200, +     makeVariableReference ] +   ]; + +// That function computes some optimizations of the above data +// structures and will be called right here. It merely takes the +// counter variables out of the global scope. + +var xpathRules = []; + +function xpathParseInit() { +  if (xpathRules.length) { +    return; +  } + +  // Some simple optimizations for the xpath expression parser: sort +  // grammar rules descending by length, so that the longest match is +  // first found. + +  xpathGrammarRules.sort(function(a,b) { +    var la = a[1].length; +    var lb = b[1].length; +    if (la < lb) { +      return 1; +    } else if (la > lb) { +      return -1; +    } else { +      return 0; +    } +  }); + +  var k = 1; +  for (var i = 0; i < xpathNonTerminals.length; ++i) { +    xpathNonTerminals[i].key = k++; +  } + +  for (i = 0; i < xpathTokenRules.length; ++i) { +    xpathTokenRules[i].key = k++; +  } + +  Log.write('XPath parse INIT: ' + k + ' rules'); + +  // Another slight optimization: sort the rules into bins according +  // to the last element (observing quantifiers), so we can restrict +  // the match against the stack to the subest of rules that match the +  // top of the stack. +  // +  // TODO(mesch): What we actually want is to compute states as in +  // bison, so that we don't have to do any explicit and iterated +  // match against the stack. + +  function push_(array, position, element) { +    if (!array[position]) { +      array[position] = []; +    } +    array[position].push(element); +  } + +  for (i = 0; i < xpathGrammarRules.length; ++i) { +    var rule = xpathGrammarRules[i]; +    var pattern = rule[1]; + +    for (var j = pattern.length - 1; j >= 0; --j) { +      if (pattern[j] == Q_1M) { +        push_(xpathRules, pattern[j-1].key, rule); +        break; +         +      } else if (pattern[j] == Q_MM || pattern[j] == Q_01) { +        push_(xpathRules, pattern[j-1].key, rule); +        --j; + +      } else { +        push_(xpathRules, pattern[j].key, rule); +        break; +      } +    } +  } + +  Log.write('XPath parse INIT: ' + xpathRules.length + ' rule bins'); +   +  var sum = 0; +  mapExec(xpathRules, function(i) { +    if (i) { +      sum += i.length; +    } +  }); +   +  Log.write('XPath parse INIT: ' + (sum / xpathRules.length) + ' average bin size'); +} + +// Local utility functions that are used by the lexer or parser. + +function xpathCollectDescendants(nodelist, node) { +  for (var n = node.firstChild; n; n = n.nextSibling) { +    nodelist.push(n); +    arguments.callee(nodelist, n); +  } +} + +function xpathCollectDescendantsReverse(nodelist, node) { +  for (var n = node.lastChild; n; n = n.previousSibling) { +    nodelist.push(n); +    arguments.callee(nodelist, n); +  } +} + + +// The entry point for the library: match an expression against a DOM +// node. Returns an XPath value. +function xpathDomEval(expr, node) { +  var expr1 = xpathParse(expr); +  var ret = expr1.evaluate(new ExprContext(node)); +  return ret; +} + +// Utility function to sort a list of nodes. Used by xsltSort() and +// nxslSelect(). +function xpathSort(input, sort) { +  if (sort.length == 0) { +    return; +  } + +  var sortlist = []; + +  for (var i = 0; i < input.nodelist.length; ++i) { +    var node = input.nodelist[i]; +    var sortitem = { node: node, key: [] }; +    var context = input.clone(node, 0, [ node ]); +     +    for (var j = 0; j < sort.length; ++j) { +      var s = sort[j]; +      var value = s.expr.evaluate(context); + +      var evalue; +      if (s.type == 'text') { +        evalue = value.stringValue(); +      } else if (s.type == 'number') { +        evalue = value.numberValue(); +      } +      sortitem.key.push({ value: evalue, order: s.order }); +    } + +    // Make the sort stable by adding a lowest priority sort by +    // id. This is very convenient and furthermore required by the +    // spec ([XSLT] - Section 10 Sorting). +    sortitem.key.push({ value: i, order: 'ascending' }); + +    sortlist.push(sortitem); +  } + +  sortlist.sort(xpathSortByKey); + +  var nodes = []; +  for (var i = 0; i < sortlist.length; ++i) { +    nodes.push(sortlist[i].node); +  } +  input.nodelist = nodes; +  input.setNode(nodes[0], 0); +} + + +// Sorts by all order criteria defined. According to the JavaScript +// spec ([ECMA] Section 11.8.5), the compare operators compare strings +// as strings and numbers as numbers. +// +// NOTE: In browsers which do not follow the spec, this breaks only in +// the case that numbers should be sorted as strings, which is very +// uncommon. + +function xpathSortByKey(v1, v2) { +  // NOTE: Sort key vectors of different length never occur in +  // xsltSort. + +  for (var i = 0; i < v1.key.length; ++i) { +    var o = v1.key[i].order == 'descending' ? -1 : 1; +    if (v1.key[i].value > v2.key[i].value) { +      return +1 * o; +    } else if (v1.key[i].value < v2.key[i].value) { +      return -1 * o; +    } +  } + +  return 0; +} diff --git a/tests/test_tools/selenium/php/TestRunner.php b/tests/test_tools/selenium/php/TestRunner.php new file mode 100644 index 00000000..a82ce0dd --- /dev/null +++ b/tests/test_tools/selenium/php/TestRunner.php @@ -0,0 +1,169 @@ + +<html> + +<head> +<HTA:APPLICATION ID="SeleniumHTARunner" APPLICATIONNAME="Selenium" > +<!-- the previous line is only relevant if you rename this +     file to "TestRunner.hta" --> + +<!-- The copyright notice and other comments have been moved to after the HTA declaration, +     to work-around a bug in IE on Win2K whereby the HTA application doesn't function correctly --> +<!-- +Copyright 2004 ThoughtWorks, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + +     http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<meta content="text/html; charset=ISO-8859-1" http-equiv="content-type" /> + +<title>Selenium Functional Test Runner</title> +<link rel="stylesheet" type="text/css" href="<?php echo $base_dir; ?>core/selenium.css" /> +<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-browserdetect.js"></script> +<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-browserbot.js"></script> +<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/prototype-1.4.0.js"></script> +<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/find_matching_child.js"></script> +<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-api.js"></script> +<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-commandhandlers.js"></script> +<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-executionloop.js"></script> +<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-testrunner.js"></script> +<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-logging.js"></script> +<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/selenium-version.js"></script> +<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/scripts/htmlutils.js"></script> +<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/xpath/misc.js"></script> +<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/xpath/dom.js"></script> +<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>core/xpath/xpath.js"></script> +<script language="JavaScript" type="text/javascript" src="<?php echo $base_dir; ?>prado-functional-test.js"></script> +<script language="JavaScript" type="text/javascript"> +    function openDomViewer() { +        var autFrame = document.getElementById('myiframe'); +        var autFrameDocument = getIframeDocument(autFrame); +        this.rootDocument = autFrameDocument; +        var domViewer = window.open('<?php echo $base_dir; ?>core/domviewer/domviewer.html'); +        return false; +    } +	 +	Logger.prototype.openLogWindow = function()  +	{ +        this.logWindow = window.open( +            "<?php echo $base_dir; ?>core/SeleniumLog.html", "SeleniumLog", +            "width=600,height=250,bottom=0,right=0,status,scrollbars,resizable" +        ); +        return this.logWindow; +    } + +	var post_results_to = "<?php echo $driver; ?>"; +	 +</script> +</head> + +<body onLoad="start();"> + +    <table class="layout"> +    <form action="" name="controlPanel"> + +      <!-- Suite, Test, Control Panel --> + +      <tr class="selenium"> +        <td width="25%" height="30%" ><iframe name="testSuiteFrame" id="testSuiteFrame" src="<?php echo $driver; ?>?testSuites" application="yes"></iframe></td> +        <td width="50%" height="30%" ><iframe name="testFrame" id="testFrame" application="yes"></iframe></td> + +		<td width="25%">         +		<table class="layout"> +		  <tr class="selenium"> +			<th width="25%" height="1" class="header"> +			  <h3><a href="http://selenium.thoughtworks.com" title="The Selenium Project">Selenium</a></h3> +			</th> +		  </tr> +		  <tr> +			<td width="25%" height="30%" id="controlPanel"> +	 +			  <fieldset> +				<legend>Execute Tests</legend> +				 +				<div> +				  <input id="modeRun" type="radio" name="runMode" value="0" checked="checked"/><label for="modeRun">Run</label> +				  <input id="modeWalk" type="radio" name="runMode" value="500" /><label for="modeWalk">Walk</label> +				  <input id="modeStep" type="radio" name="runMode" value="-1" /><label for="modeStep">Step</label> +				</div> +				 +				<div> +				  <button type="button" id="runSuite" onClick="startTestSuite();" +						  title="Run the entire Test-Suite"> +					<strong>All</strong> +				  </button> +				  <button type="button" id="runTest" onClick="runSingleTest();" +						  title="Run the current Test"> +					<em>Selected</em> +				  </button> +				  <button type="button" id="continueTest" disabled="disabled" +						  title="Continue the Test"> +					Continue +				  </button> +				</div> +				 +			  </fieldset> +	 +			  <table id="stats" align="center"> +				<tr> +				  <td colspan="2" align="right">Elapsed:</td> +				  <td id="elapsedTime" colspan="2">00.00</td> +				</tr> +				<tr> +				  <th colspan="2">Tests</th> +				  <th colspan="2">Commands</th> +				</tr> +				<tr> +				  <td class="count" id="testRuns">0</td> +				  <td>run</td> +				  <td class="count" id="commandPasses">0</td> +				  <td>passed</td> +				</tr> +				<tr> +				  <td class="count" id="testFailures">0</td> +				  <td>failed</td> +				  <td class="count" id="commandFailures">0</td> +				  <td>failed</td> +				</tr> +				<tr> +				  <td colspan="2"></td> +				  <td class="count" id="commandErrors">0</td> +				  <td>incomplete</td> +				</tr> +			  </table> +	<!-- +			  <fieldset> +				<legend>Tools</legend> +				<button type="button" id="domViewer1" onClick="openDomViewer();"> +				  View DOM +				</button>  +				<button type="button" onClick="LOG.show();"> +				  Show Log +				</button> +	 +			  </fieldset> +	--> +			</td> +		  </tr> +		  </table> +		  </td> +	  </tr> + +      <!-- AUT --> + +      <tr> +        <td colspan="3" height="70%"><iframe name="myiframe" id="myiframe" src="<?php echo $base_dir; ?>core/TestRunner-splash.html"></iframe></td> +      </tr> +    </form> +    </table> + +</body> +</html>
\ No newline at end of file diff --git a/tests/test_tools/selenium/php/results.php b/tests/test_tools/selenium/php/results.php new file mode 100644 index 00000000..77dab43b --- /dev/null +++ b/tests/test_tools/selenium/php/results.php @@ -0,0 +1,160 @@ +<?php + +class SeleniumTestCaseResult +{ +	public $name; +	public $commands = array(); +	public $result = 'passed'; +	public $failures = array(); +} + +class SeleniumTestResult +{ +	public $result = 'passed'; +	public $totalTime = 0; +	public $numTestPasses = 0; +	public $numTestFailures = 0; +	public $numCommandPasses = 0; +	public $numCommandFailures = 0; +	public $numCommandErrors = 0; +	public $suites = array(); +	public $browser = ''; +	public $date; + +	public function __construct() +	{ +		$this->parse_data(); +		$this->browser = $_SERVER['HTTP_USER_AGENT']; +		$this->date = time(); +	} + +	protected function parse_data() +	{ +		$this->result = $_POST['result']; // failed || passed +		$this->totalTime = $_POST['totalTime']; +		$this->numTestPasses = $_POST['numTestPasses']; +		$this->numTestFailures = $_POST['numTestFailures']; +		$this->numCommandPasses = $_POST['numCommandPasses']; +		$this->numCommandFailures = $_POST['numCommandFailures']; +		$this->numCommandErrors = $_POST['numCommandErrors']; + +		foreach($_POST['tests'] as $test) +		{ +			$case = new SeleniumTestCaseResult(); +			$case->name = $test['testcase']; +			$case->commands = $test['commands']; +			for($i = 0; $i < count($case->commands); $i++) +			{ +				//$trace = $case->commands[$i]['trace']; +				//$trace = html_entity_decode($trace); +				//$case->commands[$i]['trace'] = @unserialize($trace); +				if($case->commands[$i]['result'] == 'failed') +				{ +					$case->result = 'failed'; +					array_push($case->failures, $case->commands[$i]); +				} +			} + +			$this->suites[$case->name] = $case; +		} + +	} +} + +class SeleniumHtmlReporter +{ +	protected $test; + +	public function __construct($result) +	{ +		$this->test = $result; +	} + +	protected function renderHeader() +	{ +		$contents = <<<EOD +<html> +<head> +<title>Functional Test Results</title> +<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> +<style type="text/css"> +body {font-family:"Verdana";font-weight:normal;color:black;background-color:white;} +.failed { background-color: red; } .error0 { background-color: lightgray; } +.info { padding: 0.5em; margin-top: 1em; color: white; } +.passed { background-color: green; } +.error_case div { padding: 0.3em 0.2em; margin: 0.5em 0; } +.error_msg { padding: 0.5em; border-bottom:1px solid #ccc; } +.msg { color:#c00; } +.function { font-family:"Lucida Console";color: gray;} +.file { border-bottom: 1px dashed gray; } +.env { color: gray; font-size:10pt; padding: 0.5em; } +.odd { background-color: #fee; } +</style> +</head> +<body> +EOD; +		return $contents; +	} + +	public function render() +	{ +		echo $this->renderHeader(); +		echo $this->renderBody(); +		echo $this->renderFooter(); +	} + +	protected function renderBody() +	{ +		/* SeleniumTestResult */ +		$test = $this->test; +		$total = count($test->suites); +		$date = @strftime('%Y-%m-%d %H:%M',$test->date); +$contents = <<<EOD +<h1 class="suite">Functional Test Results</h1> +<div class="info {$test->result}"> +	<strong>{$total}</strong> test cases completed, +	<strong>{$test->numTestPasses}</strong> passes  +	({$test->numCommandPasses} commands), and +	<strong>{$test->numTestFailures}</strong> fails +	({$test->numCommandErrors} commands). +</div> +<div class="env"> +	{$date}, {$test->browser} +</div> +EOD; +		$count = 1; +		foreach($test->suites as $suite) +		{ +			foreach($suite->failures as $error) +				$contents .= $this->getErrorMsg($suite, $error, $count++); +		} + +		return $contents; +	} + + +	protected function getErrorMsg($suite, $info, $count) +	{ +		$parity = $count%2==0 ? 'even' : 'odd'; +		$command = explode("|",$info['command']); +$msg = <<<EOD +	<div class="error_msg {$parity}"> +		<strong>#{$count}.</strong> +		"<span class="msg">{$info['msg']}</span>" in +		<span class="function"> +			{$suite->name}::{$command[1]}('{$command[2]}'); +		</span> +	</div> +EOD; + +		return $msg; +	} + +	protected function renderFooter() +	{ +		return "</body></html>"; +	} +} + + +?>
\ No newline at end of file diff --git a/tests/test_tools/selenium/php/selenium.php b/tests/test_tools/selenium/php/selenium.php new file mode 100644 index 00000000..9ad4955b --- /dev/null +++ b/tests/test_tools/selenium/php/selenium.php @@ -0,0 +1,477 @@ +<?php +/** + * Selenium PHP driver. Saves the test command in a "out" queue (in session), + * and for each selenese request, remove the first comand from the "out" queue + * and push the results into the "in" queque (also in session). When "out" queque + * is empty, write the results to disk. + * + * Usage: See ../../example.php + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the BSD License. + * + * Copyright(c) 2004 by Wei Zhuo. All rights reserved. + * + * To contact the author write to {@link mailto:weizhuo[at]gmail[dot]com Wei Zhuo} + * The latest version of PRADO can be obtained from: + * {@link http://prado.sourceforge.net/} + * + * @author Wei Zhuo <weizhuo[at]gmail[dot]com> + * @version $Revision: 1.66 $  $Date: Wed Nov 02 10:13:17 EST 2005 10:13:17 $ + * @package Prado.tests + */ + +/** + * Selenium automatic client runner, + * + * @author Wei Zhuo<weizhuo[at]gmail[dot]com> + * @version $Revision: 1.66 $  $Date: Fri Nov 04 13:20:12 EST 2005 13:20:12 $ + * @package Prado.tests + */ + +require_once(dirname(__FILE__).'/results.php'); + +class SeleniumTestRunner +{ +	protected $driver; +	protected $base_dir = ''; + +	public function __construct($driver=null, $base_dir='../javascript/') +	{ +		if(is_null($driver) && !(php_sapi_name() == 'cli')) +			$driver = $_SERVER['SCRIPT_NAME']; +		$this->driver = $driver; +		$this->base_dir = $base_dir; +	} + +	public function render() +	{ +		if((php_sapi_name() == 'cli')) return; +		$file = dirname(__FILE__).'/TestRunner.php'; +		$driver = $this->driver; +		 +		//$base_dir = $this->base_dir; +		$base_dir = $driver.'?sr='; +		include($file); +	} + +	public function getDriver() +	{ +		return $this->driver; +	} +} + +class SeleniumTestStorage +{ +	protected $outputs = array(); +	protected $tests = array(); + +	public function getTests() +	{ +		return $this->tests; +	} + +	public function addCommand($test_case_id, $command) +	{ +		$data = array($test_case_id, $command); +		array_push($this->outputs, $data); +	} + +	public function getCommand() +	{ +		return array_shift($this->outputs); +	} + +	public function addTestCase($command, $trace_details, $test_name, $test_suite) +	{ +		$data = array(0, 0, $command, "", $trace_details, $test_name, $test_suite); +		array_push($this->tests, $data); +	} +} + +class SeleneseInterpreter +{ +	protected $storage; +	protected $tracer; + +	public function __construct($storage, $tracer) +	{ +		$this->storage = $storage; +		$this->tracer = $tracer; +	} + +	public function getTests() +	{ +		return $this->storage->getTests(); +	} + +	public function getCommand() +	{ +		$command = $this->storage->getCommand(); +		return empty($command) ? "|testComplete|||" : "{$command[1]}<{$command[0]}>"; +	} + +	public function __call($func, $args) +	{ +		if($func{0} == '_') return; +		 +		$ID = isset($args[0]) ? $args[0] : ""; +		$value = isset($args[1]) ? $args[1] : ""; +		if(strpos(strtolower($func),'htmlpresent') || strpos(strtolower($func),'htmlnotpresent')) +			$ID = htmlspecialchars($ID); +		$command = array($func, $ID, $value); +		$trace = debug_backtrace(); +		 +		if(is_int(strpos(strtolower($func), 'visible'))) +			$this->addCommand(array('pause','500',''),$trace); +		 +		return $this->addCommand($command, $trace); +	} + +	protected function addCommand($command, $trace) +	{ +		list($trace, $test, $suite) = $this->tracer->getTrace($trace); +		$test_id = $this->storage->addTestCase($command, $trace, $test, $suite); +		$this->storage->addCommand($test_id, $command); +	} +} + +class SeleniumTestTrace +{ +	protected $root; + +	public function __construct($root) +	{ +		$this->root = $root; +	} + +	public function getTrace($trace) +	{ +		$group = array_pop($trace); +		$info = $trace[3]; +		$test = $group['args'][0]->getTestList(); +		$i = count($test); +		$name = $test[$i-2].'::'.$test[$i-1]; +		$suite = $test[0]; +		unset($info['object']); +		/* +		for($i = 0; $i < count($info['args']); $i++) +		{ +			if($info['args'][$i] instanceof TControl) +				$info['args'][$i] = $info['args'][$i]->ClientID; +		}*/ +		$file = str_replace($this->root, '', $info['file']); +		$info['file'] = substr($file, 1); + 		return array($info, $name, $suite); +	} +} + +class SimpleSeleniumProxyServer +{ +	protected $runner; +	protected $int; +	protected $result_file; + +	public function __construct($runner, $int, $result_file) +	{ +		$this->int = $int; +		$this->runner = $runner; +		$this->result_file = $result_file; +	} + +	public function proxy() +	{ +		return $this->int; +	} + + +	public static function getInstance($root='/', $result_file='results.dat', $base_dir='selenium/') +	{ +		static $instance; +		if(!isset($instance)) +		{ +			$storage = new SeleniumTestStorage(); +			$tracer = new SeleniumTestTrace($root); +			$interpreter = new SeleneseInterpreter($storage, $tracer); +			$runner = new SeleniumTestRunner(null, $base_dir); +			$instance = new SimpleSeleniumProxyServer($runner, $interpreter, $result_file); +		} +		$instance->serveResults(); +		return $instance; +	} + +	public function handleRequest() +	{ +		$client = new SeleniumTestRunnerServer($this->int, $this->runner); +		$client->serve(); +		return true; +	} + +	public function serveResults() +	{ +		if(isset($_POST['result'])) +		{ +			$result = new SeleniumTestResult(); +			$reporter = new SeleniumHtmlReporter($result); +			$reporter->render(); +			exit(); +		} +	} + +} + +class SeleniumTestSuiteWriter +{ +	protected $suites; +	protected $name; +	protected $runner; + +	function __construct($suites, $name, $runner) +	{ +		$this->suites = $suites; +		$this->name = $name; +		$this->runner = $runner; + +	} + +	protected function renderHeader() +	{ +		$contents = <<<EOD +<html> +<head> +<meta content="text/html; charset=UTF-8" http-equiv="content-type"> +<title>Test Suite</title> + +</head> + +<body> + +<table     cellpadding="1" +           cellspacing="1" +           border="1"> +        <tbody> +            <tr><td><b>{$this->name}</b></td></tr> +EOD; +		return $contents; +	} + +	public function render() +	{ +		echo $this->renderHeader(); +		foreach($this->suites as $name => $suite) +		{ +			$file = $suite[0]['trace']['file']; +			$file = strtr($file,'\\','/'); +			$url = $this->runner->getDriver()."?case={$name}&file={$file}"; +			echo "<tr>\n"; +            echo "<td><a href=\"{$url}\">{$name}</a></td>\n"; +            echo "</tr>\n"; +		} +		echo $this->renderFooter(); +	} + +	protected function getJsTraceInfo() +	{ +		$contents = "var prado_trace = {};\n"; +		foreach($this->suites as $name => $suite) +		{ +			$name = $name; +			$contents .= "prado_trace['{$name}'] = ["; +			$cases = array(); +			foreach($suite as $testcase) +				$cases[] = "'".addslashes(htmlspecialchars(serialize($testcase['trace'])))."'"; +			$contents .= implode(",\n", $cases)."];\n\n"; +		} +		return $contents; +	} + +	protected function renderFooter() +	{ +		$trace = '';//$this->getJsTraceInfo(); +		$contents = <<<EOD +     </tbody> +    </table> +	<script type="text/javascript"> +	/*<![CDATA[*/ +		$trace +	/*]]>*/ +	</script> +</body> +</html> +EOD; +		return $contents; +	} +} + +class SeleniumTestCaseWriter +{ +	protected $case; +	protected $tests; + +	function __construct($case, $tests) +	{ +		$this->case = $case; +		$this->tests = $tests; +	} + +	protected function renderHeader() +	{ +		$contents = <<<EOD +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<html> +<head> +<title>{$this->case}</title> +  <meta content="text/html; charset=UTF-8" http-equiv="content-type"> +</head> +<body> +<table cellpadding="1" cellspacing="1" border="1" id=TABLE1> +  <tbody> +    <tr> +      <td rowspan="1" colspan="3"><strong>{$this->case}</strong></td> +    </tr> +EOD; +		return $contents; +	} + +	public function render() +	{ +		echo $this->renderHeader(); +		foreach($this->tests as $test) +		{ +			$t = $test['test']; +			if($t[0] == "open") +				$t[1] = "<a href=\"{$t[1]}\" target=\"_blank\">{$t[1]}</a>"; +			echo "<tr>\n"; +			echo "<td>{$t[0]}</td>\n"; +			echo "<td>{$t[1]}</td>\n"; +			echo "<td>{$t[2]}</td>\n"; +			echo "</tr>\n"; +		} +		echo $this->renderFooter(); +	} + +	protected function renderFooter() +	{ +		$contents = <<<EOD +  </tbody> +</table> +</body> +</html> +EOD; +		return $contents; +	} +} + +class SeleniumTestRunnerServer +{ +	protected $cases = array(); +	protected $trace = array(); +	protected $name; +	protected $runner; + +	public function __construct($int, $runner) +	{ +		$this->runner = $runner; +		$this->initialize($int); +	} + +	protected function initialize($int) +	{ +		foreach($int->getTests() as $command) +		{ +			$case = $command[5]; +			$this->cases[$case][] = +				array('test' => $command[2], 'trace' => $command[4]); +			if(is_null($this->name)) +				$this->name = $command[6]; +		} +	} + +	function serve() +	{ +		if($this->isTestSuiteRequest()) +		{ +			$testSuite = new SeleniumTestSuiteWriter($this->cases, +								$this->name, $this->runner); +			$testSuite->render(); +		} +		else if($this->isTestCaseRequest()) +		{ +			if(($case = $this->getTestCaseRequest()) !== false) +			{ + +				$testCase = new SeleniumTestCaseWriter($case, $this->getTestCase()); +				$testCase->render(); +			} +		} +		else +		{ +			$this->runner->render(); +		} +	} + +	protected function isTestSuiteRequest() +	{ +		return isset($_GET['testSuites']); +	} + +	protected function isTestCaseRequest() +	{ +		return isset($_GET['case']); +	} + +	public function isClientRequest() +	{ +		return !$this->isTestSuiteRequest() && !$this->isTestCaseRequest(); +	} + +	protected function getTestCaseRequest() +	{ +		$case = $_GET['case']; +		if(isset($this->cases[$case])) +			return $case; +		else return false; +	} + +	protected function getTestCase() +	{ +		$case = $_GET['case']; +		if(isset($this->cases[$case])) +			return $this->cases[$case]; +		else +			return array(); +	} +} + + +class SeleniumTestCase extends UnitTestCase +{ +	protected $selenium; +	protected $Page; + +	function __construct() +	{ +		$server = SimpleSeleniumProxyServer::getInstance(); +		if(!is_null($server)) +			$this->selenium = $server->proxy(); +		parent::__construct(); +	} + +	function getPage($class) +	{ +		$info = new ReflectionClass($class); +		return Prado::getApplication()->getTestPage($info->getFileName()); +	} + +	function __call($func, $args) +	{ +		if(count($args) == 0) +			return $this->selenium->{$func}(); +		else if	(count($args) == 1) +			return $this->selenium->{$func}($args[0]); +		else if (count($args) == 2) +			return $this->selenium->{$func}($args[0], $args[1]); +	} +} + +?>
\ No newline at end of file diff --git a/tests/test_tools/selenium/prado-functional-test.js b/tests/test_tools/selenium/prado-functional-test.js new file mode 100644 index 00000000..306f6a74 --- /dev/null +++ b/tests/test_tools/selenium/prado-functional-test.js @@ -0,0 +1,247 @@ + +/** + * Override selenium implementation. + */ +Selenium.prototype.getAttribute = function(target) { +    return this.page().findAttribute(target); +}; + + +/** + * Override selenium implementation. + */ +Selenium.prototype.isVisible = function(locator) { +    var element; +    element = this.page().findElement(locator);  + +	if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) +		var visibility = element.style["visibility"]; +	else +    	var visibility = this.findEffectiveStyleProperty(element, "visibility"); + +   	var _isDisplayed = this._isDisplayed(element); +    return (visibility != "hidden" && _isDisplayed); +}; + + +/** + * Override selenium implementation. + */ +Selenium.prototype._isDisplayed = function(element) { +    if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) +		var display = element.style["display"]; +	else +		var display = this.findEffectiveStyleProperty(element, "display"); +    if (display == "none") return false; +    if (element.parentNode.style) { +        return this._isDisplayed(element.parentNode); +    } +    return true; +}; + +Selenium.prototype.assertEmptySelection = function(selectLocator, optionLocator)  +{ +	/** +   * Verifies that the selected option of a drop-down satisfies the optionSpecifier. +   *  +   * <p>See the select command for more information about option locators.</p> +   *  +   * @param selectLocator an <a href="#locators">element locator</a> identifying a drop-down menu +   * @param optionLocator an option locator, typically just an option label (e.g. "John Smith") +   */ +    var element = this.page().findElement(selectLocator); +    var locator = this.optionLocatorFactory.fromLocatorString(optionLocator); +   return element.selectedIndex == -1; +} +	 +function runNextTest() { +    if (!runAllTests) +            return; + +    suiteTable = getIframeDocument(getSuiteFrame()).getElementsByTagName("table")[0]; + +    // Do not change the row color of the first row +    if (currentRowInSuite > 0) { +        // Provide test-status feedback +        if (testFailed) { +            setCellColor(suiteTable.rows, currentRowInSuite, 0, failColor); +        } else { +            setCellColor(suiteTable.rows, currentRowInSuite, 0, passColor); +        } + +        // Set the results from the previous test run +        setResultsData(suiteTable, currentRowInSuite); +    } + +    currentRowInSuite++; + +    // If we are done with all of the tests, set the title bar as pass or fail +    if (currentRowInSuite >= suiteTable.rows.length) { +        if (suiteFailed) { +            setCellColor(suiteTable.rows, 0, 0, failColor); +        } else { +            setCellColor(suiteTable.rows, 0, 0, passColor); +        } + +        LOG.warn("next? ", "warn"); +        // If this is an automated run (i.e., build script), then submit +        // the test results by posting to a form + +        postTestResults(suiteFailed, suiteTable); +    } + +    else { +        // Make the current row blue +        setCellColor(suiteTable.rows, currentRowInSuite, 0, workingColor); + +        testLink = suiteTable.rows[currentRowInSuite].cells[0].getElementsByTagName("a")[0]; +        testLink.focus(); + +        var testFrame = getTestFrame(); +        addLoadListener(testFrame, startTest); + +        selenium.browserbot.setIFrameLocation(testFrame, testLink.href); +    } +} + +// Post the results to a servlet, CGI-script, etc.  The URL of the +// results-handler defaults to "/postResults", but an alternative location +// can be specified by providing a "resultsUrl" query parameter. +// +// Parameters passed to the results-handler are: +//      result:         passed/failed depending on whether the suite passed or failed +//      totalTime:      the total running time in seconds for the suite. +// +//      numTestPasses:  the total number of tests which passed. +//      numTestFailures: the total number of tests which failed. +// +//      numCommandPasses: the total number of commands which passed. +//      numCommandFailures: the total number of commands which failed. +//      numCommandErrors: the total number of commands which errored. +// +//      suite:      the suite table, including the hidden column of test results +//      testTable.1 to testTable.N: the individual test tables +// +function postTestResults(suiteFailed, suiteTable) { + +    form = document.createElement("form"); +    document.body.appendChild(form); + +    form.id = "resultsForm"; +    form.method="post"; +    form.target="myiframe"; + +    var resultsUrl = post_results_to; +    if (!resultsUrl) { +        resultsUrl = "./results.php"; +    } + +    var actionAndParameters = resultsUrl.split('?',2); +    form.action = actionAndParameters[0]; +    LOG.warn(form.action) +    var resultsUrlQueryString = actionAndParameters[1]; + +    form.createHiddenField = function(name, value) { +        input = document.createElement("input"); +        input.type = "hidden"; +        input.name = name; +        input.value = value; +        this.appendChild(input); +    }; + +    if (resultsUrlQueryString) { +        var clauses = resultsUrlQueryString.split('&'); +        for (var i = 0; i < clauses.length; i++) { +            var keyValuePair = clauses[i].split('=',2); +            var key = unescape(keyValuePair[0]); +            var value = unescape(keyValuePair[1]); +            form.createHiddenField(key, value); +        } +    } + +    form.createHiddenField("result", suiteFailed == true ? "failed" : "passed"); + +    form.createHiddenField("totalTime", Math.floor((currentTime - startTime) / 1000)); +    form.createHiddenField("numTestPasses", numTestPasses); +    form.createHiddenField("numTestFailures", numTestFailures); +    form.createHiddenField("numCommandPasses", numCommandPasses); +    form.createHiddenField("numCommandFailures", numCommandFailures); +    form.createHiddenField("numCommandErrors", numCommandErrors); + +    // Create an input for each test table.  The inputs are named +    // testTable.1, testTable.2, etc. +    for (rowNum = 1; rowNum < suiteTable.rows.length;rowNum++) { +        // If there is a second column, then add a new input +        if (suiteTable.rows[rowNum].cells.length > 1) { +            var resultCell = suiteTable.rows[rowNum].cells[1]; +            parse_resultCell(resultCell,rowNum,form); +            //form.createHiddenField("tests[]", resultCell.innerHTML); +            // remove the resultCell, so it's not included in the suite HTML +            //resultCell.parentNode.removeChild(resultCell); +        } +    } + +    // Add HTML for the suite itself +    //form.createHiddenField("suite", suiteTable.parentNode.innerHTML); + +    form.submit(); +    document.body.removeChild(form); +} + +function parse_resultCell(resultCell,rowNum,form) +{ +	var div = resultCell.childNodes[0]; +	var table; +	for(var i = 0; i<div.childNodes.length; i++) +	{ +		if(div.childNodes[i].nodeName.toLowerCase() == 'table') +			table = div.childNodes[i]; +	} +	//LOG.info(div.innerHTML); +	var testname = table.rows[0].cells[0].firstChild.innerHTML; +	var resultColor = get_color_status(table.rows[0]); + +	form.createHiddenField("tests["+rowNum+"][testcase]",testname); + +	//var trace = window.testSuiteFrame.prado_trace[testname]; + +	for(var i = 1; i<table.rows.length; i++) +	{ +		var msg = table.rows[i].getAttribute("title"); +		var result = get_color_status(table.rows[i]); +		var action = table.rows[i].cells[0].innerHTML; +		var target = table.rows[i].cells[1].innerHTML; +		var param = table.rows[i].cells[2].innerHTML; +		var id = "tests["+rowNum+"][commands]["+(i-1)+"]"; +		form.createHiddenField(id+"[command]", "|"+action+"|"+target+"|"+param+"|"); +		form.createHiddenField(id+"[result]", result); +		form.createHiddenField(id+"[msg]", msg); +		//form.createHiddenField(id+"[trace]", trace[i-1]); +	} +} + +function get_color_status(element) +{ +	var color = element.getAttribute("bgcolor"); +	if(color == passColor) return "passed"; +	if(color == failColor) return "failed"; +	if(color == doneColor) return "done"; +	return ""; +} + + + + +Selenium.prototype.assertHTMLPresent = function(expectedValue) { +    var actualValue = this.page().currentDocument.body.innerHTML; +   if(actualValue.indexOf(expectedValue) >= 0) +	   return; +   Assert.fail("Unable to find '"+(expectedValue.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\""))+"' in document.body"); +}; + +Selenium.prototype.assertHTMLNotPresent = function(expectedValue) { +    var actualValue = this.page().currentDocument.body.innerHTML; +   if(actualValue.indexOf(expectedValue) < 0) +	   return; +   Assert.fail("'"+(expectedValue.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\""))+"' was found in document.body"); +};
\ No newline at end of file | 
