From 0008fe4aedf3550086caa8e686889597a07e5c0a Mon Sep 17 00:00:00 2001 From: Tobie Langel Date: Sun, 6 Jan 2008 00:34:39 +0000 Subject: [PATCH] prototype: Test.Unit refactoring. Allow running multiple instances of Test.Unit.Runner on the same page. Allow rake to run specific testcases. Closes #10704, #10705, #10706. --- CHANGELOG | 2 + Rakefile | 9 +- src/dom.js | 2 +- test/lib/jstest.rb | 10 +- test/lib/unittest.js | 380 ++++++++++++++++------------------ test/test.css | 15 +- test/unit/ajax.html | 2 +- test/unit/array.html | 2 +- test/unit/base.html | 2 +- test/unit/dom.html | 2 +- test/unit/element_mixins.html | 2 +- test/unit/enumerable.html | 2 +- test/unit/event.html | 2 +- test/unit/hash.html | 2 +- test/unit/number.html | 2 +- test/unit/position.html | 2 +- test/unit/range.html | 2 +- test/unit/selector.html | 2 +- test/unit/string.html | 2 +- test/unit/unit_tests.html | 14 +- 20 files changed, 233 insertions(+), 225 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b74c5b8..0db64ed 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Test.Unit refactoring. Allow running multiple instances of Test.Unit.Runner on the same page. Allow rake to run specific testcases (e.g.: rake test BROWSERS=firefox TESTS=array TESTCASES=testUniq,test$w). Closes #10704, #10705, #10706. [nicwilliams, Tobie Langel] + * Optimize property detection of outerHTML. Avoids triggering FOUC in Safari 3.0.4. Closes #10702. [subimage, Tobie Langel] * Add document.loaded, a boolean that is set to true once dom:loaded is fired. Setting document.loaded to true before the document is loaded prevents dom:loaded from being fired. [Tobie Langel] diff --git a/Rakefile b/Rakefile index eb8b504..1525ddc 100644 --- a/Rakefile +++ b/Rakefile @@ -37,16 +37,17 @@ task :test => [:dist, :test_units] require 'test/lib/jstest' desc "Runs all the JavaScript unit tests and collects the results" JavaScriptTestTask.new(:test_units) do |t| + testcases = ENV['TESTCASES'] tests_to_run = ENV['TESTS'] && ENV['TESTS'].split(',') browsers_to_test = ENV['BROWSERS'] && ENV['BROWSERS'].split(',') - + t.mount("/dist") t.mount("/test") Dir["test/unit/*.html"].sort.each do |test_file| - test_file = "/#{test_file}" - test_name = test_file[/.*\/(.+?)\.html/, 1] - t.run(test_file) unless tests_to_run && !tests_to_run.include?(test_name) + tests = testcases ? { :url => "/#{test_file}", :testcases => testcases } : "/#{test_file}" + test_filename = test_file[/.*\/(.+?)\.html/, 1] + t.run(tests) unless tests_to_run && !tests_to_run.include?(test_filename) end %w( safari firefox ie konqueror opera ).each do |browser| diff --git a/src/dom.js b/src/dom.js index fabc504..db4b6cd 100644 --- a/src/dom.js +++ b/src/dom.js @@ -185,7 +185,7 @@ Element.Methods = { }, descendants: function(element) { - return $(element).getElementsBySelector("*"); + return $(element).select("*"); }, firstDescendant: function(element) { diff --git a/test/lib/jstest.rb b/test/lib/jstest.rb index 6a7c0c5..8eb1e87 100644 --- a/test/lib/jstest.rb +++ b/test/lib/jstest.rb @@ -308,7 +308,12 @@ class JavaScriptTestTask < ::Rake::TaskLib browser.setup puts "\nStarted tests in #{browser}" @tests.each do |test| - browser.visit("http://localhost:4711#{test}?resultsURL=http://localhost:4711/results&t=" + ("%.6f" % Time.now.to_f)) + params = "resultsURL=http://localhost:4711/results&t=" + ("%.6f" % Time.now.to_f) + if test.is_a?(Hash) + params << "&tests=#{test[:testcases]}" if test[:testcases] + test = test[:url] + end + browser.visit("http://localhost:4711#{test}?#{params}") result = @queue.pop result.each { |k, v| results[k] += v } @@ -349,7 +354,8 @@ class JavaScriptTestTask < ::Rake::TaskLib @server.mount(path, NonCachingFileHandler, dir) end - # test should be specified as a url + # test should be specified as a url or as a hash of the form + # {:url => "url", :testcases => "testFoo,testBar"} def run(test) @tests<' + testName + ''); }, + + setStatus: function(status) { + this.getLastLogLine().addClassName(status).down('td', 1).update(status); + }, + finish: function(status, summary) { - if (!this.log) return; - this.lastLogLine.className = status; - this.statusCell.innerHTML = status; - this.messageCell.innerHTML = this._toHTML(summary); + if (!this.element) return; + this.setStatus(status); + this.message(summary); }, + message: function(message) { - if (!this.log) return; - this.messageCell.innerHTML = this._toHTML(message); + if (!this.element) return; + this.getMessageCell().update(this._toHTML(message)); }, + summary: function(summary) { - if (!this.log) return; - this.logsummary.innerHTML = this._toHTML(summary); + if (!this.element) return; + this.element.down('div').update(this._toHTML(summary)); }, + + getLastLogLine: function() { + return this.element.select('tr').last() + }, + + getMessageCell: function() { + return this.getLastLogLine().down('td', 2); + }, + _createLogTable: function() { - this.log.innerHTML = - '
' + - '' + + var html = '
running...
' + + '
' + '' + - '' + + '' + '
StatusTestMessage
'; - this.logsummary = $('logsummary') - this.loglines = $('loglines'); + this.element.update(html) + }, + + appendActionButtons: function(actions) { + actions = $H(actions); + if (!actions.any()) return; + var div = new Element("div", {className: 'action_buttons'}); + actions.inject(div, function(container, action) { + var button = new Element("input").setValue(action.key).observe("click", action.value); + button.type = "button"; + return container.insert(button); + }); + this.getMessageCell().insert(div); + }, + _toHTML: function(txt) { return txt.escapeHTML().replace(/\n/g,"
"); } -} +}); -Test.Unit.Runner = Class.create(); -Test.Unit.Runner.prototype = { +Test.Unit.Runner = Class.create({ initialize: function(testcases) { - this.options = Object.extend({ + var options = this.options = Object.extend({ testLog: 'testlog' }, arguments[1] || {}); - this.options.resultsURL = this.parseResultsURLQueryParameter(); - if (this.options.testLog) { - this.options.testLog = $(this.options.testLog) || null; - } - if(this.options.tests) { - this.tests = []; - for(var i = 0; i < this.options.tests.length; i++) { - if(/^test/.test(this.options.tests[i])) { - this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"])); - } - } - } else { - if (this.options.test) { - this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])]; - } else { - this.tests = []; - for(var testcase in testcases) { - if(/^test/.test(testcase)) { - this.tests.push(new Test.Unit.Testcase(testcase, testcases[testcase], testcases["setup"], testcases["teardown"])); - } - } - } - } + + options.resultsURL = this.queryParams.resultsURL; + options.testLog = $(options.testLog); + + this.tests = this.getTests(testcases, options); this.currentTest = 0; - this.logger = new Test.Unit.Logger(this.options.testLog); - Event.observe(window, "load", function() { - setTimeout(this.runTests.bind(this), 100); + this.logger = new Test.Unit.Logger(options.testLog); + Event.observe(window, "load", function() { + this.runTests.bind(this).delay(0.1); }.bind(this)); }, - parseResultsURLQueryParameter: function() { - return window.location.search.parseQuery()["resultsURL"]; + + queryParams: window.location.search.parseQuery(), + + getTests: function(testcases, options) { + var tests; + if (this.queryParams.tests) tests = this.queryParams.tests.split(','); + else if (options.tests) tests = options.tests; + else if (options.test) tests = [option.test]; + else tests = Object.keys(testcases).grep(/^test/); + + return tests.map(function(test) { + if (testcases[test]) + return new Test.Unit.Testcase(test, testcases[test], testcases.setup, testcases.teardown); + }).compact(); }, - // Returns: - // "ERROR" if there was an error, - // "FAILURE" if there was a failure, or - // "SUCCESS" if there was neither + getResult: function() { var results = { tests: this.tests.length, @@ -198,100 +202,38 @@ Test.Unit.Runner.prototype = { { method: 'get', parameters: this.getResult(), asynchronous: false }); } }, + runTests: function() { - var test = this.tests[this.currentTest]; - if (!test) { - // finished! - this.postResults(); - this.logger.summary(this.summary()); - return; - } - if(!test.isWaiting) { - this.logger.start(test.name); - } + var test = this.tests[this.currentTest], actions; + + if (!test) return this.finish(); + if (!test.isWaiting) this.logger.start(test.name); test.run(); if(test.isWaiting) { this.logger.message("Waiting for " + test.timeToWait + "ms"); setTimeout(this.runTests.bind(this), test.timeToWait || 1000); - } else { - this.logger.finish(test.status(), test.summary()); - var actionButtons = test.actionButtons(); - if (actionButtons) - $(this.logger.lastLogLine).down('td', 2).appendChild(actionButtons); - - this.currentTest++; - // tail recursive, hopefully the browser will skip the stackframe - this.runTests(); + return; } + + this.logger.finish(test.status(), test.summary()); + if (actions = test.actions) this.logger.appendActionButtons(actions); + this.currentTest++; + // tail recursive, hopefully the browser will skip the stackframe + this.runTests(); + }, + + finish: function() { + this.postResults(); + this.logger.summary(this.summary()); }, summary: function() { return '#{tests} tests, #{assertions} assertions, #{failures} failures, #{errors} errors' .interpolate(this.getResult()); } -} +}); -Test.Unit.Assertions = Class.create(); -Test.Unit.Assertions.prototype = { - initialize: function() { - this.assertions = 0; - this.failures = 0; - this.errors = 0; - this.messages = []; - this.actions = {}; - }, - summary: function() { - return ( - this.assertions + " assertions, " + - this.failures + " failures, " + - this.errors + " errors" + "\n" + - this.messages.join("\n")); - }, - actionButtons: function() { - if (!Object.keys(this.actions).any()) return false; - var div = $(document.createElement("div")); - div.addClassName("action_buttons"); - - for (var title in this.actions) { - var button = $(document.createElement("input")); - button.value = title; - button.type = "button"; - button.observe("click", this.actions[title]); - div.appendChild(button); - } - return div; - }, - pass: function() { - this.assertions++; - }, - fail: function(message) { - this.failures++; - - var line = ""; - try { - throw new Error("stack"); - } catch(e){ - line = (/\.html:(\d+)/.exec(e.stack || '') || ['',''])[1]; - } - - this.messages.push("Failure: " + message + (line ? " Line #" + line : "")); - }, - info: function(message) { - this.messages.push("Info: " + message); - }, - error: function(error, test) { - this.errors++; - this.actions['retry with throw'] = function() { test.run(true) }; - this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) + ")"); - }, - status: function() { - if (this.failures > 0) return 'failed'; - if (this.errors > 0) return 'error'; - return 'passed'; - }, - isRunningFromRake: (function() { - return window.location.port == 4711; - })(), +Test.Unit.Assertions = { assert: function(expression) { var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"'; try { expression ? this.pass() : @@ -497,7 +439,86 @@ Test.Unit.Assertions.prototype = { }, assertElementMatches: function(element, expression) { this.assertElementsMatch([element], expression); + } +}; + +Test.Unit.Testcase = Class.create(Test.Unit.Assertions, { + initialize: function(name, test, setup, teardown) { + this.name = name; + this.test = test || Prototype.emptyFunction; + this.setup = setup || Prototype.emptyFunction; + this.teardown = teardown || Prototype.emptyFunction; + this.messages = []; + this.actions = {}; }, + + isWaiting: false, + timeToWait: 1000, + assertions: 0, + failures: 0, + errors: 0, + isRunningFromRake: window.location.port == 4711, + + wait: function(time, nextPart) { + this.isWaiting = true; + this.test = nextPart; + this.timeToWait = time; + }, + + run: function(rethrow) { + try { + try { + if (!this.isWaiting) this.setup(); + this.isWaiting = false; + this.test(); + } finally { + if(!this.isWaiting) { + this.teardown(); + } + } + } + catch(e) { + if (rethrow) throw e; + this.error(e, this); + } + }, + + summary: function() { + var msg = '#{assertions} assertions, #{failures} failures, #{errors} errors\n'; + return msg.interpolate(this) + this.messages.join("\n"); + }, + + pass: function() { + this.assertions++; + }, + + fail: function(message) { + this.failures++; + var line = ""; + try { + throw new Error("stack"); + } catch(e){ + line = (/\.html:(\d+)/.exec(e.stack || '') || ['',''])[1]; + } + this.messages.push("Failure: " + message + (line ? " Line #" + line : "")); + }, + + info: function(message) { + this.messages.push("Info: " + message); + }, + + error: function(error, test) { + this.errors++; + this.actions['retry with throw'] = function() { test.run(true) }; + this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) + ")"); + }, + + status: function() { + if (this.failures > 0) return 'failed'; + if (this.errors > 0) return 'error'; + return 'passed'; + }, + benchmark: function(operation, iterations) { var startAt = new Date(); (iterations || 1).times(operation); @@ -506,39 +527,4 @@ Test.Unit.Assertions.prototype = { iterations + ' iterations in ' + (timeTaken/1000)+'s' ); return timeTaken; } -} - -Test.Unit.Testcase = Class.create(); -Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), { - initialize: function(name, test, setup, teardown) { - Test.Unit.Assertions.prototype.initialize.bind(this)(); - this.name = name; - this.test = test || function() {}; - this.setup = setup || function() {}; - this.teardown = teardown || function() {}; - this.isWaiting = false; - this.timeToWait = 1000; - }, - wait: function(time, nextPart) { - this.isWaiting = true; - this.test = nextPart; - this.timeToWait = time; - }, - run: function(rethrow) { - try { - try { - if (!this.isWaiting) this.setup.bind(this)(); - this.isWaiting = false; - this.test.bind(this)(); - } finally { - if(!this.isWaiting) { - this.teardown.bind(this)(); - } - } - } - catch(e) { - if (rethrow) throw e; - this.error(e, this); - } - } }); diff --git a/test/test.css b/test/test.css index bf9b2a8..6fe8f51 100644 --- a/test/test.css +++ b/test/test.css @@ -12,38 +12,39 @@ body { margin-bottom: 2em; } -#logsummary { +.logsummary { + margin-top: 1em; margin-bottom: 1em; padding: 1ex; border: 1px solid #000; font-weight: bold; } -#logtable { +.logtable { width:100%; border-collapse: collapse; border: 1px dotted #666; } -#logtable td, #logtable th { +.logtable td, .logtable th { text-align: left; padding: 3px 8px; border: 1px dotted #666; } -#logtable .passed { +.logtable .passed { background-color: #cfc; } -#logtable .failed, #logtable .error { +.logtable .failed, .logtable .error { background-color: #fcc; } -#logtable td div.action_buttons { +.logtable td div.action_buttons { display: inline; } -#logtable td div.action_buttons input { +.logtable td div.action_buttons input { margin: 0 5px; font-size: 10px; } \ No newline at end of file diff --git a/test/unit/ajax.html b/test/unit/ajax.html index 051daab..1d533ad 100644 --- a/test/unit/ajax.html +++ b/test/unit/ajax.html @@ -411,7 +411,7 @@ info(message); } }} - }, 'testlog'); + }); // ]]> diff --git a/test/unit/array.html b/test/unit/array.html index 3be5cff..288e6ad 100644 --- a/test/unit/array.html +++ b/test/unit/array.html @@ -218,7 +218,7 @@ assertEnumEqual(['a', 'b', 'c', 'd'], $w(' a b\nc\t\nd\n')); }} - }, 'testlog'); + }); // ]]> diff --git a/test/unit/base.html b/test/unit/base.html index 2cb1524..07e9632 100644 --- a/test/unit/base.html +++ b/test/unit/base.html @@ -623,7 +623,7 @@ assertEqual("valueOf", new Foo().valueOf()); }} - }, 'testlog'); + }); // ]]> diff --git a/test/unit/dom.html b/test/unit/dom.html index 082f87f..0588a78 100644 --- a/test/unit/dom.html +++ b/test/unit/dom.html @@ -1683,7 +1683,7 @@ assertEqual(Node[pair.key], pair.value); }, this); }} - }, 'testlog'); + }); function preservingBrowserDimensions(callback) { var original = document.viewport.getDimensions(); diff --git a/test/unit/element_mixins.html b/test/unit/element_mixins.html index 5851792..a7d24da 100644 --- a/test/unit/element_mixins.html +++ b/test/unit/element_mixins.html @@ -66,7 +66,7 @@ Element.addMethods(); return (input.focus != null); })); }} - }, 'testlog'); + }); // ]]> diff --git a/test/unit/enumerable.html b/test/unit/enumerable.html index 982d40c..8421eb8 100644 --- a/test/unit/enumerable.html +++ b/test/unit/enumerable.html @@ -318,7 +318,7 @@ assertEqual(26, Fixtures.Primes.size()); assertEqual(0, [].size()); }} - }, 'testlog'); + }); // ]]> diff --git a/test/unit/event.html b/test/unit/event.html index f644f2e..d96f7df 100644 --- a/test/unit/event.html +++ b/test/unit/event.html @@ -242,7 +242,7 @@ }} - }, 'testlog'); + }); document.observe("dom:loaded", function(event) { eventResults.contentLoaded = { diff --git a/test/unit/hash.html b/test/unit/hash.html index cbb5f17..3bcbec4 100644 --- a/test/unit/hash.html +++ b/test/unit/hash.html @@ -226,7 +226,7 @@ assertEqual("key=bar", new Hash(new Hash(foo)).toQueryString()); }} - }, 'testlog'); + }); // ]]> diff --git a/test/unit/number.html b/test/unit/number.html index b2cce08..fbfbf03 100644 --- a/test/unit/number.html +++ b/test/unit/number.html @@ -61,7 +61,7 @@ assertEqual('-293', (-293).toJSON()); }} - }, 'testlog'); + }); // ]]> diff --git a/test/unit/position.html b/test/unit/position.html index b516f8a..7784a1c 100644 --- a/test/unit/position.html +++ b/test/unit/position.html @@ -81,7 +81,7 @@ assert(!Position.within($('body_absolute'), 10, 20), 'outside bottom'); }} - }, 'testlog'); + }); // ]]> diff --git a/test/unit/range.html b/test/unit/range.html index 54f3de2..896b13c 100644 --- a/test/unit/range.html +++ b/test/unit/range.html @@ -86,7 +86,7 @@ ); }} - }, 'testlog'); + }); // ]]> diff --git a/test/unit/selector.html b/test/unit/selector.html index 36af5e4..d6f0b33 100644 --- a/test/unit/selector.html +++ b/test/unit/selector.html @@ -431,7 +431,7 @@ assert(typeof results[1].show == 'function'); assert(typeof results[2].show == 'function'); }} - }, 'testlog'); + }); // ]]> diff --git a/test/unit/string.html b/test/unit/string.html index 386fe7a..320c687 100644 --- a/test/unit/string.html +++ b/test/unit/string.html @@ -565,7 +565,7 @@ assertIdentical(false, 'false'.evalJSON()); assertEqual('"', '"\\""'.evalJSON()); }} - }, 'testlog'); + }); // ]]> diff --git a/test/unit/unit_tests.html b/test/unit/unit_tests.html index b98ed27..705aa4f 100644 --- a/test/unit/unit_tests.html +++ b/test/unit/unit_tests.html @@ -22,6 +22,7 @@
+
@@ -173,7 +174,18 @@ assertNotVisible('testcss1_span'); // hidden by parent }} - }, "testlog"); + }); + + new Test.Unit.Runner({ + testDummy: function() { with(this) { + assert(true); + }}, + + testMultipleTestRunner: function() { with(this) { + assertEqual('passed', $('testlog_2').down('td', 1).innerHTML); + }} + + }, {testLog: 'testlog_2'}); // ]]>