├── LICENSE ├── tests ├── chains.html ├── tests.js └── ext │ ├── qunit.css │ └── qunit.js ├── chains.js └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | This Source Code Form is subject to the terms of the Mozilla Public 2 | License, v. 2.0. If a copy of the MPL was not distributed with this 3 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | -------------------------------------------------------------------------------- /tests/chains.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chains demonstration 5 | 6 | 7 | 8 | 9 | 10 | 11 |

Chains Unit Tests

12 |

13 |
14 |

15 |
    16 |
    17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/tests.js: -------------------------------------------------------------------------------- 1 | /*global test, equal, ok, Chain */ 2 | 3 | (function() { 4 | "use strict"; 5 | 6 | var WhackAMole = Chain({ 7 | molesRemaining: function() { 8 | return this.next() + " remaining"; 9 | }, 10 | 11 | moles: 1, 12 | whackAMole: function() { 13 | this.moles--; 14 | } 15 | }, 16 | 17 | { 18 | molesRemaining: function() { 19 | return this.moles + this.next(); 20 | }, 21 | 22 | // this does not overwrite moles declared in the first link 23 | moles: 2 24 | }, 25 | 26 | { 27 | molesRemaining: function() { 28 | return " mole" + (this.moles != 1 ? "s" : ""); 29 | }, 30 | 31 | addAMole: function() { 32 | this.moles++; 33 | }, 34 | 35 | moleNames: ['Larry', 'Curley', 'Moe'], 36 | moleDOB: { 37 | Larry: '6/6/12', 38 | Curly: '6/7/12', 39 | Moe: '6/8/12' 40 | } 41 | }); 42 | 43 | var WhackARodent = Chain({ 44 | mice: 1, 45 | miceRemaining: function() { 46 | return this.mice + " " + (this.mice === 1 ? "mouse" : "mice") + " remaining"; 47 | }, 48 | 49 | rodentsRemaining: function() { 50 | return this.molesRemaining() + " " + this.miceRemaining(); 51 | } 52 | }, WhackAMole); 53 | 54 | module("Chain"); 55 | 56 | test("basic usage", function() { 57 | var whackAMole = Chain.create(WhackAMole); 58 | equal(whackAMole.molesRemaining(), "1 mole remaining"); 59 | 60 | whackAMole.addAMole(); 61 | equal(whackAMole.molesRemaining(), "2 moles remaining"); 62 | 63 | whackAMole.whackAMole(); 64 | whackAMole.whackAMole(); 65 | equal(whackAMole.molesRemaining(), "0 moles remaining"); 66 | }); 67 | 68 | test("composite usage", function() { 69 | var whackARodent = Chain.create(WhackARodent); 70 | equal(whackARodent.molesRemaining(), "1 mole remaining"); 71 | equal(whackARodent.miceRemaining(), "1 mouse remaining"); 72 | 73 | equal(whackARodent.rodentsRemaining(), "1 mole remaining 1 mouse remaining"); 74 | 75 | // Check whether deep copy works correctly 76 | var whackAMole = Chain.create(WhackAMole); 77 | 78 | // If updating these affects the moleNames and moleDOB on whackARodent, 79 | // deepCopy is not working correctly. 80 | whackAMole.moleDOB.Curley = '6/9/12'; 81 | whackAMole.moleNames.pop(); 82 | delete whackAMole.moleNames.Moe; 83 | 84 | equal(whackARodent.moleNames.length, 3, "Array copied correctly"); 85 | equal(whackARodent.moleDOB.Moe, '6/8/12', "Object copied correctly"); 86 | }); 87 | 88 | }() ); 89 | 90 | -------------------------------------------------------------------------------- /chains.js: -------------------------------------------------------------------------------- 1 | (function(exports) { 2 | "use strict"; 3 | 4 | function bind(func, obj) { 5 | var args = [].slice.call(arguments, 2); 6 | 7 | return function() { 8 | var newArgs = [].slice.call(arguments); 9 | return func.apply(obj, args.concat(newArgs)); 10 | }; 11 | } 12 | 13 | function deepCopy(item) { 14 | var copy, 15 | type = Object.prototype.toString.call(item); 16 | 17 | if (type === "[object Array]") { 18 | copy = []; 19 | for (var i=0, len=item.length; i < len; ++i) { 20 | copy[i] = deepCopy(item[i]); 21 | } 22 | } 23 | else if (type === "[object Object]") { 24 | copy = {}; 25 | for (var k in item) { 26 | copy[k] = deepCopy(item[k]); 27 | } 28 | } 29 | else { 30 | copy = item; 31 | } 32 | 33 | return copy; 34 | } 35 | 36 | function addLinkToChain(chain, link) { 37 | for (var key in link) { 38 | var item = link[key], 39 | type = Object.prototype.toString.call(item); 40 | 41 | if (type === "[object Function]") { 42 | if (!(key in chain)) { 43 | // each function in the chain with the same name gets put into the 44 | // function list. When a chain instance function is called, each item 45 | // in the funcList is called in order. 46 | var funcList = [item]; 47 | chain[key] = bind(proxyFunc, chain, funcList, 0); 48 | chain[key].__funcList = funcList; 49 | } 50 | else if (chain[key].__funcList) { 51 | // add to existing function list. 52 | chain[key].__funcList.push(item); 53 | } 54 | else { 55 | throw new Error("cannot override a non-function member with a function"); 56 | } 57 | } 58 | else if (!(key in chain)) { 59 | // not a function, make a copy of the item. 60 | chain[key] = deepCopy(item); 61 | } 62 | } 63 | } 64 | 65 | function setupChain(proxy, links) { 66 | // Go through each link in the chain, set up instance variables and 67 | // functions on the proxy. For each named function, create an array 68 | // that keeps track of the ordering of all functions with the same name. 69 | // When called, the proxy function will call each function in the array in 70 | // order. 71 | for (var i = 0, link; link = links[i]; ++i) { 72 | if (link.__links) { 73 | // A composite link, expand out each link contained within and add it 74 | // to the chain. 75 | setupChain(proxy, link.__links); 76 | } 77 | else { 78 | // Normal link. Create a proxy function for each function found that is 79 | // not already part of the proxy. 80 | addLinkToChain(proxy, link); 81 | } 82 | } 83 | } 84 | 85 | function proxyFunc(funcList, index) { 86 | /*jshint validthis: true*/ 87 | var func = funcList[index], 88 | prevNext = this.next; 89 | 90 | if (index < funcList.length) { 91 | // this.next points to the proxy function, the proxy function will 92 | // call the next item in the list. 93 | this.next = bind(proxyFunc, this, funcList, index + 1); 94 | } 95 | else { 96 | // last item in the chain, there is no next function. 97 | delete this.next; 98 | } 99 | 100 | var retval = func.apply(this, [].slice.call(arguments, 2)); 101 | 102 | // If there was no next function, remove its declaration from the proxy. 103 | this.next = prevNext; 104 | if (!this.next) delete this.next; 105 | 106 | return retval; 107 | } 108 | 109 | function Chain() { 110 | var links = [].slice.call(arguments, 0); 111 | 112 | function chain() {} 113 | chain.prototype = {}; 114 | setupChain(chain.prototype, links); 115 | chain.__links = links; 116 | return chain; 117 | } 118 | 119 | // The creation function. Called like: Chain.create(constructor_to_use); 120 | Chain.create = function(chain) { 121 | if (!chain.__links) throw new Error("invalid chain"); 122 | return new chain(); 123 | }; 124 | 125 | exports.Chain = Chain; 126 | }(window)); 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chains 2 | 3 | ## Use Composition to Fake Multiple Inheritance in Javascript 4 | 5 | Last year I wrote SuperClass, A Complete Bastardization of the Javascript 6 | Inheritance Model. Chains is a similar experiment, but instead of single 7 | inheritance with a this.super function, the idea is to give Javascript 8 | multiple-inheritance like functionality through composition. 9 | 10 | Chains compose functionality out of one or more links. A link is part mixin, 11 | part class in an inheritance hierarchy. Each link provides a set of functions. 12 | Links can be composed into any number of chains similar to a mixin. If two or 13 | more links have a function of the same name, these functions can be called in 14 | sequence like in traditional inheritance. 15 | 16 | ## The Reasons/The Need 17 | 18 | Classical-like inheritance and mixins have several limitations in Javascript. 19 | Javascript does not natively support multiple inheritance, meaning a "Class" 20 | cannot have multiple roots. A sort of multiple inheritance can be faked using 21 | mixins, but mixins have no good way of dealing with name collisions - this is 22 | what inheritance is for. 23 | 24 | Below is a simple example showing the problem. 25 | 26 | ### Simple Example 27 | 28 | A Model should be composed of the functionality of two other Classes, 29 | EventEmitter and DataStore. Both EventEmitter and DataStore contain an 30 | initialization function called init. 31 | 32 | ``` 33 | // An EventEmitter emits events. 34 | var EventEmitter = function() {}; 35 | EventEmitter.prototype.init = function() { 36 | /* do some EventEmitter initialization */ 37 | }; 38 | EventEmitter.ptotype.emit = function() { 39 | /* emit an event */ 40 | }; 41 | 42 | // A DataStore stores data. 43 | var DataStore = function() {}; 44 | DataStore.prototype.init = function() { 45 | /* do some DataStore initialization */ 46 | }; 47 | DataStore.prototype.set = function() { 48 | /* set a value in the store */ 49 | }; 50 | 51 | ``` 52 | 53 | The Model should contain the functionality of both a DataStore and an 54 | EventEmitter. A Model should have three publicly available functions, init, 55 | emit and set. 56 | 57 | Because Javascript is such a flexible language, there are many ways of doing 58 | this. A Model could "inherit" from either DataStore or EventEmitter, a Model 59 | could create an instance of DataStore or EventEmitter and proxy calls, or 60 | a model could mixin both DataStore and EventEmitter and override init to call 61 | the init function of each of the mixins. 62 | 63 | The problem is, these are verbose. They are yuck. They take too much work. 64 | I want to be lazy. 65 | 66 | ### A More Ideal Solution 67 | ``` 68 | var Model = ComposeFunctionality({ 69 | init: function() { 70 | CallInitOfBothEventEmitterAndDataStore(); 71 | 72 | // Some more Model initialization. 73 | ... 74 | } 75 | }, EventEmitter, DataStore); 76 | ``` 77 | 78 | Chains is an experiment to try to give this. Instead of pure multiple 79 | inheritance ala C++, Chains compose functionality out of one or more links. 80 | Each successive link in the chain is called from top to bottom (or left to 81 | right). Links can be reused in multiple objects, mixed in any order and free of 82 | the restrictions of inheritance. When a link is ready to pass control to the 83 | next link it calls "this.next()". 84 | 85 | ## Usage 86 | 87 | ``` 88 | // An EventEmitter emits events. 89 | var EventEmitter = { 90 | init: function() { 91 | /* do some EventEmitter initialization */ 92 | this.next(); 93 | }, 94 | emit: function() { 95 | /* emit an event */ 96 | this.next(); 97 | } 98 | }; 99 | 100 | // A DataStore stores data. 101 | var DataStore = { 102 | init: function() { 103 | /* do some DataStore initialization */ 104 | this.next(); 105 | }, 106 | set: function() { 107 | /* set a value in the store */ 108 | this.next(); 109 | } 110 | }; 111 | 112 | // The ordering of links in the Chain is very important. If there are 113 | // name collisions, functions are called top to bottom. Compare this to 114 | // traditional inheritance where functions are called bottom to top. If a 115 | // non-function variable is defined a link, redefinitions of the variable 116 | // are ignored in subsequent links. 117 | var Model = Chain({ 118 | { 119 | init: function() { 120 | this.next(); 121 | 122 | // Some more Model initialization. 123 | ... 124 | } 125 | }, 126 | EventEmitter, 127 | DataStore 128 | }); 129 | 130 | // To create an instance of Model 131 | var model = Model.create(); 132 | ``` 133 | 134 | ## How it works 135 | The code to make this happen isn't pretty. When a Chain is instantiated, 136 | a proxy function is created for each function found in the chain. The proxy 137 | function takes care of all housekeeping chores so that a call to this.next is 138 | able to correctly pass control to the next link. 139 | 140 | ## License 141 | Mozilla MPL 2.0 142 | 143 | ## Author 144 | * Shane Tomlinson 145 | * @shane_tomlinson 146 | * shane@shanetomlinson.com 147 | * set117@yahoo.com 148 | * stomlinson@mozilla.com 149 | * http://shanetomlinson.com 150 | 151 | 152 | -------------------------------------------------------------------------------- /tests/ext/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-banner { 58 | height: 5px; 59 | } 60 | 61 | #qunit-testrunner-toolbar { 62 | padding: 0.5em 0 0.5em 2em; 63 | color: #5E740B; 64 | background-color: #eee; 65 | } 66 | 67 | #qunit-userAgent { 68 | padding: 0.5em 0 0.5em 2.5em; 69 | background-color: #2b81af; 70 | color: #fff; 71 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 72 | } 73 | 74 | 75 | /** Tests: Pass/Fail */ 76 | 77 | #qunit-tests { 78 | list-style-position: inside; 79 | } 80 | 81 | #qunit-tests li { 82 | padding: 0.4em 0.5em 0.4em 2.5em; 83 | border-bottom: 1px solid #fff; 84 | list-style-position: inside; 85 | } 86 | 87 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 88 | display: none; 89 | } 90 | 91 | #qunit-tests li strong { 92 | cursor: pointer; 93 | } 94 | 95 | #qunit-tests li a { 96 | padding: 0.5em; 97 | color: #c2ccd1; 98 | text-decoration: none; 99 | } 100 | #qunit-tests li a:hover, 101 | #qunit-tests li a:focus { 102 | color: #000; 103 | } 104 | 105 | #qunit-tests ol { 106 | margin-top: 0.5em; 107 | padding: 0.5em; 108 | 109 | background-color: #fff; 110 | 111 | border-radius: 15px; 112 | -moz-border-radius: 15px; 113 | -webkit-border-radius: 15px; 114 | 115 | box-shadow: inset 0px 2px 13px #999; 116 | -moz-box-shadow: inset 0px 2px 13px #999; 117 | -webkit-box-shadow: inset 0px 2px 13px #999; 118 | } 119 | 120 | #qunit-tests table { 121 | border-collapse: collapse; 122 | margin-top: .2em; 123 | } 124 | 125 | #qunit-tests th { 126 | text-align: right; 127 | vertical-align: top; 128 | padding: 0 .5em 0 0; 129 | } 130 | 131 | #qunit-tests td { 132 | vertical-align: top; 133 | } 134 | 135 | #qunit-tests pre { 136 | margin: 0; 137 | white-space: pre-wrap; 138 | word-wrap: break-word; 139 | } 140 | 141 | #qunit-tests del { 142 | background-color: #e0f2be; 143 | color: #374e0c; 144 | text-decoration: none; 145 | } 146 | 147 | #qunit-tests ins { 148 | background-color: #ffcaca; 149 | color: #500; 150 | text-decoration: none; 151 | } 152 | 153 | /*** Test Counts */ 154 | 155 | #qunit-tests b.counts { color: black; } 156 | #qunit-tests b.passed { color: #5E740B; } 157 | #qunit-tests b.failed { color: #710909; } 158 | 159 | #qunit-tests li li { 160 | margin: 0.5em; 161 | padding: 0.4em 0.5em 0.4em 0.5em; 162 | background-color: #fff; 163 | border-bottom: none; 164 | list-style-position: inside; 165 | } 166 | 167 | /*** Passing Styles */ 168 | 169 | #qunit-tests li li.pass { 170 | color: #5E740B; 171 | background-color: #fff; 172 | border-left: 26px solid #C6E746; 173 | } 174 | 175 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 176 | #qunit-tests .pass .test-name { color: #366097; } 177 | 178 | #qunit-tests .pass .test-actual, 179 | #qunit-tests .pass .test-expected { color: #999999; } 180 | 181 | #qunit-banner.qunit-pass { background-color: #C6E746; } 182 | 183 | /*** Failing Styles */ 184 | 185 | #qunit-tests li li.fail { 186 | color: #710909; 187 | background-color: #fff; 188 | border-left: 26px solid #EE5757; 189 | } 190 | 191 | #qunit-tests > li:last-child { 192 | border-radius: 0 0 15px 15px; 193 | -moz-border-radius: 0 0 15px 15px; 194 | -webkit-border-bottom-right-radius: 15px; 195 | -webkit-border-bottom-left-radius: 15px; 196 | } 197 | 198 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 199 | #qunit-tests .fail .test-name, 200 | #qunit-tests .fail .module-name { color: #000000; } 201 | 202 | #qunit-tests .fail .test-actual { color: #EE5757; } 203 | #qunit-tests .fail .test-expected { color: green; } 204 | 205 | #qunit-banner.qunit-fail { background-color: #EE5757; } 206 | 207 | 208 | /** Result */ 209 | 210 | #qunit-testresult { 211 | padding: 0.5em 0.5em 0.5em 2.5em; 212 | 213 | color: #2b81af; 214 | background-color: #D2E0E6; 215 | 216 | border-bottom: 1px solid white; 217 | } 218 | 219 | /** Fixture */ 220 | 221 | #qunit-fixture { 222 | position: absolute; 223 | top: -10000px; 224 | left: -10000px; 225 | } 226 | -------------------------------------------------------------------------------- /tests/ext/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var defined = { 14 | setTimeout: typeof window.setTimeout !== "undefined", 15 | sessionStorage: (function() { 16 | try { 17 | return !!sessionStorage.getItem; 18 | } catch(e){ 19 | return false; 20 | } 21 | })() 22 | }; 23 | 24 | var testId = 0; 25 | 26 | var Test = function(name, testName, expected, testEnvironmentArg, async, callback) { 27 | this.name = name; 28 | this.testName = testName; 29 | this.expected = expected; 30 | this.testEnvironmentArg = testEnvironmentArg; 31 | this.async = async; 32 | this.callback = callback; 33 | this.assertions = []; 34 | }; 35 | Test.prototype = { 36 | init: function() { 37 | var tests = id("qunit-tests"); 38 | if (tests) { 39 | var b = document.createElement("strong"); 40 | b.innerHTML = "Running " + this.name; 41 | var li = document.createElement("li"); 42 | li.appendChild( b ); 43 | li.className = "running"; 44 | li.id = this.id = "test-output" + testId++; 45 | tests.appendChild( li ); 46 | } 47 | }, 48 | setup: function() { 49 | if (this.module != config.previousModule) { 50 | if ( config.previousModule ) { 51 | QUnit.moduleDone( { 52 | name: config.previousModule, 53 | failed: config.moduleStats.bad, 54 | passed: config.moduleStats.all - config.moduleStats.bad, 55 | total: config.moduleStats.all 56 | } ); 57 | } 58 | config.previousModule = this.module; 59 | config.moduleStats = { all: 0, bad: 0 }; 60 | QUnit.moduleStart( { 61 | name: this.module 62 | } ); 63 | } 64 | 65 | config.current = this; 66 | this.testEnvironment = extend({ 67 | setup: function() {}, 68 | teardown: function() {} 69 | }, this.moduleTestEnvironment); 70 | if (this.testEnvironmentArg) { 71 | extend(this.testEnvironment, this.testEnvironmentArg); 72 | } 73 | 74 | QUnit.testStart( { 75 | name: this.testName 76 | } ); 77 | 78 | // allow utility functions to access the current test environment 79 | // TODO why?? 80 | QUnit.current_testEnvironment = this.testEnvironment; 81 | 82 | try { 83 | if ( !config.pollution ) { 84 | saveGlobal(); 85 | } 86 | 87 | this.testEnvironment.setup.call(this.testEnvironment); 88 | } catch(e) { 89 | QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message ); 90 | } 91 | }, 92 | run: function() { 93 | if ( this.async ) { 94 | QUnit.stop(); 95 | } 96 | 97 | if ( config.notrycatch ) { 98 | this.callback.call(this.testEnvironment); 99 | return; 100 | } 101 | try { 102 | this.callback.call(this.testEnvironment); 103 | } catch(e) { 104 | fail("Test " + this.testName + " died, exception and test follows", e, this.callback); 105 | QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) ); 106 | // else next test will carry the responsibility 107 | saveGlobal(); 108 | 109 | // Restart the tests if they're blocking 110 | if ( config.blocking ) { 111 | start(); 112 | } 113 | } 114 | }, 115 | teardown: function() { 116 | try { 117 | this.testEnvironment.teardown.call(this.testEnvironment); 118 | checkPollution(); 119 | } catch(e) { 120 | QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message ); 121 | } 122 | }, 123 | finish: function() { 124 | if ( this.expected && this.expected != this.assertions.length ) { 125 | QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 126 | } 127 | 128 | var good = 0, bad = 0, 129 | tests = id("qunit-tests"); 130 | 131 | config.stats.all += this.assertions.length; 132 | config.moduleStats.all += this.assertions.length; 133 | 134 | if ( tests ) { 135 | var ol = document.createElement("ol"); 136 | 137 | for ( var i = 0; i < this.assertions.length; i++ ) { 138 | var assertion = this.assertions[i]; 139 | 140 | var li = document.createElement("li"); 141 | li.className = assertion.result ? "pass" : "fail"; 142 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 143 | ol.appendChild( li ); 144 | 145 | if ( assertion.result ) { 146 | good++; 147 | } else { 148 | bad++; 149 | config.stats.bad++; 150 | config.moduleStats.bad++; 151 | } 152 | } 153 | 154 | // store result when possible 155 | if ( QUnit.config.reorder && defined.sessionStorage ) { 156 | if (bad) { 157 | sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad); 158 | } else { 159 | sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName); 160 | } 161 | } 162 | 163 | if (bad == 0) { 164 | ol.style.display = "none"; 165 | } 166 | 167 | var b = document.createElement("strong"); 168 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 169 | 170 | var a = document.createElement("a"); 171 | a.innerHTML = "Rerun"; 172 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 173 | 174 | addEvent(b, "click", function() { 175 | var next = b.nextSibling.nextSibling, 176 | display = next.style.display; 177 | next.style.display = display === "none" ? "block" : "none"; 178 | }); 179 | 180 | addEvent(b, "dblclick", function(e) { 181 | var target = e && e.target ? e.target : window.event.srcElement; 182 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 183 | target = target.parentNode; 184 | } 185 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 186 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 187 | } 188 | }); 189 | 190 | var li = id(this.id); 191 | li.className = bad ? "fail" : "pass"; 192 | li.removeChild( li.firstChild ); 193 | li.appendChild( b ); 194 | li.appendChild( a ); 195 | li.appendChild( ol ); 196 | 197 | } else { 198 | for ( var i = 0; i < this.assertions.length; i++ ) { 199 | if ( !this.assertions[i].result ) { 200 | bad++; 201 | config.stats.bad++; 202 | config.moduleStats.bad++; 203 | } 204 | } 205 | } 206 | 207 | try { 208 | QUnit.reset(); 209 | } catch(e) { 210 | fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); 211 | } 212 | 213 | QUnit.testDone( { 214 | name: this.testName, 215 | failed: bad, 216 | passed: this.assertions.length - bad, 217 | total: this.assertions.length 218 | } ); 219 | }, 220 | 221 | queue: function() { 222 | var test = this; 223 | synchronize(function() { 224 | test.init(); 225 | }); 226 | function run() { 227 | // each of these can by async 228 | synchronize(function() { 229 | test.setup(); 230 | }); 231 | synchronize(function() { 232 | test.run(); 233 | }); 234 | synchronize(function() { 235 | test.teardown(); 236 | }); 237 | synchronize(function() { 238 | test.finish(); 239 | }); 240 | } 241 | // defer when previous test run passed, if storage is available 242 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName); 243 | if (bad) { 244 | run(); 245 | } else { 246 | synchronize(run); 247 | }; 248 | } 249 | 250 | }; 251 | 252 | var QUnit = { 253 | 254 | // call on start of module test to prepend name to all tests 255 | module: function(name, testEnvironment) { 256 | config.currentModule = name; 257 | config.currentModuleTestEnviroment = testEnvironment; 258 | }, 259 | 260 | asyncTest: function(testName, expected, callback) { 261 | if ( arguments.length === 2 ) { 262 | callback = expected; 263 | expected = 0; 264 | } 265 | 266 | QUnit.test(testName, expected, callback, true); 267 | }, 268 | 269 | test: function(testName, expected, callback, async) { 270 | var name = '' + testName + '', testEnvironmentArg; 271 | 272 | if ( arguments.length === 2 ) { 273 | callback = expected; 274 | expected = null; 275 | } 276 | // is 2nd argument a testEnvironment? 277 | if ( expected && typeof expected === 'object') { 278 | testEnvironmentArg = expected; 279 | expected = null; 280 | } 281 | 282 | if ( config.currentModule ) { 283 | name = '' + config.currentModule + ": " + name; 284 | } 285 | 286 | if ( !validTest(config.currentModule + ": " + testName) ) { 287 | return; 288 | } 289 | 290 | var test = new Test(name, testName, expected, testEnvironmentArg, async, callback); 291 | test.module = config.currentModule; 292 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 293 | test.queue(); 294 | }, 295 | 296 | /** 297 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 298 | */ 299 | expect: function(asserts) { 300 | config.current.expected = asserts; 301 | }, 302 | 303 | /** 304 | * Asserts true. 305 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 306 | */ 307 | ok: function(a, msg) { 308 | a = !!a; 309 | var details = { 310 | result: a, 311 | message: msg 312 | }; 313 | msg = escapeHtml(msg); 314 | QUnit.log(details); 315 | config.current.assertions.push({ 316 | result: a, 317 | message: msg 318 | }); 319 | }, 320 | 321 | /** 322 | * Checks that the first two arguments are equal, with an optional message. 323 | * Prints out both actual and expected values. 324 | * 325 | * Prefered to ok( actual == expected, message ) 326 | * 327 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 328 | * 329 | * @param Object actual 330 | * @param Object expected 331 | * @param String message (optional) 332 | */ 333 | equal: function(actual, expected, message) { 334 | QUnit.push(expected == actual, actual, expected, message); 335 | }, 336 | 337 | notEqual: function(actual, expected, message) { 338 | QUnit.push(expected != actual, actual, expected, message); 339 | }, 340 | 341 | deepEqual: function(actual, expected, message) { 342 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 343 | }, 344 | 345 | notDeepEqual: function(actual, expected, message) { 346 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 347 | }, 348 | 349 | strictEqual: function(actual, expected, message) { 350 | QUnit.push(expected === actual, actual, expected, message); 351 | }, 352 | 353 | notStrictEqual: function(actual, expected, message) { 354 | QUnit.push(expected !== actual, actual, expected, message); 355 | }, 356 | 357 | raises: function(block, expected, message) { 358 | var actual, ok = false; 359 | 360 | if (typeof expected === 'string') { 361 | message = expected; 362 | expected = null; 363 | } 364 | 365 | try { 366 | block(); 367 | } catch (e) { 368 | actual = e; 369 | } 370 | 371 | if (actual) { 372 | // we don't want to validate thrown error 373 | if (!expected) { 374 | ok = true; 375 | // expected is a regexp 376 | } else if (QUnit.objectType(expected) === "regexp") { 377 | ok = expected.test(actual); 378 | // expected is a constructor 379 | } else if (actual instanceof expected) { 380 | ok = true; 381 | // expected is a validation function which returns true is validation passed 382 | } else if (expected.call({}, actual) === true) { 383 | ok = true; 384 | } 385 | } 386 | 387 | QUnit.ok(ok, message); 388 | }, 389 | 390 | start: function() { 391 | config.semaphore--; 392 | if (config.semaphore > 0) { 393 | // don't start until equal number of stop-calls 394 | return; 395 | } 396 | if (config.semaphore < 0) { 397 | // ignore if start is called more often then stop 398 | config.semaphore = 0; 399 | } 400 | // A slight delay, to avoid any current callbacks 401 | if ( defined.setTimeout ) { 402 | window.setTimeout(function() { 403 | if ( config.timeout ) { 404 | clearTimeout(config.timeout); 405 | } 406 | 407 | config.blocking = false; 408 | process(); 409 | }, 13); 410 | } else { 411 | config.blocking = false; 412 | process(); 413 | } 414 | }, 415 | 416 | stop: function(timeout) { 417 | config.semaphore++; 418 | config.blocking = true; 419 | 420 | if ( timeout && defined.setTimeout ) { 421 | clearTimeout(config.timeout); 422 | config.timeout = window.setTimeout(function() { 423 | QUnit.ok( false, "Test timed out" ); 424 | QUnit.start(); 425 | }, timeout); 426 | } 427 | } 428 | }; 429 | 430 | // Backwards compatibility, deprecated 431 | QUnit.equals = QUnit.equal; 432 | QUnit.same = QUnit.deepEqual; 433 | 434 | // Maintain internal state 435 | var config = { 436 | // The queue of tests to run 437 | queue: [], 438 | 439 | // block until document ready 440 | blocking: true, 441 | 442 | // by default, run previously failed tests first 443 | // very useful in combination with "Hide passed tests" checked 444 | reorder: true, 445 | 446 | noglobals: false, 447 | notrycatch: false 448 | }; 449 | 450 | // Load paramaters 451 | (function() { 452 | var location = window.location || { search: "", protocol: "file:" }, 453 | params = location.search.slice( 1 ).split( "&" ), 454 | length = params.length, 455 | urlParams = {}, 456 | current; 457 | 458 | if ( params[ 0 ] ) { 459 | for ( var i = 0; i < length; i++ ) { 460 | current = params[ i ].split( "=" ); 461 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 462 | // allow just a key to turn on a flag, e.g., test.html?noglobals 463 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 464 | urlParams[ current[ 0 ] ] = current[ 1 ]; 465 | if ( current[ 0 ] in config ) { 466 | config[ current[ 0 ] ] = current[ 1 ]; 467 | } 468 | } 469 | } 470 | 471 | QUnit.urlParams = urlParams; 472 | config.filter = urlParams.filter; 473 | 474 | // Figure out if we're running the tests from a server or not 475 | QUnit.isLocal = !!(location.protocol === 'file:'); 476 | })(); 477 | 478 | // Expose the API as global variables, unless an 'exports' 479 | // object exists, in that case we assume we're in CommonJS 480 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 481 | extend(window, QUnit); 482 | window.QUnit = QUnit; 483 | } else { 484 | extend(exports, QUnit); 485 | exports.QUnit = QUnit; 486 | } 487 | 488 | // define these after exposing globals to keep them in these QUnit namespace only 489 | extend(QUnit, { 490 | config: config, 491 | 492 | // Initialize the configuration options 493 | init: function() { 494 | extend(config, { 495 | stats: { all: 0, bad: 0 }, 496 | moduleStats: { all: 0, bad: 0 }, 497 | started: +new Date, 498 | updateRate: 1000, 499 | blocking: false, 500 | autostart: true, 501 | autorun: false, 502 | filter: "", 503 | queue: [], 504 | semaphore: 0 505 | }); 506 | 507 | var tests = id( "qunit-tests" ), 508 | banner = id( "qunit-banner" ), 509 | result = id( "qunit-testresult" ); 510 | 511 | if ( tests ) { 512 | tests.innerHTML = ""; 513 | } 514 | 515 | if ( banner ) { 516 | banner.className = ""; 517 | } 518 | 519 | if ( result ) { 520 | result.parentNode.removeChild( result ); 521 | } 522 | 523 | if ( tests ) { 524 | result = document.createElement( "p" ); 525 | result.id = "qunit-testresult"; 526 | result.className = "result"; 527 | tests.parentNode.insertBefore( result, tests ); 528 | result.innerHTML = 'Running...
     '; 529 | } 530 | }, 531 | 532 | /** 533 | * Resets the test setup. Useful for tests that modify the DOM. 534 | * 535 | * If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 536 | */ 537 | reset: function() { 538 | if ( window.jQuery ) { 539 | jQuery( "#qunit-fixture" ).html( config.fixture ); 540 | } else { 541 | var main = id( 'qunit-fixture' ); 542 | if ( main ) { 543 | main.innerHTML = config.fixture; 544 | } 545 | } 546 | }, 547 | 548 | /** 549 | * Trigger an event on an element. 550 | * 551 | * @example triggerEvent( document.body, "click" ); 552 | * 553 | * @param DOMElement elem 554 | * @param String type 555 | */ 556 | triggerEvent: function( elem, type, event ) { 557 | if ( document.createEvent ) { 558 | event = document.createEvent("MouseEvents"); 559 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 560 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 561 | elem.dispatchEvent( event ); 562 | 563 | } else if ( elem.fireEvent ) { 564 | elem.fireEvent("on"+type); 565 | } 566 | }, 567 | 568 | // Safe object type checking 569 | is: function( type, obj ) { 570 | return QUnit.objectType( obj ) == type; 571 | }, 572 | 573 | objectType: function( obj ) { 574 | if (typeof obj === "undefined") { 575 | return "undefined"; 576 | 577 | // consider: typeof null === object 578 | } 579 | if (obj === null) { 580 | return "null"; 581 | } 582 | 583 | var type = Object.prototype.toString.call( obj ) 584 | .match(/^\[object\s(.*)\]$/)[1] || ''; 585 | 586 | switch (type) { 587 | case 'Number': 588 | if (isNaN(obj)) { 589 | return "nan"; 590 | } else { 591 | return "number"; 592 | } 593 | case 'String': 594 | case 'Boolean': 595 | case 'Array': 596 | case 'Date': 597 | case 'RegExp': 598 | case 'Function': 599 | return type.toLowerCase(); 600 | } 601 | if (typeof obj === "object") { 602 | return "object"; 603 | } 604 | return undefined; 605 | }, 606 | 607 | push: function(result, actual, expected, message) { 608 | var details = { 609 | result: result, 610 | message: message, 611 | actual: actual, 612 | expected: expected 613 | }; 614 | 615 | message = escapeHtml(message) || (result ? "okay" : "failed"); 616 | message = '' + message + ""; 617 | expected = escapeHtml(QUnit.jsDump.parse(expected)); 618 | actual = escapeHtml(QUnit.jsDump.parse(actual)); 619 | var output = message + ''; 620 | if (actual != expected) { 621 | output += ''; 622 | output += ''; 623 | } 624 | if (!result) { 625 | var source = sourceFromStacktrace(); 626 | if (source) { 627 | details.source = source; 628 | output += ''; 629 | } 630 | } 631 | output += "
    Expected:
    ' + expected + '
    Result:
    ' + actual + '
    Diff:
    ' + QUnit.diff(expected, actual) +'
    Source:
    ' + source +'
    "; 632 | 633 | QUnit.log(details); 634 | 635 | config.current.assertions.push({ 636 | result: !!result, 637 | message: output 638 | }); 639 | }, 640 | 641 | url: function( params ) { 642 | params = extend( extend( {}, QUnit.urlParams ), params ); 643 | var querystring = "?", 644 | key; 645 | for ( key in params ) { 646 | querystring += encodeURIComponent( key ) + "=" + 647 | encodeURIComponent( params[ key ] ) + "&"; 648 | } 649 | return window.location.pathname + querystring.slice( 0, -1 ); 650 | }, 651 | 652 | // Logging callbacks; all receive a single argument with the listed properties 653 | // run test/logs.html for any related changes 654 | begin: function() {}, 655 | // done: { failed, passed, total, runtime } 656 | done: function() {}, 657 | // log: { result, actual, expected, message } 658 | log: function() {}, 659 | // testStart: { name } 660 | testStart: function() {}, 661 | // testDone: { name, failed, passed, total } 662 | testDone: function() {}, 663 | // moduleStart: { name } 664 | moduleStart: function() {}, 665 | // moduleDone: { name, failed, passed, total } 666 | moduleDone: function() {} 667 | }); 668 | 669 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 670 | config.autorun = true; 671 | } 672 | 673 | addEvent(window, "load", function() { 674 | QUnit.begin({}); 675 | 676 | // Initialize the config, saving the execution queue 677 | var oldconfig = extend({}, config); 678 | QUnit.init(); 679 | extend(config, oldconfig); 680 | 681 | config.blocking = false; 682 | 683 | var userAgent = id("qunit-userAgent"); 684 | if ( userAgent ) { 685 | userAgent.innerHTML = navigator.userAgent; 686 | } 687 | var banner = id("qunit-header"); 688 | if ( banner ) { 689 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + 690 | '' + 691 | ''; 692 | addEvent( banner, "change", function( event ) { 693 | var params = {}; 694 | params[ event.target.name ] = event.target.checked ? true : undefined; 695 | window.location = QUnit.url( params ); 696 | }); 697 | } 698 | 699 | var toolbar = id("qunit-testrunner-toolbar"); 700 | if ( toolbar ) { 701 | var filter = document.createElement("input"); 702 | filter.type = "checkbox"; 703 | filter.id = "qunit-filter-pass"; 704 | addEvent( filter, "click", function() { 705 | var ol = document.getElementById("qunit-tests"); 706 | if ( filter.checked ) { 707 | ol.className = ol.className + " hidepass"; 708 | } else { 709 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 710 | ol.className = tmp.replace(/ hidepass /, " "); 711 | } 712 | if ( defined.sessionStorage ) { 713 | if (filter.checked) { 714 | sessionStorage.setItem("qunit-filter-passed-tests", "true"); 715 | } else { 716 | sessionStorage.removeItem("qunit-filter-passed-tests"); 717 | } 718 | } 719 | }); 720 | if ( defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 721 | filter.checked = true; 722 | var ol = document.getElementById("qunit-tests"); 723 | ol.className = ol.className + " hidepass"; 724 | } 725 | toolbar.appendChild( filter ); 726 | 727 | var label = document.createElement("label"); 728 | label.setAttribute("for", "qunit-filter-pass"); 729 | label.innerHTML = "Hide passed tests"; 730 | toolbar.appendChild( label ); 731 | } 732 | 733 | var main = id('qunit-fixture'); 734 | if ( main ) { 735 | config.fixture = main.innerHTML; 736 | } 737 | 738 | if (config.autostart) { 739 | QUnit.start(); 740 | } 741 | }); 742 | 743 | function done() { 744 | config.autorun = true; 745 | 746 | // Log the last module results 747 | if ( config.currentModule ) { 748 | QUnit.moduleDone( { 749 | name: config.currentModule, 750 | failed: config.moduleStats.bad, 751 | passed: config.moduleStats.all - config.moduleStats.bad, 752 | total: config.moduleStats.all 753 | } ); 754 | } 755 | 756 | var banner = id("qunit-banner"), 757 | tests = id("qunit-tests"), 758 | runtime = +new Date - config.started, 759 | passed = config.stats.all - config.stats.bad, 760 | html = [ 761 | 'Tests completed in ', 762 | runtime, 763 | ' milliseconds.
    ', 764 | '', 765 | passed, 766 | ' tests of ', 767 | config.stats.all, 768 | ' passed, ', 769 | config.stats.bad, 770 | ' failed.' 771 | ].join(''); 772 | 773 | if ( banner ) { 774 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 775 | } 776 | 777 | if ( tests ) { 778 | id( "qunit-testresult" ).innerHTML = html; 779 | } 780 | 781 | if ( typeof document !== "undefined" && document.title ) { 782 | // show ✖ for good, ✔ for bad suite result in title 783 | // use escape sequences in case file gets loaded with non-utf-8-charset 784 | document.title = (config.stats.bad ? "\u2716" : "\u2714") + " " + document.title; 785 | } 786 | 787 | QUnit.done( { 788 | failed: config.stats.bad, 789 | passed: passed, 790 | total: config.stats.all, 791 | runtime: runtime 792 | } ); 793 | } 794 | 795 | function validTest( name ) { 796 | var filter = config.filter, 797 | run = false; 798 | 799 | if ( !filter ) { 800 | return true; 801 | } 802 | 803 | var not = filter.charAt( 0 ) === "!"; 804 | if ( not ) { 805 | filter = filter.slice( 1 ); 806 | } 807 | 808 | if ( name.indexOf( filter ) !== -1 ) { 809 | return !not; 810 | } 811 | 812 | if ( not ) { 813 | run = true; 814 | } 815 | 816 | return run; 817 | } 818 | 819 | // so far supports only Firefox, Chrome and Opera (buggy) 820 | // could be extended in the future to use something like https://github.com/csnover/TraceKit 821 | function sourceFromStacktrace() { 822 | try { 823 | throw new Error(); 824 | } catch ( e ) { 825 | if (e.stacktrace) { 826 | // Opera 827 | return e.stacktrace.split("\n")[6]; 828 | } else if (e.stack) { 829 | // Firefox, Chrome 830 | return e.stack.split("\n")[4]; 831 | } 832 | } 833 | } 834 | 835 | function escapeHtml(s) { 836 | if (!s) { 837 | return ""; 838 | } 839 | s = s + ""; 840 | return s.replace(/[\&"<>\\]/g, function(s) { 841 | switch(s) { 842 | case "&": return "&"; 843 | case "\\": return "\\\\"; 844 | case '"': return '\"'; 845 | case "<": return "<"; 846 | case ">": return ">"; 847 | default: return s; 848 | } 849 | }); 850 | } 851 | 852 | function synchronize( callback ) { 853 | config.queue.push( callback ); 854 | 855 | if ( config.autorun && !config.blocking ) { 856 | process(); 857 | } 858 | } 859 | 860 | function process() { 861 | var start = (new Date()).getTime(); 862 | 863 | while ( config.queue.length && !config.blocking ) { 864 | if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { 865 | config.queue.shift()(); 866 | } else { 867 | window.setTimeout( process, 13 ); 868 | break; 869 | } 870 | } 871 | if (!config.blocking && !config.queue.length) { 872 | done(); 873 | } 874 | } 875 | 876 | function saveGlobal() { 877 | config.pollution = []; 878 | 879 | if ( config.noglobals ) { 880 | for ( var key in window ) { 881 | config.pollution.push( key ); 882 | } 883 | } 884 | } 885 | 886 | function checkPollution( name ) { 887 | var old = config.pollution; 888 | saveGlobal(); 889 | 890 | var newGlobals = diff( config.pollution, old ); 891 | if ( newGlobals.length > 0 ) { 892 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 893 | } 894 | 895 | var deletedGlobals = diff( old, config.pollution ); 896 | if ( deletedGlobals.length > 0 ) { 897 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 898 | } 899 | } 900 | 901 | // returns a new Array with the elements that are in a but not in b 902 | function diff( a, b ) { 903 | var result = a.slice(); 904 | for ( var i = 0; i < result.length; i++ ) { 905 | for ( var j = 0; j < b.length; j++ ) { 906 | if ( result[i] === b[j] ) { 907 | result.splice(i, 1); 908 | i--; 909 | break; 910 | } 911 | } 912 | } 913 | return result; 914 | } 915 | 916 | function fail(message, exception, callback) { 917 | if ( typeof console !== "undefined" && console.error && console.warn ) { 918 | console.error(message); 919 | console.error(exception); 920 | console.warn(callback.toString()); 921 | 922 | } else if ( window.opera && opera.postError ) { 923 | opera.postError(message, exception, callback.toString); 924 | } 925 | } 926 | 927 | function extend(a, b) { 928 | for ( var prop in b ) { 929 | if ( b[prop] === undefined ) { 930 | delete a[prop]; 931 | } else { 932 | a[prop] = b[prop]; 933 | } 934 | } 935 | 936 | return a; 937 | } 938 | 939 | function addEvent(elem, type, fn) { 940 | if ( elem.addEventListener ) { 941 | elem.addEventListener( type, fn, false ); 942 | } else if ( elem.attachEvent ) { 943 | elem.attachEvent( "on" + type, fn ); 944 | } else { 945 | fn(); 946 | } 947 | } 948 | 949 | function id(name) { 950 | return !!(typeof document !== "undefined" && document && document.getElementById) && 951 | document.getElementById( name ); 952 | } 953 | 954 | // Test for equality any JavaScript type. 955 | // Discussions and reference: http://philrathe.com/articles/equiv 956 | // Test suites: http://philrathe.com/tests/equiv 957 | // Author: Philippe Rathé 958 | QUnit.equiv = function () { 959 | 960 | var innerEquiv; // the real equiv function 961 | var callers = []; // stack to decide between skip/abort functions 962 | var parents = []; // stack to avoiding loops from circular referencing 963 | 964 | // Call the o related callback with the given arguments. 965 | function bindCallbacks(o, callbacks, args) { 966 | var prop = QUnit.objectType(o); 967 | if (prop) { 968 | if (QUnit.objectType(callbacks[prop]) === "function") { 969 | return callbacks[prop].apply(callbacks, args); 970 | } else { 971 | return callbacks[prop]; // or undefined 972 | } 973 | } 974 | } 975 | 976 | var callbacks = function () { 977 | 978 | // for string, boolean, number and null 979 | function useStrictEquality(b, a) { 980 | if (b instanceof a.constructor || a instanceof b.constructor) { 981 | // to catch short annotaion VS 'new' annotation of a declaration 982 | // e.g. var i = 1; 983 | // var j = new Number(1); 984 | return a == b; 985 | } else { 986 | return a === b; 987 | } 988 | } 989 | 990 | return { 991 | "string": useStrictEquality, 992 | "boolean": useStrictEquality, 993 | "number": useStrictEquality, 994 | "null": useStrictEquality, 995 | "undefined": useStrictEquality, 996 | 997 | "nan": function (b) { 998 | return isNaN(b); 999 | }, 1000 | 1001 | "date": function (b, a) { 1002 | return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); 1003 | }, 1004 | 1005 | "regexp": function (b, a) { 1006 | return QUnit.objectType(b) === "regexp" && 1007 | a.source === b.source && // the regex itself 1008 | a.global === b.global && // and its modifers (gmi) ... 1009 | a.ignoreCase === b.ignoreCase && 1010 | a.multiline === b.multiline; 1011 | }, 1012 | 1013 | // - skip when the property is a method of an instance (OOP) 1014 | // - abort otherwise, 1015 | // initial === would have catch identical references anyway 1016 | "function": function () { 1017 | var caller = callers[callers.length - 1]; 1018 | return caller !== Object && 1019 | typeof caller !== "undefined"; 1020 | }, 1021 | 1022 | "array": function (b, a) { 1023 | var i, j, loop; 1024 | var len; 1025 | 1026 | // b could be an object literal here 1027 | if ( ! (QUnit.objectType(b) === "array")) { 1028 | return false; 1029 | } 1030 | 1031 | len = a.length; 1032 | if (len !== b.length) { // safe and faster 1033 | return false; 1034 | } 1035 | 1036 | //track reference to avoid circular references 1037 | parents.push(a); 1038 | for (i = 0; i < len; i++) { 1039 | loop = false; 1040 | for(j=0;j= 0) { 1185 | type = "array"; 1186 | } else { 1187 | type = typeof obj; 1188 | } 1189 | return type; 1190 | }, 1191 | separator:function() { 1192 | return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; 1193 | }, 1194 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1195 | if ( !this.multiline ) 1196 | return ''; 1197 | var chr = this.indentChar; 1198 | if ( this.HTML ) 1199 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1200 | return Array( this._depth_ + (extra||0) ).join(chr); 1201 | }, 1202 | up:function( a ) { 1203 | this._depth_ += a || 1; 1204 | }, 1205 | down:function( a ) { 1206 | this._depth_ -= a || 1; 1207 | }, 1208 | setParser:function( name, parser ) { 1209 | this.parsers[name] = parser; 1210 | }, 1211 | // The next 3 are exposed so you can use them 1212 | quote:quote, 1213 | literal:literal, 1214 | join:join, 1215 | // 1216 | _depth_: 1, 1217 | // This is the list of parsers, to modify them, use jsDump.setParser 1218 | parsers:{ 1219 | window: '[Window]', 1220 | document: '[Document]', 1221 | error:'[ERROR]', //when no parser is found, shouldn't happen 1222 | unknown: '[Unknown]', 1223 | 'null':'null', 1224 | 'undefined':'undefined', 1225 | 'function':function( fn ) { 1226 | var ret = 'function', 1227 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1228 | if ( name ) 1229 | ret += ' ' + name; 1230 | ret += '('; 1231 | 1232 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1233 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1234 | }, 1235 | array: array, 1236 | nodelist: array, 1237 | arguments: array, 1238 | object:function( map ) { 1239 | var ret = [ ]; 1240 | QUnit.jsDump.up(); 1241 | for ( var key in map ) 1242 | ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(map[key]) ); 1243 | QUnit.jsDump.down(); 1244 | return join( '{', ret, '}' ); 1245 | }, 1246 | node:function( node ) { 1247 | var open = QUnit.jsDump.HTML ? '<' : '<', 1248 | close = QUnit.jsDump.HTML ? '>' : '>'; 1249 | 1250 | var tag = node.nodeName.toLowerCase(), 1251 | ret = open + tag; 1252 | 1253 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1254 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1255 | if ( val ) 1256 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1257 | } 1258 | return ret + close + open + '/' + tag + close; 1259 | }, 1260 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1261 | var l = fn.length; 1262 | if ( !l ) return ''; 1263 | 1264 | var args = Array(l); 1265 | while ( l-- ) 1266 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1267 | return ' ' + args.join(', ') + ' '; 1268 | }, 1269 | key:quote, //object calls it internally, the key part of an item in a map 1270 | functionCode:'[code]', //function calls it internally, it's the content of the function 1271 | attribute:quote, //node calls it internally, it's an html attribute value 1272 | string:quote, 1273 | date:quote, 1274 | regexp:literal, //regex 1275 | number:literal, 1276 | 'boolean':literal 1277 | }, 1278 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1279 | id:'id', 1280 | name:'name', 1281 | 'class':'className' 1282 | }, 1283 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1284 | indentChar:' ',//indentation unit 1285 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1286 | }; 1287 | 1288 | return jsDump; 1289 | })(); 1290 | 1291 | // from Sizzle.js 1292 | function getText( elems ) { 1293 | var ret = "", elem; 1294 | 1295 | for ( var i = 0; elems[i]; i++ ) { 1296 | elem = elems[i]; 1297 | 1298 | // Get the text from text nodes and CDATA nodes 1299 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1300 | ret += elem.nodeValue; 1301 | 1302 | // Traverse everything else, except comment nodes 1303 | } else if ( elem.nodeType !== 8 ) { 1304 | ret += getText( elem.childNodes ); 1305 | } 1306 | } 1307 | 1308 | return ret; 1309 | }; 1310 | 1311 | /* 1312 | * Javascript Diff Algorithm 1313 | * By John Resig (http://ejohn.org/) 1314 | * Modified by Chu Alan "sprite" 1315 | * 1316 | * Released under the MIT license. 1317 | * 1318 | * More Info: 1319 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1320 | * 1321 | * Usage: QUnit.diff(expected, actual) 1322 | * 1323 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1324 | */ 1325 | QUnit.diff = (function() { 1326 | function diff(o, n){ 1327 | var ns = new Object(); 1328 | var os = new Object(); 1329 | 1330 | for (var i = 0; i < n.length; i++) { 1331 | if (ns[n[i]] == null) 1332 | ns[n[i]] = { 1333 | rows: new Array(), 1334 | o: null 1335 | }; 1336 | ns[n[i]].rows.push(i); 1337 | } 1338 | 1339 | for (var i = 0; i < o.length; i++) { 1340 | if (os[o[i]] == null) 1341 | os[o[i]] = { 1342 | rows: new Array(), 1343 | n: null 1344 | }; 1345 | os[o[i]].rows.push(i); 1346 | } 1347 | 1348 | for (var i in ns) { 1349 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1350 | n[ns[i].rows[0]] = { 1351 | text: n[ns[i].rows[0]], 1352 | row: os[i].rows[0] 1353 | }; 1354 | o[os[i].rows[0]] = { 1355 | text: o[os[i].rows[0]], 1356 | row: ns[i].rows[0] 1357 | }; 1358 | } 1359 | } 1360 | 1361 | for (var i = 0; i < n.length - 1; i++) { 1362 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1363 | n[i + 1] == o[n[i].row + 1]) { 1364 | n[i + 1] = { 1365 | text: n[i + 1], 1366 | row: n[i].row + 1 1367 | }; 1368 | o[n[i].row + 1] = { 1369 | text: o[n[i].row + 1], 1370 | row: i + 1 1371 | }; 1372 | } 1373 | } 1374 | 1375 | for (var i = n.length - 1; i > 0; i--) { 1376 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1377 | n[i - 1] == o[n[i].row - 1]) { 1378 | n[i - 1] = { 1379 | text: n[i - 1], 1380 | row: n[i].row - 1 1381 | }; 1382 | o[n[i].row - 1] = { 1383 | text: o[n[i].row - 1], 1384 | row: i - 1 1385 | }; 1386 | } 1387 | } 1388 | 1389 | return { 1390 | o: o, 1391 | n: n 1392 | }; 1393 | } 1394 | 1395 | return function(o, n){ 1396 | o = o.replace(/\s+$/, ''); 1397 | n = n.replace(/\s+$/, ''); 1398 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1399 | 1400 | var str = ""; 1401 | 1402 | var oSpace = o.match(/\s+/g); 1403 | if (oSpace == null) { 1404 | oSpace = [" "]; 1405 | } 1406 | else { 1407 | oSpace.push(" "); 1408 | } 1409 | var nSpace = n.match(/\s+/g); 1410 | if (nSpace == null) { 1411 | nSpace = [" "]; 1412 | } 1413 | else { 1414 | nSpace.push(" "); 1415 | } 1416 | 1417 | if (out.n.length == 0) { 1418 | for (var i = 0; i < out.o.length; i++) { 1419 | str += '' + out.o[i] + oSpace[i] + ""; 1420 | } 1421 | } 1422 | else { 1423 | if (out.n[0].text == null) { 1424 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1425 | str += '' + out.o[n] + oSpace[n] + ""; 1426 | } 1427 | } 1428 | 1429 | for (var i = 0; i < out.n.length; i++) { 1430 | if (out.n[i].text == null) { 1431 | str += '' + out.n[i] + nSpace[i] + ""; 1432 | } 1433 | else { 1434 | var pre = ""; 1435 | 1436 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1437 | pre += '' + out.o[n] + oSpace[n] + ""; 1438 | } 1439 | str += " " + out.n[i].text + nSpace[i] + pre; 1440 | } 1441 | } 1442 | } 1443 | 1444 | return str; 1445 | }; 1446 | })(); 1447 | 1448 | })(this); 1449 | --------------------------------------------------------------------------------