diff --git a/README.md b/README.md index 21c7fb79..6a5de4ac 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +Update +====== + JavaScript Koans is an interactive learning environment that uses failing tests to introduce students to aspects of JavaScript in a logical sequence. The inspiration for this project comes from the Edgecase Ruby Koans and the book 'Javascript: The Good Parts'. @@ -6,4 +9,4 @@ Open the file jskoans.htm in your favourite browser and make the tests pass. The koans use the [Qunit](http://qunitjs.com/) test syntax and test runner. -Get started with Ryan Anklam's [Learn JavaScript completely On the Cloud With the JavaScript Koans and Cloud9 IDE](http://blog.bittersweetryan.com/2011/08/learn-some-javascript-completely-on.html) \ No newline at end of file +Get started with Ryan Anklam's [Learn JavaScript completely On the Cloud With the JavaScript Koans and Cloud9 IDE](http://blog.bittersweetryan.com/2011/08/learn-some-javascript-completely-on.html) diff --git a/jskoans.htm b/jskoans.htm index 1da57b5a..f9427527 100644 --- a/jskoans.htm +++ b/jskoans.htm @@ -1,10 +1,14 @@ - + + + + + @@ -22,14 +26,14 @@ - - + -

QUnit example

+

JavaScript Koans

To begin, find the file 'topics/about_asserts.js', and complete the tests.

+
    test markup, will be hidden
    diff --git a/license.txt b/license.txt new file mode 100644 index 00000000..55b99013 --- /dev/null +++ b/license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Liam McLennan + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/support/koans.js b/support/koans.js index b849dd8f..4f11ea54 100644 --- a/support/koans.js +++ b/support/koans.js @@ -14,14 +14,70 @@ Array.prototype.equalTo = function(compareTo) { return true; }; -QUnit.done = function(failures, total) { - if (failures > 0) { - var failed = $('ol#qunit-tests > li.fail'); - failed.hide(); - $(failed[0]).show(); - } - if (failures < total) { - $('h3.welcome_message').hide(); +(function() { + + var lastAssertLogReason, ignoreFurtherFailures = false; + var zenMessages = [ + "The path to enlightenment has many stones", + "Do not stray from your path, for enlightenment comes with perseverance", + "The only Zen you find on tops of mountains is the Zen you bring there", + "Enlightenment occurs when someone becomes inspired by information and uses it to enhance their life", + "Be master of mind rather than mastered by mind", + "Zen is not some kind of excitement, but concentration on our usual everyday routine", + "I think self-awareness is probably the most important thing towards being a champion", + "The reward of all action is to be found in enlightenment", + "lasting enlightenment can be achieved only through persistent exercise of real love", + "The real meaning of enlightenment is to gaze with undimmed eyes on all darkness", + "Do not think you will necessarily be aware of your own enlightenment", + "Enlightenment must come little by little - otherwise it would overwhelm", + "The greatest gift is to give people your enlightenment, to share it. It has to be the greatest", + "In the beginner's mind there are many possibilities, but in the expert's mind there are few", + "Only the hand that erases can write the true thing", + "Enlightenment is ego's ultimate disappointment", + "Man suffers only because he takes seriously what the gods made for fun", + "It is easy to believe we are each waves and forget we are also the ocean", + "Working out is my biggest hobby. It's my Zen hour. I just zone out", + "A self-motivation is an enlightenment of mind, empowerment of heart and enrichment of soul to arise, awake and ascend to achieve the noble and coveted goal even if it entails walking on its enervating path all alone" + ]; + + QUnit.config.reorder = false; + + QUnit.done(function(results) { + var failures = results.failed; + var total = results.total; + if (failures > 0) { + var failed = $('ol#qunit-tests > li.fail'); + failed.hide(); + $(failed[0]).show(); + } + if (failures < total) { + $('h3.welcome_message').hide(); + } + if (failures > 0) { + $("#zen-help").show(); + } + $("body").scrollTop($(document).height()); + }); + + QUnit.log(function(result) { + lastAssertLogReason = result.message; + }); + + QUnit.testDone(function(result) { + var message; + if (!ignoreFurtherFailures && result.failed > 0) { + ignoreFurtherFailures = true; + message = "" + randomZenMessage() + "\nTry meditating on this: " + result.module + ": " + result.name + " (" + lastAssertLogReason + ")"; + $("#zen-help").html(message.replace(/\n/g, "

    ")); + console.log(message); + } + }); + + function randomZenMessage() { + var randomIndex = Math.floor(Math.random() * zenMessages.length); + var zenMessage = zenMessages[randomIndex]; + zenMessage = zenMessage.charAt(0).toUpperCase() + zenMessage.substr(1); + return "" + zenMessage + "."; } -}; +})(); diff --git a/support/qunit.css b/support/qunit.css index e9404f59..7ba3f9a3 100644 --- a/support/qunit.css +++ b/support/qunit.css @@ -1,7 +1,17 @@ +/** + * QUnit v1.12.0 - A JavaScript Unit Testing Framework + * + * http://qunitjs.com + * + * Copyright 2012 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + /** Font Family and Sizes */ #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { - font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; + font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; } #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } @@ -10,7 +20,7 @@ /** Resets */ -#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { +#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { margin: 0; padding: 0; } @@ -20,20 +30,33 @@ #qunit-header { padding: 0.5em 0 0.5em 1em; - - color: #fff; - text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px; + + color: #8699a4; background-color: #0d3349; - - border-radius: 15px 15px 0 0; - -moz-border-radius: 15px 15px 0 0; - -webkit-border-top-right-radius: 15px; - -webkit-border-top-left-radius: 15px; + + font-size: 1.5em; + line-height: 1em; + font-weight: normal; + + border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + -webkit-border-top-right-radius: 5px; + -webkit-border-top-left-radius: 5px; } #qunit-header a { text-decoration: none; - color: white; + color: #c2ccd1; +} + +#qunit-header a:hover, +#qunit-header a:focus { + color: #fff; +} + +#qunit-testrunner-toolbar label { + display: inline-block; + padding: 0 .5em 0 .1em; } #qunit-banner { @@ -41,7 +64,10 @@ } #qunit-testrunner-toolbar { - padding: 0em 0 0.5em 2em; + padding: 0.5em 0 0.5em 2em; + color: #5E740B; + background-color: #eee; + overflow: hidden; } #qunit-userAgent { @@ -51,6 +77,9 @@ text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; } +#qunit-modulefilter-container { + float: right; +} /** Tests: Pass/Fail */ @@ -64,23 +93,75 @@ list-style-position: inside; } +#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { + display: none; +} + #qunit-tests li strong { cursor: pointer; } -#qunit-tests ol { +#qunit-tests li a { + padding: 0.5em; + color: #c2ccd1; + text-decoration: none; +} +#qunit-tests li a:hover, +#qunit-tests li a:focus { + color: #000; +} + +#qunit-tests li .runtime { + float: right; + font-size: smaller; +} + +.qunit-assert-list { margin-top: 0.5em; padding: 0.5em; - + background-color: #fff; - - border-radius: 15px; - -moz-border-radius: 15px; - -webkit-border-radius: 15px; - - box-shadow: inset 0px 2px 13px #999; - -moz-box-shadow: inset 0px 2px 13px #999; - -webkit-box-shadow: inset 0px 2px 13px #999; + + border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; +} + +.qunit-collapsed { + display: none; +} + +#qunit-tests table { + border-collapse: collapse; + margin-top: .2em; +} + +#qunit-tests th { + text-align: right; + vertical-align: top; + padding: 0 .5em 0 0; +} + +#qunit-tests td { + vertical-align: top; +} + +#qunit-tests pre { + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; +} + +#qunit-tests del { + background-color: #e0f2be; + color: #374e0c; + text-decoration: none; +} + +#qunit-tests ins { + background-color: #ffcaca; + color: #500; + text-decoration: none; } /*** Test Counts */ @@ -90,8 +171,7 @@ #qunit-tests b.failed { color: #710909; } #qunit-tests li li { - margin: 0.5em; - padding: 0.4em 0.5em 0.4em 0.5em; + padding: 5px; background-color: #fff; border-bottom: none; list-style-position: inside; @@ -100,14 +180,14 @@ /*** Passing Styles */ #qunit-tests li li.pass { - color: #5E740B; + color: #3c510c; background-color: #fff; - border-left: 26px solid #C6E746; + border-left: 10px solid #C6E746; } #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } #qunit-tests .pass .test-name { color: #366097; } - + #qunit-tests .pass .test-actual, #qunit-tests .pass .test-expected { color: #999999; } @@ -118,7 +198,15 @@ #qunit-tests li li.fail { color: #710909; background-color: #fff; - border-left: 26px solid #EE5757; + border-left: 10px solid #EE5757; + white-space: pre; +} + +#qunit-tests > li:last-child { + border-radius: 0 0 5px 5px; + -moz-border-radius: 0 0 5px 5px; + -webkit-border-bottom-right-radius: 5px; + -webkit-border-bottom-left-radius: 5px; } #qunit-tests .fail { color: #000000; background-color: #EE5757; } @@ -128,11 +216,10 @@ #qunit-tests .fail .test-actual { color: #EE5757; } #qunit-tests .fail .test-expected { color: green; } -#qunit-banner.qunit-fail, -#qunit-testrunner-toolbar { background-color: #EE5757; } +#qunit-banner.qunit-fail { background-color: #EE5757; } -/** Footer */ +/** Result */ #qunit-testresult { padding: 0.5em 0.5em 0.5em 2.5em; @@ -140,10 +227,10 @@ color: #2b81af; background-color: #D2E0E6; - border-radius: 0 0 15px 15px; - -moz-border-radius: 0 0 15px 15px; - -webkit-border-bottom-right-radius: 15px; - -webkit-border-bottom-left-radius: 15px; + border-bottom: 1px solid white; +} +#qunit-testresult .module-name { + font-weight: bold; } /** Fixture */ @@ -152,4 +239,6 @@ position: absolute; top: -10000px; left: -10000px; + width: 1000px; + height: 1000px; } diff --git a/support/qunit.js b/support/qunit.js index 6bdfc6ff..84c73907 100644 --- a/support/qunit.js +++ b/support/qunit.js @@ -1,405 +1,831 @@ -/* - * QUnit - A JavaScript Unit Testing Framework - * - * http://docs.jquery.com/QUnit +/** + * QUnit v1.12.0 - A JavaScript Unit Testing Framework + * + * http://qunitjs.com * - * Copyright (c) 2009 John Resig, Jörn Zaefferer - * Dual licensed under the MIT (MIT-LICENSE.txt) - * and GPL (GPL-LICENSE.txt) licenses. + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * https://jquery.org/license/ */ -(function(window) { +(function( window ) { + +var QUnit, + assert, + config, + onErrorFnPrev, + testId = 0, + fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + // Keep a local reference to Date (GH-283) + Date = window.Date, + setTimeout = window.setTimeout, + defined = { + setTimeout: typeof window.setTimeout !== "undefined", + sessionStorage: (function() { + var x = "qunit-test-string"; + try { + sessionStorage.setItem( x, x ); + sessionStorage.removeItem( x ); + return true; + } catch( e ) { + return false; + } + }()) + }, + /** + * Provides a normalized error string, correcting an issue + * with IE 7 (and prior) where Error.prototype.toString is + * not properly implemented + * + * Based on http://es5.github.com/#x15.11.4.4 + * + * @param {String|Error} error + * @return {String} error message + */ + errorString = function( error ) { + var name, message, + errorString = error.toString(); + if ( errorString.substring( 0, 7 ) === "[object" ) { + name = error.name ? error.name.toString() : "Error"; + message = error.message ? error.message.toString() : ""; + if ( name && message ) { + return name + ": " + message; + } else if ( name ) { + return name; + } else if ( message ) { + return message; + } else { + return "Error"; + } + } else { + return errorString; + } + }, + /** + * Makes a clone of an object using only Array or Object as base, + * and copies over the own enumerable properties. + * + * @param {Object} obj + * @return {Object} New object with only the own properties (recursively). + */ + objectValues = function( obj ) { + // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. + /*jshint newcap: false */ + var key, val, + vals = QUnit.is( "array", obj ) ? [] : {}; + for ( key in obj ) { + if ( hasOwn.call( obj, key ) ) { + val = obj[key]; + vals[key] = val === Object(val) ? objectValues(val) : val; + } + } + return vals; + }; -var QUnit = { +function Test( settings ) { + extend( this, settings ); + this.assertions = []; + this.testNumber = ++Test.count; +} - // call on start of module test to prepend name to all tests - module: function(name, testEnvironment) { - config.currentModule = name; +Test.count = 0; - synchronize(function() { - if ( config.currentModule ) { - QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); - } +Test.prototype = { + init: function() { + var a, b, li, + tests = id( "qunit-tests" ); + + if ( tests ) { + b = document.createElement( "strong" ); + b.innerHTML = this.nameHtml; - config.currentModule = name; - config.moduleTestEnvironment = testEnvironment; + // `a` initialized at top of scope + a = document.createElement( "a" ); + a.innerHTML = "Rerun"; + a.href = QUnit.url({ testNumber: this.testNumber }); + + li = document.createElement( "li" ); + li.appendChild( b ); + li.appendChild( a ); + li.className = "running"; + li.id = this.id = "qunit-test-output" + testId++; + + tests.appendChild( li ); + } + }, + setup: function() { + if ( + // Emit moduleStart when we're switching from one module to another + this.module !== config.previousModule || + // They could be equal (both undefined) but if the previousModule property doesn't + // yet exist it means this is the first test in a suite that isn't wrapped in a + // module, in which case we'll just emit a moduleStart event for 'undefined'. + // Without this, reporters can get testStart before moduleStart which is a problem. + !hasOwn.call( config, "previousModule" ) + ) { + if ( hasOwn.call( config, "previousModule" ) ) { + runLoggingCallbacks( "moduleDone", QUnit, { + name: config.previousModule, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all + }); + } + config.previousModule = this.module; config.moduleStats = { all: 0, bad: 0 }; + runLoggingCallbacks( "moduleStart", QUnit, { + name: this.module + }); + } + + config.current = this; - QUnit.moduleStart( name, testEnvironment ); + this.testEnvironment = extend({ + setup: function() {}, + teardown: function() {} + }, this.moduleTestEnvironment ); + + this.started = +new Date(); + runLoggingCallbacks( "testStart", QUnit, { + name: this.testName, + module: this.module }); - }, - asyncTest: function(testName, expected, callback) { - if ( arguments.length === 2 ) { - callback = expected; - expected = 0; - } + /*jshint camelcase:false */ - QUnit.test(testName, expected, callback, true); - }, - - test: function(testName, expected, callback, async) { - var name = '' + testName + '', testEnvironment, testEnvironmentArg; - if ( arguments.length === 2 ) { - callback = expected; - expected = null; + /** + * Expose the current test environment. + * + * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead. + */ + QUnit.current_testEnvironment = this.testEnvironment; + + /*jshint camelcase:true */ + + if ( !config.pollution ) { + saveGlobal(); } - // is 2nd argument a testEnvironment? - if ( expected && typeof expected === 'object') { - testEnvironmentArg = expected; - expected = null; + if ( config.notrycatch ) { + this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); + return; + } + try { + this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); + } catch( e ) { + QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); } + }, + run: function() { + config.current = this; - if ( config.currentModule ) { - name = '' + config.currentModule + ": " + name; + var running = id( "qunit-testresult" ); + + if ( running ) { + running.innerHTML = "Running:
    " + this.nameHtml; + } + + if ( this.async ) { + QUnit.stop(); } - if ( !validTest(config.currentModule + ": " + testName) ) { + this.callbackStarted = +new Date(); + + if ( config.notrycatch ) { + this.callback.call( this.testEnvironment, QUnit.assert ); + this.callbackRuntime = +new Date() - this.callbackStarted; return; } - synchronize(function() { + try { + this.callback.call( this.testEnvironment, QUnit.assert ); + this.callbackRuntime = +new Date() - this.callbackStarted; + } catch( e ) { + this.callbackRuntime = +new Date() - this.callbackStarted; - testEnvironment = extend({ - setup: function() {}, - teardown: function() {} - }, config.moduleTestEnvironment); - if (testEnvironmentArg) { - extend(testEnvironment,testEnvironmentArg); - } + QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); + // else next test will carry the responsibility + saveGlobal(); - QUnit.testStart( testName, testEnvironment ); - - // allow utility functions to access the current test environment - QUnit.current_testEnvironment = testEnvironment; - - config.assertions = []; - config.expected = expected; - - var tests = id("qunit-tests"); - if (tests) { - var b = document.createElement("strong"); - b.innerHTML = "Running " + name; - var li = document.createElement("li"); - li.appendChild( b ); - li.id = "current-test-output"; - tests.appendChild( li ) + // Restart the tests if they're blocking + if ( config.blocking ) { + QUnit.start(); } - + } + }, + teardown: function() { + config.current = this; + if ( config.notrycatch ) { + if ( typeof this.callbackRuntime === "undefined" ) { + this.callbackRuntime = +new Date() - this.callbackStarted; + } + this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); + return; + } else { try { - if ( !config.pollution ) { - saveGlobal(); + this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); + } catch( e ) { + QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); + } + } + checkPollution(); + }, + finish: function() { + config.current = this; + if ( config.requireExpects && this.expected === null ) { + QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); + } else if ( this.expected !== null && this.expected !== this.assertions.length ) { + QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); + } else if ( this.expected === null && !this.assertions.length ) { + QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); + } + + var i, assertion, a, b, time, li, ol, + test = this, + good = 0, + bad = 0, + tests = id( "qunit-tests" ); + + this.runtime = +new Date() - this.started; + config.stats.all += this.assertions.length; + config.moduleStats.all += this.assertions.length; + + if ( tests ) { + ol = document.createElement( "ol" ); + ol.className = "qunit-assert-list"; + + for ( i = 0; i < this.assertions.length; i++ ) { + assertion = this.assertions[i]; + + li = document.createElement( "li" ); + li.className = assertion.result ? "pass" : "fail"; + li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); + ol.appendChild( li ); + + if ( assertion.result ) { + good++; + } else { + bad++; + config.stats.bad++; + config.moduleStats.bad++; } + } - testEnvironment.setup.call(testEnvironment); - } catch(e) { - QUnit.ok( false, "Setup failed on " + name + ": " + e.message ); + // store result when possible + if ( QUnit.config.reorder && defined.sessionStorage ) { + if ( bad ) { + sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); + } else { + sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); + } } - }); - - synchronize(function() { - if ( async ) { - QUnit.stop(); + + if ( bad === 0 ) { + addClass( ol, "qunit-collapsed" ); } - try { - callback.call(testEnvironment); - } catch(e) { - fail("Test " + name + " died, exception and test follows", e, callback); - QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message ); - // else next test will carry the responsibility - saveGlobal(); - - // Restart the tests if they're blocking - if ( config.blocking ) { - start(); + // `b` initialized at top of scope + b = document.createElement( "strong" ); + b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; + + addEvent(b, "click", function() { + var next = b.parentNode.lastChild, + collapsed = hasClass( next, "qunit-collapsed" ); + ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); + }); + + addEvent(b, "dblclick", function( e ) { + var target = e && e.target ? e.target : window.event.srcElement; + if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { + target = target.parentNode; + } + if ( window.location && target.nodeName.toLowerCase() === "strong" ) { + window.location = QUnit.url({ testNumber: test.testNumber }); + } + }); + + // `time` initialized at top of scope + time = document.createElement( "span" ); + time.className = "runtime"; + time.innerHTML = this.runtime + " ms"; + + // `li` initialized at top of scope + li = id( this.id ); + li.className = bad ? "fail" : "pass"; + li.removeChild( li.firstChild ); + a = li.firstChild; + li.appendChild( b ); + li.appendChild( a ); + li.appendChild( time ); + li.appendChild( ol ); + + } else { + for ( i = 0; i < this.assertions.length; i++ ) { + if ( !this.assertions[i].result ) { + bad++; + config.stats.bad++; + config.moduleStats.bad++; } } + } + + runLoggingCallbacks( "testDone", QUnit, { + name: this.testName, + module: this.module, + failed: bad, + passed: this.assertions.length - bad, + total: this.assertions.length, + duration: this.runtime }); - synchronize(function() { - try { - checkPollution(); - testEnvironment.teardown.call(testEnvironment); - } catch(e) { - QUnit.ok( false, "Teardown failed on " + name + ": " + e.message ); - } - }); - - synchronize(function() { - try { - QUnit.reset(); - } catch(e) { - fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, QUnit.reset); - } + QUnit.reset(); - if ( config.expected && config.expected != config.assertions.length ) { - QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" ); - } + config.current = undefined; + }, - var good = 0, bad = 0, - tests = id("qunit-tests"); + queue: function() { + var bad, + test = this; - config.stats.all += config.assertions.length; - config.moduleStats.all += config.assertions.length; + synchronize(function() { + test.init(); + }); + function run() { + // each of these can by async + synchronize(function() { + test.setup(); + }); + synchronize(function() { + test.run(); + }); + synchronize(function() { + test.teardown(); + }); + synchronize(function() { + test.finish(); + }); + } - if ( tests ) { - var ol = document.createElement("ol"); + // `bad` initialized at top of scope + // defer when previous test run passed, if storage is available + bad = QUnit.config.reorder && defined.sessionStorage && + +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); - for ( var i = 0; i < config.assertions.length; i++ ) { - var assertion = config.assertions[i]; + if ( bad ) { + run(); + } else { + synchronize( run, true ); + } + } +}; - var li = document.createElement("li"); - li.className = assertion.result ? "pass" : "fail"; - li.innerHTML = assertion.message || "(no message)"; - ol.appendChild( li ); +// Root QUnit object. +// `QUnit` initialized at top of scope +QUnit = { - if ( assertion.result ) { - good++; - } else { - bad++; - config.stats.bad++; - config.moduleStats.bad++; - } - } - if (bad == 0) { - ol.style.display = "none"; - } + // call on start of module test to prepend name to all tests + module: function( name, testEnvironment ) { + config.currentModule = name; + config.currentModuleTestEnvironment = testEnvironment; + config.modules[name] = true; + }, - var b = document.createElement("strong"); - b.innerHTML = name + " (" + bad + ", " + good + ", " + config.assertions.length + ")"; - - addEvent(b, "click", function() { - var next = b.nextSibling, display = next.style.display; - next.style.display = display === "none" ? "block" : "none"; - }); - - addEvent(b, "dblclick", function(e) { - var target = e && e.target ? e.target : window.event.srcElement; - if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { - target = target.parentNode; - } - if ( window.location && target.nodeName.toLowerCase() === "strong" ) { - window.location.search = "?" + encodeURIComponent(getText([target]).replace(/\(.+\)$/, "").replace(/(^\s*|\s*$)/g, "")); - } - }); + asyncTest: function( testName, expected, callback ) { + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } - var li = id("current-test-output"); - li.id = ""; - li.className = bad ? "fail" : "pass"; - li.removeChild( li.firstChild ); - li.appendChild( b ); - li.appendChild( ol ); + QUnit.test( testName, expected, callback, true ); + }, - if ( bad ) { - var toolbar = id("qunit-testrunner-toolbar"); - if ( toolbar ) { - toolbar.style.display = "block"; - id("qunit-filter-pass").disabled = null; - id("qunit-filter-missing").disabled = null; - } - } + test: function( testName, expected, callback, async ) { + var test, + nameHtml = "" + escapeText( testName ) + ""; - } else { - for ( var i = 0; i < config.assertions.length; i++ ) { - if ( !config.assertions[i].result ) { - bad++; - config.stats.bad++; - config.moduleStats.bad++; - } - } - } + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } - QUnit.testDone( testName, bad, config.assertions.length ); + if ( config.currentModule ) { + nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml; + } - if ( !window.setTimeout && !config.queue.length ) { - done(); - } + test = new Test({ + nameHtml: nameHtml, + testName: testName, + expected: expected, + async: async, + callback: callback, + module: config.currentModule, + moduleTestEnvironment: config.currentModuleTestEnvironment, + stack: sourceFromStacktrace( 2 ) }); - synchronize( done ); + if ( !validTest( test ) ) { + return; + } + + test.queue(); }, - - /** - * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. - */ - expect: function(asserts) { - config.expected = asserts; + + // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through. + expect: function( asserts ) { + if (arguments.length === 1) { + config.current.expected = asserts; + } else { + return config.current.expected; + } }, - - /* - * LIAM MCLENNAN - added an inverse for ok - */ - not: function(a, msg) { - msg = escapeHtml(msg); - QUnit.log(a, msg); - - config.assertions.push({ - result: !a, - message: msg + + start: function( count ) { + // QUnit hasn't been initialized yet. + // Note: RequireJS (et al) may delay onLoad + if ( config.semaphore === undefined ) { + QUnit.begin(function() { + // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first + setTimeout(function() { + QUnit.start( count ); + }); }); + return; + } + + config.semaphore -= count || 1; + // don't start until equal number of stop-calls + if ( config.semaphore > 0 ) { + return; + } + // ignore if start is called more often then stop + if ( config.semaphore < 0 ) { + config.semaphore = 0; + QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); + return; + } + // A slight delay, to avoid any current callbacks + if ( defined.setTimeout ) { + setTimeout(function() { + if ( config.semaphore > 0 ) { + return; + } + if ( config.timeout ) { + clearTimeout( config.timeout ); + } + + config.blocking = false; + process( true ); + }, 13); + } else { + config.blocking = false; + process( true ); + } }, + stop: function( count ) { + config.semaphore += count || 1; + config.blocking = true; + + if ( config.testTimeout && defined.setTimeout ) { + clearTimeout( config.timeout ); + config.timeout = setTimeout(function() { + QUnit.ok( false, "Test timed out" ); + config.semaphore = 1; + QUnit.start(); + }, config.testTimeout ); + } + } +}; + +// `assert` initialized at top of scope +// Assert helpers +// All of these must either call QUnit.push() or manually do: +// - runLoggingCallbacks( "log", .. ); +// - config.current.assertions.push({ .. }); +// We attach it to the QUnit object *after* we expose the public API, +// otherwise `assert` will become a global variable in browsers (#341). +assert = { /** - * Asserts true. + * Asserts rough true-ish result. + * @name ok + * @function * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); */ - ok: function(a, msg) { - msg = escapeHtml(msg); - QUnit.log(a, msg); + ok: function( result, msg ) { + if ( !config.current ) { + throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); + } + result = !!result; + msg = msg || (result ? "okay" : "failed" ); - config.assertions.push({ - result: !!a, + var source, + details = { + module: config.current.module, + name: config.current.testName, + result: result, + message: msg + }; + + msg = "" + escapeText( msg ) + ""; + + if ( !result ) { + source = sourceFromStacktrace( 2 ); + if ( source ) { + details.source = source; + msg += "
    Source:
    " + escapeText( source ) + "
    "; + } + } + runLoggingCallbacks( "log", QUnit, details ); + config.current.assertions.push({ + result: result, message: msg }); }, /** - * Checks that the first two arguments are equal, with an optional message. + * Assert that the first two arguments are equal, with an optional message. * Prints out both actual and expected values. - * - * Prefered to ok( actual == expected, message ) - * - * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); - * - * @param Object actual - * @param Object expected - * @param String message (optional) + * @name equal + * @function + * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); */ - equal: function(actual, expected, message) { - push(expected == actual, actual, expected, message); + equal: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + QUnit.push( expected == actual, actual, expected, message ); }, - notEqual: function(actual, expected, message) { - push(expected != actual, actual, expected, message); + /** + * @name notEqual + * @function + */ + notEqual: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + QUnit.push( expected != actual, actual, expected, message ); + }, + + /** + * @name propEqual + * @function + */ + propEqual: function( actual, expected, message ) { + actual = objectValues(actual); + expected = objectValues(expected); + QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); }, - - deepEqual: function(actual, expected, message) { - push(QUnit.equiv(actual, expected), actual, expected, message); + + /** + * @name notPropEqual + * @function + */ + notPropEqual: function( actual, expected, message ) { + actual = objectValues(actual); + expected = objectValues(expected); + QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); }, - notDeepEqual: function(actual, expected, message) { - push(!QUnit.equiv(actual, expected), actual, expected, message); + /** + * @name deepEqual + * @function + */ + deepEqual: function( actual, expected, message ) { + QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); }, - strictEqual: function(actual, expected, message) { - push(expected === actual, actual, expected, message); + /** + * @name notDeepEqual + * @function + */ + notDeepEqual: function( actual, expected, message ) { + QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); }, - notStrictEqual: function(actual, expected, message) { - push(expected !== actual, actual, expected, message); + /** + * @name strictEqual + * @function + */ + strictEqual: function( actual, expected, message ) { + QUnit.push( expected === actual, actual, expected, message ); }, - raises: function(fn, message) { - try { - fn(); - ok( false, message ); - } - catch (e) { - ok( true, message ); - } + /** + * @name notStrictEqual + * @function + */ + notStrictEqual: function( actual, expected, message ) { + QUnit.push( expected !== actual, actual, expected, message ); }, - start: function() { - // A slight delay, to avoid any current callbacks - if ( window.setTimeout ) { - window.setTimeout(function() { - if ( config.timeout ) { - clearTimeout(config.timeout); - } + "throws": function( block, expected, message ) { + var actual, + expectedOutput = expected, + ok = false; - config.blocking = false; - process(); - }, 13); - } else { - config.blocking = false; - process(); + // 'expected' is optional + if ( typeof expected === "string" ) { + message = expected; + expected = null; } - }, - - stop: function(timeout) { - config.blocking = true; - if ( timeout && window.setTimeout ) { - config.timeout = window.setTimeout(function() { - QUnit.ok( false, "Test timed out" ); - QUnit.start(); - }, timeout); + config.current.ignoreGlobalErrors = true; + try { + block.call( config.current.testEnvironment ); + } catch (e) { + actual = e; + } + config.current.ignoreGlobalErrors = false; + + if ( actual ) { + // we don't want to validate thrown error + if ( !expected ) { + ok = true; + expectedOutput = null; + // expected is a regexp + } else if ( QUnit.objectType( expected ) === "regexp" ) { + ok = expected.test( errorString( actual ) ); + // expected is a constructor + } else if ( actual instanceof expected ) { + ok = true; + // expected is a validation function which returns true is validation passed + } else if ( expected.call( {}, actual ) === true ) { + expectedOutput = null; + ok = true; + } + + QUnit.push( ok, actual, expectedOutput, message ); + } else { + QUnit.pushFailure( message, null, "No exception was thrown." ); } } +}; + +/** + * @deprecated since 1.8.0 + * Kept assertion helpers in root for backwards compatibility. + */ +extend( QUnit, assert ); +/** + * @deprecated since 1.9.0 + * Kept root "raises()" for backwards compatibility. + * (Note that we don't introduce assert.raises). + */ +QUnit.raises = assert[ "throws" ]; + +/** + * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 + * Kept to avoid TypeErrors for undefined methods. + */ +QUnit.equals = function() { + QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); +}; +QUnit.same = function() { + QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); }; -// Backwards compatibility, deprecated -QUnit.equals = QUnit.equal; -QUnit.same = QUnit.deepEqual; +// We want access to the constructor's prototype +(function() { + function F() {} + F.prototype = QUnit; + QUnit = new F(); + // Make F QUnit's constructor so that we can add to the prototype later + QUnit.constructor = F; +}()); -// Maintain internal state -var config = { +/** + * Config object: Maintain internal state + * Later exposed as QUnit.config + * `config` initialized at top of scope + */ +config = { // The queue of tests to run queue: [], // block until document ready - blocking: true + blocking: true, + + // when enabled, show only failing tests + // gets persisted through sessionStorage and can be changed in UI via checkbox + hidepassed: false, + + // by default, run previously failed tests first + // very useful in combination with "Hide passed tests" checked + reorder: true, + + // by default, modify document.title when suite is done + altertitle: true, + + // when enabled, all tests must call expect() + requireExpects: false, + + // add checkboxes that are persisted in the query-string + // when enabled, the id is set to `true` as a `QUnit.config` property + urlConfig: [ + { + id: "noglobals", + label: "Check for Globals", + tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." + }, + { + id: "notrycatch", + label: "No try-catch", + tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." + } + ], + + // Set of all modules. + modules: {}, + + // logging callback queues + begin: [], + done: [], + log: [], + testStart: [], + testDone: [], + moduleStart: [], + moduleDone: [] }; -// Load paramaters +// Export global variables, unless an 'exports' object exists, +// in that case we assume we're in CommonJS (dealt with on the bottom of the script) +if ( typeof exports === "undefined" ) { + extend( window, QUnit.constructor.prototype ); + + // Expose QUnit object + window.QUnit = QUnit; +} + +// Initialize more QUnit.config and QUnit.urlParams (function() { - var location = window.location || { search: "", protocol: "file:" }, - GETParams = location.search.slice(1).split('&'); - - for ( var i = 0; i < GETParams.length; i++ ) { - GETParams[i] = decodeURIComponent( GETParams[i] ); - if ( GETParams[i] === "noglobals" ) { - GETParams.splice( i, 1 ); - i--; - config.noglobals = true; - } else if ( GETParams[i].search('=') > -1 ) { - GETParams.splice( i, 1 ); - i--; + var i, + location = window.location || { search: "", protocol: "file:" }, + params = location.search.slice( 1 ).split( "&" ), + length = params.length, + urlParams = {}, + current; + + if ( params[ 0 ] ) { + for ( i = 0; i < length; i++ ) { + current = params[ i ].split( "=" ); + current[ 0 ] = decodeURIComponent( current[ 0 ] ); + // allow just a key to turn on a flag, e.g., test.html?noglobals + current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; + urlParams[ current[ 0 ] ] = current[ 1 ]; } } - - // restrict modules/tests by get parameters - config.filters = GETParams; - + + QUnit.urlParams = urlParams; + + // String search anywhere in moduleName+testName + config.filter = urlParams.filter; + + // Exact match of the module name + config.module = urlParams.module; + + config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; + // Figure out if we're running the tests from a server or not - QUnit.isLocal = !!(location.protocol === 'file:'); -})(); + QUnit.isLocal = location.protocol === "file:"; +}()); -// Expose the API as global variables, unless an 'exports' -// object exists, in that case we assume we're in CommonJS -if ( typeof exports === "undefined" || typeof require === "undefined" ) { - extend(window, QUnit); - window.QUnit = QUnit; -} else { - extend(exports, QUnit); - exports.QUnit = QUnit; -} +// Extend QUnit object, +// these after set here because they should not be exposed as global functions +extend( QUnit, { + assert: assert, -// define these after exposing globals to keep them in these QUnit namespace only -extend(QUnit, { config: config, // Initialize the configuration options init: function() { - extend(config, { + extend( config, { stats: { all: 0, bad: 0 }, moduleStats: { all: 0, bad: 0 }, - started: +new Date, + started: +new Date(), updateRate: 1000, blocking: false, autostart: true, autorun: false, - assertions: [], - filters: [], - queue: [] + filter: "", + queue: [], + semaphore: 1 }); - var tests = id("qunit-tests"), - banner = id("qunit-banner"), - result = id("qunit-testresult"); + var tests, banner, result, + qunit = id( "qunit" ); + + if ( qunit ) { + qunit.innerHTML = + "

    " + escapeText( document.title ) + "

    " + + "

    " + + "
    " + + "

    " + + "
      "; + } + + tests = id( "qunit-tests" ); + banner = id( "qunit-banner" ); + result = id( "qunit-testresult" ); if ( tests ) { tests.innerHTML = ""; @@ -412,329 +838,673 @@ extend(QUnit, { if ( result ) { result.parentNode.removeChild( result ); } + + if ( tests ) { + result = document.createElement( "p" ); + result.id = "qunit-testresult"; + result.className = "result"; + tests.parentNode.insertBefore( result, tests ); + result.innerHTML = "Running...
       "; + } }, - - /** - * Resets the test setup. Useful for tests that modify the DOM. - */ + + // Resets the test setup. Useful for tests that modify the DOM. + /* + DEPRECATED: Use multiple tests instead of resetting inside a test. + Use testStart or testDone for custom cleanup. + This method will throw an error in 2.0, and will be removed in 2.1 + */ reset: function() { - if ( window.jQuery ) { - jQuery("#main, #qunit-fixture").html( config.fixture ); + var fixture = id( "qunit-fixture" ); + if ( fixture ) { + fixture.innerHTML = config.fixture; } }, - - /** - * Trigger an event on an element. - * - * @example triggerEvent( document.body, "click" ); - * - * @param DOMElement elem - * @param String type - */ + + // Trigger an event on an element. + // @example triggerEvent( document.body, "click" ); triggerEvent: function( elem, type, event ) { if ( document.createEvent ) { - event = document.createEvent("MouseEvents"); + event = document.createEvent( "MouseEvents" ); event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 0, 0, 0, 0, 0, false, false, false, false, 0, null); - elem.dispatchEvent( event ); + elem.dispatchEvent( event ); } else if ( elem.fireEvent ) { - elem.fireEvent("on"+type); + elem.fireEvent( "on" + type ); } }, - + // Safe object type checking is: function( type, obj ) { - return QUnit.objectType( obj ) == type; + return QUnit.objectType( obj ) === type; }, - + objectType: function( obj ) { - if (typeof obj === "undefined") { + if ( typeof obj === "undefined" ) { return "undefined"; + // consider: typeof null === object + } + if ( obj === null ) { + return "null"; + } + + var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), + type = match && match[1] || ""; + + switch ( type ) { + case "Number": + if ( isNaN(obj) ) { + return "nan"; + } + return "number"; + case "String": + case "Boolean": + case "Array": + case "Date": + case "RegExp": + case "Function": + return type.toLowerCase(); + } + if ( typeof obj === "object" ) { + return "object"; + } + return undefined; + }, + + push: function( result, actual, expected, message ) { + if ( !config.current ) { + throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); + } + + var output, source, + details = { + module: config.current.module, + name: config.current.testName, + result: result, + message: message, + actual: actual, + expected: expected + }; + + message = escapeText( message ) || ( result ? "okay" : "failed" ); + message = "" + message + ""; + output = message; + + if ( !result ) { + expected = escapeText( QUnit.jsDump.parse(expected) ); + actual = escapeText( QUnit.jsDump.parse(actual) ); + output += ""; + + if ( actual !== expected ) { + output += ""; + output += ""; + } + + source = sourceFromStacktrace(); + + if ( source ) { + details.source = source; + output += ""; + } + + output += "
      Expected:
      " + expected + "
      Result:
      " + actual + "
      Diff:
      " + QUnit.diff( expected, actual ) + "
      Source:
      " + escapeText( source ) + "
      "; + } + + runLoggingCallbacks( "log", QUnit, details ); + + config.current.assertions.push({ + result: !!result, + message: output + }); + }, + + pushFailure: function( message, source, actual ) { + if ( !config.current ) { + throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); + } + + var output, + details = { + module: config.current.module, + name: config.current.testName, + result: false, + message: message + }; + + message = escapeText( message ) || "error"; + message = "" + message + ""; + output = message; - // consider: typeof null === object + output += ""; + + if ( actual ) { + output += ""; } - if (obj === null) { - return "null"; + + if ( source ) { + details.source = source; + output += ""; } - var type = Object.prototype.toString.call( obj ) - .match(/^\[object\s(.*)\]$/)[1] || ''; + output += "
      Result:
      " + escapeText( actual ) + "
      Source:
      " + escapeText( source ) + "
      "; - switch (type) { - case 'Number': - if (isNaN(obj)) { - return "nan"; - } else { - return "number"; - } - case 'String': - case 'Boolean': - case 'Array': - case 'Date': - case 'RegExp': - case 'Function': - return type.toLowerCase(); - } - if (typeof obj === "object") { - return "object"; + runLoggingCallbacks( "log", QUnit, details ); + + config.current.assertions.push({ + result: false, + message: output + }); + }, + + url: function( params ) { + params = extend( extend( {}, QUnit.urlParams ), params ); + var key, + querystring = "?"; + + for ( key in params ) { + if ( hasOwn.call( params, key ) ) { + querystring += encodeURIComponent( key ) + "=" + + encodeURIComponent( params[ key ] ) + "&"; + } } - return undefined; + return window.location.protocol + "//" + window.location.host + + window.location.pathname + querystring.slice( 0, -1 ); }, - - // Logging callbacks - begin: function() {}, - done: function(failures, total) {}, - log: function(result, message) {}, - testStart: function(name, testEnvironment) {}, - testDone: function(name, failures, total) {}, - moduleStart: function(name, testEnvironment) {}, - moduleDone: function(name, failures, total) {} + + extend: extend, + id: id, + addEvent: addEvent, + addClass: addClass, + hasClass: hasClass, + removeClass: removeClass + // load, equiv, jsDump, diff: Attached later +}); + +/** + * @deprecated: Created for backwards compatibility with test runner that set the hook function + * into QUnit.{hook}, instead of invoking it and passing the hook function. + * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. + * Doing this allows us to tell if the following methods have been overwritten on the actual + * QUnit object. + */ +extend( QUnit.constructor.prototype, { + + // Logging callbacks; all receive a single argument with the listed properties + // run test/logs.html for any related changes + begin: registerLoggingCallback( "begin" ), + + // done: { failed, passed, total, runtime } + done: registerLoggingCallback( "done" ), + + // log: { result, actual, expected, message } + log: registerLoggingCallback( "log" ), + + // testStart: { name } + testStart: registerLoggingCallback( "testStart" ), + + // testDone: { name, failed, passed, total, duration } + testDone: registerLoggingCallback( "testDone" ), + + // moduleStart: { name } + moduleStart: registerLoggingCallback( "moduleStart" ), + + // moduleDone: { name, failed, passed, total } + moduleDone: registerLoggingCallback( "moduleDone" ) }); if ( typeof document === "undefined" || document.readyState === "complete" ) { config.autorun = true; } -addEvent(window, "load", function() { - QUnit.begin(); - +QUnit.load = function() { + runLoggingCallbacks( "begin", QUnit, {} ); + // Initialize the config, saving the execution queue - var oldconfig = extend({}, config); + var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, + urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter, + numModules = 0, + moduleNames = [], + moduleFilterHtml = "", + urlConfigHtml = "", + oldconfig = extend( {}, config ); + QUnit.init(); extend(config, oldconfig); config.blocking = false; - var userAgent = id("qunit-userAgent"); + len = config.urlConfig.length; + + for ( i = 0; i < len; i++ ) { + val = config.urlConfig[i]; + if ( typeof val === "string" ) { + val = { + id: val, + label: val, + tooltip: "[no tooltip available]" + }; + } + config[ val.id ] = QUnit.urlParams[ val.id ]; + urlConfigHtml += ""; + } + for ( i in config.modules ) { + if ( config.modules.hasOwnProperty( i ) ) { + moduleNames.push(i); + } + } + numModules = moduleNames.length; + moduleNames.sort( function( a, b ) { + return a.localeCompare( b ); + }); + moduleFilterHtml += ""; + + // `userAgent` initialized at top of scope + userAgent = id( "qunit-userAgent" ); if ( userAgent ) { userAgent.innerHTML = navigator.userAgent; } - var banner = id("qunit-header"); + + // `banner` initialized at top of scope + banner = id( "qunit-header" ); if ( banner ) { - banner.innerHTML = '' + banner.innerHTML + ''; + banner.innerHTML = "" + banner.innerHTML + " "; } - - var toolbar = id("qunit-testrunner-toolbar"); + + // `toolbar` initialized at top of scope + toolbar = id( "qunit-testrunner-toolbar" ); if ( toolbar ) { - toolbar.style.display = "none"; - - var filter = document.createElement("input"); + // `filter` initialized at top of scope + filter = document.createElement( "input" ); filter.type = "checkbox"; filter.id = "qunit-filter-pass"; - filter.disabled = true; + addEvent( filter, "click", function() { - var li = document.getElementsByTagName("li"); - for ( var i = 0; i < li.length; i++ ) { - if ( li[i].className.indexOf("pass") > -1 ) { - li[i].style.display = filter.checked ? "none" : ""; + var tmp, + ol = document.getElementById( "qunit-tests" ); + + if ( filter.checked ) { + ol.className = ol.className + " hidepass"; + } else { + tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; + ol.className = tmp.replace( / hidepass /, " " ); + } + if ( defined.sessionStorage ) { + if (filter.checked) { + sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); + } else { + sessionStorage.removeItem( "qunit-filter-passed-tests" ); } } }); + + if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { + filter.checked = true; + // `ol` initialized at top of scope + ol = document.getElementById( "qunit-tests" ); + ol.className = ol.className + " hidepass"; + } toolbar.appendChild( filter ); - var label = document.createElement("label"); - label.setAttribute("for", "qunit-filter-pass"); + // `label` initialized at top of scope + label = document.createElement( "label" ); + label.setAttribute( "for", "qunit-filter-pass" ); + label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." ); label.innerHTML = "Hide passed tests"; toolbar.appendChild( label ); - var missing = document.createElement("input"); - missing.type = "checkbox"; - missing.id = "qunit-filter-missing"; - missing.disabled = true; - addEvent( missing, "click", function() { - var li = document.getElementsByTagName("li"); - for ( var i = 0; i < li.length; i++ ) { - if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) { - li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block"; - } - } + urlConfigCheckboxesContainer = document.createElement("span"); + urlConfigCheckboxesContainer.innerHTML = urlConfigHtml; + urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input"); + // For oldIE support: + // * Add handlers to the individual elements instead of the container + // * Use "click" instead of "change" + // * Fallback from event.target to event.srcElement + addEvents( urlConfigCheckboxes, "click", function( event ) { + var params = {}, + target = event.target || event.srcElement; + params[ target.name ] = target.checked ? true : undefined; + window.location = QUnit.url( params ); }); - toolbar.appendChild( missing ); - - label = document.createElement("label"); - label.setAttribute("for", "qunit-filter-missing"); - label.innerHTML = "Hide missing tests (untested code is broken code)"; - toolbar.appendChild( label ); + toolbar.appendChild( urlConfigCheckboxesContainer ); + + if (numModules > 1) { + moduleFilter = document.createElement( "span" ); + moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); + moduleFilter.innerHTML = moduleFilterHtml; + addEvent( moduleFilter.lastChild, "change", function() { + var selectBox = moduleFilter.getElementsByTagName("select")[0], + selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); + + window.location = QUnit.url({ + module: ( selectedModule === "" ) ? undefined : selectedModule, + // Remove any existing filters + filter: undefined, + testNumber: undefined + }); + }); + toolbar.appendChild(moduleFilter); + } } - var main = id('main') || id('qunit-fixture'); + // `main` initialized at top of scope + main = id( "qunit-fixture" ); if ( main ) { config.fixture = main.innerHTML; } - if (config.autostart) { + if ( config.autostart ) { QUnit.start(); } -}); +}; -function done() { - if ( config.doneTimer && window.clearTimeout ) { - window.clearTimeout( config.doneTimer ); - config.doneTimer = null; +addEvent( window, "load", QUnit.load ); + +// `onErrorFnPrev` initialized at top of scope +// Preserve other handlers +onErrorFnPrev = window.onerror; + +// Cover uncaught exceptions +// Returning true will suppress the default browser handler, +// returning false will let it run. +window.onerror = function ( error, filePath, linerNr ) { + var ret = false; + if ( onErrorFnPrev ) { + ret = onErrorFnPrev( error, filePath, linerNr ); } - if ( config.queue.length ) { - config.doneTimer = window.setTimeout(function(){ - if ( !config.queue.length ) { - done(); - } else { - synchronize( done ); + // Treat return value as window.onerror itself does, + // Only do our handling if not suppressed. + if ( ret !== true ) { + if ( QUnit.config.current ) { + if ( QUnit.config.current.ignoreGlobalErrors ) { + return true; } - }, 13); - - return; + QUnit.pushFailure( error, filePath + ":" + linerNr ); + } else { + QUnit.test( "global failure", extend( function() { + QUnit.pushFailure( error, filePath + ":" + linerNr ); + }, { validTest: validTest } ) ); + } + return false; } + return ret; +}; + +function done() { config.autorun = true; // Log the last module results if ( config.currentModule ) { - QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); + runLoggingCallbacks( "moduleDone", QUnit, { + name: config.currentModule, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all + }); } - - var banner = id("qunit-banner"), - tests = id("qunit-tests"), - html = ['Tests completed in ', - +new Date - config.started, ' milliseconds.
      ', - '', config.stats.all - config.stats.bad, ' tests of ', config.stats.all, ' passed, ', config.stats.bad,' failed.'].join(''); + delete config.previousModule; + + var i, key, + banner = id( "qunit-banner" ), + tests = id( "qunit-tests" ), + runtime = +new Date() - config.started, + passed = config.stats.all - config.stats.bad, + html = [ + "Tests completed in ", + runtime, + " milliseconds.
      ", + "", + passed, + " assertions of ", + config.stats.all, + " passed, ", + config.stats.bad, + " failed." + ].join( "" ); if ( banner ) { - banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); + banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); } - if ( tests ) { - var result = id("qunit-testresult"); + if ( tests ) { + id( "qunit-testresult" ).innerHTML = html; + } - if ( !result ) { - result = document.createElement("p"); - result.id = "qunit-testresult"; - result.className = "result"; - tests.parentNode.insertBefore( result, tests.nextSibling ); + if ( config.altertitle && typeof document !== "undefined" && document.title ) { + // show ✖ for good, ✔ for bad suite result in title + // use escape sequences in case file gets loaded with non-utf-8-charset + document.title = [ + ( config.stats.bad ? "\u2716" : "\u2714" ), + document.title.replace( /^[\u2714\u2716] /i, "" ) + ].join( " " ); + } + + // clear own sessionStorage items if all tests passed + if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { + // `key` & `i` initialized at top of scope + for ( i = 0; i < sessionStorage.length; i++ ) { + key = sessionStorage.key( i++ ); + if ( key.indexOf( "qunit-test-" ) === 0 ) { + sessionStorage.removeItem( key ); + } } + } - result.innerHTML = html; + // scroll back to top to show results + if ( window.scrollTo ) { + window.scrollTo(0, 0); } - QUnit.done( config.stats.bad, config.stats.all ); + runLoggingCallbacks( "done", QUnit, { + failed: config.stats.bad, + passed: passed, + total: config.stats.all, + runtime: runtime + }); } -function validTest( name ) { - var i = config.filters.length, - run = false; +/** @return Boolean: true if this test should be ran */ +function validTest( test ) { + var include, + filter = config.filter && config.filter.toLowerCase(), + module = config.module && config.module.toLowerCase(), + fullName = (test.module + ": " + test.testName).toLowerCase(); - if ( !i ) { + // Internally-generated tests are always valid + if ( test.callback && test.callback.validTest === validTest ) { + delete test.callback.validTest; return true; } - - while ( i-- ) { - var filter = config.filters[i], - not = filter.charAt(0) == '!'; - if ( not ) { - filter = filter.slice(1); - } + if ( config.testNumber ) { + return test.testNumber === config.testNumber; + } - if ( name.indexOf(filter) !== -1 ) { - return !not; - } + if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { + return false; + } - if ( not ) { - run = true; - } + if ( !filter ) { + return true; + } + + include = filter.charAt( 0 ) !== "!"; + if ( !include ) { + filter = filter.slice( 1 ); + } + + // If the filter matches, we need to honour include + if ( fullName.indexOf( filter ) !== -1 ) { + return include; } - return run; + // Otherwise, do the opposite + return !include; } -function escapeHtml(s) { - s = s === null ? "" : s + ""; - return s.replace(/[\&"<>\\]/g, function(s) { - switch(s) { - case "&": return "&"; - case "\\": return "\\\\"; - case '"': return '\"'; - case "<": return "<"; - case ">": return ">"; - default: return s; +// so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) +// Later Safari and IE10 are supposed to support error.stack as well +// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack +function extractStacktrace( e, offset ) { + offset = offset === undefined ? 3 : offset; + + var stack, include, i; + + if ( e.stacktrace ) { + // Opera + return e.stacktrace.split( "\n" )[ offset + 3 ]; + } else if ( e.stack ) { + // Firefox, Chrome + stack = e.stack.split( "\n" ); + if (/^error$/i.test( stack[0] ) ) { + stack.shift(); } - }); + if ( fileName ) { + include = []; + for ( i = offset; i < stack.length; i++ ) { + if ( stack[ i ].indexOf( fileName ) !== -1 ) { + break; + } + include.push( stack[ i ] ); + } + if ( include.length ) { + return include.join( "\n" ); + } + } + return stack[ offset ]; + } else if ( e.sourceURL ) { + // Safari, PhantomJS + // hopefully one day Safari provides actual stacktraces + // exclude useless self-reference for generated Error objects + if ( /qunit.js$/.test( e.sourceURL ) ) { + return; + } + // for actual exceptions, this is useful + return e.sourceURL + ":" + e.line; + } +} +function sourceFromStacktrace( offset ) { + try { + throw new Error(); + } catch ( e ) { + return extractStacktrace( e, offset ); + } } -function push(result, actual, expected, message) { - message = escapeHtml(message) || (result ? "okay" : "failed"); - message = '' + message + ""; - expected = escapeHtml(QUnit.jsDump.parse(expected)); - actual = escapeHtml(QUnit.jsDump.parse(actual)); - var output = message + ', expected: ' + expected + ''; - if (actual != expected) { - output += ' result: ' + actual + ', diff: ' + QUnit.diff(expected, actual); +/** + * Escape text for attribute or text content. + */ +function escapeText( s ) { + if ( !s ) { + return ""; } - - // can't use ok, as that would double-escape messages - QUnit.log(result, output); - config.assertions.push({ - result: !!result, - message: output + s = s + ""; + // Both single quotes and double quotes (for attributes) + return s.replace( /['"<>&]/g, function( s ) { + switch( s ) { + case "'": + return "'"; + case "\"": + return """; + case "<": + return "<"; + case ">": + return ">"; + case "&": + return "&"; + } }); } -function synchronize( callback ) { +function synchronize( callback, last ) { config.queue.push( callback ); if ( config.autorun && !config.blocking ) { - process(); + process( last ); } } -function process() { - var start = (new Date()).getTime(); +function process( last ) { + function next() { + process( last ); + } + var start = new Date().getTime(); + config.depth = config.depth ? config.depth + 1 : 1; while ( config.queue.length && !config.blocking ) { - if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { + if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { config.queue.shift()(); - } else { - setTimeout( process, 13 ); + setTimeout( next, 13 ); break; } } + config.depth--; + if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { + done(); + } } function saveGlobal() { config.pollution = []; - + if ( config.noglobals ) { for ( var key in window ) { - config.pollution.push( key ); + if ( hasOwn.call( window, key ) ) { + // in Opera sometimes DOM element ids show up here, ignore them + if ( /^qunit-test-output/.test( key ) ) { + continue; + } + config.pollution.push( key ); + } } } } -function checkPollution( name ) { - var old = config.pollution; +function checkPollution() { + var newGlobals, + deletedGlobals, + old = config.pollution; + saveGlobal(); - - var newGlobals = diff( old, config.pollution ); + + newGlobals = diff( config.pollution, old ); if ( newGlobals.length > 0 ) { - ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); - config.expected++; + QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); } - var deletedGlobals = diff( config.pollution, old ); + deletedGlobals = diff( old, config.pollution ); if ( deletedGlobals.length > 0 ) { - ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); - config.expected++; + QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); } } // returns a new Array with the elements that are in a but not in b function diff( a, b ) { - var result = a.slice(); - for ( var i = 0; i < result.length; i++ ) { - for ( var j = 0; j < b.length; j++ ) { + var i, j, + result = a.slice(); + + for ( i = 0; i < result.length; i++ ) { + for ( j = 0; j < b.length; j++ ) { if ( result[i] === b[j] ) { - result.splice(i, 1); + result.splice( i, 1 ); i--; break; } @@ -743,210 +1513,307 @@ function diff( a, b ) { return result; } -function fail(message, exception, callback) { - if ( typeof console !== "undefined" && console.error && console.warn ) { - console.error(message); - console.error(exception); - console.warn(callback.toString()); - - } else if ( window.opera && opera.postError ) { - opera.postError(message, exception, callback.toString); - } -} - -function extend(a, b) { +function extend( a, b ) { for ( var prop in b ) { - a[prop] = b[prop]; + if ( hasOwn.call( b, prop ) ) { + // Avoid "Member not found" error in IE8 caused by messing with window.constructor + if ( !( prop === "constructor" && a === window ) ) { + if ( b[ prop ] === undefined ) { + delete a[ prop ]; + } else { + a[ prop ] = b[ prop ]; + } + } + } } return a; } -function addEvent(elem, type, fn) { +/** + * @param {HTMLElement} elem + * @param {string} type + * @param {Function} fn + */ +function addEvent( elem, type, fn ) { + // Standards-based browsers if ( elem.addEventListener ) { elem.addEventListener( type, fn, false ); - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, fn ); + // IE } else { - fn(); + elem.attachEvent( "on" + type, fn ); + } +} + +/** + * @param {Array|NodeList} elems + * @param {string} type + * @param {Function} fn + */ +function addEvents( elems, type, fn ) { + var i = elems.length; + while ( i-- ) { + addEvent( elems[i], type, fn ); + } +} + +function hasClass( elem, name ) { + return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; +} + +function addClass( elem, name ) { + if ( !hasClass( elem, name ) ) { + elem.className += (elem.className ? " " : "") + name; + } +} + +function removeClass( elem, name ) { + var set = " " + elem.className + " "; + // Class name may appear multiple times + while ( set.indexOf(" " + name + " ") > -1 ) { + set = set.replace(" " + name + " " , " "); } + // If possible, trim it for prettiness, but not necessarily + elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, ""); } -function id(name) { - return !!(typeof document !== "undefined" && document && document.getElementById) && +function id( name ) { + return !!( typeof document !== "undefined" && document && document.getElementById ) && document.getElementById( name ); } +function registerLoggingCallback( key ) { + return function( callback ) { + config[key].push( callback ); + }; +} + +// Supports deprecated method of completely overwriting logging callbacks +function runLoggingCallbacks( key, scope, args ) { + var i, callbacks; + if ( QUnit.hasOwnProperty( key ) ) { + QUnit[ key ].call(scope, args ); + } else { + callbacks = config[ key ]; + for ( i = 0; i < callbacks.length; i++ ) { + callbacks[ i ].call( scope, args ); + } + } +} + // Test for equality any JavaScript type. -// Discussions and reference: http://philrathe.com/articles/equiv -// Test suites: http://philrathe.com/tests/equiv // Author: Philippe Rathé -QUnit.equiv = function () { - - var innerEquiv; // the real equiv function - var callers = []; // stack to decide between skip/abort functions - var parents = []; // stack to avoiding loops from circular referencing - - // Call the o related callback with the given arguments. - function bindCallbacks(o, callbacks, args) { - var prop = QUnit.objectType(o); - if (prop) { - if (QUnit.objectType(callbacks[prop]) === "function") { - return callbacks[prop].apply(callbacks, args); - } else { - return callbacks[prop]; // or undefined - } - } - } - - var callbacks = function () { - - // for string, boolean, number and null - function useStrictEquality(b, a) { - if (b instanceof a.constructor || a instanceof b.constructor) { - // to catch short annotaion VS 'new' annotation of a declaration - // e.g. var i = 1; - // var j = new Number(1); - return a == b; - } else { - return a === b; - } - } - - return { - "string": useStrictEquality, - "boolean": useStrictEquality, - "number": useStrictEquality, - "null": useStrictEquality, - "undefined": useStrictEquality, - - "nan": function (b) { - return isNaN(b); - }, - - "date": function (b, a) { - return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); - }, - - "regexp": function (b, a) { - return QUnit.objectType(b) === "regexp" && - a.source === b.source && // the regex itself - a.global === b.global && // and its modifers (gmi) ... - a.ignoreCase === b.ignoreCase && - a.multiline === b.multiline; - }, - - // - skip when the property is a method of an instance (OOP) - // - abort otherwise, - // initial === would have catch identical references anyway - "function": function () { - var caller = callers[callers.length - 1]; - return caller !== Object && - typeof caller !== "undefined"; - }, - - "array": function (b, a) { - var i, j, loop; - var len; - - // b could be an object literal here - if ( ! (QUnit.objectType(b) === "array")) { - return false; - } - - len = a.length; - if (len !== b.length) { // safe and faster - return false; - } - - //track reference to avoid circular references - parents.push(a); - for (i = 0; i < len; i++) { - loop = false; - for(j=0;j= 0) { - type = "array"; - } else { - type = typeof obj; - } - return type; - }, - separator:function() { - return this.multiline ? this.HTML ? '
      ' : '\n' : this.HTML ? ' ' : ' '; - }, - indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing - if ( !this.multiline ) - return ''; - var chr = this.indentChar; - if ( this.HTML ) - chr = chr.replace(/\t/g,' ').replace(/ /g,' '); - return Array( this._depth_ + (extra||0) ).join(chr); - }, - up:function( a ) { - this._depth_ += a || 1; - }, - down:function( a ) { - this._depth_ -= a || 1; - }, - setParser:function( name, parser ) { - this.parsers[name] = parser; - }, - // The next 3 are exposed so you can use them - quote:quote, - literal:literal, - join:join, - // - _depth_: 1, - // This is the list of parsers, to modify them, use jsDump.setParser - parsers:{ - window: '[Window]', - document: '[Document]', - error:'[ERROR]', //when no parser is found, shouldn't happen - unknown: '[Unknown]', - 'null':'null', - undefined:'undefined', - 'function':function( fn ) { - var ret = 'function', - name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE - if ( name ) - ret += ' ' + name; - ret += '('; - - ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join(''); - return join( ret, this.parse(fn,'functionCode'), '}' ); + return join( "[", ret, "]" ); + } + + var reName = /^function (\w+)/, + jsDump = { + // type is used mostly internally, you can fix a (custom)type in advance + parse: function( obj, type, stack ) { + stack = stack || [ ]; + var inStack, res, + parser = this.parsers[ type || this.typeOf(obj) ]; + + type = typeof parser; + inStack = inArray( obj, stack ); + + if ( inStack !== -1 ) { + return "recursion(" + (inStack - stack.length) + ")"; + } + if ( type === "function" ) { + stack.push( obj ); + res = parser.call( this, obj, stack ); + stack.pop(); + return res; + } + return ( type === "string" ) ? parser : this.parsers.error; + }, + typeOf: function( obj ) { + var type; + if ( obj === null ) { + type = "null"; + } else if ( typeof obj === "undefined" ) { + type = "undefined"; + } else if ( QUnit.is( "regexp", obj) ) { + type = "regexp"; + } else if ( QUnit.is( "date", obj) ) { + type = "date"; + } else if ( QUnit.is( "function", obj) ) { + type = "function"; + } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { + type = "window"; + } else if ( obj.nodeType === 9 ) { + type = "document"; + } else if ( obj.nodeType ) { + type = "node"; + } else if ( + // native arrays + toString.call( obj ) === "[object Array]" || + // NodeList objects + ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) + ) { + type = "array"; + } else if ( obj.constructor === Error.prototype.constructor ) { + type = "error"; + } else { + type = typeof obj; + } + return type; }, - array: array, - nodelist: array, - arguments: array, - object:function( map ) { - var ret = [ ]; - this.up(); - for ( var key in map ) - ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) ); - this.down(); - return join( '{', ret, '}' ); + separator: function() { + return this.multiline ? this.HTML ? "
      " : "\n" : this.HTML ? " " : " "; }, - node:function( node ) { - var open = this.HTML ? '<' : '<', - close = this.HTML ? '>' : '>'; - - var tag = node.nodeName.toLowerCase(), - ret = open + tag; - - for ( var a in this.DOMAttrs ) { - var val = node[this.DOMAttrs[a]]; - if ( val ) - ret += ' ' + a + '=' + this.parse( val, 'attribute' ); + // extra can be a number, shortcut for increasing-calling-decreasing + indent: function( extra ) { + if ( !this.multiline ) { + return ""; + } + var chr = this.indentChar; + if ( this.HTML ) { + chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); } - return ret + close + open + '/' + tag + close; + return new Array( this.depth + ( extra || 0 ) ).join(chr); }, - functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function - var l = fn.length; - if ( !l ) return ''; - - var args = Array(l); - while ( l-- ) - args[l] = String.fromCharCode(97+l);//97 is 'a' - return ' ' + args.join(', ') + ' '; + up: function( a ) { + this.depth += a || 1; }, - key:quote, //object calls it internally, the key part of an item in a map - functionCode:'[code]', //function calls it internally, it's the content of the function - attribute:quote, //node calls it internally, it's an html attribute value - string:quote, - date:quote, - regexp:literal, //regex - number:literal, - 'boolean':literal - }, - DOMAttrs:{//attributes to dump from nodes, name=>realName - id:'id', - name:'name', - 'class':'className' - }, - HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) - indentChar:' ',//indentation unit - multiline:false //if true, items in a collection, are separated by a \n, else just a space. - }; + down: function( a ) { + this.depth -= a || 1; + }, + setParser: function( name, parser ) { + this.parsers[name] = parser; + }, + // The next 3 are exposed so you can use them + quote: quote, + literal: literal, + join: join, + // + depth: 1, + // This is the list of parsers, to modify them, use jsDump.setParser + parsers: { + window: "[Window]", + document: "[Document]", + error: function(error) { + return "Error(\"" + error.message + "\")"; + }, + unknown: "[Unknown]", + "null": "null", + "undefined": "undefined", + "function": function( fn ) { + var ret = "function", + // functions never have name in IE + name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; + + if ( name ) { + ret += " " + name; + } + ret += "( "; + + ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); + return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); + }, + array: array, + nodelist: array, + "arguments": array, + object: function( map, stack ) { + /*jshint forin:false */ + var ret = [ ], keys, key, val, i; + QUnit.jsDump.up(); + keys = []; + for ( key in map ) { + keys.push( key ); + } + keys.sort(); + for ( i = 0; i < keys.length; i++ ) { + key = keys[ i ]; + val = map[ key ]; + ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); + } + QUnit.jsDump.down(); + return join( "{", ret, "}" ); + }, + node: function( node ) { + var len, i, val, + open = QUnit.jsDump.HTML ? "<" : "<", + close = QUnit.jsDump.HTML ? ">" : ">", + tag = node.nodeName.toLowerCase(), + ret = open + tag, + attrs = node.attributes; + + if ( attrs ) { + for ( i = 0, len = attrs.length; i < len; i++ ) { + val = attrs[i].nodeValue; + // IE6 includes all attributes in .attributes, even ones not explicitly set. + // Those have values like undefined, null, 0, false, "" or "inherit". + if ( val && val !== "inherit" ) { + ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); + } + } + } + ret += close; - return jsDump; -})(); + // Show content of TextNode or CDATASection + if ( node.nodeType === 3 || node.nodeType === 4 ) { + ret += node.nodeValue; + } + + return ret + open + "/" + tag + close; + }, + // function calls it internally, it's the arguments part of the function + functionArgs: function( fn ) { + var args, + l = fn.length; + + if ( !l ) { + return ""; + } -// from Sizzle.js -function getText( elems ) { - var ret = "", elem; + args = new Array(l); + while ( l-- ) { + // 97 is 'a' + args[l] = String.fromCharCode(97+l); + } + return " " + args.join( ", " ) + " "; + }, + // object calls it internally, the key part of an item in a map + key: quote, + // function calls it internally, it's the content of the function + functionCode: "[code]", + // node calls it internally, it's an html attribute value + attribute: quote, + string: quote, + date: quote, + regexp: literal, + number: literal, + "boolean": literal + }, + // if true, entities are escaped ( <, >, \t, space and \n ) + HTML: false, + // indentation unit + indentChar: " ", + // if true, items in a collection, are separated by a \n, else just a space. + multiline: true + }; - for ( var i = 0; elems[i]; i++ ) { - elem = elems[i]; + return jsDump; +}()); - // Get the text from text nodes and CDATA nodes - if ( elem.nodeType === 3 || elem.nodeType === 4 ) { - ret += elem.nodeValue; +// from jquery.js +function inArray( elem, array ) { + if ( array.indexOf ) { + return array.indexOf( elem ); + } - // Traverse everything else, except comment nodes - } else if ( elem.nodeType !== 8 ) { - ret += getText( elem.childNodes ); + for ( var i = 0, length = array.length; i < length; i++ ) { + if ( array[ i ] === elem ) { + return i; } } - return ret; -}; + return -1; +} /* * Javascript Diff Algorithm @@ -1143,132 +2064,149 @@ function getText( elems ) { * * More Info: * http://ejohn.org/projects/javascript-diff-algorithm/ - * + * * Usage: QUnit.diff(expected, actual) - * - * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" + * + * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" */ QUnit.diff = (function() { - function diff(o, n){ - var ns = new Object(); - var os = new Object(); - - for (var i = 0; i < n.length; i++) { - if (ns[n[i]] == null) - ns[n[i]] = { - rows: new Array(), + /*jshint eqeqeq:false, eqnull:true */ + function diff( o, n ) { + var i, + ns = {}, + os = {}; + + for ( i = 0; i < n.length; i++ ) { + if ( !hasOwn.call( ns, n[i] ) ) { + ns[ n[i] ] = { + rows: [], o: null }; - ns[n[i]].rows.push(i); + } + ns[ n[i] ].rows.push( i ); } - - for (var i = 0; i < o.length; i++) { - if (os[o[i]] == null) - os[o[i]] = { - rows: new Array(), + + for ( i = 0; i < o.length; i++ ) { + if ( !hasOwn.call( os, o[i] ) ) { + os[ o[i] ] = { + rows: [], n: null }; - os[o[i]].rows.push(i); - } - - for (var i in ns) { - if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { - n[ns[i].rows[0]] = { - text: n[ns[i].rows[0]], - row: os[i].rows[0] - }; - o[os[i].rows[0]] = { - text: o[os[i].rows[0]], - row: ns[i].rows[0] - }; + } + os[ o[i] ].rows.push( i ); + } + + for ( i in ns ) { + if ( hasOwn.call( ns, i ) ) { + if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) { + n[ ns[i].rows[0] ] = { + text: n[ ns[i].rows[0] ], + row: os[i].rows[0] + }; + o[ os[i].rows[0] ] = { + text: o[ os[i].rows[0] ], + row: ns[i].rows[0] + }; + } } } - - for (var i = 0; i < n.length - 1; i++) { - if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && - n[i + 1] == o[n[i].row + 1]) { - n[i + 1] = { - text: n[i + 1], + + for ( i = 0; i < n.length - 1; i++ ) { + if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && + n[ i + 1 ] == o[ n[i].row + 1 ] ) { + + n[ i + 1 ] = { + text: n[ i + 1 ], row: n[i].row + 1 }; - o[n[i].row + 1] = { - text: o[n[i].row + 1], + o[ n[i].row + 1 ] = { + text: o[ n[i].row + 1 ], row: i + 1 }; } } - - for (var i = n.length - 1; i > 0; i--) { - if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && - n[i - 1] == o[n[i].row - 1]) { - n[i - 1] = { - text: n[i - 1], + + for ( i = n.length - 1; i > 0; i-- ) { + if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && + n[ i - 1 ] == o[ n[i].row - 1 ]) { + + n[ i - 1 ] = { + text: n[ i - 1 ], row: n[i].row - 1 }; - o[n[i].row - 1] = { - text: o[n[i].row - 1], + o[ n[i].row - 1 ] = { + text: o[ n[i].row - 1 ], row: i - 1 }; } } - + return { o: o, n: n }; } - - return function(o, n){ - o = o.replace(/\s+$/, ''); - n = n.replace(/\s+$/, ''); - var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); - - var str = ""; - - var oSpace = o.match(/\s+/g); - if (oSpace == null) { - oSpace = [" "]; + + return function( o, n ) { + o = o.replace( /\s+$/, "" ); + n = n.replace( /\s+$/, "" ); + + var i, pre, + str = "", + out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), + oSpace = o.match(/\s+/g), + nSpace = n.match(/\s+/g); + + if ( oSpace == null ) { + oSpace = [ " " ]; } else { - oSpace.push(" "); + oSpace.push( " " ); } - var nSpace = n.match(/\s+/g); - if (nSpace == null) { - nSpace = [" "]; + + if ( nSpace == null ) { + nSpace = [ " " ]; } else { - nSpace.push(" "); + nSpace.push( " " ); } - - if (out.n.length == 0) { - for (var i = 0; i < out.o.length; i++) { - str += '' + out.o[i] + oSpace[i] + ""; + + if ( out.n.length === 0 ) { + for ( i = 0; i < out.o.length; i++ ) { + str += "" + out.o[i] + oSpace[i] + ""; } } else { - if (out.n[0].text == null) { - for (n = 0; n < out.o.length && out.o[n].text == null; n++) { - str += '' + out.o[n] + oSpace[n] + ""; + if ( out.n[0].text == null ) { + for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { + str += "" + out.o[n] + oSpace[n] + ""; } } - - for (var i = 0; i < out.n.length; i++) { + + for ( i = 0; i < out.n.length; i++ ) { if (out.n[i].text == null) { - str += '' + out.n[i] + nSpace[i] + ""; + str += "" + out.n[i] + nSpace[i] + ""; } else { - var pre = ""; - - for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { - pre += '' + out.o[n] + oSpace[n] + ""; + // `pre` initialized at top of scope + pre = ""; + + for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { + pre += "" + out.o[n] + oSpace[n] + ""; } str += " " + out.n[i].text + nSpace[i] + pre; } } } - + return str; - } -})(); + }; +}()); + +// for CommonJS environments, export everything +if ( typeof exports !== "undefined" ) { + extend( exports, QUnit.constructor.prototype ); +} -})(this); +// get at whatever the global object is, like window in browsers +}( (function() {return this;}.call()) )); diff --git a/testem.json b/testem.json new file mode 100644 index 00000000..5e601c60 --- /dev/null +++ b/testem.json @@ -0,0 +1,4 @@ +{ + "framework": "qunit", + "test_page": "jskoans.htm" +} diff --git a/topics/about_arrays.js b/topics/about_arrays.js index 9e32f400..b2de2547 100644 --- a/topics/about_arrays.js +++ b/topics/about_arrays.js @@ -1,27 +1,28 @@ - module("About Arrays (topics/about_arrays.js)"); test("array literal syntax and indexing", function() { var favouriteThings = ["cellar door", 42, true]; // note that array elements do not have to be of the same type - equals(favouriteThings[0], __, 'what is in the first position of the array?'); - equals(favouriteThings[1], __, 'what is in the second position of the array?'); - equals(favouriteThings[2], __, 'what is in the third position of the array?'); + equal(__, favouriteThings[0], 'what is in the first position of the array?'); + equal(__, favouriteThings[1], 'what is in the second position of the array?'); + equal(__, favouriteThings[2], 'what is in the third position of the array?'); }); test("array type", function() { - equals(typeof([]), __, 'what is the type of an array?'); + equal(__, typeof([]), 'what is the type of an array?'); }); test("length", function() { var collection = ['a','b','c']; - equals(collection.length, __, 'what is the length of the collection array?'); + equal(__, collection.length, 'what is the length of the collection array?'); }); test("splice", function() { var daysOfWeek = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; var workingWeek = daysOfWeek.splice(__, __); - ok(workingWeek.equalTo([__]), 'what is the value of workingWeek?'); - ok(daysOfWeek.equalTo([__]), 'what is the value of daysOfWeek?'); + var weekend = daysOfWeek; + + deepEqual(workingWeek, ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], 'what is the value of workingWeek?'); + deepEqual(weekend, ['Saturday', 'Sunday'], 'what is the value of weekend?'); }); test("stack methods", function() { @@ -29,6 +30,16 @@ test("stack methods", function() { stack.push("first"); stack.push("second"); - equals(stack.pop(), __, 'what will be the first value popped off the stack?'); - equals(stack.pop(), __, 'what will be the second value popped off the stack?'); + equal(__, stack.pop(), 'what will be the first value popped off the stack?'); + equal(__, stack.pop(), 'what will be the second value popped off the stack?'); +}); + +test("queue methods", function() { + var queue = []; + queue.push("first"); + queue.push("second"); + queue.unshift("third"); + + equal(__, queue.shift(), 'what will be shifted out first?'); + equal(__, queue.shift(), 'what will be shifted out second?'); }); diff --git a/topics/about_asserts.js b/topics/about_asserts.js index f94dde46..baf7fc75 100644 --- a/topics/about_asserts.js +++ b/topics/about_asserts.js @@ -2,13 +2,13 @@ module("About Asserts (topics/about_asserts.js)"); test("ok", function() { - ok(__, 'what will satisfy the ok assertion?'); + ok(__ === true, 'what will satisfy the ok assertion?'); }); -test("not", function() { - not(__, 'what is a false value?'); +test("not ok", function() { + ok(__ === false, 'what is a false value?'); }); -test("equals", function() { - equals(1+1, __, 'what will satisfy the equals assertion?'); +test("equal", function() { + equal(__, 1 + 1, 'what will satisfy the equal assertion?'); }); diff --git a/topics/about_assignment.js b/topics/about_assignment.js index 3b5c9831..4532861e 100644 --- a/topics/about_assignment.js +++ b/topics/about_assignment.js @@ -3,10 +3,10 @@ module("About Assignment (topics/about_assignment.js)"); test("local variables", function() { var temp = __; - equals(1, temp, "Assign a value to the variable temp"); + equal(temp, 1, "Assign a value to the variable temp"); }); test("global variables", function() { - temp = 1; - equals(temp, window.__, 'global variables are assigned to the window object'); + temp = 1; // Not using var is an example. Always use var in practise. + equal(window.__, temp, 'global variables are assigned to the window object'); }); diff --git a/topics/about_control_structures.js b/topics/about_control_structures.js index 8d2007df..aca1623b 100644 --- a/topics/about_control_structures.js +++ b/topics/about_control_structures.js @@ -5,7 +5,7 @@ test("if", function() { if (2 > 0) { isPositive = true; } - equals(isPositive, __, 'what is the value of isPositive?'); + equal(__, isPositive, 'what is the value of isPositive?'); }); test("for", function() { @@ -13,7 +13,7 @@ test("for", function() { for (var i = 1; i <= 3; i++) { counter = counter + i; } - equals(counter, __, 'what is the value of counter?'); + equal(__, counter, 'what is the value of counter?'); }); test("for in", function() { @@ -24,18 +24,18 @@ test("for in", function() { }; var result = ""; // for in enumerates the property names of an object - for (property_name in person) { - result = result + property_name; - }; - equals(result, __, 'what is the value of result?'); + for (var property_name in person) { + result = result + property_name; + } + equal(__, result, 'what is the value of result?'); }); test("ternary operator", function() { var fruit = true ? "apple" : "orange"; - equals(fruit, __, 'what is the value of fruit?'); + equal(__, fruit, 'what is the value of fruit?'); fruit = false ? "apple" : "orange"; - equals(fruit, __, 'now what is the value of fruit?'); + equal(__, fruit, 'now what is the value of fruit?'); }); test("switch", function() { @@ -48,7 +48,7 @@ test("switch", function() { result = 2; break; } - equals(result, __, 'what is the value of result?'); + equal(__, result, 'what is the value of result?'); }); test("switch default case", function() { @@ -64,10 +64,10 @@ test("switch default case", function() { result = "Merry"; break; } - equals(result, __, 'what is the value of result?'); + equal(__, result, 'what is the value of result?'); }); test("null coalescing", function() { var result = null || "a value"; - equals(result, __, 'what is the value of result?'); + equal(__, result, 'what is the value of result?'); }); diff --git a/topics/about_equality.js b/topics/about_equality.js index 797a653e..fe3e3d21 100644 --- a/topics/about_equality.js +++ b/topics/about_equality.js @@ -2,11 +2,11 @@ module("About Equality (topics/about_equality.js)"); test("numeric equality", function() { - equals(3 + __, 7, 'hmmmm?'); + equal(3 + __, 7, ""); }); test("string equality", function() { - equals("3" + __, "37", "concatenate the strings"); + equal("3" + __, "37", "concatenate the strings"); }); test("equality without type coercion", function() { @@ -18,5 +18,6 @@ test("equality with type coercion", function() { }); test("string literals", function() { - equals("frankenstein", '__', "quote types are interchangable, but must match."); + equal(__, "frankenstein", "quote types are interchangable, but must match."); + equal(__, 'frankenstein', "quote types can use both single and double quotes."); }); diff --git a/topics/about_functions_and_closure.js b/topics/about_functions_and_closure.js index 04a22b91..7477278f 100644 --- a/topics/about_functions_and_closure.js +++ b/topics/about_functions_and_closure.js @@ -3,33 +3,33 @@ module("About Functions And Closure (topics/about_functions_and_closure.js)"); test("defining functions directly", function() { var result = "a"; function changeResult() { - // the ability to access a variables defined in the same scope as the function is known as 'closure' + // the ability to access variables defined in the same scope as the function is known as 'closure' result = "b"; - }; + }; changeResult(); - equals(result, __, 'what is the value of result?'); + equal(__, result, 'what is the value of result?'); }); test("assigning functions to variables", function() { var triple = function(input) { return input * 3; }; - equals(triple(4), __, 'what is triple 4?'); + equal(__, triple(4), 'what is triple 4?'); }); -test("self invoking functions", function() { +test("self invoking functions", function() { var publicValue = "shared"; // self invoking functions are used to provide scoping and to alias variables (function(pv) { var secretValue = "password"; - equals(pv, __, 'what is the value of pv?'); - equals(typeof(secretValue), "__", "is secretValue available in this context?"); - equals(typeof(publicValue), "__", "is publicValue available in this context?"); + equal(__, pv, 'what is the value of pv?'); + equal("__", typeof(secretValue), "is secretValue available in this context?"); + equal("__", typeof(publicValue), "is publicValue available in this context?"); })(publicValue); - equals(typeof(secretValue), "__", "is secretValue available in this context?"); - equals(typeof(publicValue), "__", "is publicValue available in this context?"); + equal("__", typeof(secretValue), "is secretValue available in this context?"); + equal("__", typeof(publicValue), "is publicValue available in this context?"); }); test("arguments array", function() { @@ -42,8 +42,8 @@ test("arguments array", function() { // __ }; - equals(add(1,2,3,4,5), 15, "add 1,2,3,4,5"); - equals(add(4,7,-2), 9, "add 4,7,-2"); + equal(15, add(1,2,3,4,5), "add 1,2,3,4,5"); + equal(9, add(4,7,-2), "add 4,7,-2"); }); test("using call to invoke function",function(){ @@ -52,12 +52,12 @@ test("using call to invoke function",function(){ }; //another way to invoke a function is to use the call function which allows - //you to set the callers "this" context. Call can take any number of arguments: + //you to set the caller's "this" context. Call can take any number of arguments: //the first one is always the context that this should be set to in the called - //function, and the arguments to be sent to the function,multiple arguments are separated by commas. + //function, and the arguments to be sent to the function, multiple arguments are separated by commas. var result = invokee.call("I am this!", "Where did it come from?"); - equals(result,__,"what will the value of invokee's this be?"); + equal(__, result, "what will the value of invokee's this be?"); }); test("using apply to invoke function",function(){ @@ -67,9 +67,9 @@ test("using apply to invoke function",function(){ //similar to the call function is the apply function. Apply only has two //arguments: the first is the context that this should be set to in the called - //function and and array of arguments to be passed into the called function. + //function and the second is the array of arguments to be passed into the called function. var result = invokee.apply("I am this!", ["I am arg1","I am arg2"]); - equals(result,__,"what will the value of invokee's this be?"); + equal(__, result, "what will the value of invokee's this be?"); }); diff --git a/topics/about_numbers.js b/topics/about_numbers.js index 672f3318..1319acd8 100644 --- a/topics/about_numbers.js +++ b/topics/about_numbers.js @@ -4,13 +4,13 @@ module("About Numbers (topics/about_numbers.js)"); test("types", function() { var typeOfIntegers = typeof(6); var typeOfFloats = typeof(3.14159); - equals(typeOfIntegers === typeOfFloats, __, 'are ints and floats the same type?'); - equals(typeOfIntegers, __, 'what is the javascript numeric type?'); - equals(1.0, __, 'what is a integer number equivalent to 1.0?'); + equal(__, typeOfIntegers === typeOfFloats, 'are ints and floats the same type?'); + equal(__, typeOfIntegers, 'what is the javascript numeric type?'); + equal(__, 1.0, 'what is a integer number equivalent to 1.0?'); }); test("NaN", function() { var resultOfFailedOperations = 7/'apple'; - equals(isNaN(resultOfFailedOperations), __, 'what will satisfy the equals assertion?'); - equals(resultOfFailedOperations == NaN, __, 'is NaN == NaN?'); + equal(__, isNaN(resultOfFailedOperations), 'what will satisfy the equals assertion?'); + equal(__, resultOfFailedOperations == NaN, 'is NaN == NaN?'); }); diff --git a/topics/about_objects.js b/topics/about_objects.js index 35185b32..24c03533 100644 --- a/topics/about_objects.js +++ b/topics/about_objects.js @@ -3,32 +3,32 @@ module("About Objects (topics/about_objects.js)"); test("object type", function() { var empty_object = {}; - equals(typeof(empty_object), __, 'what is the type of an object?'); + equal(__, typeof(empty_object), 'what is the type of an object?'); }); test("object literal notation", function() { var person = { __:__, __:__ - }; - equals(person.name, "Amory Blaine", 'what is the person\'s name?'); - equals(person.age, 102, 'what is the person\'s age?'); + }; + equal("Amory Blaine", person.name, "what is the person's name?"); + equal(102, person.age, "what is the person's age?"); }); test("dynamically adding properties", function() { var person = {}; person.__ = "Amory Blaine"; person.__ = 102; - equals(person.name, "Amory Blaine", 'what is the person\'s name?'); - equals(person.age, 102, 'what is the person\'s age?'); + equal("Amory Blaine", person.name, "what is the person's name?"); + equal(102, person.age, "what is the person's age?"); }); test("adding properties from strings", function() { var person = {}; person["__"] = "Amory Blaine"; person["__"] = 102; - equals(person.name, "Amory Blaine", 'what is the person\'s name?'); - equals(person.age, 102, 'what is the person\'s age?'); + equal("Amory Blaine", person.name, "what is the person's name?"); + equal(102, person.age, "what is the person's age?"); }); test("adding functions", function() { @@ -39,5 +39,5 @@ test("adding functions", function() { return __; // HINT: use the 'this' keyword to refer to the person object. } }; - equals(person.toString(), "I Amory Blaine am 102 years old.", 'what should the toString function be?'); + equal("I Amory Blaine am 102 years old.", person.toString(), "what should the toString function be?"); }); diff --git a/topics/about_operators.js b/topics/about_operators.js index 09df716b..9859900b 100644 --- a/topics/about_operators.js +++ b/topics/about_operators.js @@ -7,7 +7,7 @@ test("addition", function() { for (var i = 0; i <= 5; i++) { result = result + i; } - equals(result, __, "What is the value of result?"); + equal(__, result, "What is the value of result?"); }); test("assignment addition", function() { @@ -16,7 +16,7 @@ test("assignment addition", function() { //the code below is just like saying result = result + i; but is more concise result += i; } - equals(result, __, "What is the value of result?"); + equal(__, result, "What is the value of result?"); }); test("subtraction", function() { @@ -24,7 +24,7 @@ test("subtraction", function() { for (var i = 0; i <= 2; i++) { result = result - i; } - equals(result, __, "What is the value of result?"); + equal(__, result, "What is the value of result?"); }); test("assignment subtraction", function() { @@ -32,7 +32,7 @@ test("assignment subtraction", function() { for (var i = 0; i <= 2; i++) { result -= i; } - equals(result, __, "What is the value of result?"); + equal(__, result, "What is the value of result?"); }); //Assignment operators are available for multiplication and division as well @@ -43,5 +43,5 @@ test("modulus", function() { var x = 5; //again this is exactly the same as result = result % x result %= x; - equals(result, __, "What is the value of result?"); + equal(__, result, "What is the value of result?"); }); diff --git a/topics/about_prototypal_inheritance.js b/topics/about_prototypal_inheritance.js index 23b7386c..811c040e 100644 --- a/topics/about_prototypal_inheritance.js +++ b/topics/about_prototypal_inheritance.js @@ -16,7 +16,7 @@ Mammal.prototype = { test("defining a 'class'", function() { var eric = new Mammal("Eric"); - equals(eric.sayHi(), __, 'what will Eric say?'); + equal(__, eric.sayHi(), 'what will Eric say?'); }); // add another function to the Mammal 'type' that uses the sayHi function @@ -26,7 +26,7 @@ Mammal.prototype.favouriteSaying = function() { test("more functions", function() { var bobby = new Mammal("Bobby"); - equals(bobby.favouriteSaying(), __, "what is Bobby's favourite saying?"); + equal(__, bobby.favouriteSaying(), "what is Bobby's favourite saying?"); }); test("calling functions added to a prototype after an object was created", function() { @@ -36,7 +36,7 @@ test("calling functions added to a prototype after an object was created", funct }; // the following statement asks the paul object to call a function that was added // to the Mammal prototype after paul was constructed. - equals(paul.numberOfLettersInName(), __, "how long is Paul's name?"); + equal(__, paul.numberOfLettersInName(), "how long is Paul's name?"); }); // helper function for inheritance. @@ -56,6 +56,6 @@ extend(Bat, Mammal); test("Inheritance", function() { var lenny = new Bat("Lenny", "1.5m"); - equals(lenny.sayHi(), __, "what does Lenny say?"); - equals(lenny.wingspan, __, "what is Lenny's wingspan?"); + equal(__, lenny.sayHi(), "what does Lenny say?"); + equal(__, lenny.wingspan, "what is Lenny's wingspan?"); }); diff --git a/topics/about_prototype_chain.js b/topics/about_prototype_chain.js index cf9f11e1..46e3b4df 100644 --- a/topics/about_prototype_chain.js +++ b/topics/about_prototype_chain.js @@ -27,29 +27,29 @@ child.b = 2; * */ test("Is there an 'a' and 'b' own property on child?", function () { - equals(child.hasOwnProperty('a'), __, 'child.hasOwnProperty(\'a\')?'); - equals(child.hasOwnProperty('b'), __, 'child.hasOwnProperty(\'b\')?'); + equal(__, child.hasOwnProperty('a'), 'child.hasOwnProperty(\'a\')?'); + equal(__, child.hasOwnProperty('b'), 'child.hasOwnProperty(\'b\')?'); }); test("Is there an 'a' and 'b' property on child?", function () { - equals(child.a, __, 'what is \'a\' value?'); - equals(child.b, __, 'what is \'b\' value?'); + equal(__, child.a, 'what is \'a\' value?'); + equal(__, child.b, 'what is \'b\' value?'); }); test("If 'b' was removed, whats b value?", function () { delete child.b; - equals(child.b, __, 'what is \'b\' value now?'); + equal(__, child.b, 'what is \'b\' value now?'); }); test("Is there a 'c' own property on child?", function () { - equals(child.hasOwnProperty('c'), __, 'child.hasOwnProperty(\'c\')?'); + equal(__, child.hasOwnProperty('c'), 'child.hasOwnProperty(\'c\')?'); }); // Is there a 'c' own property on child? No, check its prototype // Is there a 'c' own property on child.[[Prototype]]? Yes, its value is... test("Is there a 'c' property on child?", function () { - equals(child.c, __, 'what is the value of child.c?'); + equal(__, child.c, 'what is the value of child.c?'); }); @@ -57,7 +57,7 @@ test("Is there a 'c' property on child?", function () { // Is there a 'd' own property on child.[[Prototype]]? No, check it prototype // child.[[Prototype]].[[Prototype]] is null, stop searching, no property found, return... test("Is there an 'd' property on child?", function () { - equals(child.d, __, 'what is the value of child.d?'); + equal(__, child.d, 'what is the value of child.d?'); }); diff --git a/topics/about_reflection.js b/topics/about_reflection.js index c75c70f9..63868648 100644 --- a/topics/about_reflection.js +++ b/topics/about_reflection.js @@ -1,27 +1,27 @@ module("About Reflection (topics/about_reflection.js)"); -var A = function() { - this.aprop = "A"; +function A() { + this.aprop = "A"; }; -var B = function() { +function B() { this.bprop = "B"; }; B.prototype = new A(); test("typeof", function() { - equals(typeof({}), __, 'what is the type of an empty object?'); - equals(typeof('apple'), __, 'what is the type of a string?'); - equals(typeof(-5), __, 'what is the type of -5?'); - equals(typeof(false), __, 'what is the type of false?'); + equal(__, typeof({}), 'what is the type of an empty object?'); + equal(__, typeof('apple'), 'what is the type of a string?'); + equal(__, typeof(-5), 'what is the type of -5?'); + equal(__, typeof(false), 'what is the type of false?'); }); test("property enumeration", function() { var keys = []; var values = []; var person = {name: 'Amory Blaine', age: 102, unemployed: true}; - for(propertyName in person) { + for(var propertyName in person) { keys.push(propertyName); values.push(person[propertyName]); } @@ -31,15 +31,16 @@ test("property enumeration", function() { test("hasOwnProperty", function() { var b = new B(); + var propertyName; var keys = []; for (propertyName in b) { keys.push(propertyName); } - equals(keys.length, __, 'how many elements are in the keys array?'); - deepEqual(keys, [__, __], 'what are the properties of the array?'); + equal(__, keys.length, 'how many elements are in the keys array?'); + deepEqual([__, __], keys, 'what are the properties of the array?'); - // hasOwnProperty returns true if the parameter is a property directly on the object, + // hasOwnProperty returns true if the parameter is a property directly on the object, // but not if it is a property accessible via the prototype chain. var ownKeys = []; for(propertyName in b) { @@ -47,21 +48,21 @@ test("hasOwnProperty", function() { ownKeys.push(propertyName); } } - equals(ownKeys.length, __, 'how many elements are in the ownKeys array?'); - deepEqual(ownKeys, [__], 'what are the own properties of the array?'); + equal(__, ownKeys.length, 'how many elements are in the ownKeys array?'); + deepEqual([__], ownKeys, 'what are the own properties of the array?'); }); test("constructor property", function () { var a = new A(); var b = new B(); - equals(typeof(a.constructor), __, "what is the type of a's constructor?"); - equals(a.constructor.name, __, "what is the name of a's constructor?"); - equals(b.constructor.name, __, "what is the name of b's constructor?"); + equal(__, typeof(a.constructor), "what is the type of a's constructor?"); + equal(__, a.constructor.name, "what is the name of a's constructor?"); + equal(__, b.constructor.name, "what is the name of b's constructor?"); }); test("eval", function() { // eval executes a string var result = ""; eval("result = 'apple' + ' ' + 'pie'"); - equals(result, __, 'what is the value of result?'); + equal(__, result, 'what is the value of result?'); }); diff --git a/topics/about_regular_expressions.js b/topics/about_regular_expressions.js index 8d409fca..b49bc723 100644 --- a/topics/about_regular_expressions.js +++ b/topics/about_regular_expressions.js @@ -9,7 +9,7 @@ test("exec", function() { test("test", function() { var containsSelect = /select/.test(" select * from users "); - equals(containsSelect, __, 'does the string provided contain "select"?'); + equal(__, containsSelect, 'does the string provided contain "select"?'); }); test("match", function() { @@ -19,13 +19,13 @@ test("match", function() { test("replace", function() { var pie = "apple pie".replace("apple", "strawberry"); - equals(pie, __, 'what is the value of pie?'); + equal(__, pie, 'what is the value of pie?'); pie = "what if 6 turned out to be 9?".replace(/\d/g, function(number) { // the second parameter can be a string or a function var map = {'6': 'six','9': 'nine'}; return map[number]; }); - equals(pie, __, 'what is the value of pie?'); + equal(__, pie, 'what is the value of pie?'); }); // THE END diff --git a/topics/about_scope.js b/topics/about_scope.js index 02e5f70d..efa802a0 100644 --- a/topics/about_scope.js +++ b/topics/about_scope.js @@ -3,7 +3,7 @@ module("About Scope (topics/about_scope.js)"); thisIsAGlobalVariable = 77; test("global variables", function() { - equals(thisIsAGlobalVariable, __, 'is thisIsAGlobalVariable defined in this scope?'); + equal(__, thisIsAGlobalVariable, 'is thisIsAGlobalVariable defined in this scope?'); }); test("variables declared inside of a function", function() { @@ -12,10 +12,10 @@ test("variables declared inside of a function", function() { // this is a self-invoking function. Notice that it calls itself at the end (). (function() { var innerVariable = "inner"; - equals(outerVariable, __, 'is outerVariable defined in this scope?'); - equals(innerVariable, __, 'is innerVariable defined in this scope?'); + equal(__, outerVariable, 'is outerVariable defined in this scope?'); + equal(__, innerVariable, 'is innerVariable defined in this scope?'); })(); - equals(outerVariable, __, 'is outerVariable defined in this scope?'); - equals(typeof(innerVariable), __, 'is innerVariable defined in this scope?'); + equal(__, outerVariable, 'is outerVariable defined in this scope?'); + equal(__, typeof(innerVariable), 'is innerVariable defined in this scope?'); }); diff --git a/topics/about_strings.js b/topics/about_strings.js index 68fa6894..18f9c68a 100644 --- a/topics/about_strings.js +++ b/topics/about_strings.js @@ -4,31 +4,31 @@ module("About Strings (topics/about_strings.js)"); test("delimiters", function() { var singleQuotedString = 'apple'; var doubleQuotedString = "apple"; - equals(singleQuotedString === doubleQuotedString, __, 'are the two strings equal?'); + equal(__, singleQuotedString === doubleQuotedString, 'are the two strings equal?'); }); test("concatenation", function() { var fruit = "apple"; var dish = "pie"; - equals(fruit + " " + dish, __, 'what is the value of fruit + " " + dish?'); + equal(__, fruit + " " + dish, 'what is the value of fruit + " " + dish?'); }); test("character Type", function() { var characterType = typeof("Amory".charAt(1)); // typeof will be explained in about reflection - equals(characterType, __, 'Javascript has no character type'); + equal(__, characterType, 'Javascript has no character type'); }); test("escape character", function() { var stringWithAnEscapedCharacter = "\u0041pple"; - equals(stringWithAnEscapedCharacter, __, 'what is the value of stringWithAnEscapedCharacter?'); + equal(__, stringWithAnEscapedCharacter, 'what is the value of stringWithAnEscapedCharacter?'); }); test("string.length", function() { var fruit = "apple"; - equals(fruit.length, __, 'what is the value of fruit.length?'); + equal(__, fruit.length, 'what is the value of fruit.length?'); }); test("slice", function() { var fruit = "apple pie"; - equals(fruit.slice(0,5), __, 'what is the value of fruit.slice(0,5)?'); + equal(__, fruit.slice(0,5), 'what is the value of fruit.slice(0,5)?'); }); diff --git a/topics/about_this.js b/topics/about_this.js index f1127556..85185f04 100644 --- a/topics/about_this.js +++ b/topics/about_this.js @@ -7,23 +7,23 @@ test("'this' inside a method", function () { return "Hello, my name is " + this.__; } } - equals(person.intro(), "Hello, my name is bob"); + equal(person.intro(), "Hello, my name is bob", "If an object has a method can you access properties inside it?"); }); test("'this' on unattached function", function () { var person = { - name: 'bob', + globalName: 'bob', intro: function () { - return "Hello, my name is " + this.name; + return "Hello, my name is " + this.globalName; } } var alias = person.intro; // if the function is not called as an object property 'this' is the global context - // (window in a browser) + // (window in a browser). This is an example. Please do not do this in practise. window.__ = 'Peter'; - equals("Hello, my name is Peter", alias()); + equal(alias(), "Hello, my name is Peter", "What does 'this' refer to when it is not part of an object?"); }); test("'this' set explicitly", function () { @@ -36,7 +36,7 @@ test("'this' set explicitly", function () { // calling a function with 'call' lets us assign 'this' explicitly var message = person.intro.call({__: "Frank"}); - equals(message, "Hello, my name is Frank"); + equal(message, "Hello, my name is Frank", "What does 'this' refer to when you use the 'call()' method?"); }); // extra credit: underscore.js has a 'bind' function http://documentcloud.github.com/underscore/#bind diff --git a/topics/about_truthyness.js b/topics/about_truthyness.js index 9c3f2319..9b524c14 100644 --- a/topics/about_truthyness.js +++ b/topics/about_truthyness.js @@ -3,20 +3,20 @@ module("About Truthyness (topics/about_truthyness.js)"); test("truthyness of positive numbers", function() { var oneIsTruthy = 1 ? true : false; - equals(oneIsTruthy, __, 'is one truthy?'); + equal(__, oneIsTruthy, 'is one truthy?'); }); test("truthyness of negative numbers", function() { var negativeOneIsTruthy = -1 ? true : false; - equals(negativeOneIsTruthy, __, 'is -1 truthy?'); + equal(__, negativeOneIsTruthy, 'is -1 truthy?'); }); test("truthyness of zero", function() { var zeroIsTruthy = 0 ? true : false; - equals(zeroIsTruthy, __, 'is 0 truthy?'); + equal(__, zeroIsTruthy, 'is 0 truthy?'); }); test("truthyness of null", function() { var nullIsTruthy = null ? true : false; - equals(nullIsTruthy, __, 'is null truthy?'); + equal(__, nullIsTruthy, 'is null truthy?'); });