diff options
author | wei <> | 2006-09-30 05:18:39 +0000 |
---|---|---|
committer | wei <> | 2006-09-30 05:18:39 +0000 |
commit | 7eafd95bfa21c9c8603e9c437a9d96dc880d92e7 (patch) | |
tree | 9ebe3012fc2a931a7a9e1d0c1b3eef33b1ed16ba /tests/test_tools/selenium/core/scripts | |
parent | 36a61db3144c111b722db89baf5a1d55c1c9a6b2 (diff) |
Update selenium to 0.8
Diffstat (limited to 'tests/test_tools/selenium/core/scripts')
17 files changed, 5996 insertions, 1836 deletions
diff --git a/tests/test_tools/selenium/core/scripts/htmlutils.js b/tests/test_tools/selenium/core/scripts/htmlutils.js index fcb1ee44..4d78e1a6 100644 --- a/tests/test_tools/selenium/core/scripts/htmlutils.js +++ b/tests/test_tools/selenium/core/scripts/htmlutils.js @@ -14,20 +14,21 @@ * 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 + +// This script contains a badly-organised collection of miscellaneous +// functions that really better homes. String.prototype.trim = function() { - var result = this.replace( /^\s+/g, "" );// strip leading - return result.replace( /\s+$/g, "" );// strip trailing + 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); + return this.charAt(0).toLowerCase() + this.substr(1); }; String.prototype.ucfirst = function() { - return this.charAt(0).toUpperCase() + this.substr(1); + return this.charAt(0).toUpperCase() + this.substr(1); }; String.prototype.startsWith = function(str) { return this.indexOf(str) == 0; @@ -37,23 +38,12 @@ String.prototype.startsWith = function(str) { 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) - { + var isRecentFirefox = (browserVersion.isFirefox && browserVersion.firefoxVersion >= "1.5"); + if (isRecentFirefox || browserVersion.isKonqueror || browserVersion.isSafari || browserVersion.isOpera) { + text = getTextContent(element); + } else if (element.textContent) { text = element.textContent; - } - else if(element.innerText) - { + } else if (element.innerText) { text = element.innerText; } @@ -63,62 +53,34 @@ function getText(element) { 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 getTextContent(element, preformatted) { + if (element.nodeType == 3 /*Node.TEXT_NODE*/) { + var text = element.data; + if (!preformatted) { + text = text.replace(/\n|\r|\t/g, " "); + } + return text; } - -} - -function tagIs(element, tags) -{ - var tag = element.tagName; - for (var i = 0; i < tags.length; i++) - { - if (tags[i] == tag) - { - return true; + if (element.nodeType == 1 /*Node.ELEMENT_NODE*/) { + var childrenPreformatted = preformatted || (element.tagName == "PRE"); + var text = ""; + for (var i = 0; i < element.childNodes.length; i++) { + var child = element.childNodes.item(i); + text += getTextContent(child, childrenPreformatted); } + // Handle block elements that introduce newlines + // -- From HTML spec: + //<!ENTITY % block + // "P | %heading; | %list; | %preformatted; | DL | DIV | NOSCRIPT | + // BLOCKQUOTE | F:wORM | HR | TABLE | FIELDSET | ADDRESS"> + // + // TODO: should potentially introduce multiple newlines to separate blocks + if (element.tagName == "P" || element.tagName == "BR" || element.tagName == "HR" || element.tagName == "DIV") { + text += "\n"; + } + return text; } - return false; + return ''; } /** @@ -145,25 +107,36 @@ function normalizeSpaces(text) 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, " "); + var nbspPattern = new RegExp(String.fromCharCode(160), "g"); + if (browserVersion.isSafari) { + return replaceAll(text, String.fromCharCode(160), " "); + } else { + return text.replace(nbspPattern, " "); + } +} + +function replaceAll(text, oldText, newText) { + while (text.indexOf(oldText) != -1) { + text = text.replace(oldText, newText); + } + return text; } + function xmlDecode(text) { - text = text.replace(/"/g, '"'); - text = text.replace(/'/g, "'"); - text = text.replace(/</g, "<"); - text = text.replace(/>/g, ">"); - text = text.replace(/&/g, "&"); - return text; + 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) { + if (element.textContent) { element.textContent = text; - } else if(element.innerText) { + } else if (element.innerText) { element.innerText = text; } } @@ -191,43 +164,105 @@ function triggerEvent(element, eventType, canBubble) { } } -function triggerKeyEvent(element, eventType, keycode, canBubble) { +function getKeyCodeFromKeySequence(keySequence) { + var match = /^\\(\d{1,3})$/.exec(keySequence); + if (match != null) { + return match[1]; + } + match = /^.$/.exec(keySequence); + if (match != null) { + return match[0].charCodeAt(0); + } + // this is for backward compatibility with existing tests + // 1 digit ascii codes will break however because they are used for the digit chars + match = /^\d{2,3}$/.exec(keySequence); + if (match != null) { + return match[0]; + } + throw SeleniumError("invalid keySequence"); +} + +function triggerKeyEvent(element, eventType, keySequence, canBubble) { + var keycode = getKeyCodeFromKeySequence(keySequence); canBubble = (typeof(canBubble) == undefined) ? true : canBubble; if (element.fireEvent) { - keyEvent = parent.frames['myiframe'].document.createEventObject(); - keyEvent.keyCode=keycode; - element.fireEvent('on' + eventType, keyEvent); + keyEvent = element.ownerDocument.createEventObject(); + keyEvent.keyCode = keycode; + element.fireEvent('on' + eventType, keyEvent); } else { - var evt; - if( window.KeyEvent ) { - evt = document.createEvent('KeyEvents'); - evt.initKeyEvent(eventType, true, true, window, false, false, false, false, keycode, keycode); - } else { - evt = document.createEvent('UIEvents'); - evt.initUIEvent( eventType, true, true, window, 1 ); - evt.keyCode = keycode; - } - + 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) { +function triggerMouseEvent(element, eventType, canBubble, clientX, clientY) { + clientX = clientX ? clientX : 0; + clientY = clientY ? clientY : 0; + + // TODO: set these attributes -- they don't seem to be needed by the initial test cases, but that could change... + var screenX = 0; + var screenY = 0; + canBubble = (typeof(canBubble) == undefined) ? true : canBubble; if (element.fireEvent) { - element.fireEvent('on' + eventType); + LOG.error("element has fireEvent"); + if (!screenX && !screenY && !clientX && !clientY) { + element.fireEvent('on' + eventType); + } + else { + var ieEvent = element.ownerDocument.createEventObject(); + ieEvent.detail = 0; + ieEvent.screenX = screenX; + ieEvent.screenY = screenY; + ieEvent.clientX = clientX; + ieEvent.clientY = clientY; + ieEvent.ctrlKey = false; + ieEvent.altKey = false; + ieEvent.shiftKey = false; + ieEvent.metaKey = false; + ieEvent.button = 1; + ieEvent.relatedTarget = null; + + // when we go this route, window.event is never set to contain the event we have just created. + // ideally we could just slide it in as follows in the try-block below, but this normally + // doesn't work. This is why I try to avoid this code path, which is only required if we need to + // set attributes on the event (e.g., clientX). + try { + window.event = ieEvent; + } + catch(e) { + // getting an "Object does not support this action or property" error. Save the event away + // for future reference. + // TODO: is there a way to update window.event? + + // work around for http://jira.openqa.org/browse/SEL-280 -- make the event available somewhere: + selenium.browserbot.getCurrentWindow().selenium_event = ieEvent; + } + element.fireEvent('on' + eventType, ieEvent); + } } else { + LOG.error("element doesn't have fireEvent"); var evt = document.createEvent('MouseEvents'); if (evt.initMouseEvent) { - evt.initMouseEvent(eventType, canBubble, true, document.defaultView, 1, 0, 0, 0, 0, false, false, false, false, 0, null) + LOG.error("element has initMouseEvent"); + //Safari + evt.initMouseEvent(eventType, canBubble, true, document.defaultView, 1, screenX, screenY, clientX, clientY, false, false, false, false, 0, null) } - else - { - // Safari + else { + LOG.error("element doesen't has initMouseEvent"); // TODO we should be initialising other mouse-event related attributes here evt.initEvent(eventType, canBubble, true); } @@ -236,6 +271,7 @@ function triggerMouseEvent(element, eventType, canBubble) { } function removeLoadListener(element, command) { + LOG.info('Removing loadListenter for ' + element + ', ' + command); if (window.removeEventListener) element.removeEventListener("load", command, true); else if (window.detachEvent) @@ -243,17 +279,11 @@ function removeLoadListener(element, command) { } function addLoadListener(element, command) { + LOG.info('Adding loadListenter for ' + element + ', ' + command); if (window.addEventListener && !browserVersion.isOpera) - element.addEventListener("load",command, true); + 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); + element.attachEvent("onload", command); } /** @@ -261,19 +291,19 @@ function addUnloadListener(element, command) { * 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'; + 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 ""; + var bases = document.getElementsByTagName("base"); + if (bases && bases.length && bases[0].href) { + return bases[0].href; + } + return ""; } function describe(object, delimiter) { @@ -291,10 +321,15 @@ PatternMatcher.prototype = { selectStrategy: function(pattern) { this.pattern = pattern; - var strategyName = 'glob'; // by default + var strategyName = 'glob'; + // by default if (/^([a-z-]+):(.*)/.test(pattern)) { - strategyName = RegExp.$1; - pattern = RegExp.$2; + var possibleNewStrategyName = RegExp.$1; + var possibleNewPattern = RegExp.$2; + if (PatternMatcher.strategies[possibleNewStrategyName]) { + strategyName = possibleNewStrategyName; + pattern = possibleNewPattern; + } } var matchStrategy = PatternMatcher.strategies[strategyName]; if (!matchStrategy) { @@ -320,9 +355,9 @@ PatternMatcher.matches = function(pattern, actual) { PatternMatcher.strategies = { - /** - * Exact matching, e.g. "exact:***" - */ +/** + * Exact matching, e.g. "exact:***" + */ exact: function(expected) { this.expected = expected; this.matches = function(actual) { @@ -330,9 +365,9 @@ PatternMatcher.strategies = { }; }, - /** - * Match by regular expression, e.g. "regexp:^[0-9]+$" - */ +/** + * Match by regular expression, e.g. "regexp:^[0-9]+$" + */ regexp: function(regexpString) { this.regexp = new RegExp(regexpString); this.matches = function(actual) { @@ -340,17 +375,24 @@ PatternMatcher.strategies = { }; }, - /** - * "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. - */ + regex: function(regexpString) { + this.regexp = new RegExp(regexpString); + this.matches = function(actual) { + return this.regexp.test(actual); + }; + }, + +/** + * "globContains" (aka "wildmat") patterns, e.g. "glob:one,two,*", + * but don't require a perfect match; instead succeed if actual + * contains something that matches globString. + * Making this distinction is motivated by a bug in IE6 which + * leads to the browser hanging if we implement *TextPresent tests + * by just matching against a regular expression beginning and + * ending with ".*". The globcontains strategy allows us to satisfy + * the functional needs of the *TextPresent ops more efficiently + * and so avoid running into this IE6 freeze. + */ globContains: function(globString) { this.regexp = new RegExp(PatternMatcher.regexpFromGlobContains(globString)); this.matches = function(actual) { @@ -359,9 +401,9 @@ PatternMatcher.strategies = { }, - /** - * "glob" (aka "wildmat") patterns, e.g. "glob:one,two,*" - */ +/** + * "glob" (aka "wildmat") patterns, e.g. "glob:one,two,*" + */ glob: function(globString) { this.regexp = new RegExp(PatternMatcher.regexpFromGlob(globString)); this.matches = function(actual) { @@ -393,9 +435,9 @@ var Assert = { throw new AssertionFailedError(message); }, - /* - * Assert.equals(comment?, expected, actual) - */ +/* +* Assert.equals(comment?, expected, actual) +*/ equals: function() { var args = new AssertionArguments(arguments); if (args.expected === args.actual) { @@ -406,9 +448,9 @@ var Assert = { "' but was '" + args.actual + "'"); }, - /* - * Assert.matches(comment?, pattern, actual) - */ +/* +* Assert.matches(comment?, pattern, actual) +*/ matches: function() { var args = new AssertionArguments(arguments); if (PatternMatcher.matches(args.expected, args.actual)) { @@ -419,9 +461,9 @@ var Assert = { "' did not match '" + args.expected + "'"); }, - /* - * Assert.notMtches(comment?, pattern, actual) - */ +/* +* Assert.notMtches(comment?, pattern, actual) +*/ notMatches: function() { var args = new AssertionArguments(arguments); if (!PatternMatcher.matches(args.expected, args.actual)) { @@ -447,8 +489,6 @@ function AssertionArguments(args) { } } - - function AssertionFailedError(message) { this.isAssertionFailedError = true; this.isSeleniumError = true; @@ -460,4 +500,130 @@ function SeleniumError(message) { var error = new Error(message); error.isSeleniumError = true; return error; -}; +} + +var Effect = new Object(); + +Object.extend(Effect, { + highlight : function(element) { + var highLightColor = "yellow"; + if (element.originalColor == undefined) { // avoid picking up highlight + element.originalColor = Element.getStyle(element, "background-color"); + } + Element.setStyle(element, {"background-color" : highLightColor}); + window.setTimeout(function() { + //if element is orphan, probably page of it has already gone, so ignore + if (!element.parentNode) { + return; + } + Element.setStyle(element, {"background-color" : element.originalColor}); + }, 200); + } +}); + + +// for use from vs.2003 debugger +function objToString(obj) { + var s = ""; + for (key in obj) { + var line = key + "->" + obj[key]; + line.replace("\n", " "); + s += line + "\n"; + } + return s; +} + +var seenReadyStateWarning = false; + +function openSeparateApplicationWindow(url) { + // resize the Selenium window itself + window.resizeTo(1200, 500); + window.moveTo(window.screenX, 0); + + var appWindow = window.open(url + '?start=true', 'main'); + try { + var windowHeight = 500; + if (window.outerHeight) { + windowHeight = window.outerHeight; + } else if (document.documentElement && document.documentElement.offsetHeight) { + windowHeight = document.documentElement.offsetHeight; + } + + if (window.screenLeft && !window.screenX) window.screenX = window.screenLeft; + if (window.screenTop && !window.screenY) window.screenY = window.screenTop; + + appWindow.resizeTo(1200, screen.availHeight - windowHeight - 60); + appWindow.moveTo(window.screenX, window.screenY + windowHeight + 25); + } catch (e) { + LOG.error("Couldn't resize app window"); + LOG.exception(e); + } + + + if (window.document.readyState == null && !seenReadyStateWarning) { + alert("Beware! Mozilla bug 300992 means that we can't always reliably detect when a new page has loaded. Install the Selenium IDE extension or the readyState extension available from selenium.openqa.org to make page load detection more reliable."); + seenReadyStateWarning = true; + } + + return appWindow; +} + +var URLConfiguration = Class.create(); +Object.extend(URLConfiguration.prototype, { + initialize: function() { + }, + _isQueryParameterTrue: function (name) { + var parameterValue = this._getQueryParameter(name); + if (parameterValue == null) return false; + if (parameterValue.toLowerCase() == "true") return true; + if (parameterValue.toLowerCase() == "on") return true; + return false; + }, + + _getQueryParameter: function(searchKey) { + var str = this.queryString + if (str == null) return null; + var clauses = str.split('&'); + for (var i = 0; i < clauses.length; i++) { + var keyValuePair = clauses[i].split('=', 2); + var key = unescape(keyValuePair[0]); + if (key == searchKey) { + return unescape(keyValuePair[1]); + } + } + return null; + }, + + _extractArgs: function() { + var str = SeleniumHTARunner.commandLine; + if (str == null || str == "") return new Array(); + var matches = str.match(/(?:\"([^\"]+)\"|(?!\"([^\"]+)\")(\S+))/g); + // We either want non quote stuff ([^"]+) surrounded by quotes + // or we want to look-ahead, see that the next character isn't + // a quoted argument, and then grab all the non-space stuff + // this will return for the line: "foo" bar + // the results "\"foo\"" and "bar" + + // So, let's unquote the quoted arguments: + var args = new Array; + for (var i = 0; i < matches.length; i++) { + args[i] = matches[i]; + args[i] = args[i].replace(/^"(.*)"$/, "$1"); + } + return args; + }, + + isMultiWindowMode:function() { + return this._isQueryParameterTrue('multiWindow'); + } +}); + + +function safeScrollIntoView(element) { + if (element.scrollIntoView) { + element.scrollIntoView(false); + return; + } + // TODO: work out how to scroll browsers that don't support + // scrollIntoView (like Konqueror) +} diff --git a/tests/test_tools/selenium/core/scripts/injection.html b/tests/test_tools/selenium/core/scripts/injection.html new file mode 100644 index 00000000..d41fbe69 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/injection.html @@ -0,0 +1,72 @@ +<script language="JavaScript"> + if (window["selenium_has_been_loaded_into_this_window"]==null) + { +__SELENIUM_JS__ + +// Some background on the code below: broadly speaking, where we are relative to other windows +// when running in proxy injection mode depends on whether we are in a frame set file or not. +// +// In regular HTML files, the selenium JavaScript is injected into an iframe called "selenium" +// in order to reduce its impact on the JavaScript environment (through namespace pollution, +// etc.). So in regular HTML files, we need to look at the parent of the current window when we want +// a handle to, e.g., the application window. +// +// In frame set files, we can't use an iframe, so we put the JavaScript in the head element and share +// the window with the frame set. So in this case, we need to look at the current window, not the +// parent when looking for, e.g., the application window. (TODO: Perhaps I should have just +// assigned a regular frame for selenium?) +// +BrowserBot.prototype.getContentWindow = function() { + if (window["seleniumInSameWindow"] != null) return window; + return window.parent; +}; + +BrowserBot.prototype.getTargetWindow = function(windowName) { + if (window["seleniumInSameWindow"] != null) return window; + return window.parent; +}; + +BrowserBot.prototype.getCurrentWindow = function() { + if (window["seleniumInSameWindow"] != null) return window; + return window.parent; +}; + +LOG.openLogWindow = function(message, className) { + // disable for now +}; + +BrowserBot.prototype.relayToRC = function(name) { + var object = eval(name); + var s = 'state:' + serializeObject(name, object) + "\n"; + sendToRC(s); +} + +BrowserBot.prototype.relayBotToRC = function(s) { + this.relayToRC("selenium." + s); +} + +function selenium_frameRunTest(oldOnLoadRoutine) { + if (oldOnLoadRoutine) { + eval(oldOnLoadRoutine); + } + runSeleniumTest(); +} + +function seleniumOnLoad() { + injectedSessionId = @SESSION_ID@; + window["selenium_has_been_loaded_into_this_window"] = true; + runSeleniumTest(); +} + +if (window.addEventListener) { + window.addEventListener("load", seleniumOnLoad, false); // firefox +} else if (window.attachEvent){ + window.attachEvent("onload", seleniumOnLoad); // IE +} +else { + throw "causing a JavaScript error to tell the world that I did not arrange to be run on load"; +} + +injectedSessionId = @SESSION_ID@; +} +</script> diff --git a/tests/test_tools/selenium/core/scripts/injection_iframe.html b/tests/test_tools/selenium/core/scripts/injection_iframe.html new file mode 100644 index 00000000..bc26e859 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/injection_iframe.html @@ -0,0 +1,7 @@ +<script language="JavaScript"> + // Ideally I would avoid polluting the namespace by enclosing this snippet with + // curly braces, but I want to make it easy to look at what URL I used for anyone + // who is interested in looking into http://jira.openqa.org/browse/SRC-101: + var _sel_url_ = "http://" + location.host + "/selenium-server/core/scripts/injection.html"; + document.write('<iframe name="selenium" width=0 height=0 id="selenium" src="' + _sel_url_ + '"></iframe>'); +</script> diff --git a/tests/test_tools/selenium/core/scripts/js2html.js b/tests/test_tools/selenium/core/scripts/js2html.js new file mode 100644 index 00000000..a384dce3 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/js2html.js @@ -0,0 +1,70 @@ +/*
+
+This is an experiment in using the Narcissus JavaScript engine
+to allow Selenium scripts to be written in plain JavaScript.
+
+The 'jsparse' function will compile each high level block into a Selenium table script.
+
+
+TODO:
+1) Test! (More browsers, more sample scripts)
+2) Stepping and walking lower levels of the parse tree
+3) Calling Selenium commands directly from JavaScript
+4) Do we want comments to appear in the TestRunner?
+5) Fix context so variables don't have to be global
+ For now, variables defined with "var" won't be found
+ if used later on in a script.
+6) Fix formatting
+*/
+
+
+function jsparse() {
+ var script = document.getElementById('sejs')
+ var fname = 'javascript script';
+ parse_result = parse(script.text, fname, 0);
+
+ var x2 = new ExecutionContext(GLOBAL_CODE);
+ ExecutionContext.current = x2;
+
+
+ var new_test_source = '';
+ var new_line = '';
+
+ for (i=0;i<parse_result.$length;i++){
+ var the_start = parse_result[i].start;
+ var the_end;
+ if ( i == (parse_result.$length-1)) {
+ the_end = parse_result.tokenizer.source.length;
+ } else {
+ the_end = parse_result[i+1].start;
+ }
+
+ var script_fragment = parse_result.tokenizer.source.slice(the_start,the_end)
+
+ new_line = '<tr><td style="display:none;" class="js">getEval</td>' +
+ '<td style="display:none;">currentTest.doNextCommand()</td>' +
+ '<td style="white-space: pre;">' + script_fragment + '</td>' +
+ '<td></td></tr>\n';
+ new_test_source += new_line;
+ //eval(script_fragment);
+
+
+ };
+
+
+
+ execute(parse_result,x2)
+
+ // Create HTML Table
+ body = document.body
+ body.innerHTML += "<table class='selenium' id='se-js-table'>"+
+ "<tbody>" +
+ "<tr><td>// " + document.title + "</td></tr>" +
+ new_test_source +
+ "</tbody" +
+ "</table>";
+
+ //body.innerHTML = "<pre>" + parse_result + "</pre>"
+}
+
+
diff --git a/tests/test_tools/selenium/core/scripts/narcissus-defs.js b/tests/test_tools/selenium/core/scripts/narcissus-defs.js new file mode 100644 index 00000000..5869397d --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/narcissus-defs.js @@ -0,0 +1,175 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (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.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is the Narcissus JavaScript engine. + * + * The Initial Developer of the Original Code is + * Brendan Eich <brendan@mozilla.org>. + * Portions created by the Initial Developer are Copyright (C) 2004 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/* + * Narcissus - JS implemented in JS. + * + * Well-known constants and lookup tables. Many consts are generated from the + * tokens table via eval to minimize redundancy, so consumers must be compiled + * separately to take advantage of the simple switch-case constant propagation + * done by SpiderMonkey. + */ + +// jrh +//module('JS.Defs'); + +GLOBAL = this; + +var tokens = [ + // End of source. + "END", + + // Operators and punctuators. Some pair-wise order matters, e.g. (+, -) + // and (UNARY_PLUS, UNARY_MINUS). + "\n", ";", + ",", + "=", + "?", ":", "CONDITIONAL", + "||", + "&&", + "|", + "^", + "&", + "==", "!=", "===", "!==", + "<", "<=", ">=", ">", + "<<", ">>", ">>>", + "+", "-", + "*", "/", "%", + "!", "~", "UNARY_PLUS", "UNARY_MINUS", + "++", "--", + ".", + "[", "]", + "{", "}", + "(", ")", + + // Nonterminal tree node type codes. + "SCRIPT", "BLOCK", "LABEL", "FOR_IN", "CALL", "NEW_WITH_ARGS", "INDEX", + "ARRAY_INIT", "OBJECT_INIT", "PROPERTY_INIT", "GETTER", "SETTER", + "GROUP", "LIST", + + // Terminals. + "IDENTIFIER", "NUMBER", "STRING", "REGEXP", + + // Keywords. + "break", + "case", "catch", "const", "continue", + "debugger", "default", "delete", "do", + "else", "enum", + "false", "finally", "for", "function", + "if", "in", "instanceof", + "new", "null", + "return", + "switch", + "this", "throw", "true", "try", "typeof", + "var", "void", + "while", "with", + // Extensions + "require", "bless", "mixin", "import" +]; + +// Operator and punctuator mapping from token to tree node type name. +// NB: superstring tokens (e.g., ++) must come before their substring token +// counterparts (+ in the example), so that the opRegExp regular expression +// synthesized from this list makes the longest possible match. +var opTypeNames = { + '\n': "NEWLINE", + ';': "SEMICOLON", + ',': "COMMA", + '?': "HOOK", + ':': "COLON", + '||': "OR", + '&&': "AND", + '|': "BITWISE_OR", + '^': "BITWISE_XOR", + '&': "BITWISE_AND", + '===': "STRICT_EQ", + '==': "EQ", + '=': "ASSIGN", + '!==': "STRICT_NE", + '!=': "NE", + '<<': "LSH", + '<=': "LE", + '<': "LT", + '>>>': "URSH", + '>>': "RSH", + '>=': "GE", + '>': "GT", + '++': "INCREMENT", + '--': "DECREMENT", + '+': "PLUS", + '-': "MINUS", + '*': "MUL", + '/': "DIV", + '%': "MOD", + '!': "NOT", + '~': "BITWISE_NOT", + '.': "DOT", + '[': "LEFT_BRACKET", + ']': "RIGHT_BRACKET", + '{': "LEFT_CURLY", + '}': "RIGHT_CURLY", + '(': "LEFT_PAREN", + ')': "RIGHT_PAREN" +}; + +// Hash of keyword identifier to tokens index. NB: we must null __proto__ to +// avoid toString, etc. namespace pollution. +var keywords = {__proto__: null}; + +// Define const END, etc., based on the token names. Also map name to index. +var consts = " "; +for (var i = 0, j = tokens.length; i < j; i++) { + if (i > 0) + consts += "; "; + var t = tokens[i]; + if (/^[a-z]/.test(t)) { + consts += t.toUpperCase(); + keywords[t] = i; + } else { + consts += (/^\W/.test(t) ? opTypeNames[t] : t); + } + consts += " = " + i; + tokens[t] = i; +} +eval(consts + ";"); + +// Map assignment operators to their indexes in the tokens array. +var assignOps = ['|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%']; + +for (i = 0, j = assignOps.length; i < j; i++) { + t = assignOps[i]; + assignOps[t] = tokens[t]; +} diff --git a/tests/test_tools/selenium/core/scripts/narcissus-exec.js b/tests/test_tools/selenium/core/scripts/narcissus-exec.js new file mode 100644 index 00000000..e2c88f81 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/narcissus-exec.js @@ -0,0 +1,1054 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * vim: set ts=4 sw=4 et tw=80: + * + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (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.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is the Narcissus JavaScript engine. + * + * The Initial Developer of the Original Code is + * Brendan Eich <brendan@mozilla.org>. + * Portions created by the Initial Developer are Copyright (C) 2004 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/* + * Narcissus - JS implemented in JS. + * + * Execution of parse trees. + * + * Standard classes except for eval, Function, Array, and String are borrowed + * from the host JS environment. Function is metacircular. Array and String + * are reflected via wrapping the corresponding native constructor and adding + * an extra level of prototype-based delegation. + */ + +// jrh +//module('JS.Exec'); +// end jrh + +GLOBAL_CODE = 0; EVAL_CODE = 1; FUNCTION_CODE = 2; + +function ExecutionContext(type) { + this.type = type; +} + +// jrh +var agenda = new Array(); +var skip_setup = 0; +// end jrh + +var global = { + // Value properties. + NaN: NaN, Infinity: Infinity, undefined: undefined, + alert : function(msg) { alert(msg) }, + confirm : function(msg) { return confirm(msg) }, + document : document, + window : window, + // jrh + //debug: window.open('','debugwindow','width=600,height=400,scrollbars=yes,resizable=yes'), + // end jrh + navigator : navigator, + XMLHttpRequest : function() { return new XMLHttpRequest() }, + // Function properties. + eval: function(s) { + if (typeof s != "string") { + return s; + } + + var x = ExecutionContext.current; + var x2 = new ExecutionContext(EVAL_CODE); + x2.thisObject = x.thisObject; + x2.caller = x.caller; + x2.callee = x.callee; + x2.scope = x.scope; + ExecutionContext.current = x2; + try { + execute(parse(s), x2); + } catch (e) { + x.result = x2.result; + throw e; + } finally { + ExecutionContext.current = x; + } + return x2.result; + }, + parseInt: parseInt, parseFloat: parseFloat, + isNaN: isNaN, isFinite: isFinite, + decodeURI: decodeURI, encodeURI: encodeURI, + decodeURIComponent: decodeURIComponent, + encodeURIComponent: encodeURIComponent, + + // Class constructors. Where ECMA-262 requires C.length == 1, we declare + // a dummy formal parameter. + Object: Object, + Function: function(dummy) { + var p = "", b = "", n = arguments.length; + if (n) { + var m = n - 1; + if (m) { + p += arguments[0]; + for (var k = 1; k < m; k++) + p += "," + arguments[k]; + } + b += arguments[m]; + } + + // XXX We want to pass a good file and line to the tokenizer. + // Note the anonymous name to maintain parity with Spidermonkey. + var t = new Tokenizer("anonymous(" + p + ") {" + b + "}"); + + // NB: Use the STATEMENT_FORM constant since we don't want to push this + // function onto the null compilation context. + var f = FunctionDefinition(t, null, false, STATEMENT_FORM); + var s = {object: global, parent: null}; + return new FunctionObject(f, s); + }, + Array: function(dummy) { + // Array when called as a function acts as a constructor. + return GLOBAL.Array.apply(this, arguments); + }, + String: function(s) { + // Called as function or constructor: convert argument to string type. + s = arguments.length ? "" + s : ""; + if (this instanceof String) { + // Called as constructor: save the argument as the string value + // of this String object and return this object. + this.value = s; + return this; + } + return s; + }, + Boolean: Boolean, Number: Number, Date: Date, RegExp: RegExp, + Error: Error, EvalError: EvalError, RangeError: RangeError, + ReferenceError: ReferenceError, SyntaxError: SyntaxError, + TypeError: TypeError, URIError: URIError, + + // Other properties. + Math: Math, + + // Extensions to ECMA. + //snarf: snarf, + evaluate: evaluate, + load: function(s) { + if (typeof s != "string") + return s; + var req = new XMLHttpRequest(); + req.open('GET', s, false); + req.send(null); + + evaluate(req.responseText, s, 1) + }, + print: print, version: null +}; + +// jrh +//global.debug.document.body.innerHTML = '' +// end jrh + +// Helper to avoid Object.prototype.hasOwnProperty polluting scope objects. +function hasDirectProperty(o, p) { + return Object.prototype.hasOwnProperty.call(o, p); +} + +// Reflect a host class into the target global environment by delegation. +function reflectClass(name, proto) { + var gctor = global[name]; + gctor.prototype = proto; + proto.constructor = gctor; + return proto; +} + +// Reflect Array -- note that all Array methods are generic. +reflectClass('Array', new Array); + +// Reflect String, overriding non-generic methods. +var gSp = reflectClass('String', new String); +gSp.toSource = function () { return this.value.toSource(); }; +gSp.toString = function () { return this.value; }; +gSp.valueOf = function () { return this.value; }; +global.String.fromCharCode = String.fromCharCode; + +var XCp = ExecutionContext.prototype; +ExecutionContext.current = XCp.caller = XCp.callee = null; +XCp.scope = {object: global, parent: null}; +XCp.thisObject = global; +XCp.result = undefined; +XCp.target = null; +XCp.ecmaStrictMode = false; + +function Reference(base, propertyName, node) { + this.base = base; + this.propertyName = propertyName; + this.node = node; +} + +Reference.prototype.toString = function () { return this.node.getSource(); } + +function getValue(v) { + if (v instanceof Reference) { + if (!v.base) { + throw new ReferenceError(v.propertyName + " is not defined", + v.node.filename(), v.node.lineno); + } + return v.base[v.propertyName]; + } + return v; +} + +function putValue(v, w, vn) { + if (v instanceof Reference) + return (v.base || global)[v.propertyName] = w; + throw new ReferenceError("Invalid assignment left-hand side", + vn.filename(), vn.lineno); +} + +function isPrimitive(v) { + var t = typeof v; + return (t == "object") ? v === null : t != "function"; +} + +function isObject(v) { + var t = typeof v; + return (t == "object") ? v !== null : t == "function"; +} + +// If r instanceof Reference, v == getValue(r); else v === r. If passed, rn +// is the node whose execute result was r. +function toObject(v, r, rn) { + switch (typeof v) { + case "boolean": + return new global.Boolean(v); + case "number": + return new global.Number(v); + case "string": + return new global.String(v); + case "function": + return v; + case "object": + if (v !== null) + return v; + } + var message = r + " (type " + (typeof v) + ") has no properties"; + throw rn ? new TypeError(message, rn.filename(), rn.lineno) + : new TypeError(message); +} + +function execute(n, x) { + if (!this.new_block) + new_block = new Array(); + //alert (n) + var a, f, i, j, r, s, t, u, v; + switch (n.type) { + case FUNCTION: + if (n.functionForm != DECLARED_FORM) { + if (!n.name || n.functionForm == STATEMENT_FORM) { + v = new FunctionObject(n, x.scope); + if (n.functionForm == STATEMENT_FORM) + x.scope.object[n.name] = v; + } else { + t = new Object; + x.scope = {object: t, parent: x.scope}; + try { + v = new FunctionObject(n, x.scope); + t[n.name] = v; + } finally { + x.scope = x.scope.parent; + } + } + } + break; + + case SCRIPT: + t = x.scope.object; + a = n.funDecls; + for (i = 0, j = a.length; i < j; i++) { + s = a[i].name; + f = new FunctionObject(a[i], x.scope); + t[s] = f; + } + a = n.varDecls; + for (i = 0, j = a.length; i < j; i++) { + u = a[i]; + s = u.name; + if (u.readOnly && hasDirectProperty(t, s)) { + throw new TypeError("Redeclaration of const " + s, + u.filename(), u.lineno); + } + if (u.readOnly || !hasDirectProperty(t, s)) { + t[s] = null; + } + } + // FALL THROUGH + + case BLOCK: + for (i = 0, j = n.$length; i < j; i++) { + //jrh + //execute(n[i], x); + //new_block.unshift([n[i], x]); + new_block.push([n[i], x]); + } + new_block.reverse(); + agenda = agenda.concat(new_block); + //agenda = new_block.concat(agenda) + // end jrh + break; + + case IF: + if (getValue(execute(n.condition, x))) + execute(n.thenPart, x); + else if (n.elsePart) + execute(n.elsePart, x); + break; + + case SWITCH: + s = getValue(execute(n.discriminant, x)); + a = n.cases; + var matchDefault = false; + switch_loop: + for (i = 0, j = a.length; ; i++) { + if (i == j) { + if (n.defaultIndex >= 0) { + i = n.defaultIndex - 1; // no case matched, do default + matchDefault = true; + continue; + } + break; // no default, exit switch_loop + } + t = a[i]; // next case (might be default!) + if (t.type == CASE) { + u = getValue(execute(t.caseLabel, x)); + } else { + if (!matchDefault) // not defaulting, skip for now + continue; + u = s; // force match to do default + } + if (u === s) { + for (;;) { // this loop exits switch_loop + if (t.statements.length) { + try { + execute(t.statements, x); + } catch (e) { + if (!(e == BREAK && x.target == n)) { throw e } + break switch_loop; + } + } + if (++i == j) + break switch_loop; + t = a[i]; + } + // NOT REACHED + } + } + break; + + case FOR: + // jrh + // added "skip_setup" so initialization doesn't get called + // on every call.. + if (!skip_setup) + n.setup && getValue(execute(n.setup, x)); + // FALL THROUGH + case WHILE: + // jrh + //while (!n.condition || getValue(execute(n.condition, x))) { + if (!n.condition || getValue(execute(n.condition, x))) { + try { + // jrh + //execute(n.body, x); + new_block.push([n.body, x]); + agenda.push([n.body, x]) + //agenda.unshift([n.body, x]) + // end jrh + } catch (e) { + if (e == BREAK && x.target == n) { + break; + } else if (e == CONTINUE && x.target == n) { + // jrh + // 'continue' is invalid inside an 'if' clause + // I don't know what commenting this out will break! + //continue; + // end jrh + + } else { + throw e; + } + } + n.update && getValue(execute(n.update, x)); + // jrh + new_block.unshift([n, x]) + agenda.splice(agenda.length-1,0,[n, x]) + //agenda.splice(1,0,[n, x]) + skip_setup = 1 + // end jrh + } else { + skip_setup = 0 + } + + break; + + case FOR_IN: + u = n.varDecl; + if (u) + execute(u, x); + r = n.iterator; + s = execute(n.object, x); + v = getValue(s); + + // ECMA deviation to track extant browser JS implementation behavior. + t = (v == null && !x.ecmaStrictMode) ? v : toObject(v, s, n.object); + a = []; + for (i in t) + a.push(i); + for (i = 0, j = a.length; i < j; i++) { + putValue(execute(r, x), a[i], r); + try { + execute(n.body, x); + } catch (e) { + if (e == BREAK && x.target == n) { + break; + } else if (e == CONTINUE && x.target == n) { + continue; + } else { + throw e; + } + } + } + break; + + case DO: + do { + try { + execute(n.body, x); + } catch (e) { + if (e == BREAK && x.target == n) { + break; + } else if (e == CONTINUE && x.target == n) { + continue; + } else { + throw e; + } + } + } while (getValue(execute(n.condition, x))); + break; + + case BREAK: + case CONTINUE: + x.target = n.target; + throw n.type; + + case TRY: + try { + execute(n.tryBlock, x); + } catch (e) { + if (!(e == THROW && (j = n.catchClauses.length))) { + throw e; + } + e = x.result; + x.result = undefined; + for (i = 0; ; i++) { + if (i == j) { + x.result = e; + throw THROW; + } + t = n.catchClauses[i]; + x.scope = {object: {}, parent: x.scope}; + x.scope.object[t.varName] = e; + try { + if (t.guard && !getValue(execute(t.guard, x))) + continue; + execute(t.block, x); + break; + } finally { + x.scope = x.scope.parent; + } + } + } finally { + if (n.finallyBlock) + execute(n.finallyBlock, x); + } + break; + + case THROW: + x.result = getValue(execute(n.exception, x)); + throw THROW; + + case RETURN: + x.result = getValue(execute(n.value, x)); + throw RETURN; + + case WITH: + r = execute(n.object, x); + t = toObject(getValue(r), r, n.object); + x.scope = {object: t, parent: x.scope}; + try { + execute(n.body, x); + } finally { + x.scope = x.scope.parent; + } + break; + + case VAR: + case CONST: + for (i = 0, j = n.$length; i < j; i++) { + u = n[i].initializer; + if (!u) + continue; + t = n[i].name; + for (s = x.scope; s; s = s.parent) { + if (hasDirectProperty(s.object, t)) + break; + } + u = getValue(execute(u, x)); + if (n.type == CONST) + s.object[t] = u; + else + s.object[t] = u; + } + break; + + case DEBUGGER: + throw "NYI: " + tokens[n.type]; + + case REQUIRE: + var req = new XMLHttpRequest(); + req.open('GET', n.filename, 'false'); + + case SEMICOLON: + if (n.expression) + // print debugging statements + + var the_start = n.start + var the_end = n.end + var the_statement = parse_result.tokenizer.source.slice(the_start,the_end) + //global.debug.document.body.innerHTML += ('<pre>>>> <b>' + the_statement + '</b></pre>') + LOG.info('>>>' + the_statement) + x.result = getValue(execute(n.expression, x)); + //if (x.result) + //global.debug.document.body.innerHTML += ( '<pre>>>> ' + x.result + '</pre>') + + break; + + case LABEL: + try { + execute(n.statement, x); + } catch (e) { + if (!(e == BREAK && x.target == n)) { throw e } + } + break; + + case COMMA: + for (i = 0, j = n.$length; i < j; i++) + v = getValue(execute(n[i], x)); + break; + + case ASSIGN: + r = execute(n[0], x); + t = n[0].assignOp; + if (t) + u = getValue(r); + v = getValue(execute(n[1], x)); + if (t) { + switch (t) { + case BITWISE_OR: v = u | v; break; + case BITWISE_XOR: v = u ^ v; break; + case BITWISE_AND: v = u & v; break; + case LSH: v = u << v; break; + case RSH: v = u >> v; break; + case URSH: v = u >>> v; break; + case PLUS: v = u + v; break; + case MINUS: v = u - v; break; + case MUL: v = u * v; break; + case DIV: v = u / v; break; + case MOD: v = u % v; break; + } + } + putValue(r, v, n[0]); + break; + + case CONDITIONAL: + v = getValue(execute(n[0], x)) ? getValue(execute(n[1], x)) + : getValue(execute(n[2], x)); + break; + + case OR: + v = getValue(execute(n[0], x)) || getValue(execute(n[1], x)); + break; + + case AND: + v = getValue(execute(n[0], x)) && getValue(execute(n[1], x)); + break; + + case BITWISE_OR: + v = getValue(execute(n[0], x)) | getValue(execute(n[1], x)); + break; + + case BITWISE_XOR: + v = getValue(execute(n[0], x)) ^ getValue(execute(n[1], x)); + break; + + case BITWISE_AND: + v = getValue(execute(n[0], x)) & getValue(execute(n[1], x)); + break; + + case EQ: + v = getValue(execute(n[0], x)) == getValue(execute(n[1], x)); + break; + + case NE: + v = getValue(execute(n[0], x)) != getValue(execute(n[1], x)); + break; + + case STRICT_EQ: + v = getValue(execute(n[0], x)) === getValue(execute(n[1], x)); + break; + + case STRICT_NE: + v = getValue(execute(n[0], x)) !== getValue(execute(n[1], x)); + break; + + case LT: + v = getValue(execute(n[0], x)) < getValue(execute(n[1], x)); + break; + + case LE: + v = getValue(execute(n[0], x)) <= getValue(execute(n[1], x)); + break; + + case GE: + v = getValue(execute(n[0], x)) >= getValue(execute(n[1], x)); + break; + + case GT: + v = getValue(execute(n[0], x)) > getValue(execute(n[1], x)); + break; + + case IN: + v = getValue(execute(n[0], x)) in getValue(execute(n[1], x)); + break; + + case INSTANCEOF: + t = getValue(execute(n[0], x)); + u = getValue(execute(n[1], x)); + if (isObject(u) && typeof u.__hasInstance__ == "function") + v = u.__hasInstance__(t); + else + v = t instanceof u; + break; + + case LSH: + v = getValue(execute(n[0], x)) << getValue(execute(n[1], x)); + break; + + case RSH: + v = getValue(execute(n[0], x)) >> getValue(execute(n[1], x)); + break; + + case URSH: + v = getValue(execute(n[0], x)) >>> getValue(execute(n[1], x)); + break; + + case PLUS: + v = getValue(execute(n[0], x)) + getValue(execute(n[1], x)); + break; + + case MINUS: + v = getValue(execute(n[0], x)) - getValue(execute(n[1], x)); + break; + + case MUL: + v = getValue(execute(n[0], x)) * getValue(execute(n[1], x)); + break; + + case DIV: + v = getValue(execute(n[0], x)) / getValue(execute(n[1], x)); + break; + + case MOD: + v = getValue(execute(n[0], x)) % getValue(execute(n[1], x)); + break; + + case DELETE: + t = execute(n[0], x); + v = !(t instanceof Reference) || delete t.base[t.propertyName]; + break; + + case VOID: + getValue(execute(n[0], x)); + break; + + case TYPEOF: + t = execute(n[0], x); + if (t instanceof Reference) + t = t.base ? t.base[t.propertyName] : undefined; + v = typeof t; + break; + + case NOT: + v = !getValue(execute(n[0], x)); + break; + + case BITWISE_NOT: + v = ~getValue(execute(n[0], x)); + break; + + case UNARY_PLUS: + v = +getValue(execute(n[0], x)); + break; + + case UNARY_MINUS: + v = -getValue(execute(n[0], x)); + break; + + case INCREMENT: + case DECREMENT: + t = execute(n[0], x); + u = Number(getValue(t)); + if (n.postfix) + v = u; + putValue(t, (n.type == INCREMENT) ? ++u : --u, n[0]); + if (!n.postfix) + v = u; + break; + + case DOT: + r = execute(n[0], x); + t = getValue(r); + u = n[1].value; + v = new Reference(toObject(t, r, n[0]), u, n); + break; + + case INDEX: + r = execute(n[0], x); + t = getValue(r); + u = getValue(execute(n[1], x)); + v = new Reference(toObject(t, r, n[0]), String(u), n); + break; + + case LIST: + // Curse ECMA for specifying that arguments is not an Array object! + v = {}; + for (i = 0, j = n.$length; i < j; i++) { + u = getValue(execute(n[i], x)); + v[i] = u; + } + v.length = i; + break; + + case CALL: + r = execute(n[0], x); + a = execute(n[1], x); + f = getValue(r); + if (isPrimitive(f) || typeof f.__call__ != "function") { + throw new TypeError(r + " is not callable", + n[0].filename(), n[0].lineno); + } + t = (r instanceof Reference) ? r.base : null; + if (t instanceof Activation) + t = null; + v = f.__call__(t, a, x); + break; + + case NEW: + case NEW_WITH_ARGS: + r = execute(n[0], x); + f = getValue(r); + if (n.type == NEW) { + a = {}; + a.length = 0; + } else { + a = execute(n[1], x); + } + if (isPrimitive(f) || typeof f.__construct__ != "function") { + throw new TypeError(r + " is not a constructor", + n[0].filename(), n[0].lineno); + } + v = f.__construct__(a, x); + break; + + case ARRAY_INIT: + v = []; + for (i = 0, j = n.$length; i < j; i++) { + if (n[i]) + v[i] = getValue(execute(n[i], x)); + } + v.length = j; + break; + + case OBJECT_INIT: + v = {}; + for (i = 0, j = n.$length; i < j; i++) { + t = n[i]; + if (t.type == PROPERTY_INIT) { + v[t[0].value] = getValue(execute(t[1], x)); + } else { + f = new FunctionObject(t, x.scope); + /* + u = (t.type == GETTER) ? '__defineGetter__' + : '__defineSetter__'; + v[u](t.name, thunk(f, x)); + */ + } + } + break; + + case NULL: + v = null; + break; + + case THIS: + v = x.thisObject; + break; + + case TRUE: + v = true; + break; + + case FALSE: + v = false; + break; + + case IDENTIFIER: + for (s = x.scope; s; s = s.parent) { + if (n.value in s.object) + break; + } + v = new Reference(s && s.object, n.value, n); + break; + + case NUMBER: + case STRING: + case REGEXP: + v = n.value; + break; + + case GROUP: + v = execute(n[0], x); + break; + + default: + throw "PANIC: unknown operation " + n.type + ": " + uneval(n); + } + return v; +} + +function Activation(f, a) { + for (var i = 0, j = f.params.length; i < j; i++) + this[f.params[i]] = a[i]; + this.arguments = a; +} + +// Null Activation.prototype's proto slot so that Object.prototype.* does not +// pollute the scope of heavyweight functions. Also delete its 'constructor' +// property so that it doesn't pollute function scopes. + +Activation.prototype.__proto__ = null; +delete Activation.prototype.constructor; + +function FunctionObject(node, scope) { + this.node = node; + this.scope = scope; + this.length = node.params.length; + var proto = {}; + this.prototype = proto; + proto.constructor = this; +} + +var FOp = FunctionObject.prototype = { + // Internal methods. + __call__: function (t, a, x) { + var x2 = new ExecutionContext(FUNCTION_CODE); + x2.thisObject = t || global; + x2.caller = x; + x2.callee = this; + a.callee = this; + var f = this.node; + x2.scope = {object: new Activation(f, a), parent: this.scope}; + + ExecutionContext.current = x2; + try { + execute(f.body, x2); + } catch (e) { + if (!(e == RETURN)) { throw e } else if (e == RETURN) { + return x2.result; + } + if (e != THROW) { throw e } + x.result = x2.result; + throw THROW; + } finally { + ExecutionContext.current = x; + } + return undefined; + }, + + __construct__: function (a, x) { + var o = new Object; + var p = this.prototype; + if (isObject(p)) + o.__proto__ = p; + // else o.__proto__ defaulted to Object.prototype + + var v = this.__call__(o, a, x); + if (isObject(v)) + return v; + return o; + }, + + __hasInstance__: function (v) { + if (isPrimitive(v)) + return false; + var p = this.prototype; + if (isPrimitive(p)) { + throw new TypeError("'prototype' property is not an object", + this.node.filename(), this.node.lineno); + } + var o; + while ((o = v.__proto__)) { + if (o == p) + return true; + v = o; + } + return false; + }, + + // Standard methods. + toString: function () { + return this.node.getSource(); + }, + + apply: function (t, a) { + // Curse ECMA again! + if (typeof this.__call__ != "function") { + throw new TypeError("Function.prototype.apply called on" + + " uncallable object"); + } + + if (t === undefined || t === null) + t = global; + else if (typeof t != "object") + t = toObject(t, t); + + if (a === undefined || a === null) { + a = {}; + a.length = 0; + } else if (a instanceof Array) { + var v = {}; + for (var i = 0, j = a.length; i < j; i++) + v[i] = a[i]; + v.length = i; + a = v; + } else if (!(a instanceof Object)) { + // XXX check for a non-arguments object + throw new TypeError("Second argument to Function.prototype.apply" + + " must be an array or arguments object", + this.node.filename(), this.node.lineno); + } + + return this.__call__(t, a, ExecutionContext.current); + }, + + call: function (t) { + // Curse ECMA a third time! + var a = Array.prototype.splice.call(arguments, 1); + return this.apply(t, a); + } +}; + +// Connect Function.prototype and Function.prototype.constructor in global. +reflectClass('Function', FOp); + +// Help native and host-scripted functions be like FunctionObjects. +var Fp = Function.prototype; +var REp = RegExp.prototype; + +if (!('__call__' in Fp)) { + Fp.__call__ = function (t, a, x) { + // Curse ECMA yet again! + a = Array.prototype.splice.call(a, 0, a.length); + return this.apply(t, a); + }; + + REp.__call__ = function (t, a, x) { + a = Array.prototype.splice.call(a, 0, a.length); + return this.exec.apply(this, a); + }; + + Fp.__construct__ = function (a, x) { + switch (a.length) { + case 0: + return new this(); + case 1: + return new this(a[0]); + case 2: + return new this(a[0], a[1]); + case 3: + return new this(a[0], a[1], a[2]); + case 4: + return new this(a[0], a[1], a[2], a[3]); + case 5: + return new this(a[0], a[1], a[2], a[3], a[4]); + case 6: + return new this(a[0], a[1], a[2], a[3], a[4], a[5]); + case 7: + return new this(a[0], a[1], a[2], a[3], a[4], a[5], a[6]); + } + throw "PANIC: too many arguments to constructor"; + } + + // Since we use native functions such as Date along with host ones such + // as global.eval, we want both to be considered instances of the native + // Function constructor. + Fp.__hasInstance__ = function (v) { + return v instanceof Function || v instanceof global.Function; + }; +} + +function thunk(f, x) { + return function () { return f.__call__(this, arguments, x); }; +} + +function evaluate(s, f, l) { + if (typeof s != "string") + return s; + + var x = ExecutionContext.current; + var x2 = new ExecutionContext(GLOBAL_CODE); + ExecutionContext.current = x2; + try { + execute(parse(s, f, l), x2); + } catch (e) { + if (e != THROW) { throw e } + if (x) { + x.result = x2.result; + throw(THROW); + } + throw x2.result; + } finally { + ExecutionContext.current = x; + } + return x2.result; +} diff --git a/tests/test_tools/selenium/core/scripts/narcissus-parse.js b/tests/test_tools/selenium/core/scripts/narcissus-parse.js new file mode 100644 index 00000000..d6acb836 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/narcissus-parse.js @@ -0,0 +1,1003 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (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.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is the Narcissus JavaScript engine. + * + * The Initial Developer of the Original Code is + * Brendan Eich <brendan@mozilla.org>. + * Portions created by the Initial Developer are Copyright (C) 2004 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): Richard Hundt <www.plextk.org> + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/* + * Narcissus - JS implemented in JS. + * + * Lexical scanner and parser. + */ + +// jrh +//module('JS.Parse'); + +// Build a regexp that recognizes operators and punctuators (except newline). +var opRegExp = +/^;|^,|^\?|^:|^\|\||^\&\&|^\||^\^|^\&|^===|^==|^=|^!==|^!=|^<<|^<=|^<|^>>>|^>>|^>=|^>|^\+\+|^\-\-|^\+|^\-|^\*|^\/|^%|^!|^~|^\.|^\[|^\]|^\{|^\}|^\(|^\)/; + +// A regexp to match floating point literals (but not integer literals). +var fpRegExp = /^\d+\.\d*(?:[eE][-+]?\d+)?|^\d+(?:\.\d*)?[eE][-+]?\d+|^\.\d+(?:[eE][-+]?\d+)?/; + +function Tokenizer(s, f, l) { + this.cursor = 0; + this.source = String(s); + this.tokens = []; + this.tokenIndex = 0; + this.lookahead = 0; + this.scanNewlines = false; + this.scanOperand = true; + this.filename = f || ""; + this.lineno = l || 1; +} + +Tokenizer.prototype = { + input : function() { + return this.source.substring(this.cursor); + }, + + done : function() { + return this.peek() == END; + }, + + token : function() { + return this.tokens[this.tokenIndex]; + }, + + match: function (tt) { + return this.get() == tt || this.unget(); + }, + + mustMatch: function (tt) { + if (!this.match(tt)) + throw this.newSyntaxError("Missing " + this.tokens[tt].toLowerCase()); + return this.token(); + }, + + peek: function () { + var tt; + if (this.lookahead) { + tt = this.tokens[(this.tokenIndex + this.lookahead) & 3].type; + } else { + tt = this.get(); + this.unget(); + } + return tt; + }, + + peekOnSameLine: function () { + this.scanNewlines = true; + var tt = this.peek(); + this.scanNewlines = false; + return tt; + }, + + get: function () { + var token; + while (this.lookahead) { + --this.lookahead; + this.tokenIndex = (this.tokenIndex + 1) & 3; + token = this.tokens[this.tokenIndex]; + if (token.type != NEWLINE || this.scanNewlines) + return token.type; + } + + for (;;) { + var input = this.input(); + var rx = this.scanNewlines ? /^[ \t]+/ : /^\s+/; + var match = input.match(rx); + if (match) { + var spaces = match[0]; + this.cursor += spaces.length; + var newlines = spaces.match(/\n/g); + if (newlines) + this.lineno += newlines.length; + input = this.input(); + } + + if (!(match = input.match(/^\/(?:\*(?:.|\n)*?\*\/|\/.*)/))) + break; + var comment = match[0]; + this.cursor += comment.length; + newlines = comment.match(/\n/g); + if (newlines) + this.lineno += newlines.length + } + + this.tokenIndex = (this.tokenIndex + 1) & 3; + token = this.tokens[this.tokenIndex]; + if (!token) + this.tokens[this.tokenIndex] = token = {}; + if (!input) + return token.type = END; + if ((match = input.match(fpRegExp))) { + token.type = NUMBER; + token.value = parseFloat(match[0]); + } else if ((match = input.match(/^0[xX][\da-fA-F]+|^0[0-7]*|^\d+/))) { + token.type = NUMBER; + token.value = parseInt(match[0]); + } else if ((match = input.match(/^((\$\w*)|(\w+))/))) { + var id = match[0]; + token.type = keywords[id] || IDENTIFIER; + token.value = id; + } else if ((match = input.match(/^"(?:\\.|[^"])*"|^'(?:[^']|\\.)*'/))) { + token.type = STRING; + token.value = eval(match[0]); + } else if (this.scanOperand && + (match = input.match(/^\/((?:\\.|[^\/])+)\/([gi]*)/))) { + token.type = REGEXP; + token.value = new RegExp(match[1], match[2]); + } else if ((match = input.match(opRegExp))) { + var op = match[0]; + if (assignOps[op] && input[op.length] == '=') { + token.type = ASSIGN; + token.assignOp = GLOBAL[opTypeNames[op]]; + match[0] += '='; + } else { + token.type = GLOBAL[opTypeNames[op]]; + if (this.scanOperand && + (token.type == PLUS || token.type == MINUS)) { + token.type += UNARY_PLUS - PLUS; + } + token.assignOp = null; + } + //debug('token.value => '+op+', token.type => '+token.type); + token.value = op; + } else { + throw this.newSyntaxError("Illegal token"); + } + + token.start = this.cursor; + this.cursor += match[0].length; + token.end = this.cursor; + token.lineno = this.lineno; + return token.type; + }, + + unget: function () { + if (++this.lookahead == 4) throw "PANIC: too much lookahead!"; + this.tokenIndex = (this.tokenIndex - 1) & 3; + }, + + newSyntaxError: function (m) { + var e = new SyntaxError(m, this.filename, this.lineno); + e.source = this.source; + e.cursor = this.cursor; + return e; + } +}; + +function CompilerContext(inFunction) { + this.inFunction = inFunction; + this.stmtStack = []; + this.funDecls = []; + this.varDecls = []; +} + +var CCp = CompilerContext.prototype; +CCp.bracketLevel = CCp.curlyLevel = CCp.parenLevel = CCp.hookLevel = 0; +CCp.ecmaStrictMode = CCp.inForLoopInit = false; + +function Script(t, x) { + var n = Statements(t, x); + n.type = SCRIPT; + n.funDecls = x.funDecls; + n.varDecls = x.varDecls; + return n; +} + +// Node extends Array, which we extend slightly with a top-of-stack method. +Array.prototype.top = function() { + return this.length && this[this.length-1]; +} + +function NarcNode(t, type) { + var token = t.token(); + if (token) { + this.type = type || token.type; + this.value = token.value; + this.lineno = token.lineno; + this.start = token.start; + this.end = token.end; + } else { + this.type = type; + this.lineno = t.lineno; + } + this.tokenizer = t; + for (var i = 2; i < arguments.length; i++) + this.push(arguments[i]); +} + +var Np = NarcNode.prototype = new Array(); +Np.constructor = NarcNode; +Np.$length = 0; +Np.toSource = Object.prototype.toSource; + +// Always use push to add operands to an expression, to update start and end. +Np.push = function (kid) { + if (kid.start < this.start) + this.start = kid.start; + if (this.end < kid.end) + this.end = kid.end; + //debug('length before => '+this.$length); + this[this.$length] = kid; + this.$length++; + //debug('length after => '+this.$length); +} + +NarcNode.indentLevel = 0; + +function tokenstr(tt) { + var t = tokens[tt]; + return /^\W/.test(t) ? opTypeNames[t] : t.toUpperCase(); +} + +Np.toString = function () { + var a = []; + for (var i in this) { + if (this.hasOwnProperty(i) && i != 'type') + a.push({id: i, value: this[i]}); + } + a.sort(function (a,b) { return (a.id < b.id) ? -1 : 1; }); + INDENTATION = " "; + var n = ++NarcNode.indentLevel; + var s = "{\n" + INDENTATION.repeat(n) + "type: " + tokenstr(this.type); + for (i = 0; i < a.length; i++) + s += ",\n" + INDENTATION.repeat(n) + a[i].id + ": " + a[i].value; + n = --NarcNode.indentLevel; + s += "\n" + INDENTATION.repeat(n) + "}"; + return s; +} + +Np.getSource = function () { + return this.tokenizer.source.slice(this.start, this.end); +}; + +Np.filename = function () { return this.tokenizer.filename; }; + +String.prototype.repeat = function (n) { + var s = "", t = this + s; + while (--n >= 0) + s += t; + return s; +} + +// Statement stack and nested statement handler. +function nest(t, x, node, func, end) { + x.stmtStack.push(node); + var n = func(t, x); + x.stmtStack.pop(); + end && t.mustMatch(end); + return n; +} + +function Statements(t, x) { + var n = new NarcNode(t, BLOCK); + x.stmtStack.push(n); + while (!t.done() && t.peek() != RIGHT_CURLY) + n.push(Statement(t, x)); + x.stmtStack.pop(); + return n; +} + +function Block(t, x) { + t.mustMatch(LEFT_CURLY); + var n = Statements(t, x); + t.mustMatch(RIGHT_CURLY); + return n; +} + +DECLARED_FORM = 0; EXPRESSED_FORM = 1; STATEMENT_FORM = 2; + +function Statement(t, x) { + var i, label, n, n2, ss, tt = t.get(); + + // Cases for statements ending in a right curly return early, avoiding the + // common semicolon insertion magic after this switch. + switch (tt) { + case FUNCTION: + return FunctionDefinition(t, x, true, + (x.stmtStack.length > 1) + ? STATEMENT_FORM + : DECLARED_FORM); + + case LEFT_CURLY: + n = Statements(t, x); + t.mustMatch(RIGHT_CURLY); + return n; + + case IF: + n = new NarcNode(t); + n.condition = ParenExpression(t, x); + x.stmtStack.push(n); + n.thenPart = Statement(t, x); + n.elsePart = t.match(ELSE) ? Statement(t, x) : null; + x.stmtStack.pop(); + return n; + + case SWITCH: + n = new NarcNode(t); + t.mustMatch(LEFT_PAREN); + n.discriminant = Expression(t, x); + t.mustMatch(RIGHT_PAREN); + n.cases = []; + n.defaultIndex = -1; + x.stmtStack.push(n); + t.mustMatch(LEFT_CURLY); + while ((tt = t.get()) != RIGHT_CURLY) { + switch (tt) { + case DEFAULT: + if (n.defaultIndex >= 0) + throw t.newSyntaxError("More than one switch default"); + // FALL THROUGH + case CASE: + n2 = new NarcNode(t); + if (tt == DEFAULT) + n.defaultIndex = n.cases.length; + else + n2.caseLabel = Expression(t, x, COLON); + break; + default: + throw t.newSyntaxError("Invalid switch case"); + } + t.mustMatch(COLON); + n2.statements = new NarcNode(t, BLOCK); + while ((tt=t.peek()) != CASE && tt != DEFAULT && tt != RIGHT_CURLY) + n2.statements.push(Statement(t, x)); + n.cases.push(n2); + } + x.stmtStack.pop(); + return n; + + case FOR: + n = new NarcNode(t); + n.isLoop = true; + t.mustMatch(LEFT_PAREN); + if ((tt = t.peek()) != SEMICOLON) { + x.inForLoopInit = true; + if (tt == VAR || tt == CONST) { + t.get(); + n2 = Variables(t, x); + } else { + n2 = Expression(t, x); + } + x.inForLoopInit = false; + } + if (n2 && t.match(IN)) { + n.type = FOR_IN; + if (n2.type == VAR) { + if (n2.$length != 1) { + throw new SyntaxError("Invalid for..in left-hand side", + t.filename, n2.lineno); + } + + // NB: n2[0].type == IDENTIFIER and n2[0].value == n2[0].name. + n.iterator = n2[0]; + n.varDecl = n2; + } else { + n.iterator = n2; + n.varDecl = null; + } + n.object = Expression(t, x); + } else { + n.setup = n2 || null; + t.mustMatch(SEMICOLON); + n.condition = (t.peek() == SEMICOLON) ? null : Expression(t, x); + t.mustMatch(SEMICOLON); + n.update = (t.peek() == RIGHT_PAREN) ? null : Expression(t, x); + } + t.mustMatch(RIGHT_PAREN); + n.body = nest(t, x, n, Statement); + return n; + + case WHILE: + n = new NarcNode(t); + n.isLoop = true; + n.condition = ParenExpression(t, x); + n.body = nest(t, x, n, Statement); + return n; + + case DO: + n = new NarcNode(t); + n.isLoop = true; + n.body = nest(t, x, n, Statement, WHILE); + n.condition = ParenExpression(t, x); + if (!x.ecmaStrictMode) { + // <script language="JavaScript"> (without version hints) may need + // automatic semicolon insertion without a newline after do-while. + // See http://bugzilla.mozilla.org/show_bug.cgi?id=238945. + t.match(SEMICOLON); + return n; + } + break; + + case BREAK: + case CONTINUE: + n = new NarcNode(t); + if (t.peekOnSameLine() == IDENTIFIER) { + t.get(); + n.label = t.token().value; + } + ss = x.stmtStack; + i = ss.length; + label = n.label; + if (label) { + do { + if (--i < 0) + throw t.newSyntaxError("Label not found"); + } while (ss[i].label != label); + } else { + do { + if (--i < 0) { + throw t.newSyntaxError("Invalid " + ((tt == BREAK) + ? "break" + : "continue")); + } + } while (!ss[i].isLoop && (tt != BREAK || ss[i].type != SWITCH)); + } + n.target = ss[i]; + break; + + case TRY: + n = new NarcNode(t); + n.tryBlock = Block(t, x); + n.catchClauses = []; + while (t.match(CATCH)) { + n2 = new NarcNode(t); + t.mustMatch(LEFT_PAREN); + n2.varName = t.mustMatch(IDENTIFIER).value; + if (t.match(IF)) { + if (x.ecmaStrictMode) + throw t.newSyntaxError("Illegal catch guard"); + if (n.catchClauses.length && !n.catchClauses.top().guard) + throw t.newSyntaxError("Guarded catch after unguarded"); + n2.guard = Expression(t, x); + } else { + n2.guard = null; + } + t.mustMatch(RIGHT_PAREN); + n2.block = Block(t, x); + n.catchClauses.push(n2); + } + if (t.match(FINALLY)) + n.finallyBlock = Block(t, x); + if (!n.catchClauses.length && !n.finallyBlock) + throw t.newSyntaxError("Invalid try statement"); + return n; + + case CATCH: + case FINALLY: + throw t.newSyntaxError(tokens[tt] + " without preceding try"); + + case THROW: + n = new NarcNode(t); + n.exception = Expression(t, x); + break; + + case RETURN: + if (!x.inFunction) + throw t.newSyntaxError("Invalid return"); + n = new NarcNode(t); + tt = t.peekOnSameLine(); + if (tt != END && tt != NEWLINE && tt != SEMICOLON && tt != RIGHT_CURLY) + n.value = Expression(t, x); + break; + + case WITH: + n = new NarcNode(t); + n.object = ParenExpression(t, x); + n.body = nest(t, x, n, Statement); + return n; + + case VAR: + case CONST: + n = Variables(t, x); + break; + + case DEBUGGER: + n = new NarcNode(t); + break; + + case REQUIRE: + n = new NarcNode(t); + n.classPath = ParenExpression(t, x); + break; + + case NEWLINE: + case SEMICOLON: + n = new NarcNode(t, SEMICOLON); + n.expression = null; + return n; + + default: + if (tt == IDENTIFIER && t.peek() == COLON) { + label = t.token().value; + ss = x.stmtStack; + for (i = ss.length-1; i >= 0; --i) { + if (ss[i].label == label) + throw t.newSyntaxError("Duplicate label"); + } + t.get(); + n = new NarcNode(t, LABEL); + n.label = label; + n.statement = nest(t, x, n, Statement); + return n; + } + + n = new NarcNode(t, SEMICOLON); + t.unget(); + n.expression = Expression(t, x); + n.end = n.expression.end; + break; + } + + if (t.lineno == t.token().lineno) { + tt = t.peekOnSameLine(); + if (tt != END && tt != NEWLINE && tt != SEMICOLON && tt != RIGHT_CURLY) + throw t.newSyntaxError("Missing ; before statement"); + } + t.match(SEMICOLON); + return n; +} + +function FunctionDefinition(t, x, requireName, functionForm) { + var f = new NarcNode(t); + if (f.type != FUNCTION) + f.type = (f.value == "get") ? GETTER : SETTER; + if (t.match(IDENTIFIER)) { + f.name = t.token().value; + } + else if (requireName) + throw t.newSyntaxError("Missing function identifier"); + + t.mustMatch(LEFT_PAREN); + f.params = []; + var tt; + while ((tt = t.get()) != RIGHT_PAREN) { + if (tt != IDENTIFIER) + throw t.newSyntaxError("Missing formal parameter"); + f.params.push(t.token().value); + if (t.peek() != RIGHT_PAREN) + t.mustMatch(COMMA); + } + + t.mustMatch(LEFT_CURLY); + var x2 = new CompilerContext(true); + f.body = Script(t, x2); + t.mustMatch(RIGHT_CURLY); + f.end = t.token().end; + + f.functionForm = functionForm; + if (functionForm == DECLARED_FORM) { + x.funDecls.push(f); + } + + return f; +} + +function Variables(t, x) { + var n = new NarcNode(t); + do { + t.mustMatch(IDENTIFIER); + var n2 = new NarcNode(t); + n2.name = n2.value; + if (t.match(ASSIGN)) { + if (t.token().assignOp) + throw t.newSyntaxError("Invalid variable initialization"); + n2.initializer = Expression(t, x, COMMA); + } + n2.readOnly = (n.type == CONST); + n.push(n2); + x.varDecls.push(n2); + } while (t.match(COMMA)); + return n; +} + +function ParenExpression(t, x) { + t.mustMatch(LEFT_PAREN); + var n = Expression(t, x); + t.mustMatch(RIGHT_PAREN); + return n; +} + +var opPrecedence = { + SEMICOLON: 0, + COMMA: 1, + ASSIGN: 2, HOOK: 2, COLON: 2, CONDITIONAL: 2, + // The above all have to have the same precedence, see bug 330975. + OR: 4, + AND: 5, + BITWISE_OR: 6, + BITWISE_XOR: 7, + BITWISE_AND: 8, + EQ: 9, NE: 9, STRICT_EQ: 9, STRICT_NE: 9, + LT: 10, LE: 10, GE: 10, GT: 10, IN: 10, INSTANCEOF: 10, + LSH: 11, RSH: 11, URSH: 11, + PLUS: 12, MINUS: 12, + MUL: 13, DIV: 13, MOD: 13, + DELETE: 14, VOID: 14, TYPEOF: 14, // PRE_INCREMENT: 14, PRE_DECREMENT: 14, + NOT: 14, BITWISE_NOT: 14, UNARY_PLUS: 14, UNARY_MINUS: 14, + INCREMENT: 15, DECREMENT: 15, // postfix + NEW: 16, + DOT: 17 +}; + +// Map operator type code to precedence. +for (i in opPrecedence) + opPrecedence[GLOBAL[i]] = opPrecedence[i]; + +var opArity = { + COMMA: -2, + ASSIGN: 2, + CONDITIONAL: 3, + OR: 2, + AND: 2, + BITWISE_OR: 2, + BITWISE_XOR: 2, + BITWISE_AND: 2, + EQ: 2, NE: 2, STRICT_EQ: 2, STRICT_NE: 2, + LT: 2, LE: 2, GE: 2, GT: 2, IN: 2, INSTANCEOF: 2, + LSH: 2, RSH: 2, URSH: 2, + PLUS: 2, MINUS: 2, + MUL: 2, DIV: 2, MOD: 2, + DELETE: 1, VOID: 1, TYPEOF: 1, // PRE_INCREMENT: 1, PRE_DECREMENT: 1, + NOT: 1, BITWISE_NOT: 1, UNARY_PLUS: 1, UNARY_MINUS: 1, + INCREMENT: 1, DECREMENT: 1, // postfix + NEW: 1, NEW_WITH_ARGS: 2, DOT: 2, INDEX: 2, CALL: 2, + ARRAY_INIT: 1, OBJECT_INIT: 1, GROUP: 1 +}; + +// Map operator type code to arity. +for (i in opArity) + opArity[GLOBAL[i]] = opArity[i]; + +function Expression(t, x, stop) { + var n, id, tt, operators = [], operands = []; + var bl = x.bracketLevel, cl = x.curlyLevel, pl = x.parenLevel, + hl = x.hookLevel; + + function reduce() { + //debug('OPERATORS => '+operators); + var n = operators.pop(); + var op = n.type; + var arity = opArity[op]; + if (arity == -2) { + // Flatten left-associative trees. + var left = operands.length >= 2 && operands[operands.length-2]; + if (left.type == op) { + var right = operands.pop(); + left.push(right); + return left; + } + arity = 2; + } + + // Always use push to add operands to n, to update start and end. + var a = operands.splice(operands.length - arity, operands.length); + for (var i = 0; i < arity; i++) { + n.push(a[i]); + } + + // Include closing bracket or postfix operator in [start,end). + if (n.end < t.token().end) + n.end = t.token().end; + + operands.push(n); + return n; + } + +loop: + while ((tt = t.get()) != END) { + //debug('TT => '+tokens[tt]); + if (tt == stop && + x.bracketLevel == bl && x.curlyLevel == cl && x.parenLevel == pl && + x.hookLevel == hl) { + // Stop only if tt matches the optional stop parameter, and that + // token is not quoted by some kind of bracket. + break; + } + switch (tt) { + case SEMICOLON: + // NB: cannot be empty, Statement handled that. + break loop; + + case ASSIGN: + case HOOK: + case COLON: + if (t.scanOperand) + break loop; + // Use >, not >=, for right-associative ASSIGN and HOOK/COLON. + while (operators.length && opPrecedence[operators.top().type] > opPrecedence[tt] || + (tt == COLON && operators.top().type == ASSIGN)) { + reduce(); + } + if (tt == COLON) { + n = operators.top(); + if (n.type != HOOK) + throw t.newSyntaxError("Invalid label"); + n.type = CONDITIONAL; + --x.hookLevel; + } else { + operators.push(new NarcNode(t)); + if (tt == ASSIGN) + operands.top().assignOp = t.token().assignOp; + else + ++x.hookLevel; // tt == HOOK + } + t.scanOperand = true; + break; + + case IN: + // An in operator should not be parsed if we're parsing the head of + // a for (...) loop, unless it is in the then part of a conditional + // expression, or parenthesized somehow. + if (x.inForLoopInit && !x.hookLevel && + !x.bracketLevel && !x.curlyLevel && !x.parenLevel) { + break loop; + } + // FALL THROUGH + case COMMA: + // Treat comma as left-associative so reduce can fold left-heavy + // COMMA trees into a single array. + // FALL THROUGH + case OR: + case AND: + case BITWISE_OR: + case BITWISE_XOR: + case BITWISE_AND: + case EQ: case NE: case STRICT_EQ: case STRICT_NE: + case LT: case LE: case GE: case GT: + case INSTANCEOF: + case LSH: case RSH: case URSH: + case PLUS: case MINUS: + case MUL: case DIV: case MOD: + case DOT: + if (t.scanOperand) + break loop; + while (operators.length && opPrecedence[operators.top().type] >= opPrecedence[tt]) + reduce(); + if (tt == DOT) { + t.mustMatch(IDENTIFIER); + operands.push(new NarcNode(t, DOT, operands.pop(), new NarcNode(t))); + } else { + operators.push(new NarcNode(t)); + t.scanOperand = true; + } + break; + + case DELETE: case VOID: case TYPEOF: + case NOT: case BITWISE_NOT: case UNARY_PLUS: case UNARY_MINUS: + case NEW: + if (!t.scanOperand) + break loop; + operators.push(new NarcNode(t)); + break; + + case INCREMENT: case DECREMENT: + if (t.scanOperand) { + operators.push(new NarcNode(t)); // prefix increment or decrement + } else { + // Use >, not >=, so postfix has higher precedence than prefix. + while (operators.length && opPrecedence[operators.top().type] > opPrecedence[tt]) + reduce(); + n = new NarcNode(t, tt, operands.pop()); + n.postfix = true; + operands.push(n); + } + break; + + case FUNCTION: + if (!t.scanOperand) + break loop; + operands.push(FunctionDefinition(t, x, false, EXPRESSED_FORM)); + t.scanOperand = false; + break; + + case NULL: case THIS: case TRUE: case FALSE: + case IDENTIFIER: case NUMBER: case STRING: case REGEXP: + if (!t.scanOperand) + break loop; + operands.push(new NarcNode(t)); + t.scanOperand = false; + break; + + case LEFT_BRACKET: + if (t.scanOperand) { + // Array initialiser. Parse using recursive descent, as the + // sub-grammar here is not an operator grammar. + n = new NarcNode(t, ARRAY_INIT); + while ((tt = t.peek()) != RIGHT_BRACKET) { + if (tt == COMMA) { + t.get(); + n.push(null); + continue; + } + n.push(Expression(t, x, COMMA)); + if (!t.match(COMMA)) + break; + } + t.mustMatch(RIGHT_BRACKET); + operands.push(n); + t.scanOperand = false; + } else { + // Property indexing operator. + operators.push(new NarcNode(t, INDEX)); + t.scanOperand = true; + ++x.bracketLevel; + } + break; + + case RIGHT_BRACKET: + if (t.scanOperand || x.bracketLevel == bl) + break loop; + while (reduce().type != INDEX) + continue; + --x.bracketLevel; + break; + + case LEFT_CURLY: + if (!t.scanOperand) + break loop; + // Object initialiser. As for array initialisers (see above), + // parse using recursive descent. + ++x.curlyLevel; + n = new NarcNode(t, OBJECT_INIT); + object_init: + if (!t.match(RIGHT_CURLY)) { + do { + tt = t.get(); + if ((t.token().value == "get" || t.token().value == "set") && + t.peek() == IDENTIFIER) { + if (x.ecmaStrictMode) + throw t.newSyntaxError("Illegal property accessor"); + n.push(FunctionDefinition(t, x, true, EXPRESSED_FORM)); + } else { + switch (tt) { + case IDENTIFIER: + case NUMBER: + case STRING: + id = new NarcNode(t); + break; + case RIGHT_CURLY: + if (x.ecmaStrictMode) + throw t.newSyntaxError("Illegal trailing ,"); + break object_init; + default: + throw t.newSyntaxError("Invalid property name"); + } + t.mustMatch(COLON); + n.push(new NarcNode(t, PROPERTY_INIT, id, + Expression(t, x, COMMA))); + } + } while (t.match(COMMA)); + t.mustMatch(RIGHT_CURLY); + } + operands.push(n); + t.scanOperand = false; + --x.curlyLevel; + break; + + case RIGHT_CURLY: + if (!t.scanOperand && x.curlyLevel != cl) + throw "PANIC: right curly botch"; + break loop; + + case LEFT_PAREN: + if (t.scanOperand) { + operators.push(new NarcNode(t, GROUP)); + } else { + while (operators.length && opPrecedence[operators.top().type] > opPrecedence[NEW]) + reduce(); + + // Handle () now, to regularize the n-ary case for n > 0. + // We must set scanOperand in case there are arguments and + // the first one is a regexp or unary+/-. + n = operators.top(); + t.scanOperand = true; + if (t.match(RIGHT_PAREN)) { + if (n.type == NEW) { + --operators.length; + n.push(operands.pop()); + } else { + n = new NarcNode(t, CALL, operands.pop(), + new NarcNode(t, LIST)); + } + operands.push(n); + t.scanOperand = false; + break; + } + if (n.type == NEW) + n.type = NEW_WITH_ARGS; + else + operators.push(new NarcNode(t, CALL)); + } + ++x.parenLevel; + break; + + case RIGHT_PAREN: + if (t.scanOperand || x.parenLevel == pl) + break loop; + while ((tt = reduce().type) != GROUP && tt != CALL && + tt != NEW_WITH_ARGS) { + continue; + } + if (tt != GROUP) { + n = operands.top(); + if (n[1].type != COMMA) + n[1] = new NarcNode(t, LIST, n[1]); + else + n[1].type = LIST; + } + --x.parenLevel; + break; + + // Automatic semicolon insertion means we may scan across a newline + // and into the beginning of another statement. If so, break out of + // the while loop and let the t.scanOperand logic handle errors. + default: + break loop; + } + } + if (x.hookLevel != hl) + throw t.newSyntaxError("Missing : after ?"); + if (x.parenLevel != pl) + throw t.newSyntaxError("Missing ) in parenthetical"); + if (x.bracketLevel != bl) + throw t.newSyntaxError("Missing ] in index expression"); + if (t.scanOperand) + throw t.newSyntaxError("Missing operand"); + + // Resume default mode, scanning for operands, not operators. + t.scanOperand = true; + t.unget(); + + while (operators.length) + reduce(); + return operands.pop(); +} + +function parse(s, f, l) { + var t = new Tokenizer(s, f, l); + var x = new CompilerContext(false); + var n = Script(t, x); + if (!t.done()) + throw t.newSyntaxError("Syntax error"); + return n; +} + +debug = function(msg) { + document.body.appendChild(document.createTextNode(msg)); + document.body.appendChild(document.createElement('br')); +} + diff --git a/tests/test_tools/selenium/core/scripts/se2html.js b/tests/test_tools/selenium/core/scripts/se2html.js new file mode 100644 index 00000000..67054a49 --- /dev/null +++ b/tests/test_tools/selenium/core/scripts/se2html.js @@ -0,0 +1,63 @@ +/*
+
+This is an experiment in creating a "selenese" parser that drastically
+cuts down on the line noise associated with writing tests in HTML.
+
+The 'parse' function will accept the follow sample commands.
+
+test-cases:
+ //comment
+ command "param"
+ command "param" // comment
+ command "param" "param2"
+ command "param" "param2" // this is a comment
+
+TODO:
+1) Deal with multiline parameters
+2) Escape quotes properly
+3) Determine whether this should/will become the "preferred" syntax
+ for delivered Selenium self-test scripts
+*/
+
+
+function separse(doc) {
+ // Get object
+ script = doc.getElementById('testcase')
+ // Split into lines
+ lines = script.text.split('\n');
+
+
+ var command_pattern = / *(\w+) *"([^"]*)" *(?:"([^"]*)"){0,1}(?: *(\/\/ *.+))*/i;
+ var comment_pattern = /^ *(\/\/ *.+)/
+
+ // Regex each line into selenium command and convert into table row.
+ // eg. "<command> <quote> <quote> <comment>"
+ var new_test_source = '';
+ var new_line = '';
+ for (var x=0; x < lines.length; x++) {
+ result = lines[x].match(command_pattern);
+ if (result != null) {
+ new_line = "<tr><td>" + (result[1] || ' ') + "</td>" +
+ "<td>" + (result[2] || ' ') + "</td>" +
+ "<td>" + (result[3] || ' ') + "</td>" +
+ "<td>" + (result[4] || ' ') + "</td></tr>\n";
+ new_test_source += new_line;
+ }
+ result = lines[x].match(comment_pattern);
+ if (result != null) {
+ new_line = '<tr><td rowspan="1" colspan="4">' +
+ (result[1] || ' ') +
+ '</td></tr>';
+ new_test_source += new_line;
+ }
+ }
+
+ // Create HTML Table
+ body = doc.body
+ body.innerHTML += "<table class='selenium' id='testtable'>"+
+ new_test_source +
+ "</table>";
+
+}
+
+
diff --git a/tests/test_tools/selenium/core/scripts/selenium-api.js b/tests/test_tools/selenium/core/scripts/selenium-api.js index ad0509ee..e8e587f7 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-api.js +++ b/tests/test_tools/selenium/core/scripts/selenium-api.js @@ -15,12 +15,14 @@ * */ +// TODO: stop navigating this.page().document() ... it breaks encapsulation + 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. @@ -28,7 +30,7 @@ function Selenium(browserbot) { * <blockquote> * <em>locatorType</em><strong>=</strong><em>argument</em> * </blockquote> - * + * * <p> * We support the following strategies for locating elements: * </p> @@ -40,7 +42,7 @@ function Selenium(browserbot) { * (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"> @@ -49,15 +51,15 @@ function Selenium(browserbot) { * </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"> @@ -65,15 +67,15 @@ function Selenium(browserbot) { * <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> @@ -82,15 +84,24 @@ function Selenium(browserbot) { * <ul class="first last simple"> * <li>link=The link text</li> * </ul> - * + * + * </dd> + * + * <dt><strong>css</strong>=<em>cssSelectorSyntax</em></dt> + * <dd>Select the element using css selectors. Please refer to <a href="http://www.w3.org/TR/REC-CSS2/selector.html">CSS2 selectors</a>, <a href="http://www.w3.org/TR/2001/CR-css3-selectors-20011113/">CSS3 selectors</a> for more information. You can also check the TestCssLocators test in the selenium test suite for an example of usage, which is included in the downloaded selenium core package. + * <ul class="first last simple"> + * <li>css=a[href="#id3"]</li> + * <li>css=span#firstChild + span</li> + * </ul> * </dd> + * <dd>Currently the css selector locator supports all css1, css2 and css3 selectors except namespace in css3, some pseudo classes(:nth-of-type, :nth-last-of-type, :first-of-type, :last-of-type, :only-of-type, :visited, :hover, :active, :focus, :indeterminate) and pseudo elements(::first-line, ::first-letter, ::selection, ::before, ::after). </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> @@ -103,7 +114,7 @@ function Selenium(browserbot) { * <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> @@ -114,7 +125,7 @@ function Selenium(browserbot) { * </blockquote> * * <h3><a name="patterns"></a>String-match Patterns</h3> - * + * * <p> * Various Pattern syntaxes are available for matching string values: * </p> @@ -130,7 +141,7 @@ function Selenium(browserbot) { * <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> @@ -145,18 +156,23 @@ function Selenium(browserbot) { this.page = function() { return browserbot.getCurrentPage(); }; + this.defaultTimeout = Selenium.DEFAULT_TIMEOUT; } -Selenium.createForFrame = function(frame) { - return new Selenium(BrowserBot.createForFrame(frame)); +Selenium.DEFAULT_TIMEOUT = 30 * 1000; + +Selenium.createForWindow = function(window) { + if (!window.location) { + throw "error: not a window!"; + } + return new Selenium(BrowserBot.createForWindow(window)); }; Selenium.prototype.reset = function() { - /** - * Clear out all stored variables and select the null (starting) window - */ - storedVars = new Object(); + this.defaultTimeout = Selenium.DEFAULT_TIMEOUT; + // todo: this.browserbot.reset() this.browserbot.selectWindow("null"); + this.browserbot.resetPopups(); }; Selenium.prototype.doClick = function(locator) { @@ -164,12 +180,31 @@ 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.doClickAt = function(locator, coordString) { + /** + * 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. + * + * Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to + * get null event arguments. Read the bug for more details, including a workaround. + * * @param locator an element locator - * + * @param coordString specifies the x,y position (i.e. - 10,20) of the mouse + * event relative to the element returned by the locator. + * */ var element = this.page().findElement(locator); - this.page().clickElement(element); + var clientXY = getClientXY(element, coordString) + this.page().clickElement(element, clientXY[0], clientXY[1]); }; Selenium.prototype.doFireEvent = function(locator, eventName) { @@ -184,70 +219,179 @@ Selenium.prototype.doFireEvent = function(locator, eventName) { triggerEvent(element, eventName, false); }; -Selenium.prototype.doKeyPress = function(locator, keycode) { +Selenium.prototype.doKeyPress = function(locator, keySequence) { /** * 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. + * @param keySequence Either be a string("\" followed by the numeric keycode + * of the key to be pressed, normally the ASCII value of that key), or a single + * character. For example: "w", "\119". */ var element = this.page().findElement(locator); - triggerKeyEvent(element, 'keypress', keycode, true); + triggerKeyEvent(element, 'keypress', keySequence, true); }; -Selenium.prototype.doKeyDown = function(locator, keycode) { +Selenium.prototype.doKeyDown = function(locator, keySequence) { /** * 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. + * @param keySequence Either be a string("\" followed by the numeric keycode + * of the key to be pressed, normally the ASCII value of that key), or a single + * character. For example: "w", "\119". */ var element = this.page().findElement(locator); - triggerKeyEvent(element, 'keydown', keycode, true); + triggerKeyEvent(element, 'keydown', keySequence, true); }; -Selenium.prototype.doKeyUp = function(locator, keycode) { +Selenium.prototype.doKeyUp = function(locator, keySequence) { /** * 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. + * @param keySequence Either be a string("\" followed by the numeric keycode + * of the key to be pressed, normally the ASCII value of that key), or a single + * character. For example: "w", "\119". */ var element = this.page().findElement(locator); - triggerKeyEvent(element, 'keyup', keycode, true); + triggerKeyEvent(element, 'keyup', keySequence, true); }; +function getClientXY(element, coordString) { + // Parse coordString + var coords = null; + var x; + var y; + if (coordString) { + coords = coordString.split(/,/); + x = Number(coords[0]); + y = Number(coords[1]); + } + else { + x = y = 0; + } + + // Get position of element, + // Return 2 item array with clientX and clientY + return [Selenium.prototype.getElementPositionLeft(element) + x, Selenium.prototype.getElementPositionTop(element) + y]; +} + 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.doMouseOut = function(locator) { + /** + * Simulates a user moving the mouse pointer away from the specified element. + * + * @param locator an <a href="#locators">element locator</a> + */ + var element = this.page().findElement(locator); + triggerMouseEvent(element, 'mouseout', 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.doMouseDownAt = function(locator, coordString) { + /** + * Simulates a user pressing the mouse button (without releasing it yet) on + * the specified element. + * + * Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to + * get null event arguments. Read the bug for more details, including a workaround. + * + * @param locator an <a href="#locators">element locator</a> + * @param coordString specifies the x,y position (i.e. - 10,20) of the mouse + * event relative to the element returned by the locator. + */ + var element = this.page().findElement(locator); + var clientXY = getClientXY(element, coordString) + + triggerMouseEvent(element, 'mousedown', true, clientXY[0], clientXY[1]); +}; + +Selenium.prototype.doMouseUp = 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, 'mouseup', true); +}; + +Selenium.prototype.doMouseUpAt = function(locator, coordString) { + /** + * Simulates a user pressing the mouse button (without releasing it yet) on + * the specified element. + * + * Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to + * get null event arguments. Read the bug for more details, including a workaround. + * + * @param locator an <a href="#locators">element locator</a> + * @param coordString specifies the x,y position (i.e. - 10,20) of the mouse + * event relative to the element returned by the locator. + */ + var element = this.page().findElement(locator); + var clientXY = getClientXY(element, coordString) + + triggerMouseEvent(element, 'mouseup', true, clientXY[0], clientXY[1]); +}; + +Selenium.prototype.doMouseMove = 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, 'mousemove', true); +}; + +Selenium.prototype.doMouseMoveAt = function(locator, coordString) { + /** + * Simulates a user pressing the mouse button (without releasing it yet) on + * the specified element. + * + * Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to + * get null event arguments. Read the bug for more details, including a workaround. + * * @param locator an <a href="#locators">element locator</a> + * @param coordString specifies the x,y position (i.e. - 10,20) of the mouse + * event relative to the element returned by the locator. */ + var element = this.page().findElement(locator); - triggerMouseEvent(element, 'mousedown', true); + var clientXY = getClientXY(element, coordString) + + triggerMouseEvent(element, 'mousemove', true, clientXY[0], clientXY[1]); }; 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 */ @@ -267,7 +411,7 @@ Selenium.prototype.findToggleButton = function(locator) { 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; @@ -276,7 +420,7 @@ Selenium.prototype.doCheck = function(locator) { 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; @@ -285,7 +429,7 @@ Selenium.prototype.doUncheck = function(locator) { 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 @@ -305,11 +449,11 @@ Selenium.prototype.doSelect = function(selectLocator, optionLocator) { * <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> @@ -318,7 +462,7 @@ Selenium.prototype.doSelect = function(selectLocator, optionLocator) { * <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> @@ -326,8 +470,8 @@ Selenium.prototype.doSelect = function(selectLocator, optionLocator) { * <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) */ @@ -381,26 +525,47 @@ 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 (browserVersion.isHTA) { + // run the code in the correct window so alerts are handled correctly even in HTA mode + var win = this.browserbot.getCurrentWindow(); + var now = new Date().getTime(); + var marker = 'marker' + now; + win[marker] = form; + win.setTimeout("var actuallySubmit = "+marker+".onsubmit(); if (actuallySubmit) { "+marker+".submit(); };", 0); + // pause for at least 20ms for this command to run + testLoop.waitForCondition = function () { + return new Date().getTime() > (now + 20); + } + } else { + actuallySubmit = form.onsubmit(); + if (actuallySubmit) { + form.submit(); + } + } + } else { + form.submit(); } - if (actuallySubmit) { - form.submit(); + +}; + +Selenium.prototype.makePageLoadCondition = function(timeout) { + if (timeout == null) { + timeout = this.defaultTimeout; } - + return decorateFunctionWithTimeout(this._isNewPageLoaded.bind(this), timeout); }; 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. * @@ -408,11 +573,11 @@ Selenium.prototype.doOpen = function(url) { * 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; + return this.makePageLoadCondition(); }; Selenium.prototype.doSelectWindow = function(windowID) { @@ -420,12 +585,99 @@ 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.doSelectFrame = function(locator) { + /** + * Selects a frame within the current window. (You may invoke this command + * multiple times to select nested frames.) To select the parent frame, use + * "relative=parent" as a locator; to select the top frame, use "relative=top". + * + * <p>You may also use a DOM expression to identify the frame you want directly, + * like this: <code>dom=frames["main"].frames["subframe"]</code></p> + * + * @param locator an <a href="#locators">element locator</a> identifying a frame or iframe + */ + this.browserbot.selectFrame(locator); +}; + +Selenium.prototype.getLogMessages = function() { + /** + * Return the contents of the log. + * + * <p>This is a placeholder intended to make the code generator make this API + * available to clients. The selenium server will intercept this call, however, + * and return its recordkeeping of log messages since the last call to this API. + * Thus this code in JavaScript will never be called.</p> + * + * <p>The reason I opted for a servercentric solution is to be able to support + * multiple frames served from different domains, which would break a + * centralized JavaScript logging mechanism under some conditions.</p> + * + * @return string all log messages seen since the last call to this API + */ + return "getLogMessages should be implemented in the selenium server"; +}; + + +Selenium.prototype.getWhetherThisFrameMatchFrameExpression = function(currentFrameString, target) { + /** + * Determine whether current/locator identify the frame containing this running code. + * + * <p>This is useful in proxy injection mode, where this code runs in every + * browser frame and window, and sometimes the selenium server needs to identify + * the "current" frame. In this case, when the test calls selectFrame, this + * routine is called for each frame to figure out which one has been selected. + * The selected frame will return true, while all others will return false.</p> + * + * @param currentFrameString starting frame + * @param target new frame (which might be relative to the current one) + * @return boolean true if the new frame is this code's window + */ + var isDom = false; + if (target.indexOf("dom=") == 0) { + target = target.substr(4); + isDom = true; + } + var t; + try { + eval("t=" + currentFrameString + "." + target); + } catch (e) { + } + var autWindow = this.browserbot.getCurrentWindow(); + if (t != null) { + if (t.window == autWindow) { + return true; + } + return false; + } + if (isDom) { + return false; + } + var currentFrame; + eval("currentFrame=" + currentFrameString); + if (target == "relative=up") { + if (currentFrame.window.parent == autWindow) { + return true; + } + return false; + } + if (target == "relative=top") { + if (currentFrame.window.top == autWindow) { + return true; + } + return false; + } + if (autWindow.name == target && currentFrame.window == autWindow.parent) { + return true; + } + return false; +}; + Selenium.prototype.doWaitForPopUp = function(windowID, timeout) { /** * Waits for a popup window to appear and load up. @@ -436,21 +688,36 @@ Selenium.prototype.doWaitForPopUp = function(windowID, timeout) { if (isNaN(timeout)) { throw new SeleniumError("Timeout is not a number: " + timeout); } - - testLoop.waitForCondition = function () { - var targetWindow = selenium.browserbot.getTargetWindow(windowID); + + var popupLoadedPredicate = function () { + var targetWindow = selenium.browserbot.getWindowByName(windowID, true); if (!targetWindow) return false; if (!targetWindow.location) return false; if ("about:blank" == targetWindow.location) return false; + if (browserVersion.isKonqueror) { + if ("/" == targetWindow.location.href) { + // apparently Konqueror uses this as the temporary location, instead of about:blank + return false; + } + } + if (browserVersion.isSafari) { + if(targetWindow.location.href == selenium.browserbot.buttonWindow.location.href) { + // Apparently Safari uses this as the temporary location, instead of about:blank + // what a world! + LOG.debug("DGF what a world!"); + return false; + } + } if (!targetWindow.document) return false; - if (!targetWindow.document.readyState) return true; + if (!selenium.browserbot.getCurrentWindow().document.readyState) { + // This is Firefox, with no readyState extension + return true; + } if ('complete' != targetWindow.document.readyState) return false; return true; }; - - testLoop.waitForConditionStart = new Date().getTime(); - testLoop.waitForConditionTimeout = timeout; - + + return decorateFunctionWithTimeout(popupLoadedPredicate, timeout); } Selenium.prototype.doWaitForPopUp.dontCheckAlertsAndConfirms = true; @@ -461,7 +728,7 @@ Selenium.prototype.doChooseCancelOnNextConfirmation = function() { * 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(); }; @@ -471,8 +738,8 @@ 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); @@ -481,7 +748,7 @@ Selenium.prototype.doAnswerOnNextPrompt = function(answer) { Selenium.prototype.doGoBack = function() { /** * Simulates the user clicking the "back" button on their browser. - * + * */ this.page().goBack(); }; @@ -489,23 +756,32 @@ Selenium.prototype.doGoBack = function() { 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. - */ + /** + * Simulates the user clicking the "close" button in the titlebar of a popup + * window or tab. + */ this.page().close(); }; +Selenium.prototype.ensureNoUnhandledPopups = function() { + if (this.browserbot.hasAlerts()) { + throw new SeleniumError("There was an unexpected Alert! [" + this.browserbot.getNextAlert() + "]"); + } + if ( this.browserbot.hasConfirmations() ) { + throw new SeleniumError("There was an unexpected Confirmation! [" + this.browserbot.getNextConfirmation() + "]"); + } +}; + Selenium.prototype.isAlertPresent = function() { /** * Has an alert occurred? - * + * * <p> * This function never throws an exception * </p> @@ -513,10 +789,11 @@ Selenium.prototype.isAlertPresent = function() { */ return this.browserbot.hasAlerts(); }; + Selenium.prototype.isPromptPresent = function() { /** * Has a prompt occurred? - * + * * <p> * This function never throws an exception * </p> @@ -524,10 +801,11 @@ Selenium.prototype.isPromptPresent = function() { */ return this.browserbot.hasPrompts(); }; + Selenium.prototype.isConfirmationPresent = function() { /** * Has confirm() been called? - * + * * <p> * This function never throws an exception * </p> @@ -538,14 +816,14 @@ Selenium.prototype.isConfirmationPresent = function() { 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> @@ -562,26 +840,26 @@ 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()) { @@ -590,19 +868,19 @@ Selenium.prototype.getConfirmation = function() { 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> @@ -616,18 +894,18 @@ Selenium.prototype.getPrompt = function() { 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; + return this.page().getCurrentWindow().location; }; Selenium.prototype.getTitle = function() { /** Gets the title of the current page. - * + * * @return string the title of the current page */ - return this.page().title(); + return this.page().getTitle(); }; @@ -645,7 +923,7 @@ 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 */ @@ -659,7 +937,7 @@ Selenium.prototype.getText = function(locator) { * 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 */ @@ -668,9 +946,9 @@ Selenium.prototype.getText = function(locator) { }; Selenium.prototype.getEval = function(script) { - /** Gets the result of evaluating the specified JavaScript snippet. The snippet may + /** 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> @@ -679,7 +957,7 @@ Selenium.prototype.getEval = function(script) { * 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 */ @@ -697,7 +975,7 @@ 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 + * @return boolean true if the checkbox is checked, false otherwise */ var element = this.page().findElement(locator); if (element.checked == null) { @@ -710,7 +988,7 @@ 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 */ @@ -742,24 +1020,6 @@ Selenium.prototype.getTable = function(tableCellAddress) { 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. * @@ -842,9 +1102,9 @@ Selenium.prototype.isSomethingSelected = function(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) { @@ -886,7 +1146,7 @@ Selenium.prototype.findSelectedOptionProperty = function(locator, property) { 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 */ @@ -898,7 +1158,7 @@ Selenium.prototype.getSelectOptions = function(selectLocator) { var option = element.options[i].text.replace(/,/g, "\\,"); selectOptions.push(option); } - + return selectOptions.join(","); }; @@ -906,6 +1166,10 @@ Selenium.prototype.getSelectOptions = function(selectLocator) { Selenium.prototype.getAttribute = function(attributeLocator) { /** * Gets the value of an element attribute. + * + * Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to + * get null event arguments. Read the bug for more details, including a workaround. + * * @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 */ @@ -924,15 +1188,15 @@ Selenium.prototype.isTextPresent = function(pattern) { */ 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); + var patternMatcher = new PatternMatcher(pattern); + if (patternMatcher.strategy == PatternMatcher.strategies.glob) { + patternMatcher.matcher = new PatternMatcher.strategies.globContains(pattern); + } + else if (patternMatcher.strategy == PatternMatcher.strategies.exact) { + pattern = pattern.substring("exact:".length); // strip off "exact:" + return allText.indexOf(pattern) != -1; } + return patternMatcher.matches(allText); }; Selenium.prototype.isElementPresent = function(locator) { @@ -956,19 +1220,14 @@ Selenium.prototype.isVisible = function(locator) { * 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); + element = this.page().findElement(locator); + var visibility = this.findEffectiveStyleProperty(element, "visibility"); + var _isDisplayed = this._isDisplayed(element); return (visibility != "hidden" && _isDisplayed); }; @@ -982,10 +1241,7 @@ Selenium.prototype.findEffectiveStyleProperty = function(element, property) { }; Selenium.prototype._isDisplayed = function(element) { - if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) - var display = element.style["display"]; - else - var display = this.findEffectiveStyleProperty(element, "display"); + var display = this.findEffectiveStyleProperty(element, "display"); if (display == "none") return false; if (element.parentNode.style) { return this._isDisplayed(element.parentNode); @@ -997,8 +1253,8 @@ Selenium.prototype.findEffectiveStyle = function(element) { if (element.style == undefined) { return undefined; // not a styled element } - var window = this.browserbot.getContentWindow(); - if (window.getComputedStyle) { + var window = this.browserbot.getCurrentWindow(); + if (window.getComputedStyle) { // DOM-Level-2-CSS return window.getComputedStyle(element, null); } @@ -1016,7 +1272,7 @@ 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 */ @@ -1029,9 +1285,9 @@ Selenium.prototype.isEditable = function(locator) { 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(); @@ -1039,9 +1295,9 @@ Selenium.prototype.getAllButtons = function() { 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(); @@ -1049,28 +1305,175 @@ Selenium.prototype.getAllLinks = function() { 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._getTestAppParentOfAllWindows = 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 + */ + if (this.browserbot.getCurrentWindow().opener!=null) { + return this.browserbot.getCurrentWindow().opener; + } + if (this.browserbot.buttonWindow!=null) { + return this.browserbot.buttonWindow; + } + return top; // apparently we are in proxy injection mode +}; + +Selenium.prototype.getAttributeFromAllWindows = function(attributeName) { + /** Returns every instance of some attribute from all known windows. + * + * @param attributeName name of an attribute on the windows + * @return string[] the set of values of this attribute from all known windows. + */ + var attributes = new Array(); + var testAppParentOfAllWindows = this._getTestAppParentOfAllWindows(); + attributes.push(eval("testAppParentOfAllWindows." + attributeName)); + var selenium = testAppParentOfAllWindows.selenium==null ? testAppParentOfAllWindows.parent.selenium : testAppParentOfAllWindows.selenium; + for (windowName in selenium.browserbot.openedWindows) + { + attributes.push(eval("selenium.browserbot.openedWindows[windowName]." + attributeName)); + } + return attributes; +}; + +Selenium.prototype.findWindow = function(soughtAfterWindowPropertyValue) { + var testAppParentOfAllWindows = this._getTestAppParentOfAllWindows(); + var targetPropertyName = "name"; + if (soughtAfterWindowPropertyValue.match("^title=")) { + targetPropertyName = "document.title"; + soughtAfterWindowPropertyValue = soughtAfterWindowPropertyValue.replace(/^title=/, ""); + } + else { + // matching "name": + // If we are not in proxy injection mode, then the top-level test window will be named myiframe. + // But as far as the interface goes, we are expected to match a blank string to this window, if + // we are searching with respect to the widow name. + // So make a special case so that this logic will work: + if (PatternMatcher.matches(soughtAfterWindowPropertyValue, "")) { + return this.browserbot.getCurrentWindow(); + } + } + + if (PatternMatcher.matches(soughtAfterWindowPropertyValue, eval("testAppParentOfAllWindows." + targetPropertyName))) { + return testAppParentOfAllWindows; + } + for (windowName in selenium.browserbot.openedWindows) { + var openedWindow = selenium.browserbot.openedWindows[windowName]; + if (PatternMatcher.matches(soughtAfterWindowPropertyValue, eval("openedWindow." + targetPropertyName))) { + return openedWindow; + } + } + throw new SeleniumError("could not find window with property " + targetPropertyName + " matching " + soughtAfterWindowPropertyValue); +}; + +Selenium.prototype.doDragdrop = function(locator, movementsString) { + /** Drags an element a certain distance and then drops it + * Beware of http://jira.openqa.org/browse/SEL-280, which will lead some event handlers to + * get null event arguments. Read the bug for more details, including a workaround. + * + * @param movementsString offset in pixels from the current location to which the element should be moved, e.g., "+70,-300" + * @param locator an element locator + */ + var element = this.page().findElement(locator); + var clientStartXY = getClientXY(element) + var clientStartX = clientStartXY[0]; + var clientStartY = clientStartXY[1]; + + var movements = movementsString.split(/,/); + var movementX = Number(movements[0]); + var movementY = Number(movements[1]); + + var clientFinishX = ((clientStartX + movementX) < 0) ? 0 : (clientStartX + movementX); + var clientFinishY = ((clientStartY + movementY) < 0) ? 0 : (clientStartY + movementY); + + var movementXincrement = (movementX > 0) ? 1 : -1; + var movementYincrement = (movementY > 0) ? 1 : -1; + + triggerMouseEvent(element, 'mousedown', true, clientStartX, clientStartY); + var clientX = clientStartX; + var clientY = clientStartY; + while ((clientX != clientFinishX) || (clientY != clientFinishY)) { + if (clientX != clientFinishX) { + clientX += movementXincrement; + } + if (clientY != clientFinishY) { + clientY += movementYincrement; + } + triggerMouseEvent(element, 'mousemove', true, clientX, clientY); + } + triggerMouseEvent(element, 'mouseup', true, clientFinishX, clientFinishY); +}; + +Selenium.prototype.doWindowFocus = function(windowName) { +/** Gives focus to a window + * + * @param windowName name of the window to be given focus + */ + this.findWindow(windowName).focus(); +}; + + +Selenium.prototype.doWindowMaximize = function(windowName) { +/** Resize window to take up the entire screen + * + * @param windowName name of the window to be enlarged + */ + var window = this.findWindow(windowName); + if (window!=null && window.screen) { + window.moveTo(0,0); + window.outerHeight = screen.availHeight; + window.outerWidth = screen.availWidth; + } +}; + +Selenium.prototype.getAllWindowIds = function() { + /** Returns the IDs of all windows that the browser knows about. + * + * @return string[] the IDs of all windows that the browser knows about. + */ + return this.getAttributeFromAllWindows("id"); +}; + +Selenium.prototype.getAllWindowNames = function() { + /** Returns the names of all windows that the browser knows about. + * + * @return string[] the names of all windows that the browser knows about. + */ + return this.getAttributeFromAllWindows("name"); +}; + +Selenium.prototype.getAllWindowTitles = function() { + /** Returns the titles of all windows that the browser knows about. + * + * @return string[] the titles of all windows that the browser knows about. + */ + return this.getAttributeFromAllWindows("document.title"); +}; + 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; + return this.page().document().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. */ @@ -1081,13 +1484,13 @@ Selenium.prototype.doSetCursorPosition = function(locator, position) { if (position == -1) { position = element.value.length; } - + if( element.setSelectionRange && !browserVersion.isOpera) { element.focus(); - element.setSelectionRange(/*start*/position,/*end*/position); - } + element.setSelectionRange(/*start*/position,/*end*/position); + } else if( element.createTextRange ) { - triggerEvent(element, 'focus', false); + triggerEvent(element, 'focus', false); var range = element.createTextRange(); range.collapse(true); range.moveEnd('character',position); @@ -1096,22 +1499,205 @@ Selenium.prototype.doSetCursorPosition = function(locator, position) { } } +Selenium.prototype.getElementIndex = function(locator) { + /** + * Get the relative index of an element to its parent (starting from 0). The comment node and empty text node + * will be ignored. + * + * @param locator an <a href="#locators">element locator</a> pointing to an element + * @return number of relative index of the element to its parent (starting from 0) + */ + var element = this.page().findElement(locator); + var previousSibling; + var index = 0; + while ((previousSibling = element.previousSibling) != null) { + if (!this._isCommentOrEmptyTextNode(previousSibling)) { + index++; + } + element = previousSibling; + } + return index; +} + +Selenium.prototype.isOrdered = function(locator1, locator2) { + /** + * Check if these two elements have same parent and are ordered. Two same elements will + * not be considered ordered. + * + * @param locator1 an <a href="#locators">element locator</a> pointing to the first element + * @param locator2 an <a href="#locators">element locator</a> pointing to the second element + * @return boolean true if two elements are ordered and have same parent, false otherwise + */ + var element1 = this.page().findElement(locator1); + var element2 = this.page().findElement(locator2); + if (element1 === element2) return false; + + var previousSibling; + while ((previousSibling = element2.previousSibling) != null) { + if (previousSibling === element1) { + return true; + } + element2 = previousSibling; + } + return false; +} + +Selenium.prototype._isCommentOrEmptyTextNode = function(node) { + return node.nodeType == 8 || ((node.nodeType == 3) && !(/[^\t\n\r ]/.test(node.data))); +} + +Selenium.prototype.getElementPositionLeft = function(locator) { + /** + * Retrieves the horizontal position of an element + * + * @param locator an <a href="#locators">element locator</a> pointing to an element OR an element itself + * @return number of pixels from the edge of the frame. + */ + var element; + if ("string"==typeof locator) { + element = this.page().findElement(locator); + } + else { + element = locator; + } + var x = element.offsetLeft; + var elementParent = element.offsetParent; + + while (elementParent != null) + { + if(document.all) + { + if( (elementParent.tagName != "TABLE") && (elementParent.tagName != "BODY") ) + { + x += elementParent.clientLeft; + } + } + else // Netscape/DOM + { + if(elementParent.tagName == "TABLE") + { + var parentBorder = parseInt(elementParent.border); + if(isNaN(parentBorder)) + { + var parentFrame = elementParent.getAttribute('frame'); + if(parentFrame != null) + { + x += 1; + } + } + else if(parentBorder > 0) + { + x += parentBorder; + } + } + } + x += elementParent.offsetLeft; + elementParent = elementParent.offsetParent; + } + return x; +}; + +Selenium.prototype.getElementPositionTop = function(locator) { + /** + * Retrieves the vertical position of an element + * + * @param locator an <a href="#locators">element locator</a> pointing to an element OR an element itself + * @return number of pixels from the edge of the frame. + */ + var element; + if ("string"==typeof locator) { + element = this.page().findElement(locator); + } + else { + element = locator; + } + + var y = 0; + + while (element != null) + { + if(document.all) + { + if( (element.tagName != "TABLE") && (element.tagName != "BODY") ) + { + y += element.clientTop; + } + } + else // Netscape/DOM + { + if(element.tagName == "TABLE") + { + var parentBorder = parseInt(element.border); + if(isNaN(parentBorder)) + { + var parentFrame = element.getAttribute('frame'); + if(parentFrame != null) + { + y += 1; + } + } + else if(parentBorder > 0) + { + y += parentBorder; + } + } + } + y += element.offsetTop; + + // Netscape can get confused in some cases, such that the height of the parent is smaller + // than that of the element (which it shouldn't really be). If this is the case, we need to + // exclude this element, since it will result in too large a 'top' return value. + if (element.offsetParent && element.offsetParent.offsetHeight && element.offsetParent.offsetHeight < element.offsetHeight) + { + // skip the parent that's too small + element = element.offsetParent.offsetParent; + } + else + { + // Next up... + element = element.offsetParent; + } + } + return y; +}; + +Selenium.prototype.getElementWidth = function(locator) { + /** + * Retrieves the width of an element + * + * @param locator an <a href="#locators">element locator</a> pointing to an element + * @return number width of an element in pixels + */ + var element = this.page().findElement(locator); + return element.offsetWidth; +}; + +Selenium.prototype.getElementHeight = function(locator) { + /** + * Retrieves the height of an element + * + * @param locator an <a href="#locators">element locator</a> pointing to an element + * @return number height of an element in pixels + */ + var element = this.page().findElement(locator); + return element.offsetHeight; +}; + 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 doc = this.page().getDocument(); var win = this.browserbot.getCurrentWindow(); if( doc.selection && !browserVersion.isOpera){ - var selectRange = doc.selection.createRange().duplicate(); var elementRange = element.createTextRange(); selectRange.move("character",0); @@ -1126,28 +1712,28 @@ Selenium.prototype.getCursorPosition = function(locator) { var answer = String(elementRange.text).replace(/\r/g,"").length; return answer; } else { - if (typeof(element.selectionStart) != undefined) { + 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 @@ -1163,8 +1749,8 @@ 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> - * + * It is used to generate commands like assertExpression and waitForExpression.</p> + * * @param expression the value to return * @return string the value passed in */ @@ -1176,7 +1762,7 @@ 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 @@ -1187,13 +1773,9 @@ Selenium.prototype.doWaitForCondition = function(script, timeout) { if (isNaN(timeout)) { throw new SeleniumError("Timeout is not a number: " + timeout); } - - testLoop.waitForCondition = function () { + return decorateFunctionWithTimeout(function () { return eval(script); - }; - - testLoop.waitForConditionStart = new Date().getTime(); - testLoop.waitForConditionTimeout = timeout; + }, timeout); }; Selenium.prototype.doWaitForCondition.dontCheckAlertsAndConfirms = true; @@ -1206,23 +1788,33 @@ Selenium.prototype.doSetTimeout = function(timeout) { * The default timeout is 30 seconds. * @param timeout a timeout in milliseconds, after which the action will return with an error */ - testLoop.waitForConditionTimeout = timeout; + this.defaultTimeout = parseInt(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); + if (isNaN(timeout)) { + throw new SeleniumError("Timeout is not a number: " + timeout); + } + // in pi-mode, the test and the harness share the window; thus if we are executing this code, then we have loaded + if (window["proxyInjectionMode"] == null || !window["proxyInjectionMode"]) { + return this.makePageLoadCondition(timeout); + } +}; + +Selenium.prototype._isNewPageLoaded = function() { + return this.browserbot.isNewPageLoaded(); }; Selenium.prototype.doWaitForPageToLoad.dontCheckAlertsAndConfirms = true; @@ -1264,6 +1856,55 @@ Selenium.prototype.replaceVariables = function(str) { return stringResult; }; +Selenium.prototype.getCookie = function() { + /** + * Return all cookies of the current page under test. + * + * @return string all cookies of the current page under test + */ + var doc = this.page().document(); + return doc.cookie; +}; + +Selenium.prototype.doCreateCookie = function(nameValuePair, optionsString) { + /** + * Create a new cookie whose path and domain are same with those of current page + * under test, unless you specified a path for this cookie explicitly. + * + * @param nameValuePair name and value of the cookie in a format "name=value" + * @param optionsString options for the cookie. Currently supported options include 'path' and 'max_age'. + * the optionsString's format is "path=/path/, max_age=60". The order of options are irrelevant, the unit + * of the value of 'max_age' is second. + */ + var results = /[^\s=\[\]\(\),"\/\?@:;]+=[^\s=\[\]\(\),"\/\?@:;]*/.test(nameValuePair); + if (!results) { + throw new SeleniumError("Invalid parameter."); + } + var cookie = nameValuePair.trim(); + results = /max_age=(\d+)/.exec(optionsString); + if (results) { + var expireDateInMilliseconds = (new Date()).getTime() + results[1] * 1000; + cookie += "; expires=" + new Date(expireDateInMilliseconds).toGMTString(); + } + results = /path=([^\s,]+)[,]?/.exec(optionsString); + if (results) { + cookie += "; path=" + results[1]; + } + this.page().document().cookie = cookie; +} + +Selenium.prototype.doDeleteCookie = function(name,path) { + /** + * Delete a named cookie with specified path. + * + * @param name the name of the cookie to be deleted + * @param path the path property of the cookie to be deleted + */ + // set the expire time of the cookie to be deleted to one minute before now. + var expireDateInMilliseconds = (new Date()).getTime() + (-1 * 1000); + this.page().document().cookie = name.trim() + "=deleted; path=" + path.trim() + "; expires=" + new Date(expireDateInMilliseconds).toGMTString(); +} + /** * Factory for creating "Option Locators". @@ -1398,5 +2039,3 @@ OptionLocatorFactory.prototype.OptionLocatorById = function(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 index 8df46865..22df0fdb 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-browserbot.js +++ b/tests/test_tools/selenium/core/scripts/selenium-browserbot.js @@ -27,9 +27,17 @@ // 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; +var BrowserBot = function(topLevelApplicationWindow) { + this.topWindow = topLevelApplicationWindow; + + // the buttonWindow is the Selenium window + // it contains the Run/Pause buttons... this should *not* be the AUT window + // todo: Here the buttonWindow is not Selenium window. It will be set to Selenium window in pollForLoad. + // Change this!!! + this.buttonWindow = this.topWindow; + // not sure what this is used for this.currentPage = null; + this.currentWindow = this.topWindow; this.currentWindowName = null; this.modalDialogTest = null; @@ -42,49 +50,60 @@ var BrowserBot = function(frame) { this.newPageLoaded = false; this.pageLoadError = null; + this.uniqueId = new Date().getTime(); + this.pollingForLoad = new Object(); + this.windowPollers = new Array(); + var self = this; this.recordPageLoad = function() { - LOG.debug("Page load detected"); + LOG.debug("Page load detected"); try { - LOG.debug("Page load location=" + self.getCurrentWindow().location); + LOG.debug("Page load location=" + self.getCurrentWindow(true).location); } catch (e) { - self.pageLoadError = e; - return; + self.pageLoadError = e; + return; } self.currentPage = null; self.newPageLoaded = true; }; this.isNewPageLoaded = function() { - if (this.pageLoadError) throw this.pageLoadError; + if (this.pageLoadError) { + var e = this.pageLoadError; + this.pageLoadError = null; + throw e; + } return self.newPageLoaded; }; }; -BrowserBot.createForFrame = function(frame) { +BrowserBot.createForWindow = function(window) { var browserbot; + LOG.debug('createForWindow'); LOG.debug("browserName: " + browserVersion.name); LOG.debug("userAgent: " + navigator.userAgent); if (browserVersion.isIE) { - browserbot = new IEBrowserBot(frame); + browserbot = new IEBrowserBot(window); } else if (browserVersion.isKonqueror) { - browserbot = new KonquerorBrowserBot(frame); + browserbot = new KonquerorBrowserBot(window); + } + else if (browserVersion.isOpera) { + browserbot = new OperaBrowserBot(window); } else if (browserVersion.isSafari) { - browserbot = new SafariBrowserBot(frame); + browserbot = new SafariBrowserBot(window); } else { - LOG.info("Using MozillaBrowserBot") // Use mozilla by default - browserbot = new MozillaBrowserBot(frame); + browserbot = new MozillaBrowserBot(window); } - - // Modify the test IFrame so that page loads are detected. - addLoadListener(browserbot.getFrame(), browserbot.recordPageLoad); + browserbot.getCurrentWindow(); + // todo: why? return browserbot; }; +// todo: rename? This doesn't actually "do" anything. BrowserBot.prototype.doModalDialogTest = function(test) { this.modalDialogTest = test; }; @@ -98,67 +117,138 @@ BrowserBot.prototype.setNextPromptResult = function(result) { }; BrowserBot.prototype.hasAlerts = function() { - return (this.recordedAlerts.length > 0) ; + return (this.recordedAlerts.length > 0); }; +BrowserBot.prototype.relayBotToRC = function() { +}; +// override in injection.html + +BrowserBot.prototype.resetPopups = function() { + this.recordedAlerts = []; + this.recordedConfirmations = []; + this.recordedPrompts = []; +} + BrowserBot.prototype.getNextAlert = function() { - return this.recordedAlerts.shift(); + var t = this.recordedAlerts.shift(); + this.relayBotToRC("browserbot.recordedAlerts"); + return t; }; BrowserBot.prototype.hasConfirmations = function() { - return (this.recordedConfirmations.length > 0) ; + return (this.recordedConfirmations.length > 0); }; BrowserBot.prototype.getNextConfirmation = function() { - return this.recordedConfirmations.shift(); + var t = this.recordedConfirmations.shift(); + this.relayBotToRC("browserbot.recordedConfirmations"); + return t; }; BrowserBot.prototype.hasPrompts = function() { - return (this.recordedPrompts.length > 0) ; + return (this.recordedPrompts.length > 0); }; BrowserBot.prototype.getNextPrompt = function() { - return this.recordedPrompts.shift(); + var t = this.recordedPrompts.shift(); + this.relayBotToRC("browserbot.recordedPrompts"); + return t; }; -BrowserBot.prototype.getFrame = function() { - return this.frame; +BrowserBot.prototype._windowClosed = function(win) { + var c = win.closed; + if (c == null) return true; + return c; +}; + +BrowserBot.prototype._modifyWindow = function(win) { + if (this._windowClosed(win)) { + LOG.error("modifyWindow: Window was closed!"); + return null; + } + LOG.debug('modifyWindow ' + this.uniqueId + ":" + win[this.uniqueId]); + if (!win[this.uniqueId]) { + win[this.uniqueId] = true; + this.modifyWindowToRecordPopUpDialogs(win, this); + this.currentPage = PageBot.createForWindow(this); + this.newPageLoaded = false; + } + this.modifySeparateTestWindowToDetectPageLoads(win); + return win; }; 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; + this._selectWindowByName(target); + } else { + this._selectTopWindow(); + } +}; + +BrowserBot.prototype._selectTopWindow = function() { + this.currentWindowName = null; + this.currentWindow = this.topWindow; +} + +BrowserBot.prototype._selectWindowByName = function(target) { + this.currentWindow = this.getWindowByName(target, false); + this.currentWindowName = target; +} + +BrowserBot.prototype.selectFrame = function(target) { + if (target == "relative=up") { + this.currentWindow = this.getCurrentWindow().parent; + } else if (target == "relative=top") { + this.currentWindow = this.topWindow; + } else { + var frame = this.getCurrentPage().findElement(target); + if (frame == null) { + throw new SeleniumError("Not found: " + target); + } + // now, did they give us a frame or a frame ELEMENT? + if (frame.contentWindow) { + // this must be a frame element + this.currentWindow = frame.contentWindow; + } else if (frame.document) { + // must be an actual window frame + this.currentWindow = frame; + } else { + // neither + throw new SeleniumError("Not a frame: " + target); } } + this.currentPage = null; }; BrowserBot.prototype.openLocation = function(target) { // We're moving to a new page - clear the current one + var win = this.getCurrentWindow(); + LOG.debug("openLocation newPageLoaded = false"); this.currentPage = null; this.newPageLoaded = false; - this.setOpenLocation(target); + this.setOpenLocation(win, target); }; BrowserBot.prototype.setIFrameLocation = function(iframe, location) { iframe.src = location; }; -BrowserBot.prototype.setOpenLocation = function(location) { - this.getCurrentWindow().location.href = location; +BrowserBot.prototype.setOpenLocation = function(win, loc) { + + // is there a Permission Denied risk here? setting a timeout breaks Firefox + //win.setTimeout(function() { win.location.href = loc; }, 0); + win.location.href = loc; }; 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.currentPage = PageBot.createForWindow(this); this.newPageLoaded = false; } @@ -166,14 +256,18 @@ BrowserBot.prototype.getCurrentPage = function() { }; BrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModify, browserBot) { + var self = this; + windowToModify.alert = function(alert) { browserBot.recordedAlerts.push(alert); + self.relayBotToRC("browserbot.recordedAlerts"); }; windowToModify.confirm = function(message) { browserBot.recordedConfirmations.push(message); var result = browserBot.nextConfirmResult; browserBot.nextConfirmResult = true; + self.relayBotToRC("browserbot.recordedConfirmations"); return result; }; @@ -182,6 +276,7 @@ BrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModify, var result = !browserBot.nextConfirmResult ? null : browserBot.nextPromptResult; browserBot.nextConfirmResult = true; browserBot.nextPromptResult = ''; + self.relayBotToRC("browserbot.recordedPrompts"); return result; }; @@ -196,102 +291,249 @@ BrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModify, }; /** - * 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) { +BrowserBot.prototype.modifySeparateTestWindowToDetectPageLoads = function(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); + if (!windowObject) { + LOG.warn("modifySeparateTestWindowToDetectPageLoads: no windowObject!"); + return; + } + if (this._windowClosed(windowObject)) { + LOG.info("modifySeparateTestWindowToDetectPageLoads: windowObject was closed"); + return; + } + var oldMarker = this.isPollingForLoad(windowObject); + if (oldMarker) { + LOG.debug("modifySeparateTestWindowToDetectPageLoads: already polling this window: " + oldMarker); + return; + } + + var marker = 'selenium' + new Date().getTime(); + LOG.debug("Starting pollForLoad (" + marker + "): " + windowObject.document.location); + this.pollingForLoad[marker] = true; + // if this is a frame, add a load listener, otherwise, attach a poller + if (this._getFrameElement(windowObject)) { + LOG.debug("modifySeparateTestWindowToDetectPageLoads: this window is a frame; attaching a load listener"); + addLoadListener(windowObject.frameElement, this.recordPageLoad); + windowObject.frameElement[marker] = true; + windowObject.frameElement[this.uniqueId] = marker; + } else { + windowObject.document.location[marker] = true; + windowObject[this.uniqueId] = marker; + this.pollForLoad(this.recordPageLoad, windowObject, windowObject.document, windowObject.location, windowObject.location.href, marker); } }; +BrowserBot.prototype._getFrameElement = function(win) { + var frameElement = null; + try { + frameElement = win.frameElement; + } catch (e) { + } // on IE, checking frameElement on a pop-up results in a "No such interface supported" exception + return frameElement; +} + /** * 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; +BrowserBot.prototype.pollForLoad = function(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker) { + LOG.debug("pollForLoad original (" + marker + "): " + originalHref); + try { - windowClosed = windowObject.closed; + if (this._windowClosed(windowObject)) { + LOG.debug("pollForLoad WINDOW CLOSED (" + marker + ")"); + delete this.pollingForLoad[marker]; + return; + } + // todo: Change this!!! + // under multi-window layout, buttonWindow should be TestRunner window + // but only after the _windowClosed checking, we can ensure that this.topWindow exists + // then we can assign the TestRunner window to buttonWindow + this.buttonWindow = windowObject.opener; + + var isSamePage = this._isSamePage(windowObject, originalDocument, originalLocation, originalHref, marker); + var rs = this.getReadyState(windowObject, windowObject.document); + + if (!isSamePage && rs == 'complete') { + var currentHref = windowObject.location.href; + LOG.debug("pollForLoad FINISHED (" + marker + "): " + rs + " (" + currentHref + ")"); + delete this.pollingForLoad[marker]; + this._modifyWindow(windowObject); + var newMarker = this.isPollingForLoad(windowObject); + if (!newMarker) { + LOG.debug("modifyWindow didn't start new poller: " + newMarker); + this.modifySeparateTestWindowToDetectPageLoads(windowObject); + } + newMarker = this.isPollingForLoad(windowObject); + LOG.debug("pollForLoad (" + marker + ") restarting " + newMarker); + if (/(TestRunner-splash|Blank)\.html\?start=true$/.test(currentHref)) { + LOG.debug("pollForLoad Oh, it's just the starting page. Never mind!"); + } else if (this.currentWindow[this.uniqueId] == newMarker) { + loadFunction(); + } else { + LOG.debug("pollForLoad page load detected in non-current window; ignoring"); + } + return; + } + LOG.debug("pollForLoad continue (" + marker + "): " + currentHref); + this.reschedulePoller(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker); } 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 + LOG.error("Exception during pollForLoad; this should get noticed soon (" + e.message + ")!"); + LOG.exception(e); + this.pageLoadError = e; } - if (null == windowClosed) windowClosed = true; - if (windowClosed) { - this.pollingForLoad = false; - return; +}; + +BrowserBot.prototype._isSamePage = function(windowObject, originalDocument, originalLocation, originalHref, marker) { + var currentDocument = windowObject.document; + var currentLocation = windowObject.location; + var currentHref = currentLocation.href + + var sameDoc = this._isSameDocument(originalDocument, currentDocument); + + var sameLoc = (originalLocation === currentLocation); + var sameHref = (originalHref === currentHref); + var markedLoc = currentLocation[marker]; + + if (browserVersion.isKonqueror || browserVersion.isSafari) { + // the mark disappears too early on these browsers + markedLoc = true; } + return sameDoc && sameLoc && sameHref && markedLoc +}; - LOG.debug("pollForLoad original: " + originalHref); - try { +BrowserBot.prototype._isSameDocument = function(originalDocument, currentDocument) { + return originalDocument === currentDocument; +}; - var currentLocation = windowObject.location; - var currentHref = currentLocation.href - var sameLoc = (originalLocation === currentLocation); - var sameHref = (originalHref === currentHref); - var rs = windowObject.document.readyState; +BrowserBot.prototype.getReadyState = function(windowObject, currentDocument) { + var rs = currentDocument.readyState; + if (rs == null) { + if ((this.buttonWindow!=null && this.buttonWindow.document.readyState == null) // not proxy injection mode (and therefore buttonWindow isn't null) + || (top.document.readyState == null)) { // proxy injection mode (and therefore everything's in the top window, but buttonWindow doesn't exist) + // uh oh! we're probably on Firefox with no readyState extension installed! + // We'll have to just take a guess as to when the document is loaded; this guess + // will never be perfect. :-( + if (typeof currentDocument.getElementsByTagName != 'undefined' + && typeof currentDocument.getElementById != 'undefined' + && ( currentDocument.getElementsByTagName('body')[0] != null + || currentDocument.body != null )) { + if (windowObject.frameElement && windowObject.location.href == "about:blank" && windowObject.frameElement.src != "about:blank") { + LOG.info("getReadyState not loaded, frame location was about:blank, but frame src = " + windowObject.frameElement.src); + return null; + } + LOG.debug("getReadyState = windowObject.frames.length = " + windowObject.frames.length); + for (var i = 0; i < windowObject.frames.length; i++) { + LOG.debug("i = " + i); + if (this.getReadyState(windowObject.frames[i], windowObject.frames[i].document) != 'complete') { + LOG.debug("getReadyState aha! the nested frame " + windowObject.frames[i].name + " wasn't ready!"); + return null; + } + } - if (rs == null) rs = 'complete'; + rs = 'complete'; + } else { + LOG.debug("pollForLoad readyState was null and DOM appeared to not be ready yet"); + } + } + } + else if (rs == "loading" && browserVersion.isIE) { + LOG.debug("pageUnloading = true!!!!"); + this.pageUnloading = true; + } + LOG.debug("getReadyState returning " + rs); + return rs; +}; - 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; - } +/** This function isn't used normally, but was the way we used to schedule pollers: + asynchronously executed autonomous units. This is deprecated, but remains here + for future reference. + */ +BrowserBot.prototype.XXXreschedulePoller = function(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker) { + var self = this; + window.setTimeout(function() { + self.pollForLoad(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker); + }, 500); }; +/** This function isn't used normally, but is useful for debugging asynchronous pollers + * To enable it, rename it to "reschedulePoller", so it will override the + * existing reschedulePoller function + */ +BrowserBot.prototype.XXXreschedulePoller = function(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker) { + var doc = this.buttonWindow.document; + var button = doc.createElement("button"); + var buttonName = doc.createTextNode(marker + " - " + windowObject.name); + button.appendChild(buttonName); + var tools = doc.getElementById("tools"); + var self = this; + button.onclick = function() { + tools.removeChild(button); + self.pollForLoad(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker); + }; + tools.appendChild(button); + window.setTimeout(button.onclick, 500); +}; + +BrowserBot.prototype.reschedulePoller = function(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker) { + var self = this; + var pollerFunction = function() { + self.pollForLoad(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker); + }; + this.windowPollers.push(pollerFunction); +}; -BrowserBot.prototype.getContentWindow = function() { - return this.getFrame().contentWindow || frames[this.getFrame().id]; +BrowserBot.prototype.runScheduledPollers = function() { + var oldPollers = this.windowPollers; + this.windowPollers = new Array(); + for (var i = 0; i < oldPollers.length; i++) { + oldPollers[i].call(); + } +}; + +BrowserBot.prototype.isPollingForLoad = function(win) { + var marker; + if (this._getFrameElement(win)) { + marker = win.frameElement[this.uniqueId]; + } else { + marker = win[this.uniqueId]; + } + if (!marker) { + LOG.debug("isPollingForLoad false, missing uniqueId " + this.uniqueId + ": " + marker); + return false; + } + if (!this.pollingForLoad[marker]) { + LOG.debug("isPollingForLoad false, this.pollingForLoad[" + marker + "]: " + this.pollingForLoad[marker]); + return false; + } + return marker; }; -BrowserBot.prototype.getTargetWindow = function(windowName) { - LOG.debug("getTargetWindow(" + windowName + ")"); +BrowserBot.prototype.getWindowByName = function(windowName, doNotModify) { + LOG.debug("getWindowByName(" + 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); + targetWindow = this.topWindow[windowName]; } if (!targetWindow) { throw new SeleniumError("Window does not exist"); } + if (!doNotModify) { + this._modifyWindow(targetWindow); + } return targetWindow; }; -BrowserBot.prototype.getCurrentWindow = function() { - var testWindow = this.getContentWindow().window; - if (this.currentWindowName != null) { - testWindow = this.getTargetWindow(this.currentWindowName); +BrowserBot.prototype.getCurrentWindow = function(doNotModify) { + var testWindow = this.currentWindow; + if (!doNotModify) { + this._modifyWindow(testWindow); } return testWindow; }; @@ -313,11 +555,27 @@ KonquerorBrowserBot.prototype.setIFrameLocation = function(iframe, location) { iframe.src = location; }; -KonquerorBrowserBot.prototype.setOpenLocation = function(location) { +KonquerorBrowserBot.prototype.setOpenLocation = function(win, loc) { // 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; + win.location.href = "about:blank"; + win.location.href = loc; + // force the current polling thread to detect a page load + var marker = this.isPollingForLoad(win); + if (marker) { + delete win.location[marker]; + } +}; + +KonquerorBrowserBot.prototype._isSameDocument = function(originalDocument, currentDocument) { + // under Konqueror, there may be this case: + // originalDocument and currentDocument are different objects + // while their location are same. + if (originalDocument) { + return originalDocument.location == currentDocument.location + } else { + return originalDocument === currentDocument; + } }; function SafariBrowserBot(frame) { @@ -326,6 +584,20 @@ function SafariBrowserBot(frame) { SafariBrowserBot.prototype = new BrowserBot; SafariBrowserBot.prototype.setIFrameLocation = KonquerorBrowserBot.prototype.setIFrameLocation; +SafariBrowserBot.prototype.setOpenLocation = KonquerorBrowserBot.prototype.setOpenLocation; + + +function OperaBrowserBot(frame) { + BrowserBot.call(this, frame); +} +OperaBrowserBot.prototype = new BrowserBot; +OperaBrowserBot.prototype.setIFrameLocation = function(iframe, location) { + if (iframe.src == location) { + iframe.src = location + '?reload'; + } else { + iframe.src = location; + } +} function IEBrowserBot(frame) { BrowserBot.call(this, frame); @@ -345,7 +617,7 @@ IEBrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModif 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; + var fullURL = base_ref + "TestRunner.html?singletest=" + escape(browserBot.modalDialogTest) + "&autoURL=" + escape(url) + "&runInterval=" + runOptions.runInterval; browserBot.modalDialogTest = null; var returnValue = oldShowModalDialog(fullURL, args, features); @@ -353,6 +625,85 @@ IEBrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModif }; }; +IEBrowserBot.prototype.modifySeparateTestWindowToDetectPageLoads = function(windowObject) { + this.pageUnloading = false; + this.permDeniedCount = 0; + var self = this; + var pageUnloadDetector = function() { + self.pageUnloading = true; + }; + windowObject.attachEvent("onbeforeunload", pageUnloadDetector); + BrowserBot.prototype.modifySeparateTestWindowToDetectPageLoads.call(this, windowObject); +}; + +IEBrowserBot.prototype.pollForLoad = function(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker) { + BrowserBot.prototype.pollForLoad.call(this, loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker); + if (this.pageLoadError) { + if (this.pageUnloading) { + var self = this; + LOG.warn("pollForLoad UNLOADING (" + marker + "): caught exception while firing events on unloading page: " + this.pageLoadError.message); + this.reschedulePoller(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker); + this.pageLoadError = null; + return; + } else if (((this.pageLoadError.message == "Permission denied") || (/^Access is denied/.test(this.pageLoadError.message))) + && this.permDeniedCount++ < 4) { + var self = this; + LOG.warn("pollForLoad (" + marker + "): " + this.pageLoadError.message + " (" + this.permDeniedCount + "), waiting to see if it goes away"); + this.reschedulePoller(loadFunction, windowObject, originalDocument, originalLocation, originalHref, marker); + this.pageLoadError = null; + return; + } + //handy for debugging! + //throw this.pageLoadError; + } +}; + +IEBrowserBot.prototype._windowClosed = function(win) { + try { + var c = win.closed; + // frame windows claim to be non-closed when their parents are closed + // but you can't access their document objects in that case + if (!c) { + try { + win.document; + } catch (de) { + if (de.message == "Permission denied") { + // the window is probably unloading, which means it's probably not closed yet + return false; + } + else if (/^Access is denied/.test(de.message)) { + // rare variation on "Permission denied"? + LOG.debug("IEBrowserBot.windowClosed: got " + de.message + " (this.pageUnloading=" + this.pageUnloading + "); assuming window is unloading, probably not closed yet"); + return false; + } else { + // this is probably one of those frame window situations + LOG.debug("IEBrowserBot.windowClosed: couldn't read win.document, assume closed: " + de.message + " (this.pageUnloading=" + this.pageUnloading + ")"); + return true; + } + } + } + if (c == null) { + LOG.debug("IEBrowserBot.windowClosed: win.closed was null, assuming closed"); + return true; + } + return c; + } catch (e) { + // Got an exception trying to read win.closed; we'll have to take a guess! + if (browserVersion.isHTA) { + if (e.message == "Permission denied") { + // the window is probably unloading, which means it's probably not closed yet + return false; + } else { + // there's a good chance that we've lost contact with the window object if it is closed + return true; + } + } else { + // the window is probably unloading, which means it's probably not closed yet + return false; + } + } +}; + SafariBrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToModify, browserBot) { BrowserBot.prototype.modifyWindowToRecordPopUpDialogs(windowToModify, browserBot); @@ -381,15 +732,12 @@ SafariBrowserBot.prototype.modifyWindowToRecordPopUpDialogs = function(windowToM }; }; -var PageBot = function(pageWindow) { - if (pageWindow) { - this.currentWindow = pageWindow; - this.currentDocument = pageWindow.document; - this.location = pageWindow.location; - this.title = function() {return this.currentDocument.title;}; - } +var PageBot = function(browserbot) { + this.browserbot = browserbot; + this._registerAllLocatorFunctions(); +}; - // Register all locateElementBy* functions +PageBot.prototype._registerAllLocatorFunctions = function() { // TODO - don't do this in the constructor - only needed once ever this.locationStrategies = {}; for (var functionName in this) { @@ -409,71 +757,86 @@ var PageBot = function(pageWindow) { /** * Find a locator based on a prefix. */ - this.findElementBy = function(locatorType, locator, inDocument) { - var locatorFunction = this.locationStrategies[locatorType]; + this.findElementBy = function(locatorType, locator, inDocument, inWindow) { + var locatorFunction = this.locationStrategies[locatorType]; if (! locatorFunction) { throw new SeleniumError("Unrecognised locator type: '" + locatorType + "'"); } - return locatorFunction.call(this, locator, inDocument); + return locatorFunction.call(this, locator, inDocument, inWindow); }; /** * The implicit locator, that is used when no prefix is supplied. */ - this.locationStrategies['implicit'] = function(locator, inDocument) { + this.locationStrategies['implicit'] = function(locator, inDocument, inWindow) { if (locator.startsWith('//')) { - return this.locateElementByXPath(locator, inDocument); + return this.locateElementByXPath(locator, inDocument, inWindow); } if (locator.startsWith('document.')) { - return this.locateElementByDomTraversal(locator, inDocument); + return this.locateElementByDomTraversal(locator, inDocument, inWindow); } - return this.locateElementByIdentifier(locator, inDocument); + return this.locateElementByIdentifier(locator, inDocument, inWindow); }; +} -}; +PageBot.prototype.getDocument = function() { + return this.getCurrentWindow().document; +} + +PageBot.prototype.getCurrentWindow = function() { + return this.browserbot.getCurrentWindow(); +} -PageBot.createForWindow = function(windowObject) { +PageBot.prototype.getTitle = function() { + var t = this.getDocument().title; + if (typeof(t) == "string") { + t = t.trim(); + } + return t; +} + +// todo: this is a bad name ... we're not passing a window in +PageBot.createForWindow = function(browserbot) { if (browserVersion.isIE) { - return new IEPageBot(windowObject); + return new IEPageBot(browserbot); } else if (browserVersion.isKonqueror) { - return new KonquerorPageBot(windowObject); + return new KonquerorPageBot(browserbot); } else if (browserVersion.isSafari) { - return new SafariPageBot(windowObject); + return new SafariPageBot(browserbot); } else if (browserVersion.isOpera) { - return new OperaPageBot(windowObject); + return new OperaPageBot(browserbot); } else { - LOG.info("Using MozillaPageBot") // Use mozilla by default - return new MozillaPageBot(windowObject); + return new MozillaPageBot(browserbot); } }; -var MozillaPageBot = function(pageWindow) { - PageBot.call(this, pageWindow); +var MozillaPageBot = function(browserbot) { + PageBot.call(this, browserbot); }; MozillaPageBot.prototype = new PageBot(); -var KonquerorPageBot = function(pageWindow) { - PageBot.call(this, pageWindow); +var KonquerorPageBot = function(browserbot) { + PageBot.call(this, browserbot); }; KonquerorPageBot.prototype = new PageBot(); -var SafariPageBot = function(pageWindow) { - PageBot.call(this, pageWindow); +var SafariPageBot = function(browserbot) { + PageBot.call(this, browserbot); }; SafariPageBot.prototype = new PageBot(); -var IEPageBot = function(pageWindow) { - PageBot.call(this, pageWindow); +var IEPageBot = function(browserbot) { + PageBot.call(this, browserbot); }; IEPageBot.prototype = new PageBot(); -OperaPageBot = function(pageWindow) { - PageBot.call(this, pageWindow); +var OperaPageBot = function(browserbot) { + PageBot.call(this, browserbot); }; OperaPageBot.prototype = new PageBot(); @@ -491,14 +854,14 @@ PageBot.prototype.findElement = function(locator) { locatorString = result[2]; } - var element = this.findElementBy(locatorType, locatorString, this.currentDocument); + var element = this.findElementBy(locatorType, locatorString, this.getDocument(), this.getCurrentWindow()); if (element != null) { - return element; + return this.highlight(element); } - for (var i = 0; i < this.currentWindow.frames.length; i++) { - element = this.findElementBy(locatorType, locatorString, this.currentWindow.frames[i].document); + for (var i = 0; i < this.getCurrentWindow().frames.length; i++) { + element = this.findElementBy(locatorType, locatorString, this.getCurrentWindow().frames[i].document, this.getCurrentWindow().frames[i]); if (element != null) { - return element; + return this.highlight(element); } } @@ -506,27 +869,41 @@ PageBot.prototype.findElement = function(locator) { throw new SeleniumError("Element " + locator + " not found"); }; +PageBot.prototype.highlight = function (element) { + if (shouldHighlightLocatedElement) { + Effect.highlight(element); + } + return element; +} + +// as a static variable. +var shouldHighlightLocatedElement = false; + +PageBot.prototype.setHighlightElement = function (shouldHighlight) { + shouldHighlightLocatedElement = shouldHighlight; +} + /** * 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) +PageBot.prototype.locateElementByIdentifier = function(identifier, inDocument, inWindow) { + return PageBot.prototype.locateElementById(identifier, inDocument, inWindow) + || PageBot.prototype.locateElementByName(identifier, inDocument, inWindow) || null; }; /** * In IE, getElementById() also searches by name - this is an optimisation for IE. */ -IEPageBot.prototype.locateElementByIdentifer = function(identifier, inDocument) { +IEPageBot.prototype.locateElementByIdentifer = function(identifier, inDocument, inWindow) { 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) { +PageBot.prototype.locateElementById = function(identifier, inDocument, inWindow) { var element = inDocument.getElementById(identifier); if (element && element.id === identifier) { return element; @@ -540,7 +917,7 @@ PageBot.prototype.locateElementById = function(identifier, inDocument) { * Find an element by name, refined by (optional) element-filter * expressions. */ -PageBot.prototype.locateElementByName = function(locator, document) { +PageBot.prototype.locateElementByName = function(locator, document, inWindow) { var elements = document.getElementsByTagName("*"); var filters = locator.split(' '); @@ -558,18 +935,21 @@ PageBot.prototype.locateElementByName = function(locator, document) { }; /** -* 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; - } + * Finds an element using by evaluating the specfied string. + */ +PageBot.prototype.locateElementByDomTraversal = function(domTraversal, inDocument, inWindow) { - // Trim the leading 'document' - domTraversal = domTraversal.substr(9); - var locatorScript = "inDocument." + domTraversal; - var element = eval(locatorScript); + var element = null; + try { + if (browserVersion.isOpera) { + element = inWindow.eval(domTraversal); + } else { + element = eval("inWindow." + domTraversal); + } + } catch (e) { + e.isSeleniumError = true; + throw e; + } if (!element) { return null; @@ -580,10 +960,10 @@ PageBot.prototype.locateElementByDomTraversal = function(domTraversal, inDocumen PageBot.prototype.locateElementByDomTraversal.prefix = "dom"; /** -* Finds an element identified by the xpath expression. Expressions _must_ -* begin with "//". -*/ -PageBot.prototype.locateElementByXPath = function(xpath, inDocument) { + * Finds an element identified by the xpath expression. Expressions _must_ + * begin with "//". + */ +PageBot.prototype.locateElementByXPath = function(xpath, inDocument, inWindow) { // Trim any trailing "/": not valid xpath, and remains from attribute // locator. @@ -602,30 +982,30 @@ PageBot.prototype.locateElementByXPath = function(xpath, inDocument) { // 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) - ); + 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._findElementByTagNameAndText( + inDocument, + match[1].toUpperCase(), + match[2].slice(1, -1) + ); } - return this.findElementUsingFullXPath(xpath, inDocument); + return this._findElementUsingFullXPath(xpath, inDocument); }; -PageBot.prototype.findElementByTagNameAndAttributeValue = function( - inDocument, tagName, attributeName, attributeValue -) { +PageBot.prototype._findElementByTagNameAndAttributeValue = function( + inDocument, tagName, attributeName, attributeValue + ) { if (browserVersion.isIE && attributeName == "class") { attributeName = "className"; } @@ -639,9 +1019,9 @@ PageBot.prototype.findElementByTagNameAndAttributeValue = function( return null; }; -PageBot.prototype.findElementByTagNameAndText = function( - inDocument, tagName, text -) { +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) { @@ -651,10 +1031,25 @@ PageBot.prototype.findElementByTagNameAndText = function( return null; }; -PageBot.prototype.findElementUsingFullXPath = function(xpath, inDocument) { +PageBot.prototype._namespaceResolver = function(prefix) { + if (prefix == 'html' || prefix == 'xhtml' || prefix == 'x') { + return 'http://www.w3.org/1999/xhtml'; + } else if (prefix == 'mathml') { + return 'http://www.w3.org/1998/Math/MathML'; + } else { + throw new Error("Unknown namespace: " + prefix + "."); + } +} + +PageBot.prototype._findElementUsingFullXPath = function(xpath, inDocument, inWindow) { + // HUGE hack - remove namespace from xpath for IE + if (browserVersion.isIE) { + xpath = xpath.replace(/x:/g, '') + } + // Use document.evaluate() if it's available if (inDocument.evaluate) { - return inDocument.evaluate(xpath, inDocument, null, 0, null).iterateNext(); + return inDocument.evaluate(xpath, inDocument, this._namespaceResolver, 0, null).iterateNext(); } // If not, fall back to slower JavaScript implementation @@ -668,10 +1063,10 @@ PageBot.prototype.findElementUsingFullXPath = function(xpath, inDocument) { }; /** -* Finds a link element with text matching the expression supplied. Expressions must -* begin with "link:". -*/ -PageBot.prototype.locateElementByLinkText = function(linkText, inDocument) { + * Finds a link element with text matching the expression supplied. Expressions must + * begin with "link:". + */ +PageBot.prototype.locateElementByLinkText = function(linkText, inDocument, inWindow) { var links = inDocument.getElementsByTagName('a'); for (var i = 0; i < links.length; i++) { var element = links[i]; @@ -684,9 +1079,9 @@ PageBot.prototype.locateElementByLinkText = function(linkText, inDocument) { 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. -*/ + * 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("@"); @@ -728,7 +1123,6 @@ PageBot.prototype.selectOption = function(element, optionToSelect) { if (changed) { triggerEvent(element, 'change', true); } - triggerEvent(element, 'blur', false); }; /* @@ -741,7 +1135,6 @@ PageBot.prototype.addSelection = function(element, option) { option.selected = true; triggerEvent(element, 'change', true); } - triggerEvent(element, 'blur', false); }; /* @@ -754,7 +1147,6 @@ PageBot.prototype.removeSelection = function(element, option) { option.selected = false; triggerEvent(element, 'change', true); } - triggerEvent(element, 'blur', false); }; PageBot.prototype.checkMultiselect = function(element) { @@ -768,71 +1160,88 @@ PageBot.prototype.checkMultiselect = function(element) { 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); + var maxLengthAttr = element.getAttribute("maxLength"); + var actualValue = stringValue; + if (maxLengthAttr != null) { + var maxLength = parseInt(maxLengthAttr); + if (stringValue.length > maxLength) { + LOG.warn("BEFORE") + actualValue = stringValue.substr(0, maxLength); + LOG.warn("AFTER") + } } - triggerEvent(element, 'blur', false); + element.value = actualValue; + // DGF this used to be skipped in chrome URLs, but no longer. Is xpcnativewrappers to blame? + triggerEvent(element, 'change', true); }; -MozillaPageBot.prototype.clickElement = function(element) { +MozillaPageBot.prototype.clickElement = function(element, clientX, clientY) { 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); - + + element.addEventListener("click", function(evt) { + preventDefault = evt.getPreventDefault(); + }, false); + // Trigger the click event. - triggerMouseEvent(element, 'click', true); + triggerMouseEvent(element, 'click', true, clientX, clientY); // 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. + var targetWindow = this.browserbot._getTargetWindow(element); if (element.href) { - this.currentWindow.location.href = element.href; - } - else if (element.parentNode && element.parentNode.href) { - this.currentWindow.location.href = element.parentNode.href; + targetWindow.location.href = element.href; + } else { + this.browserbot._handleClickingImagesInsideLinks(targetWindow, element); } } - if (this.windowClosed()) { + if (this._windowClosed()) { return; } - triggerEvent(element, 'blur', false); }; -OperaPageBot.prototype.clickElement = function(element) { +BrowserBot.prototype._handleClickingImagesInsideLinks = function(targetWindow, element) { + if (element.parentNode && element.parentNode.href) { + targetWindow.location.href = element.parentNode.href; + } +} + +BrowserBot.prototype._getTargetWindow = function(element) { + var targetWindow = this.getCurrentWindow(); + if (element.target) { + var frame = this._getFrameFromGlobal(element.target); + targetWindow = frame.contentWindow; + } + return targetWindow; +} + +BrowserBot.prototype._getFrameFromGlobal = function(target) { + pagebot = PageBot.createForWindow(this); + return pagebot.findElementBy("implicit", target, this.topWindow.document, this.topWindow); +} + +OperaPageBot.prototype.clickElement = function(element, clientX, clientY) { 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()) { + triggerMouseEvent(element, 'click', true, clientX, clientY); + + if (this._windowClosed()) { return; } - triggerEvent(element, 'blur', false); }; -KonquerorPageBot.prototype.clickElement = function(element) { +KonquerorPageBot.prototype.clickElement = function(element, clientX, clientY) { triggerEvent(element, 'focus', false); @@ -840,20 +1249,17 @@ KonquerorPageBot.prototype.clickElement = function(element) { element.click(); } else { - triggerMouseEvent(element, 'click', true); + triggerMouseEvent(element, 'click', true, clientX, clientY); } - if (this.windowClosed()) { + if (this._windowClosed()) { return; } - triggerEvent(element, 'blur', false); }; -SafariPageBot.prototype.clickElement = function(element) { - +SafariPageBot.prototype.clickElement = function(element, clientX, clientY) { triggerEvent(element, 'focus', false); - var wasChecked = element.checked; // For form element it is simple. @@ -862,47 +1268,20 @@ SafariPageBot.prototype.clickElement = function(element) { } // 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. - } + var targetWindow = this.browserbot._getTargetWindow(element); + // todo: what if the target anchor is on another page? + if (element.href && element.href.indexOf("#") != -1) { + var b = targetWindow.document.getElementById(element.href.split("#")[1]); + targetWindow.document.body.scrollTop = b.offsetTop; + } else { + triggerMouseEvent(element, 'click', true, clientX, clientY); } - } - if (this.windowClosed()) { - return; } - triggerEvent(element, 'blur', false); }; -IEPageBot.prototype.clickElement = function(element) { +IEPageBot.prototype.clickElement = function(element, clientX, clientY) { triggerEvent(element, 'focus', false); @@ -911,17 +1290,19 @@ IEPageBot.prototype.clickElement = function(element) { // 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); - + var pageUnloadDetector = function() { + pageUnloading = true; + }; + this.getCurrentWindow().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); + this.getCurrentWindow().detachEvent("onbeforeunload", pageUnloadDetector); - if (this.windowClosed()) { + if (this._windowClosed()) { return; } @@ -930,12 +1311,13 @@ IEPageBot.prototype.clickElement = function(element) { 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.logHook = function() { + }; LOG.warn("Caught exception when firing events on unloading page: " + e.message); return; } @@ -943,16 +1325,16 @@ IEPageBot.prototype.clickElement = function(element) { } }; -PageBot.prototype.windowClosed = function(element) { - return this.currentWindow.closed; +PageBot.prototype._windowClosed = function(element) { + return selenium.browserbot._windowClosed(this.getCurrentWindow()); }; PageBot.prototype.bodyText = function() { - return getText(this.currentDocument.body); + return getText(this.getDocument().body); }; PageBot.prototype.getAllButtons = function() { - var elements = this.currentDocument.getElementsByTagName('input'); + var elements = this.getDocument().getElementsByTagName('input'); var result = ''; for (var i = 0; i < elements.length; i++) { @@ -968,7 +1350,7 @@ PageBot.prototype.getAllButtons = function() { PageBot.prototype.getAllFields = function() { - var elements = this.currentDocument.getElementsByTagName('input'); + var elements = this.getDocument().getElementsByTagName('input'); var result = ''; for (var i = 0; i < elements.length; i++) { @@ -983,7 +1365,7 @@ PageBot.prototype.getAllFields = function() { }; PageBot.prototype.getAllLinks = function() { - var elements = this.currentDocument.getElementsByTagName('a'); + var elements = this.getDocument().getElementsByTagName('a'); var result = ''; for (var i = 0; i < elements.length; i++) { @@ -996,10 +1378,13 @@ PageBot.prototype.getAllLinks = function() { }; PageBot.prototype.setContext = function(strContext, logLevel) { - //set the current test title - document.getElementById("context").innerHTML=strContext; - if (logLevel!=null) { - LOG.setLogLevelThreshold(logLevel); + //set the current test title + var ctx = document.getElementById("context"); + if (ctx != null) { + ctx.innerHTML = strContext; + } + if (logLevel != null) { + LOG.setLogLevelThreshold(logLevel); } }; @@ -1008,27 +1393,23 @@ function isDefined(value) { } 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); - } + this.getCurrentWindow().history.back(); }; PageBot.prototype.goForward = function() { - this.currentWindow.history.forward(); + this.getCurrentWindow().history.forward(); }; PageBot.prototype.close = function() { - if (browserVersion.isChrome) { - this.currentWindow.close(); - } else { - this.currentWindow.eval("window.close();"); - } + if (browserVersion.isChrome || browserVersion.isSafari) { + this.getCurrentWindow().close(); + } else { + this.getCurrentWindow().eval("window.close();"); + } }; PageBot.prototype.refresh = function() { - this.currentWindow.location.reload(true); + this.getCurrentWindow().location.reload(true); }; /** @@ -1043,7 +1424,7 @@ PageBot.prototype.selectElementsBy = function(filterType, filter, elements) { return filterFunction(filter, elements); }; -PageBot.filterFunctions = {}; +PageBot.filterFunctions = {}; PageBot.filterFunctions.name = function(name, elements) { var selectedElements = []; @@ -1076,10 +1457,10 @@ PageBot.filterFunctions.index = function(index, elements) { return [elements[index]]; }; -PageBot.prototype.selectElements = function(filterExpr, elements, defaultFilterType) { +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) { @@ -1094,11 +1475,11 @@ PageBot.prototype.selectElements = function(filterExpr, elements, defaultFilterT * Find an element by class */ PageBot.prototype.locateElementByClass = function(locator, document) { - return Element.findFirstMatchingChild(document, - function(element) { - return element.className == locator - } - ); + return Element.findFirstMatchingChild(document, + function(element) { + return element.className == locator + } + ); } /** @@ -1106,9 +1487,18 @@ PageBot.prototype.locateElementByClass = function(locator, document) { */ PageBot.prototype.locateElementByAlt = function(locator, document) { return Element.findFirstMatchingChild(document, - function(element) { - return element.alt == locator - } - ); + function(element) { + return element.alt == locator + } + ); } +/** + * Find an element by css selector + */ +PageBot.prototype.locateElementByCss = function(locator, document) { + var elements = cssQuery(locator, document); + if (elements.length != 0) + return elements[0]; + return null; +} diff --git a/tests/test_tools/selenium/core/scripts/selenium-browserdetect.js b/tests/test_tools/selenium/core/scripts/selenium-browserdetect.js index 137a1518..d97e5a58 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-browserdetect.js +++ b/tests/test_tools/selenium/core/scripts/selenium-browserdetect.js @@ -1,30 +1,30 @@ /* -* 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. -* -*/ + * 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. +// 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() { +var BrowserVersion = function() { this.name = navigator.appName; - if (window.opera != null) - { + if (window.opera != null) { this.browser = BrowserVersion.OPERA; this.isOpera = true; return; @@ -33,66 +33,60 @@ BrowserVersion = function() { 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; - } + 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") - { + 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; + this.isHTA = true; } if ("0" == navigator.appMinorVersion) { - this.preSV1 = true; + this.preSV1 = true; } return; } - if (navigator.userAgent.indexOf('Safari') != -1) - { + if (navigator.userAgent.indexOf('Safari') != -1) { this.browser = BrowserVersion.SAFARI; this.isSafari = true; this.khtml = true; return; } - if (navigator.userAgent.indexOf('Konqueror') != -1) - { + if (navigator.userAgent.indexOf('Konqueror') != -1) { this.browser = BrowserVersion.KONQUEROR; this.isKonqueror = true; this.khtml = true; return; } - if (navigator.userAgent.indexOf('Firefox') != -1) - { + 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) - { + if (result) { this.firefoxVersion = result[1]; } checkChrome(); return; } - if (navigator.userAgent.indexOf('Gecko') != -1) - { + if (navigator.userAgent.indexOf('Gecko') != -1) { this.browser = BrowserVersion.MOZILLA; this.isMozilla = true; this.isGecko = true; @@ -111,5 +105,4 @@ BrowserVersion.FIREFOX = "Firefox"; BrowserVersion.MOZILLA = "Mozilla"; BrowserVersion.UNKNOWN = "Unknown"; -browserVersion = new BrowserVersion(); - +var 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 index ee01ea76..c11a80ad 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-commandhandlers.js +++ b/tests/test_tools/selenium/core/scripts/selenium-commandhandlers.js @@ -15,38 +15,58 @@ * */ function CommandHandlerFactory() { - this.actions = {}; - this.asserts = {}; - this.accessors = {}; var self = this; + this.handlers = {}; + this.registerAction = function(name, action, wait, dontCheckAlertsAndConfirms) { var handler = new ActionHandler(action, wait, dontCheckAlertsAndConfirms); - this.actions[name] = handler; + this.handlers[name] = handler; }; this.registerAccessor = function(name, accessor) { var handler = new AccessorHandler(accessor); - this.accessors[name] = handler; + this.handlers[name] = handler; }; this.registerAssert = function(name, assertion, haltOnFailure) { var handler = new AssertHandler(assertion, haltOnFailure); - this.asserts[name] = handler; + this.handlers[name] = handler; }; - + this.getCommandHandler = function(name) { - return this.actions[name] || this.accessors[name] || this.asserts[name] || null; + return this.handlers[name] || null; // todo: why null, and not undefined? }; - this.registerAll = function(commandObject) { - registerAllAccessors(commandObject); - registerAllActions(commandObject); - registerAllAsserts(commandObject); + // 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 matchForGetter = /^get([A-Z].+)$/.exec(functionName); + if (matchForGetter != null) { + var accessor = commandObject[functionName]; + var baseName = matchForGetter[1]; + self.registerAccessor(functionName, accessor); + self.registerAssertionsBasedOnAccessor(accessor, baseName); + self.registerStoreCommandBasedOnAccessor(accessor, baseName); + self.registerWaitForCommandsBasedOnAccessor(accessor, baseName); + } + var matchForIs = /^is([A-Z].+)$/.exec(functionName); + if (matchForIs != null) { + var accessor = commandObject[functionName]; + var baseName = matchForIs[1]; + var predicate = self.createPredicateFromBooleanAccessor(accessor); + self.registerAccessor(functionName, accessor); + self.registerAssertionsBasedOnAccessor(accessor, baseName, predicate); + self.registerStoreCommandBasedOnAccessor(accessor, baseName); + self.registerWaitForCommandsBasedOnAccessor(accessor, baseName, predicate); + } + } }; - var registerAllActions = function(commandObject) { + var _registerAllActions = function(commandObject) { for (var functionName in commandObject) { var result = /^do([A-Z].+)$/.exec(functionName); if (result != null) { @@ -63,8 +83,7 @@ function CommandHandlerFactory() { } }; - - var registerAllAsserts = function(commandObject) { + var _registerAllAsserts = function(commandObject) { for (var functionName in commandObject) { var result = /^assert([A-Z].+)$/.exec(functionName); if (result != null) { @@ -81,7 +100,12 @@ function CommandHandlerFactory() { } }; - + this.registerAll = function(commandObject) { + _registerAllAccessors(commandObject); + _registerAllActions(commandObject); + _registerAllAsserts(commandObject); + }; + // 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. @@ -95,7 +119,7 @@ function CommandHandlerFactory() { } }; }; - + // 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. @@ -109,20 +133,20 @@ function CommandHandlerFactory() { } }; }; - + // 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]); + 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]); + accessorResult = accessor.call(this, arguments[0]); } else { - accessorResult = accessor.call(this); + accessorResult = accessor.call(this); } if (accessorResult) { return new PredicateResult(true, "true"); @@ -131,17 +155,17 @@ function CommandHandlerFactory() { } }; }; - + // 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) { + 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. @@ -152,124 +176,90 @@ function CommandHandlerFactory() { 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); - } - }; + return function(target, value) { + var result = predicate.call(this, target, value); + if (!result.isTrue) { + Assert.fail(result.message); + } + }; }; - + + + var _negtiveName = function(baseName) { + var matchResult = /^(.*)Present$/.exec(baseName); + if (matchResult != null) { + return matchResult[1] + "NotPresent"; + } + return "Not" + baseName; + }; + // 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) { + 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); - } + self.registerAssert("assert" + _negtiveName(baseName), negativeAssertion, true); + self.registerAssert("verify" + _negtiveName(baseName), 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; + return function(target, value) { + var seleniumApi = this; + return 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; + } + }; + }; }; - + // 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); + 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. + self.registerAction("waitFor"+_negtiveName(baseName), waitForNotAction, false, true); + //TODO decide remove "waitForNot.*Present" action name or not + //for the back compatiblity issues we still make waitForNot.*Present availble + 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); - } + 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); }; - - + } function PredicateResult(isTrue, message) { @@ -301,36 +291,34 @@ function ActionHandler(action, wait, dontCheckAlerts) { ActionHandler.prototype = new CommandHandler; ActionHandler.prototype.execute = function(seleniumApi, command) { if (this.checkAlerts && (null==/(Alert|Confirmation)(Not)?Present/.exec(command.command))) { - this.checkForAlerts(seleniumApi); + seleniumApi.ensureNoUnhandledPopups(); } - var processState = this.executor.call(seleniumApi, command.target, command.value); + var terminationCondition = 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() + "]"); + if (terminationCondition == undefined && this.wait) { + terminationCondition = seleniumApi.makePageLoadCondition(); } + return new ActionResult(terminationCondition); }; +function ActionResult(terminationCondition) { + this.terminationCondition = terminationCondition; +} + 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; + return new AccessorResult(returnValue); }; +function AccessorResult(result) { + this.result = result; +} + /** * Handler for assertions and verifications. */ @@ -339,10 +327,9 @@ function AssertHandler(assertion, haltOnFailure) { } AssertHandler.prototype = new CommandHandler; AssertHandler.prototype.execute = function(seleniumApi, command) { - var result = new CommandResult(); + var result = new AssertResult(); 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) { @@ -352,20 +339,24 @@ AssertHandler.prototype.execute = function(seleniumApi, command) { var error = new SeleniumError(e.failureMessage); throw error; } - result.failed = true; - result.failureMessage = e.failureMessage; + result.setFailed(e.failureMessage); } return result; }; - -function CommandResult(processState) { - this.processState = processState; - this.result = null; +function AssertResult() { + this.passed = true; +} +AssertResult.prototype.setFailed = function(message) { + this.passed = null; + this.failed = true; + this.failureMessage = message; } -function SeleniumCommand(command, target, value) { +function SeleniumCommand(command, target, value, isBreakpoint) { this.command = command; this.target = target; this.value = value; + this.isBreakpoint = isBreakpoint; } + diff --git a/tests/test_tools/selenium/core/scripts/selenium-executionloop.js b/tests/test_tools/selenium/core/scripts/selenium-executionloop.js index 14c1a07a..d59fc148 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-executionloop.js +++ b/tests/test_tools/selenium/core/scripts/selenium-executionloop.js @@ -14,253 +14,173 @@ * limitations under the License. */ -SELENIUM_PROCESS_WAIT = "wait"; - function TestLoop(commandFactory) { - this.commandFactory = commandFactory; - this.waitForConditionTimeout = 30 * 1000; // 30 seconds +} + +TestLoop.prototype = { - this.start = function() { + start : function() { selenium.reset(); - LOG.debug("testLoop.start()"); + LOG.debug("currentTest.start()"); this.continueTest(); - }; + }, - /** - * Select the next command and continue the test. - */ - this.continueTest = function() { - LOG.debug("testLoop.continueTest() - acquire the next command"); + continueTest : function() { + /** + * Select the next command and continue the test. + */ + LOG.debug("currentTest.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) { + this.continueTestAtCurrentCommand(); + } // otherwise, just finish and let the callback invoke continueTestAtCurrentCommand() + }, + + continueTestAtCurrentCommand : function() { + LOG.debug("currentTest.continueTestAtCurrentCommand()"); + if (this.currentCommand) { // TODO: rename commandStarted to commandSelected, OR roll it into nextCommand this.commandStarted(this.currentCommand); - this.resumeAfterDelay(); + this._resumeAfterDelay(); } else { - this.testComplete(); + this._testComplete(); } - } - - /** - * Pause, then execute the current command. - */ - this.resumeAfterDelay = function() { + }, + + _resumeAfterDelay : function() { + /** + * Pause, then execute the current command. + */ // 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) { + if (this.currentCommand.isBreakpoint || delay < 0) { // Pause: enable the "next/continue" button this.pause(); } else { - window.setTimeout("testLoop.resume()", delay); + window.setTimeout(this.resume.bind(this), delay); } - }; + }, - /** - * Select the next command and continue the test. - */ - this.resume = function() { - LOG.debug("testLoop.resume() - actually execute"); + resume: function() { + /** + * Select the next command and continue the test. + */ + LOG.debug("currentTest.resume() - actually execute"); try { - this.executeCurrentCommand(); - this.waitForConditionStart = new Date().getTime(); + selenium.browserbot.runScheduledPollers(); + this._executeCurrentCommand(); this.continueTestWhenConditionIsTrue(); } catch (e) { - this.handleCommandError(e); - this.testComplete(); + 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() { - + }, + + _testComplete : function() { + selenium.ensureNoUnhandledPopups(); + this.testComplete(); + }, + + _executeCurrentCommand : function() { + /** + * Execute the current command. + * + * @return a function which will be used to determine when + * execution can continue, or null if we can continue immediately + */ 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"); + 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) { + this.waitForCondition = result.terminationCondition; + + }, + + _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; + 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()"); + }, + + continueTestWhenConditionIsTrue: function () { + /** + * 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. + */ + LOG.debug("currentTest.continueTestWhenConditionIsTrue()"); + selenium.browserbot.runScheduledPollers(); 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"); - } + LOG.debug("condition satisfied; let's continueTest()"); + this.waitForCondition = null; + this.continueTest(); + } else { + LOG.debug("waitForCondition was false; keep waiting!"); + window.setTimeout(this.continueTestWhenConditionIsTrue.bind(this), 100); + } + } catch (e) { + var lastResult = {}; + lastResult.failed = true; + lastResult.failureMessage = e.message; + this.commandComplete(lastResult); + this.testComplete(); + } + }, - var expectFailureCommandFactory = - new ExpectFailureCommandFactory(testLoop.commandFactory, message, "failure"); - expectFailureCommandFactory.baseExecutor = executeCommandAndReturnFailureMessage; - testLoop.commandFactory = expectFailureCommandFactory; -}; + pause : function() {}, + nextCommand : function() {}, + commandStarted : function() {}, + commandComplete : function() {}, + commandError : function() {}, + testComplete : function() {}, -/** - * 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"); + getCommandInterval : function() { + return 0; } - 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); +function decorateFunctionWithTimeout(f, timeout) { + if (f == null) { return null; } - catch (expected) { - return expected.message; + if (isNaN(timeout)) { + throw new SeleniumError("Timeout is not a number: " + timeout); } -}; - + var now = new Date().getTime(); + var timeoutTime = now + timeout; + return function() { + if (new Date().getTime() > timeoutTime) { + throw new SeleniumError("Timed out after " + timeout + "ms"); + } + return f(); + }; +} diff --git a/tests/test_tools/selenium/core/scripts/selenium-logging.js b/tests/test_tools/selenium/core/scripts/selenium-logging.js index b0fc67e4..25e11463 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-logging.js +++ b/tests/test_tools/selenium/core/scripts/selenium-logging.js @@ -1,33 +1,31 @@ /* -* 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. -*/ + * 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 = { + pendingMessages: new Array(), + setLogLevelThreshold: function(logLevel) { - this.pendingLogLevelThreshold = 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)"); + // NOTE: log messages will be discarded until the log window is + // fully loaded. }, getLogWindow: function() { @@ -37,10 +35,12 @@ Logger.prototype = { 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)"; + // can't just directly log because that action would loop back + // to this code infinitely + var pendingMessage = new LogMessage("info", "Log level programmatically set to " + this.pendingLogLevelThreshold + " (presumably by driven-mode test code)"); + this.pendingMessages.push(pendingMessage); - this.pendingLogLevelThreshold = null; // let's only go this way one time + this.pendingLogLevelThreshold = null; // let's only go this way one time } return this.logWindow; @@ -49,8 +49,9 @@ Logger.prototype = { openLogWindow: function() { this.logWindow = window.open( getDocumentBase(document) + "SeleniumLog.html", "SeleniumLog", - "width=600,height=250,bottom=0,right=0,status,scrollbars,resizable" + "width=600,height=1000,bottom=0,right=0,status,scrollbars,resizable" ); + this.logWindow.moveTo(window.screenX + 1210, window.screenY + window.outerHeight - 1400); return this.logWindow; }, @@ -60,28 +61,42 @@ Logger.prototype = { } }, + logHook: function(message, className) { + }, + log: function(message, className) { var logWindow = this.getLogWindow(); + this.logHook(message, className); if (logWindow) { if (logWindow.append) { - if (this.pendingInfoMessage) { - logWindow.append("info: " + this.pendingInfoMessage, "info"); - this.pendingInfoMessage = null; + if (this.pendingMessages.length > 0) { + logWindow.append("info: Appending missed logging messages", "info"); + while (this.pendingMessages.length > 0) { + var msg = this.pendingMessages.shift(); + logWindow.append(msg.type + ": " + msg.msg, msg.type); + } + logWindow.append("info: Done appending missed logging messages", "info"); } logWindow.append(className + ": " + message, className); } + } else { + // uncomment this to turn on background logging + /* these logging messages are never flushed, which creates + an enormous array of strings that never stops growing. Only + turn this on if you need it for debugging! */ + //this.pendingMessages.push(new LogMessage(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; + 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; } }, @@ -110,3 +125,7 @@ Logger.prototype = { var LOG = new Logger(); +var LogMessage = function(msg, type) { + this.type = type; + this.msg = msg; +} diff --git a/tests/test_tools/selenium/core/scripts/selenium-seleneserunner.js b/tests/test_tools/selenium/core/scripts/selenium-seleneserunner.js index 041b3bf9..99c7efbc 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-seleneserunner.js +++ b/tests/test_tools/selenium/core/scripts/selenium-seleneserunner.js @@ -23,278 +23,429 @@ doneColor = "#FFFFCC"; slowMode = false; +var injectedSessionId; var cmd1 = document.createElement("div"); var cmd2 = document.createElement("div"); var cmd3 = document.createElement("div"); var cmd4 = document.createElement("div"); var postResult = "START"; +var debugMode = false; +var relayToRC = null; +var proxyInjectionMode = false; +var uniqueId = 'sel_' + Math.round(100000 * Math.random()); + +var SeleneseRunnerOptions = Class.create(); +Object.extend(SeleneseRunnerOptions.prototype, URLConfiguration.prototype); +Object.extend(SeleneseRunnerOptions.prototype, { + initialize: function() { + this._acquireQueryString(); + }, + getDebugMode: function() { + return this._getQueryParameter("debugMode"); + }, + + getContinue: function() { + return this._getQueryParameter("continue"); + }, + + getBaseUrl: function() { + return this._getQueryParameter("baseUrl"); + }, + + getDriverHost: function() { + return this._getQueryParameter("driverhost"); + }, + + getDriverPort: function() { + return this._getQueryParameter("driverport"); + }, + + getSessionId: function() { + return this._getQueryParameter("sessionId"); + }, + + _acquireQueryString: function () { + if (this.queryString) return; + if (browserVersion.isHTA) { + var args = this._extractArgs(); + if (args.length < 2) return null; + this.queryString = args[1]; + } else if (proxyInjectionMode) { + this.queryString = selenium.browserbot.getCurrentWindow().location.search.substr(1); + } else { + this.queryString = top.location.search.substr(1); + } + } -queryString = null; +}); +var runOptions; -function runTest() { - var testAppFrame = document.getElementById('myiframe'); - selenium = Selenium.createForFrame(testAppFrame); +function runSeleniumTest() { + runOptions = new SeleneseRunnerOptions(); + var testAppWindow; + + if (runOptions.isMultiWindowMode()) { + testAppWindow = openSeparateApplicationWindow('Blank.html'); + } else if ($('myiframe') != null) { + testAppWindow = $('myiframe').contentWindow; + } + else { + proxyInjectionMode = true; + testAppWindow = window; + } + selenium = Selenium.createForWindow(testAppWindow); + if (!debugMode) { + debugMode = runOptions.getDebugMode(); + } + if (proxyInjectionMode) { + LOG.log = logToRc; + selenium.browserbot._modifyWindow(testAppWindow); + } + else if (debugMode) { + LOG.logHook = logToRc; + } + window.selenium = selenium; 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(); -} + currentTest = new SeleneseRunner(commandFactory); -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); - } + if (document.getElementById("commandList") != null) { + document.getElementById("commandList").appendChild(cmd4); + document.getElementById("commandList").appendChild(cmd3); + document.getElementById("commandList").appendChild(cmd2); + document.getElementById("commandList").appendChild(cmd1); + } + + var doContinue = runOptions.getContinue(); + if (doContinue != null) postResult = "OK"; + + currentTest.start(); } -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"); +function buildBaseUrl() { + var baseUrl = runOptions.getBaseUrl(); + if (baseUrl != null) { + return baseUrl; } - return args; + var s = window.location.href + var slashPairOffset = s.indexOf("//") + "//".length + var pathSlashOffset = s.substring(slashPairOffset).indexOf("/") + return s.substring(0, slashPairOffset + pathSlashOffset) + "/selenium-server/core/"; } -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 logToRc(message, logLevel) { + if (logLevel == null) { + logLevel = "debug"; + } + if (debugMode) { + sendToRC("logLevel=" + logLevel + ":" + message.replace(/[\n\r\015]/g, " ") + "\n"); } } -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 isArray(x) { + return ((typeof x) == "object") && (x["length"] != null); } -function buildDriverParams() { - var params = ""; +function serializeString(name, s) { + return name + "=unescape(\"" + escape(s) + "\");"; +} - var host = getQueryVariable("driverhost"); - var port = getQueryVariable("driverport"); - if (host != undefined && port != undefined) { - params = params + "&driverhost=" + host + "&driverport=" + port; +function serializeObject(name, x) +{ + var s = ''; + + if (isArray(x)) + { + s = name + "=new Array(); "; + var len = x["length"]; + for (var j = 0; j < len; j++) + { + s += serializeString(name + "[" + j + "]", x[j]); + } } - - var sessionId = getQueryVariable("sessionId"); - if (sessionId != undefined) { - params = params + "&sessionId=" + sessionId; + else if (typeof x == "string") + { + s = serializeString(name, x); + } + else + { + throw "unrecognized object not encoded: " + name + "(" + x + ")"; } + return s; +} - return params; +function relayBotToRC(s) { } -function preventBrowserCaching() { - var t = (new Date()).getTime(); - return "&counterToMakeURsUniqueAndSoStopPageCachingInTheBrowser=" + t; -} +function setSeleniumWindowName(seleniumWindowName) { + selenium.browserbot.getCurrentWindow()['seleniumWindowName'] = seleniumWindowName; +} -function nextCommand() { - xmlHttp = XmlHttp.create(); - try { - - var url = buildBaseUrl(); +function slowClicked() { + slowMode = !slowMode; +} + +SeleneseRunner = Class.create(); +Object.extend(SeleneseRunner.prototype, new TestLoop()); +Object.extend(SeleneseRunner.prototype, { + initialize : function(commandFactory) { + this.commandFactory = commandFactory; + this.requiresCallBack = true; + this.commandNode = null; + this.xmlHttpForCommandsAndResults = null; + }, + + nextCommand : function() { + var urlParms = ""; if (postResult == "START") { - url = url + "driver/?seleniumStart=true" + buildDriverParams() + preventBrowserCaching(); + urlParms += "seleniumStart=true"; + } + this.xmlHttpForCommandsAndResults = XmlHttp.create(); + sendToRC(postResult, urlParms, this._HandleHttpResponse.bind(this), this.xmlHttpForCommandsAndResults); + }, + + commandStarted : function(command) { + this.commandNode = document.createElement("div"); + var innerHTML = command.command + '('; + if (command.target != null && command.target != "") { + innerHTML += command.target; + if (command.value != null && command.value != "") { + innerHTML += ', ' + command.value; + } + } + innerHTML += ")"; + this.commandNode.innerHTML = innerHTML; + this.commandNode.style.backgroundColor = workingColor; + if (document.getElementById("commandList") != null) { + 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 = this.commandNode; + document.getElementById("commandList").appendChild(cmd4); + document.getElementById("commandList").appendChild(cmd3); + document.getElementById("commandList").appendChild(cmd2); + document.getElementById("commandList").appendChild(cmd1); + } + }, + + commandComplete : function(result) { + + if (result.failed) { + if (postResult == "CONTINUATION") { + currentTest.aborted = true; + } + postResult = result.failureMessage; + this.commandNode.title = result.failureMessage; + this.commandNode.style.backgroundColor = failColor; + } else if (result.passed) { + postResult = "OK"; + this.commandNode.style.backgroundColor = passColor; } else { - url = url + "driver/?" + buildDriverParams() + preventBrowserCaching(); + if (result.result == null) { + postResult = "OK"; + } else { + postResult = "OK," + result.result; + } + this.commandNode.style.backgroundColor = doneColor; } - 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" + }, + + commandError : function(message) { + postResult = "ERROR: " + message; + this.commandNode.style.backgroundColor = errorColor; + this.commandNode.title = message; + }, + + testComplete : function() { + window.status = "Selenium Tests Complete, for this Test" + // Continue checking for new results + this.continueTest(); + postResult = "START"; + }, + + _HandleHttpResponse : function() { + if (this.xmlHttpForCommandsAndResults.readyState == 4) { + if (this.xmlHttpForCommandsAndResults.status == 200) { + var command = this._extractCommand(this.xmlHttpForCommandsAndResults); + this.currentCommand = command; + this.continueTestAtCurrentCommand(); + } else { + var s = 'xmlHttp returned: ' + this.xmlHttpForCommandsAndResults.status + ": " + this.xmlHttpForCommandsAndResults.statusText; + LOG.error(s); + this.currentCommand = null; + setTimeout(this.continueTestAtCurrentCommand.bind(this), 2000); + } + } - 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); + _extractCommand : function(xmlHttp) { + if (slowMode) { + this._delay(2000); + } + + var command; + try { + var re = new RegExp("^(.*?)\n((.|[\r\n])*)"); + if (re.exec(xmlHttp.responseText)) { + command = RegExp.$1; + var rest = RegExp.$2; + rest = rest.trim(); + if (rest) { + eval(rest); + } + } + else { + command = xmlHttp.responseText; + } + } catch (e) { + alert('could not get responseText: ' + e.message); + } + if (command.substr(0, '|testComplete'.length) == '|testComplete') { + return null; + } + + return this._createCommandFromRequest(command); + }, + + + _delay : function(millis) { + var startMillis = new Date(); + while (true) { + milli = new Date(); + if (milli - startMillis > millis) { + break; + } + } + }, + +// Parses a URI query string into a SeleniumCommand object + _createCommandFromRequest : function(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); } - var command; - try { - command = xmlHttp.responseText; - } catch (e) { - alert('could not get responseText: ' + e.message); +}) + + +function sendToRC(dataToBePosted, urlParms, callback, xmlHttpObject, async) { + if (async == null) { + async = true; } - if (command.substr(0,'|testComplete'.length)=='|testComplete') { - return null; + if (xmlHttpObject == null) { + xmlHttpObject = XmlHttp.create(); } + var url = buildBaseUrl() + "driver/?" + if (urlParms) { + url += urlParms; + } + url += "&localFrameAddress=" + (proxyInjectionMode ? makeAddressToAUTFrame() : "top"); + url += "&seleniumWindowName=" + getSeleniumWindowName(); + url += "&uniqueId=" + uniqueId; - 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; - } + if (callback == null) { + callback = function() { + }; } - 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); + url += buildDriverParams() + preventBrowserCaching(); + xmlHttpObject.open("POST", url, async); + xmlHttpObject.onreadystatechange = callback; + xmlHttpObject.send(dataToBePosted); + return null; } -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 buildDriverParams() { + var params = ""; + + var host = runOptions.getDriverHost(); + var port = runOptions.getDriverPort(); + if (host != undefined && port != undefined) { + params = params + "&driverhost=" + host + "&driverport=" + port; } -} -function commandError(message) { - postResult = "ERROR: " + message; - commandNode.style.backgroundColor = errorColor; - commandNode.title = message; + var sessionId = runOptions.getSessionId(); + if (sessionId == undefined) { + sessionId = injectedSessionId; + } + if (sessionId != undefined) { + params = params + "&sessionId=" + sessionId; + } + return params; } -function slowClicked() { - slowMode = !slowMode; +function preventBrowserCaching() { + var t = (new Date()).getTime(); + return "&counterToMakeURsUniqueAndSoStopPageCachingInTheBrowser=" + t; } -function delay(millis) { - startMillis = new Date(); - while (true) { - milli = new Date(); - if (milli-startMillis > millis) { - break; - } +// Return the name of the current window in the selenium recordkeeping. +// +// In selenium, the additional widow has no name. +// +// Additional pop-ups are associated with names given by the argument to the routine waitForPopUp. +// +// I try to arrange for widows which are opened in such manner to track their own names using the top-level property +// seleniumWindowName, but it is possible that this property will not be available (if the widow has just reloaded +// itself). In this case, return "?". +// +function getSeleniumWindowName() { + var w = (proxyInjectionMode ? selenium.browserbot.getCurrentWindow() : window); + if (w.opener == null) { + return ""; } + if (w["seleniumWindowName"] == null) { + return "?"; + } + return w["seleniumWindowName"]; } -function getIframeDocument(iframe) { - if (iframe.contentDocument) { - return iframe.contentDocument; - } - else { - return iframe.contentWindow.document; +// construct a JavaScript expression which leads to my frame (i.e., the frame containing the window +// in which this code is operating) +function makeAddressToAUTFrame(w, frameNavigationalJSexpression) +{ + if (w == null) + { + w = top; + frameNavigationalJSexpression = "top"; } -} -// 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]; + if (w == selenium.browserbot.getCurrentWindow()) + { + return frameNavigationalJSexpression; } - 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); + for (var j = 0; j < w.frames.length; j++) + { + var t = makeAddressToAUTFrame(w.frames[j], frameNavigationalJSexpression + ".frames[" + j + "]"); + if (t != null) + { + return t; + } } - return new SeleniumCommand(cmd, arg1, arg2); + return null; } - diff --git a/tests/test_tools/selenium/core/scripts/selenium-testrunner.js b/tests/test_tools/selenium/core/scripts/selenium-testrunner.js index 1ced0a11..b5104d39 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-testrunner.js +++ b/tests/test_tools/selenium/core/scripts/selenium-testrunner.js @@ -15,433 +15,584 @@ * */ -// The current row in the list of tests (test suite) -currentRowInSuite = 0; +// An object representing the current test, used external +var currentTest = null; // TODO: get rid of this global, which mirrors the htmlTestRunner.currentTest +var selenium = null; + +var htmlTestRunner; +var HtmlTestRunner = Class.create(); +Object.extend(HtmlTestRunner.prototype, { + initialize: function() { + this.metrics = new Metrics(); + this.controlPanel = new HtmlTestRunnerControlPanel(); + this.htmlTestSuite = null; + this.testFailed = false; + this.currentTest = null; + this.runAllTests = false; + this.appWindow = null; + // we use a timeout here to make sure the LOG has loaded first, so we can see _every_ error + setTimeout(function() { + this.loadSuiteFrame(); + }.bind(this), 500); + }, -// An object representing the current test -currentTest = null; + markFailed: function() { + this.testFailed = true; + this.htmlTestSuite.markFailed(); + }, -// Whether or not the jsFT should run all tests in the suite -runAllTests = false; + loadSuiteFrame: function() { + if (selenium == null) { + selenium = Selenium.createForWindow(this._getApplicationWindow()); + this._registerCommandHandlers(); + } + this.controlPanel.setHighlightOption(); + var testSuiteName = this.controlPanel.getTestSuiteName(); + if (testSuiteName) { + suiteFrame.load(testSuiteName, this._onloadTestSuite.bind(this)); + } + }, -// Whether or not the current test has any errors; -testFailed = false; -suiteFailed = false; + _getApplicationWindow: function () { + if (this.controlPanel.isMultiWindowMode()) { + return this._getSeparateApplicationWindow(); + } + return $('myiframe').contentWindow; + }, -// Colors used to provide feedback -passColor = "#ccffcc"; -doneColor = "#eeffee"; -failColor = "#ffcccc"; -workingColor = "#ffffcc"; + _getSeparateApplicationWindow: function () { + if (this.appWindow == null) { + this.appWindow = openSeparateApplicationWindow('TestRunner-splash.html'); + } + return this.appWindow; + }, -// Holds the handlers for each command. -commandHandlers = null; + _onloadTestSuite:function () { + this.htmlTestSuite = new HtmlTestSuite(suiteFrame.getDocument()); + if (! this.htmlTestSuite.isAvailable()) { + return; + } + if (this.controlPanel.isAutomatedRun()) { + htmlTestRunner.startTestSuite(); + } else if (this.controlPanel.getAutoUrl()) { + //todo what is the autourl doing, left to check it out + addLoadListener(this._getApplicationWindow(), this._startSingleTest.bind(this)); + this._getApplicationWindow().src = this.controlPanel.getAutoUrl(); + } else { + this.htmlTestSuite.getSuiteRows()[0].loadTestCase(); + } + }, -// The number of tests run -numTestPasses = 0; + _startSingleTest:function () { + removeLoadListener(getApplicationWindow(), this._startSingleTest.bind(this)); + var singleTestName = this.controlPanel.getSingleTestName(); + testFrame.load(singleTestName, this.startTest.bind(this)); + }, -// The number of tests that have failed -numTestFailures = 0; + _registerCommandHandlers: function () { + this.commandFactory = new CommandHandlerFactory(); + this.commandFactory.registerAll(selenium); + }, -// The number of commands which have passed -numCommandPasses = 0; + startTestSuite: function() { + this.controlPanel.reset(); + this.metrics.resetMetrics(); + this.htmlTestSuite.reset(); + this.runAllTests = true; + this.runNextTest(); + }, -// The number of commands which have failed -numCommandFailures = 0; + runNextTest: function () { + if (!this.runAllTests) { + return; + } + this.htmlTestSuite.runNextTestInSuite(); + }, -// The number of commands which have caused errors (element not found) -numCommandErrors = 0; + startTest: function () { + this.controlPanel.reset(); + testFrame.scrollToTop(); + //todo: move testFailed and storedVars to TestCase + this.testFailed = false; + storedVars = new Object(); + this.currentTest = new HtmlRunnerTestLoop(testFrame.getCurrentTestCase(), this.metrics, this.commandFactory); + currentTest = this.currentTest; + this.currentTest.start(); + }, -// The time that the test was started. -startTime = null; + runSingleTest:function() { + this.runAllTests = false; + this.metrics.resetMetrics(); + this.startTest(); + } +}); -// The current time. -currentTime = null; +var FeedbackColors = Class.create(); +Object.extend(FeedbackColors, { + passColor : "#ccffcc", + doneColor : "#eeffee", + failColor : "#ffcccc", + workingColor : "#ffffcc", + breakpointColor : "#cccccc" +}); -// An simple enum for failureType -ERROR = 0; -FAILURE = 1; -runInterval = 0; +var 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; - } - } -} +/** SeleniumFrame encapsulates an iframe element */ +var SeleniumFrame = Class.create(); +Object.extend(SeleniumFrame.prototype, { -function continueCurrentTest() { - document.getElementById('continueTest').disabled = true; - testLoop.resume(); -} + initialize : function(frame) { + this.frame = frame; + addLoadListener(this.frame, this._handleLoad.bind(this)); + }, -function getApplicationFrame() { - return document.getElementById('myiframe'); -} + getDocument : function() { + return this.frame.contentWindow.document; + }, -function getSuiteFrame() { - return document.getElementById('testSuiteFrame'); -} + _handleLoad: function() { + this._onLoad(); + if (this.loadCallback) { + this.loadCallback(); + this.loadCallback = null; + } + }, -function getTestFrame(){ - return document.getElementById('testFrame'); -} + _onLoad: function() { + }, -function loadAndRunIfAuto() { - loadSuiteFrame(); -} + scrollToTop : function() { + this.frame.contentWindow.scrollTo(0, 0); + }, -function start() { - queryString = null; - setRunInterval(); - loadSuiteFrame(); -} + _setLocation: function(location) { + if (browserVersion.isSafari) { + // safari doesn't reload the page when the location equals to current location. + // hence, set the location to blank so that the page will reload automatically. + this.frame.src = "about:blank"; + this.frame.src = location; + } else { + this.frame.contentWindow.location.replace(location); + } + }, -function loadSuiteFrame() { - var testAppFrame = document.getElementById('myiframe'); - selenium = Selenium.createForFrame(testAppFrame); - registerCommandHandlers(); + load: function(/* url, [callback] */) { + if (arguments.length > 1) { + this.loadCallback = arguments[1]; - //set the runInterval if there is a queryParameter for it - var tempRunInterval = getQueryParameter("runInterval"); - if (tempRunInterval) { - runInterval = tempRunInterval; + } + this._setLocation(arguments[0]); } - document.getElementById("modeRun").onclick = setRunInterval; - document.getElementById('modeWalk').onclick = setRunInterval; - document.getElementById('modeStep').onclick = setRunInterval; - document.getElementById('continueTest').onclick = continueCurrentTest; +}); + +/** HtmlTestFrame - encapsulates the test-case iframe element */ +var HtmlTestFrame = Class.create(); +Object.extend(HtmlTestFrame.prototype, SeleniumFrame.prototype); +Object.extend(HtmlTestFrame.prototype, { - var testSuiteName = getQueryParameter("test"); + _onLoad: function() { + this.setCurrentTestCase(); + }, + + setCurrentTestCase: function() { + //todo: this is not good looking + this.currentTestCase = new HtmlTestCase(this.getDocument(), htmlTestRunner.htmlTestSuite.getCurrentRow()); + }, - if (testSuiteName) { - addLoadListener(getSuiteFrame(), onloadTestSuite); - getSuiteFrame().src = testSuiteName; - } else { - onloadTestSuite(); + getCurrentTestCase: function() { + return this.currentTestCase; } -} -function startSingleTest() { - removeLoadListener(getApplicationFrame(), startSingleTest); - var singleTestName = getQueryParameter("singletest"); - addLoadListener(getTestFrame(), startTest); - getTestFrame().src = singleTestName; +}); + +function onSeleniumLoad() { + suiteFrame = new SeleniumFrame(getSuiteFrame()); + testFrame = new HtmlTestFrame(getTestFrame()); + htmlTestRunner = new HtmlTestRunner(); } -function getIframeDocument(iframe) -{ - if (iframe.contentDocument) { - return iframe.contentDocument; - } - else { - return iframe.contentWindow.document; + +var suiteFrame; +var testFrame; +function getSuiteFrame() { + var f = $('testSuiteFrame'); + if (f == null) { + f = top; + // proxyInjection mode does not set myiframe } + return f; } -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); - } +function getTestFrame() { + var f = $('testFrame'); + if (f == null) { + f = top; + // proxyInjection mode does not set myiframe } + return f; +} + +var HtmlTestRunnerControlPanel = Class.create(); +Object.extend(HtmlTestRunnerControlPanel.prototype, URLConfiguration.prototype); +Object.extend(HtmlTestRunnerControlPanel.prototype, { + initialize: function() { + this._acquireQueryString(); + + this.runInterval = 0; - suiteTable = getIframeDocument(getSuiteFrame()).getElementsByTagName("table")[0]; - if (suiteTable!=null) { + this.highlightOption = $('highlightOption'); + this.pauseButton = $('pauseTest'); + this.stepButton = $('stepTest'); - if (isAutomatedRun()) { - startTestSuite(); - } else if (getQueryParameter("autoURL")) { + this.highlightOption.onclick = (function() { + this.setHighlightOption(); + }).bindAsEventListener(this); + this.pauseButton.onclick = this.pauseCurrentTest.bindAsEventListener(this); + this.stepButton.onclick = this.stepCurrentTest.bindAsEventListener(this); - addLoadListener(getApplicationFrame(), startSingleTest); + this.speedController = new Control.Slider('speedHandle', 'speedTrack', { + range: $R(0, 1000), + onSlide: this.setRunInterval.bindAsEventListener(this), + onChange: this.setRunInterval.bindAsEventListener(this) + }); - getApplicationFrame().src = getQueryParameter("autoURL"); + this._parseQueryParameter(); + }, + + setHighlightOption: function () { + var isHighlight = this.highlightOption.checked; + selenium.browserbot.getCurrentPage().setHighlightElement(isHighlight); + }, - } else { - testLink = suiteTable.rows[currentRowInSuite+1].cells[0].getElementsByTagName("a")[0]; - getTestFrame().src = testLink.href; + _parseQueryParameter: function() { + var tempRunInterval = this._getQueryParameter("runInterval"); + if (tempRunInterval) { + this.setRunInterval(tempRunInterval); } - } -} + this.highlightOption.checked = this._getQueryParameter("highlight"); + }, -// 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; + setRunInterval: function(runInterval) { + this.runInterval = runInterval; + }, - // For mozilla-like browsers - if(eventObj) - srcObj = eventObj.target; + setToPauseAtNextCommand: function() { + this.runInterval = -1; + }, - // For IE-like browsers - else if (getSuiteFrame().contentWindow.event) - srcObj = getSuiteFrame().contentWindow.event.srcElement; + pauseCurrentTest: function () { + this.setToPauseAtNextCommand(); + this._switchPauseButtonToContinue(); + }, - // The target row (the event source is not consistently reported by browsers) - row = srcObj.parentNode.parentNode.rowIndex || srcObj.parentNode.parentNode.parentNode.rowIndex; + continueCurrentTest: function () { + this.reset(); + currentTest.resume(); + }, - // 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; - } + reset: function() { + this.runInterval = this.speedController.value; + this._switchContinueButtonToPause(); + }, - return false; - }; -} + _switchContinueButtonToPause: function() { + this.pauseButton.innerHTML = "Pause"; + this.pauseButton.onclick = this.pauseCurrentTest.bindAsEventListener(this); + }, -function isQueryParameterTrue(name) { - parameterValue = getQueryParameter(name); - return (parameterValue != null && parameterValue.toLowerCase() == "true"); -} + _switchPauseButtonToContinue: function() { + $('stepTest').disabled = false; + this.pauseButton.innerHTML = "Continue"; + this.pauseButton.onclick = this.continueCurrentTest.bindAsEventListener(this); + }, -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); - } -} + stepCurrentTest: function () { + this.setToPauseAtNextCommand(); + currentTest.resume(); + }, -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; -} + isAutomatedRun: function() { + return this._isQueryParameterTrue("auto"); + }, + + shouldSaveResultsToFile: function() { + return this._isQueryParameterTrue("save"); + }, + + closeAfterTests: function() { + return this._isQueryParameterTrue("close"); + }, -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]); + getTestSuiteName: function() { + return this._getQueryParameter("test"); + }, + + getSingleTestName: function() { + return this._getQueryParameter("singletest"); + }, + + getAutoUrl: function() { + return this._getQueryParameter("autoURL"); + }, + + getResultsUrl: function() { + return this._getQueryParameter("resultsUrl"); + }, + + _acquireQueryString: function() { + if (this.queryString) return; + if (browserVersion.isHTA) { + var args = this._extractArgs(); + if (args.length < 2) return null; + this.queryString = args[1]; + } else { + this.queryString = location.search.substr(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(); -} +var AbstractResultAwareRow = Class.create(); +Object.extend(AbstractResultAwareRow.prototype, { -function runSingleTest() { - runAllTests = false; - resetMetrics(); - startTest(); -} + initialize: function(trElement) { + this.trElement = trElement; + }, -function startTest() { - removeLoadListener(getTestFrame(), startTest); + markWorking: function() { + this.trElement.bgColor = FeedbackColors.workingColor; + safeScrollIntoView(this.trElement); + }, - // Scroll to the top of the test frame - if (getTestFrame().contentWindow) { - getTestFrame().contentWindow.scrollTo(0,0); - } - else { - frames['testFrame'].scrollTo(0,0); + markPassed: function() { + this.trElement.bgColor = FeedbackColors.passColor; + }, + + markDone: function() { + this.trElement.bgColor = FeedbackColors.doneColor; + }, + + markFailed: function() { + this.trElement.bgColor = FeedbackColors.failColor; } - currentTest = new HtmlTest(getIframeDocument(getTestFrame())); +}); - testFailed = false; - storedVars = new Object(); +var HtmlTestCaseRow = Class.create(); +Object.extend(HtmlTestCaseRow.prototype, AbstractResultAwareRow.prototype); +Object.extend(HtmlTestCaseRow.prototype, { - testLoop = initialiseTestLoop(); - testLoop.start(); -} + getCommand: function () { + return new SeleniumCommand(getText(this.trElement.cells[0]), + getText(this.trElement.cells[1]), + getText(this.trElement.cells[2]), + this.isBreakpoint()); + }, -function HtmlTest(testDocument) { - this.init(testDocument); -} + markFailed: function(errorMsg) { + this.trElement.bgColor = FeedbackColors.failColor; + this.setMessage(errorMsg); + }, -HtmlTest.prototype = { + setMessage: function(message) { + this.trElement.cells[2].innerHTML = message; + }, - 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]); - } + reset: function() { + this.trElement.bgColor = ''; + var thirdCell = this.trElement.cells[2]; + if (thirdCell) { + if (thirdCell.originalHTML) { + thirdCell.innerHTML = thirdCell.originalHTML; + } else { + thirdCell.originalHTML = thirdCell.innerHTML; } } }, - addCommandRow: function(row) { - if (row.cells[2] && row.cells[2].originalHTML) { - row.cells[2].innerHTML = row.cells[2].originalHTML; + onClick: function() { + if (this.trElement.isBreakpoint == undefined) { + this.trElement.isBreakpoint = true; + this.trElement.beforeBackgroundColor = Element.getStyle(this.trElement, "backgroundColor"); + Element.setStyle(this.trElement, {"background-color" : FeedbackColors.breakpointColor}); + } else { + this.trElement.isBreakpoint = undefined; + Element.setStyle(this.trElement, {"background-color" : this.trElement.beforeBackgroundColor}); } - row.bgColor = ""; - this.commandRows.push(row); }, - nextCommand: function() { - if (this.commandRows.length > 0) { - this.currentRow = this.commandRows.shift(); - } else { - this.currentRow = null; + addBreakpointSupport: function() { + Element.setStyle(this.trElement, {"cursor" : "pointer"}); + this.trElement.onclick = function() { + this.onClick(); + }.bindAsEventListener(this); + }, + + isBreakpoint: function() { + if (this.trElement.isBreakpoint == undefined || this.trElement.isBreakpoint == null) { + return false } - return this.currentRow; + return this.trElement.isBreakpoint; } +}); + +var HtmlTestSuiteRow = Class.create(); +Object.extend(HtmlTestSuiteRow.prototype, AbstractResultAwareRow.prototype); +Object.extend(HtmlTestSuiteRow.prototype, { + + initialize: function(trElement, testFrame, htmlTestSuite) { + this.trElement = trElement; + this.testFrame = testFrame; + this.htmlTestSuite = htmlTestSuite; + this.link = trElement.getElementsByTagName("a")[0]; + this.link.onclick = this._onClick.bindAsEventListener(this); + }, -}; + reset: function() { + this.trElement.bgColor = ''; + }, -function startTestSuite() { - resetMetrics(); - currentRowInSuite = 0; - runAllTests = true; - suiteFailed = false; + _onClick: function() { + // todo: just send a message to the testSuite + this.loadTestCase(null); + return false; + }, - runNextTest(); -} + loadTestCase: function(onloadFunction) { + this.htmlTestSuite.currentRowInSuite = this.trElement.rowIndex - 1; + // If the row has a stored results table, use that + var resultsFromPreviousRun = this.trElement.cells[1]; + if (resultsFromPreviousRun) { + // this.testFrame.restoreTestCase(resultsFromPreviousRun.innerHTML); + var testBody = this.testFrame.getDocument().body; + testBody.innerHTML = resultsFromPreviousRun.innerHTML; + testFrame.setCurrentTestCase(); + if (onloadFunction) { + onloadFunction(); + } + } else { + this.testFrame.load(this.link.href, onloadFunction); + } + }, -function runNextTest() { - if (!runAllTests) - return; + saveTestResults: function() { + // todo: GLOBAL ACCESS! + var resultHTML = this.testFrame.getDocument().body.innerHTML; + if (!resultHTML) return; - suiteTable = getIframeDocument(getSuiteFrame()).getElementsByTagName("table")[0]; + // todo: why create this div? + var divElement = this.trElement.ownerDocument.createElement("div"); + divElement.innerHTML = resultHTML; - // 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); - } + var hiddenCell = this.trElement.ownerDocument.createElement("td"); + hiddenCell.appendChild(divElement); + hiddenCell.style.display = "none"; - // Set the results from the previous test run - setResultsData(suiteTable, currentRowInSuite); + this.trElement.appendChild(hiddenCell); } - 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); +var HtmlTestSuite = Class.create(); +Object.extend(HtmlTestSuite.prototype, { + + initialize: function(suiteDocument) { + this.suiteDocument = suiteDocument; + this.suiteRows = this._collectSuiteRows(); + this.titleRow = this.getTestTable().rows[0]; + this.title = this.titleRow.cells[0].innerHTML; + this.reset(); + }, + + reset: function() { + this.failed = false; + this.currentRowInSuite = -1; + this.titleRow.bgColor = ''; + this.suiteRows.each(function(row) { + row.reset(); + }); + }, + + getTitle: function() { + return this.title; + }, + + getSuiteRows: function() { + return this.suiteRows; + }, + + getTestTable: function() { + var tables = $A(this.suiteDocument.getElementsByTagName("table")); + return tables[0]; + }, + + isAvailable: function() { + return this.getTestTable() != null; + }, + + _collectSuiteRows: function () { + var result = []; + for (rowNum = 1; rowNum < this.getTestTable().rows.length; rowNum++) { + var rowElement = this.getTestTable().rows[rowNum]; + result.push(new HtmlTestSuiteRow(rowElement, testFrame, this)); } + return result; + }, - // 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); - } + getCurrentRow: function() { + return this.suiteRows[this.currentRowInSuite]; + }, - else { - // Make the current row blue - setCellColor(suiteTable.rows, currentRowInSuite, 0, workingColor); + markFailed: function() { + this.failed = true; + this.titleRow.bgColor = FeedbackColors.failColor; + }, - testLink = suiteTable.rows[currentRowInSuite].cells[0].getElementsByTagName("a")[0]; - testLink.focus(); + markDone: function() { + if (!this.failed) { + this.titleRow.bgColor = FeedbackColors.passColor; + } + }, - var testFrame = getTestFrame(); - addLoadListener(testFrame, startTest); + _startCurrentTestCase: function() { + this.getCurrentRow().markWorking(); + this.getCurrentRow().loadTestCase(htmlTestRunner.startTest.bind(htmlTestRunner)); + }, - selenium.browserbot.setIFrameLocation(testFrame, testLink.href); - } -} + _onTestSuiteComplete: function() { + this.markDone(); + new TestResult(this.failed, this.getTestTable()).post(); + }, -function setCellColor(tableRows, row, col, colorStr) { - tableRows[row].cells[col].bgColor = colorStr; -} + _updateSuiteWithResultOfPreviousTest: function() { + if (this.currentRowInSuite >= 0) { + this.getCurrentRow().saveTestResults(); + } + }, -// 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; + runNextTestInSuite: function() { + this._updateSuiteWithResultOfPreviousTest(); + this.currentRowInSuite++; - var tableNode = suiteTable.ownerDocument.createElement("div"); - tableNode.innerHTML = resultTable; + // If we are done with all of the tests, set the title bar as pass or fail + if (this.currentRowInSuite >= this.suiteRows.length) { + this._onTestSuiteComplete(); + } else { + this._startCurrentTestCase(); + } + } - 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); -} +}); + +var TestResult = Class.create(); +Object.extend(TestResult.prototype, { // Post the results to a servlet, CGI-script, etc. The URL of the // results-handler defaults to "/postResults", but an alternative location @@ -461,288 +612,584 @@ function setResultsData(suiteTable, row) { // suite: the suite table, including the hidden column of test results // testTable.1 to testTable.N: the individual test tables // -function postTestResults(suiteFailed, suiteTable) { + initialize: function (suiteFailed, suiteTable) { + this.controlPanel = htmlTestRunner.controlPanel; + this.metrics = htmlTestRunner.metrics; + this.suiteFailed = suiteFailed; + this.suiteTable = suiteTable; + }, - form = document.createElement("form"); - document.body.appendChild(form); + post: function () { + if (!this.controlPanel.isAutomatedRun()) { + return; + } + var form = document.createElement("form"); + document.body.appendChild(form); - form.id = "resultsForm"; - form.method="post"; - form.target="myiframe"; + form.id = "resultsForm"; + form.method = "post"; + form.target = "myiframe"; - var resultsUrl = getQueryParameter("resultsUrl"); - if (!resultsUrl) { - resultsUrl = "./postResults"; - } + var resultsUrl = this.controlPanel.getResultsUrl(); + if (!resultsUrl) { + resultsUrl = "./postResults"; + } - var actionAndParameters = resultsUrl.split('?',2); - form.action = actionAndParameters[0]; - var resultsUrlQueryString = actionAndParameters[1]; + 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 = function(name, value) { - input = document.createElement("input"); - input.type = "hidden"; - input.name = name; - input.value = value; - this.appendChild(input); - }; + form.createHiddenField("selenium.version", Selenium.version); + form.createHiddenField("selenium.revision", Selenium.revision); + + form.createHiddenField("result", this.suiteFailed ? "failed" : "passed"); + + form.createHiddenField("totalTime", Math.floor((this.metrics.currentTime - this.metrics.startTime) / 1000)); + form.createHiddenField("numTestPasses", this.metrics.numTestPasses); + form.createHiddenField("numTestFailures", this.metrics.numTestFailures); + form.createHiddenField("numCommandPasses", this.metrics.numCommandPasses); + form.createHiddenField("numCommandFailures", this.metrics.numCommandFailures); + form.createHiddenField("numCommandErrors", this.metrics.numCommandErrors); + + // Create an input for each test table. The inputs are named + // testTable.1, testTable.2, etc. + for (rowNum = 1; rowNum < this.suiteTable.rows.length; rowNum++) { + // If there is a second column, then add a new input + if (this.suiteTable.rows[rowNum].cells.length > 1) { + var resultCell = this.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); - 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); + // Add HTML for the suite itself + form.createHiddenField("suite", this.suiteTable.parentNode.innerHTML); + + if (this.controlPanel.shouldSaveResultsToFile()) { + this._saveToFile(resultsUrl, form); + } else { + form.submit(); } - } + document.body.removeChild(form); + if (this.controlPanel.closeAfterTests()) { + window.top.close(); + } + }, - 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); + _saveToFile: function (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(); } - - form.createHiddenField("numTestTotal", rowNum); +}); - // Add HTML for the suite itself - form.createHiddenField("suite", suiteTable.parentNode.innerHTML); +/** HtmlTestCase encapsulates an HTML test document */ +var HtmlTestCase = Class.create(); +Object.extend(HtmlTestCase.prototype, { - if (isQueryParameterTrue("save")) { - saveToFile(resultsUrl, form); - } else { - form.submit(); - } - document.body.removeChild(form); - if (isQueryParameterTrue("close")) { - window.top.close(); - } -} + initialize: function(testDocument, htmlTestSuiteRow) { + if (testDocument == null) { + throw "testDocument should not be null"; + } + if (htmlTestSuiteRow == null) { + throw "htmlTestSuiteRow should not be null"; + } + this.testDocument = testDocument; + this.htmlTestSuiteRow = htmlTestSuiteRow; + this.commandRows = this._collectCommandRows(); + this.nextCommandRowIndex = 0; + this._addBreakpointSupport(); + }, -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(); -} + _collectCommandRows: function () { + var commandRows = []; + var tables = $A(this.testDocument.getElementsByTagName("table")); + var self = this; + tables.each(function (table) { + $A(table.rows).each(function(candidateRow) { + if (self.isCommandRow(candidateRow)) { + commandRows.push(new HtmlTestCaseRow(candidateRow)); + } + }.bind(this)); + }); + return commandRows; + }, -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); + isCommandRow: function (row) { + return row.cells.length >= 3; + }, - currentTime = new Date().getTime(); + reset: function() { + /** + * reset the test to runnable state + */ + this.nextCommandRowIndex = 0; + + this._setTitleColor(''); + this.commandRows.each(function(row) { + row.reset(); + }); + + // remove any additional fake "error" row added to the end of the document + var errorElement = this.testDocument.getElementById('error'); + if (errorElement) { + Element.remove(errorElement); + } + }, - timeDiff = currentTime - startTime; - totalSecs = Math.floor(timeDiff / 1000); + getCommandRows: function () { + return this.commandRows; + }, - minutes = Math.floor(totalSecs / 60); - seconds = totalSecs % 60; + _setTitleColor: function(color) { + var headerRow = this.testDocument.getElementsByTagName("tr")[0]; + if (headerRow) { + headerRow.bgColor = color; + } + }, - setText(document.getElementById("elapsedTime"), pad(minutes)+":"+pad(seconds)); -} + markFailed: function() { + this._setTitleColor(FeedbackColors.failColor); + this.htmlTestSuiteRow.markFailed(); + }, -// Puts a leading 0 on num if it is less than 10 -function pad (num) { - return (num > 9) ? num : "0" + num; -} + markPassed: function() { + this._setTitleColor(FeedbackColors.passColor); + this.htmlTestSuiteRow.markPassed(); + }, -/* - * 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); + addErrorMessage: function(errorMsg, currentRow) { + if (currentRow) { + currentRow.markFailed(errorMsg); + } else { + var errorElement = this.testDocument.createElement("p"); + errorElement.id = "error"; + errorElement.innerHTML = errorMsg; + this.testDocument.body.appendChild(errorElement); + Element.setStyle(errorElement, {'backgroundColor': FeedbackColors.failColor}); + } + }, -} + _addBreakpointSupport: function() { + this.commandRows.each(function(row) { + row.addBreakpointSupport(); + }); + }, -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; -} + hasMoreCommandRows: function() { + return this.nextCommandRowIndex < this.commandRows.length; + }, -function nextCommand() { - var row = currentTest.nextCommand(); - if (row == null) { + getNextCommandRow: function() { + if (this.hasMoreCommandRows()) { + return this.commandRows[this.nextCommandRowIndex++]; + } 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(); -} +// TODO: split out an JavascriptTestCase class to handle the "sejs" stuff + +var get_new_rows = function() { + var row_array = new Array(); + for (var i = 0; i < new_block.length; i++) { + + var new_source = (new_block[i][0].tokenizer.source.slice(new_block[i][0].start, + new_block[i][0].end)); -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; + var row = '<td style="display:none;" class="js">getEval</td>' + + '<td style="display:none;">currentTest.doNextCommand()</td>' + + '<td style="white-space: pre;">' + new_source + '</td>' + + '<td></td>' + + row_array.push(row); } -} + return row_array; +}; -function commandError(errorMessage) { - numCommandErrors += 1; - recordFailure(errorMessage); -} -function recordFailure(errorMsg) { - LOG.warn("recordFailure: " + errorMsg); - // Set cell background to red - currentTest.currentRow.bgColor = failColor; +var Metrics = Class.create(); +Object.extend(Metrics.prototype, { + initialize: function() { + // The number of tests run + this.numTestPasses = 0; + // The number of tests that have failed + this.numTestFailures = 0; + // The number of commands which have passed + this.numCommandPasses = 0; + // The number of commands which have failed + this.numCommandFailures = 0; + // The number of commands which have caused errors (element not found) + this.numCommandErrors = 0; + // The time that the test was started. + this.startTime = null; + // The current time. + this.currentTime = null; + }, + + printMetrics: function() { + setText($('commandPasses'), this.numCommandPasses); + setText($('commandFailures'), this.numCommandFailures); + setText($('commandErrors'), this.numCommandErrors); + setText($('testRuns'), this.numTestPasses + this.numTestFailures); + setText($('testFailures'), this.numTestFailures); - // Set error message - currentTest.currentRow.cells[2].innerHTML = errorMsg; - currentTest.currentRow.title = errorMsg; - testFailed = true; - suiteFailed = true; -} + this.currentTime = new Date().getTime(); + + var timeDiff = this.currentTime - this.startTime; + var totalSecs = Math.floor(timeDiff / 1000); + + var minutes = Math.floor(totalSecs / 60); + var seconds = totalSecs % 60; + + setText($('elapsedTime'), this._pad(minutes) + ":" + this._pad(seconds)); + }, -function testComplete() { - var resultColor = passColor; - if (testFailed) { - resultColor = failColor; - numTestFailures += 1; - } else { - numTestPasses += 1; +// Puts a leading 0 on num if it is less than 10 + _pad: function(num) { + return (num > 9) ? num : "0" + num; + }, + + resetMetrics: function() { + this.numTestPasses = 0; + this.numTestFailures = 0; + this.numCommandPasses = 0; + this.numCommandFailures = 0; + this.numCommandErrors = 0; + this.startTime = new Date().getTime(); } - if (currentTest.headerRow) { - currentTest.headerRow.bgColor = resultColor; +}); + +var HtmlRunnerCommandFactory = Class.create(); +Object.extend(HtmlRunnerCommandFactory.prototype, { + + initialize: function(seleniumCommandFactory, testLoop) { + this.seleniumCommandFactory = seleniumCommandFactory; + this.testLoop = testLoop; + this.handlers = { + pause: { + execute: function(selenium, command) { + testLoop.pauseInterval = command.target; + return {}; + } + } + }; + //todo: register commands + }, + + getCommandHandler: function(command) { + if (this.handlers[command]) { + return this.handlers[command]; + } + return this.seleniumCommandFactory.getCommandHandler(command); } - - printMetrics(); - window.setTimeout("runNextTest()", 1); -} +}); -Selenium.prototype.doPause = function(waitTime) { - testLoop.pauseInterval = waitTime; -}; +var HtmlRunnerTestLoop = Class.create(); +Object.extend(HtmlRunnerTestLoop.prototype, new TestLoop()); +Object.extend(HtmlRunnerTestLoop.prototype, { + initialize: function(htmlTestCase, metrics, seleniumCommandFactory) { + + this.commandFactory = new HtmlRunnerCommandFactory(seleniumCommandFactory, this); + this.metrics = metrics; + + this.htmlTestCase = htmlTestCase; + + se = selenium; + global.se = selenium; + + this.currentRow = null; + this.currentRowIndex = 0; + + // used for selenium tests in javascript + this.currentItem = null; + this.commandAgenda = new Array(); + + this.htmlTestCase.reset(); + + this.sejsElement = this.htmlTestCase.testDocument.getElementById('sejs'); + if (this.sejsElement) { + var fname = 'Selenium JavaScript'; + parse_result = parse(this.sejsElement.innerHTML, fname, 0); + + var x2 = new ExecutionContext(GLOBAL_CODE); + ExecutionContext.current = x2; + + execute(parse_result, x2) + } + }, + + _advanceToNextRow: function() { + if (this.htmlTestCase.hasMoreCommandRows()) { + this.currentRow = this.htmlTestCase.getNextCommandRow(); + if (this.sejsElement) { + this.currentItem = agenda.pop(); + this.currentRowIndex++; + } + } else { + this.currentRow = null; + this.currentItem = null; + } + }, + + nextCommand : function() { + this._advanceToNextRow(); + if (this.currentRow == null) { + return null; + } + return this.currentRow.getCommand(); + }, + + commandStarted : function() { + $('pauseTest').disabled = false; + this.currentRow.markWorking(); + this.metrics.printMetrics(); + }, + + commandComplete : function(result) { + if (result.failed) { + this.metrics.numCommandFailures += 1; + this._recordFailure(result.failureMessage); + } else if (result.passed) { + this.metrics.numCommandPasses += 1; + this.currentRow.markPassed(); + } else { + this.currentRow.markDone(); + } + }, + + commandError : function(errorMessage) { + this.metrics.numCommandErrors += 1; + this._recordFailure(errorMessage); + }, + + _recordFailure : function(errorMsg) { + LOG.warn("currentTest.recordFailure: " + errorMsg); + htmlTestRunner.markFailed(); + this.htmlTestCase.addErrorMessage(errorMsg, this.currentRow); + }, + + testComplete : function() { + $('pauseTest').disabled = true; + $('stepTest').disabled = true; + if (htmlTestRunner.testFailed) { + this.htmlTestCase.markFailed(); + this.metrics.numTestFailures += 1; + } else { + this.htmlTestCase.markPassed(); + this.metrics.numTestPasses += 1; + } + + this.metrics.printMetrics(); + + window.setTimeout(function() { + htmlTestRunner.runNextTest(); + }, 1); + }, + + getCommandInterval : function() { + return htmlTestRunner.controlPanel.runInterval; + }, + + pause : function() { + htmlTestRunner.controlPanel.pauseCurrentTest(); + }, + + doNextCommand: function() { + var _n = this.currentItem[0]; + var _x = this.currentItem[1]; + + new_block = new Array() + execute(_n, _x); + if (new_block.length > 0) { + var the_table = this.htmlTestCase.testDocument.getElementById("se-js-table") + var loc = this.currentRowIndex + var new_rows = get_new_rows() + + // make the new statements visible on screen... + for (var i = 0; i < new_rows.length; i++) { + the_table.insertRow(loc + 1); + the_table.rows[loc + 1].innerHTML = new_rows[i]; + this.commandRows.unshift(the_table.rows[loc + 1]) + } + + } + } + +}); -Selenium.prototype.doPause.dontCheckAlertsAndConfirms = true; Selenium.prototype.doBreak = function() { - document.getElementById('modeStep').checked = true; - runInterval = -1; + /** Halt the currently running test, and wait for the user to press the Continue button. + * This command is useful for debugging, but be careful when using it, because it will + * force automated tests to hang until a user intervenes manually. + */ + // todo: should not refer to controlPanel directly + htmlTestRunner.controlPanel.setToPauseAtNextCommand(); }; +Selenium.prototype.doStore = function(expression, variableName) { + /** This command is a synonym for storeExpression. + * @param expression the value to store + * @param variableName the name of a <a href="#storedVars">variable</a> in which the result is to be stored. + */ + storedVars[variableName] = expression; +} + /* * Click on the located element, and attach a callback to notify * when the page is reloaded. */ -Selenium.prototype.doModalDialogTest = function(returnValue) { +// DGF TODO this code has been broken for some time... what is it trying to accomplish? +Selenium.prototype.XXXdoModalDialogTest = 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; +Selenium.prototype.doEcho = function(message) { + /** Prints the specified message into the third table cell in your Selenese tables. + * Useful for debugging. + * @param message the message to print + */ + currentTest.currentRow.setMessage(message); +} + +Selenium.prototype.assertSelected = function(selectLocator, optionLocator) { + /** + * Verifies that the selected option of a drop-down satisfies the optionSpecifier. <i>Note that this command is deprecated; you should use assertSelectedLabel, assertSelectedValue, assertSelectedIndex, or assertSelectedId instead.</i> + * + * <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"); } - var element = this.page().findElement(target); - storedVars[varName] = getInputValue(element); + locator.assertSelected(element); }; -/* - * Store the text of an element in a variable +/** + * 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.doStoreText = function(target, varName) { - var element = this.page().findElement(target); - storedVars[varName] = getText(element); +Selenium.prototype.assertFailureOnNext = function(message) { + if (!message) { + throw new Error("Message must be provided"); + } + + var expectFailureCommandFactory = + new ExpectFailureCommandFactory(currentTest.commandFactory, message, "failure", executeCommandAndReturnFailureMessage); + currentTest.commandFactory = expectFailureCommandFactory; }; -/* - * Store the value of an element attribute in a variable +/** + * 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.doStoreAttribute = function(target, varName) { - storedVars[varName] = this.page().findAttribute(target); +Selenium.prototype.assertErrorOnNext = function(message) { + if (!message) { + throw new Error("Message must be provided"); + } + + var expectFailureCommandFactory = + new ExpectFailureCommandFactory(currentTest.commandFactory, message, "error", executeCommandAndReturnErrorMessage); + currentTest.commandFactory = expectFailureCommandFactory; }; -/* - * Store the result of a literal value - */ -Selenium.prototype.doStore = function(value, varName) { - storedVars[varName] = value; +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; + } }; -Selenium.prototype.doEcho = function(msg) { - currentTest.currentRow.cells[2].innerHTML = msg; +function ExpectFailureCommandHandler(baseHandler, originalCommandFactory, expectedErrorMessage, errorType, decoratedExecutor) { + this.execute = function() { + var baseFailureMessage = decoratedExecutor(baseHandler, arguments); + var result = {}; + 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; + } + } + currentTest.commandFactory = originalCommandFactory; + return result; + }; } + +function ExpectFailureCommandFactory(originalCommandFactory, expectedErrorMessage, errorType, decoratedExecutor) { + this.getCommandHandler = function(name) { + var baseHandler = originalCommandFactory.getCommandHandler(name); + return new ExpectFailureCommandHandler(baseHandler, originalCommandFactory, expectedErrorMessage, errorType, decoratedExecutor); + }; +}; diff --git a/tests/test_tools/selenium/core/scripts/selenium-version.js b/tests/test_tools/selenium/core/scripts/selenium-version.js index 1fee837b..c4a5508c 100644 --- a/tests/test_tools/selenium/core/scripts/selenium-version.js +++ b/tests/test_tools/selenium/core/scripts/selenium-version.js @@ -1,5 +1,5 @@ -Selenium.version = "@VERSION@"; -Selenium.revision = "@REVISION@"; +Selenium.version = "0.8.0"; +Selenium.revision = "1472:1473"; window.top.document.title += " v" + Selenium.version + " [" + Selenium.revision + "]"; |