├── test ├── index.css ├── tests.html ├── tests.js ├── qunit-1.11.0.css └── qunit-1.11.0.js ├── examples ├── ex1.html ├── ex2.html ├── ex2.js └── ex1.js ├── README.md └── quadtree.js /test/index.css: -------------------------------------------------------------------------------- 1 | canvas { 2 | border:1px solid #333; 3 | text-align:center; 4 | float:left; 5 | margin-right:10px; 6 | margin-bottom:20px; 7 | } 8 | 9 | h1 { border-bottom: 1px solid #333; } 10 | 11 | h2 { border-top: 1px solid #333; } 12 | 13 | #text { clear:both; } 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QuadTree Tests 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/ex1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 9 | 12 | 15 | 18 |
19 | 22 |
23 |
24 | 25 |
26 | Collision Checks / Step:
27 | FPS: 28 | 29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | QuadTree Implementation in JavaScript 2 | ======== 3 | 4 | **Author:** 5 | 6 | * silflow 7 | 8 | ### Usage ### 9 | To create a new empty Quadtree, do this: 10 | 11 | args = { 12 | // mandatory fields 13 | x : x coordinate 14 | y : y coordinate 15 | w : width 16 | h : height 17 | 18 | // optional fields 19 | maxChildren : max children per node 20 | maxDepth : max depth of the tree 21 | }; 22 | 23 | var tree = QUAD.init(args); 24 | 25 | ### Available methods ### 26 | tree.insert() 27 | 28 | takes arrays or single items. every item must contain the following properties: 29 | 30 | var item = { 31 | // mandatory fields 32 | x : x coordinate 33 | y : y coordinate 34 | w : width 35 | h : height 36 | } 37 | if the item does not contain all of those fields, the behaviour of the tree is not defined 38 | 39 | tree.retrieve(selector, callback) 40 | 41 | iterates all items that match the selector and invokes the supplied callback on them. 42 | 43 | var selector = { 44 | // mandatory fields 45 | x : topLeft coordinate, 46 | y : topRight coordinate, 47 | w : selection width 48 | h : selection height 49 | } 50 | 51 | tree.retrieve(selector, function(item) { 52 | doSomethingWith(item); 53 | }); 54 | 55 | **NOTE**: The result contains all items in quadtree-regions that are overlapping with the selector. 56 | 57 | tree.clear() 58 | removes all items from the quadtree. -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | 2 | // test setup 3 | var topLeft = {x : 1, y : 1, w : 1, h: 1}; 4 | var topRight = {x : 490, y : 1, w : 1, h: 1}; 5 | var botRight = {x : 490, y : 490, w : 1, h: 1}; 6 | var botLeft = {x : 1, y : 490, w : 1, h: 1}; 7 | var itemArray = [topLeft, botRight, botLeft, topRight]; 8 | 9 | var args = { 10 | x : 0, 11 | y : 0, 12 | h : 500, 13 | w : 500, 14 | maxChildren : 1 15 | } 16 | 17 | test("retrieve top left region", function() { 18 | 19 | var tree = QUAD.init(args); 20 | tree.insert(itemArray); 21 | tree.retrieve({x : 2,y : 2, h: 2, w: 2}, 22 | function(item) { 23 | deepEqual(item, topLeft, "retrieve top left"); 24 | }); 25 | }); 26 | 27 | test("retrieve top right region", function() { 28 | var tree = QUAD.init(args); 29 | tree.insert(itemArray); 30 | tree.retrieve({x : 400,y : 2, h: 2, w: 2}, 31 | function(item) { 32 | deepEqual(item, topRight, "retrieve top right"); 33 | }); 34 | }); 35 | 36 | test("retrieve bottom right region", function() { 37 | 38 | var tree = QUAD.init(args); 39 | tree.insert(itemArray); 40 | tree.retrieve({x : 400,y : 400, h: 2, w: 2}, 41 | function(item) { 42 | deepEqual(item, botRight, "retrieve bottom right"); 43 | }); 44 | }); 45 | 46 | test("retrieve top right region", function() { 47 | var tree = QUAD.init(args); 48 | tree.insert(itemArray); 49 | tree.retrieve({x : 400,y : 2, h: 2, w: 2}, 50 | function(item) { 51 | deepEqual(item, topRight, "retrieve bottom left"); 52 | }); 53 | }); -------------------------------------------------------------------------------- /examples/ex2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | QuadTree Demo 5 | 6 | 7 | 8 | 9 |
10 |

Collision Detection: QuadTree vs. Brute Force

11 | 12 | Your Browser does not support HTML5 13 | 14 |
15 |

Objects in the tree: 0

16 |

Objects in current region: 0

17 |

Brute force collision checks: 0

18 |

Collision checks with quadtree: 0

19 | 20 | 21 |
22 |
23 |
24 |

Performance

25 |

The performance gain using the quadtree data structure depends on the geographic 26 | location of the objects. Worst case complexity (All objects are 27 | in the same region) is O(n^2), excactly the same as brute force. 28 | Plus, the quadtree has to be build freshly for each set of collision checks. This also takes 29 | some time. As a consequence, brute force may actually be less expensive in some (very few) 30 | scenarios. However, most of the time, collision detection with quadtree will be 31 | significantly faster.

32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /examples/ex2.js: -------------------------------------------------------------------------------- 1 | /* f***ing grusige code, aber isch jo nur e demo :) */ 2 | 3 | function init_canvas(elementId) { 4 | var canvas, ctx; 5 | try { 6 | canvas = document.getElementById(elementId); 7 | ctx = canvas.getContext('2d'); 8 | } catch(e) { 9 | return false; 10 | } 11 | ctx.height = canvas.height; 12 | ctx.width = canvas.width; 13 | ctx.clear = function() { 14 | this.clearRect(0,0,this.canvas.width,this.canvas.height) 15 | } 16 | return ctx; 17 | } 18 | 19 | //convenience 20 | function addEvent(el, evt, fn) { 21 | if (el.addEventListener) { 22 | el.addEventListener(evt, fn, false); 23 | } else if (el.attachEvent) { 24 | el.attachEvent('on' + evt, fn); 25 | } 26 | } 27 | 28 | // init canvas and quadtree 29 | var ctx = init_canvas('quadtree'); 30 | var tree = QUAD.init({ 31 | x : 0, 32 | y : 0, 33 | w : ctx.width, 34 | h : ctx.height, 35 | maxChildren : 1 36 | }); 37 | tree.clear(); 38 | var objects = []; 39 | ctx.font = "40pt Arial"; 40 | ctx.fillText("Click me!", 135, ctx.height/2); 41 | 42 | var insert = true; 43 | 44 | // orb prototype 45 | var orb = { 46 | w:15, 47 | h:15, 48 | draw : function () { 49 | 50 | ctx.beginPath(); 51 | ctx.rect(this.x, this.y, this.w, this.h); 52 | 53 | ctx.closePath(); 54 | ctx.stroke(); 55 | 56 | }, 57 | fill : function () { 58 | ctx.fillStyle = "rgb(200,0,0)"; 59 | 60 | ctx.fillRect(this.x, this.y, this.w, this.h); 61 | 62 | } 63 | } 64 | var in_sel = document.getElementById('in_sel'); 65 | in_sel.onclick = function () { 66 | insert = !insert; 67 | if (insert) { 68 | in_sel.value = "Switch to select"; 69 | } else { 70 | in_sel.value = "Switch to insert"; 71 | } 72 | 73 | } 74 | 75 | document.getElementById('quadtree').onclick = function (event){ 76 | 77 | //get x y coords of the cursor 78 | pos_x = event.offsetX?(event.offsetX):event.pageX-document.getElementById("quadtree").offsetLeft; 79 | pos_y = event.offsetY?(event.offsetY):event.pageY-document.getElementById("quadtree").offsetTop; 80 | 81 | if (event.button == 3) { 82 | alert(event.button); 83 | } 84 | 85 | // clear canvas 86 | ctx.beginPath(); 87 | ctx.clear(); 88 | ctx.closePath(); 89 | 90 | for (var i = 0; i < objects.length; i++) { 91 | objects[i].draw(); // draw the objects 92 | } 93 | 94 | var grplen = 0; 95 | // fill out all objects of the current group 96 | if (insert) { 97 | //create an orb with cursor coordinates 98 | var o = Object.create(orb); 99 | o.x = pos_x; 100 | o.y = pos_y; 101 | o.draw(); 102 | objects.push(o); 103 | tree.insert(o); 104 | tree.retrieve(o, function(item) { 105 | item.fill(); 106 | ++grplen; 107 | }); 108 | } else { 109 | tree.retrieve({ 110 | x: pos_x, 111 | y: pos_y, 112 | h : 0, 113 | w : 0 114 | }, function(item) { 115 | item.fill(); 116 | ++grplen; 117 | }); 118 | } 119 | 120 | var quadcount = 0; 121 | var len = objects.length; 122 | 123 | for (i = 0; i < len; i++) { 124 | // count quadtree collision checks 125 | tree.retrieve(objects[i], function() { 126 | ++quadcount; 127 | }); 128 | --quadcount; 129 | } 130 | 131 | drawRegions(tree.root); 132 | 133 | // display stats 134 | document.getElementById('objtotal').innerHTML = len; 135 | document.getElementById('objgroup').innerHTML = grplen; 136 | document.getElementById('brute').innerHTML = len * (len-1); 137 | document.getElementById('quad').innerHTML = quadcount; 138 | }; 139 | 140 | document.getElementById('clear').onclick = function () { 141 | tree.clear(); 142 | ctx.clear(); 143 | objects.length = 0; 144 | ctx.clear(0,0,ctx.width, ctx.height); 145 | ctx.fillStyle='rgb(0,0,0)'; 146 | ctx.fillText("Click me!", 135, ctx.height/2); 147 | drawRegions(tree.root); 148 | 149 | document.getElementById('objtotal').innerHTML = 0; 150 | document.getElementById('objgroup').innerHTML = 0; 151 | document.getElementById('brute').innerHTML = 0; 152 | document.getElementById('quad').innerHTML = 0; 153 | } 154 | 155 | // draws the region-frames 156 | var drawRegions = function (node) { 157 | 158 | var nodes = node.getNodes(); 159 | if (nodes) { 160 | for (var i = 0; i < nodes.length; i++) { 161 | drawRegions(nodes[i]); 162 | } 163 | } 164 | ctx.beginPath(); 165 | ctx.rect(node.x, node.y, node.w, node.h); 166 | ctx.stroke(); 167 | ctx.closePath(); 168 | } -------------------------------------------------------------------------------- /test/qunit-1.11.0.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.11.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 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-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 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: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 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-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-collapse: collapse; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } 245 | -------------------------------------------------------------------------------- /examples/ex1.js: -------------------------------------------------------------------------------- 1 | // global var for EX1 2 | var EX1 = {}; 3 | 4 | EX1.orb = { 5 | draw : function () { 6 | EX1.ctx.strokeStyle = "rgb(0,0,0)"; 7 | EX1.ctx.beginPath(); 8 | EX1.ctx.rect(this.x, this.y, this.w, this.h); 9 | EX1.ctx.closePath(); 10 | EX1.ctx.stroke(); 11 | }, 12 | 13 | fill : function () { 14 | EX1.ctx.fillStyle = "rgb(150,0,0)"; 15 | EX1.ctx.fillRect(this.x, this.y, this.w, this.h); 16 | }, 17 | move : function (dt) { 18 | this.x += (this.dx / 1000) * dt; 19 | this.y += (this.dy / 1000) * dt; 20 | if (this.x < 0 || this.x > EX1.width) { 21 | this.dx *= -1; 22 | } 23 | if (this.y < 0 || this.y > EX1.height) { 24 | this.dy *= -1; 25 | } 26 | } 27 | }; 28 | 29 | EX1.simulation = (function () { 30 | var interval, dt, i, frames, finterval, 31 | drawRegions = true; 32 | return { 33 | start : function (t) { 34 | dt = t; 35 | if (!interval) { 36 | // start simulation interval 37 | interval = setInterval(this.simulate, dt); 38 | // start fps interval. this calculates the current frame rate and sets the counter 39 | // to zero 40 | finterval = setInterval(function () { 41 | document.getElementById("ex1-fps").innerHTML = frames * 4; 42 | frames = 0; 43 | }, 250); 44 | } 45 | }, 46 | 47 | stop : function () { clearInterval(interval); clearInterval(finterval); }, 48 | 49 | toggleRegions : function () { drawRegions = !drawRegions; }, 50 | 51 | simulate : function () { 52 | var n = EX1.elements.length; 53 | // clear the canvas 54 | EX1.ctx.clearRect(0, 0, EX1.width, EX1.height); 55 | // draw the quadtree regions 56 | if (drawRegions) { 57 | QUAD.drawRegions(EX1.tree.root, EX1.ctx); 58 | } 59 | // collision detection 60 | EX1.CD.check(EX1.elements[i]); 61 | // draw and move the elements 62 | for (i = 0; i < n; i++) { 63 | EX1.elements[i].draw(); 64 | EX1.elements[i].move(dt); 65 | } 66 | // display check count 67 | document.getElementById("ex1-checks").innerHTML = EX1.CD.getNChecks(); 68 | // increase frame count 69 | frames++; 70 | } 71 | }; 72 | }()); 73 | 74 | EX1.init = function (numberOfElements) { 75 | var canvas, args, i, n = numberOfElements || 500; 76 | // init canvas or display error message 77 | if (!(canvas = document.getElementById("ex1-canv"))) { 78 | document.getElementsByTagName("body")[0].innerHTML = "Canvas Error"; 79 | } 80 | // init canvas context and save canvas properties for further use 81 | EX1.ctx = canvas.getContext("2d"); 82 | EX1.height = canvas.height; 83 | EX1.width = canvas.width; 84 | // init the quadtree 85 | args = {x : 0, y : 0, h : EX1.height, w : EX1.width, maxChildren : 5, maxDepth : 5}; 86 | EX1.tree = QUAD.init(args); 87 | // init the array that holds the objects 88 | EX1.elements = []; 89 | // fill the array with fresh objects 90 | for (i = 0; i < n; i++) { 91 | EX1.elements.push(Object.create(EX1.orb, { 92 | // set a random position 93 | x : {value: Math.random() * EX1.width, writable: true, enumerable: true, configurable: false }, 94 | y : {value: Math.random() * EX1.height, writable: true, enumerable: true, configurable: false }, 95 | // set random speed 96 | dx : {value: Math.random() * 100, writable: true, enumerable: true, configurable: false }, 97 | dy : {value: Math.random() * 100, writable: true, enumerable: true, configurable: false }, 98 | // set random size 99 | h : {value: (Math.random() * 10) + 5, writable: true, enumerable: true, configurable: false}, 100 | w : {value: (Math.random() * 10) + 5, writable: true, enumerable: true, configurable: false} 101 | })); 102 | } 103 | // init buttons 104 | EX1.init_controls(); 105 | // start simulation 106 | EX1.simulation.start(16); 107 | }; 108 | 109 | EX1.CD = (function () { 110 | var bruteForce = false, 111 | nChecks; 112 | return { 113 | check : function (orb) { 114 | // reset check counter 115 | nChecks = 0; 116 | // select check algrithm 117 | if (!bruteForce) { 118 | this.quadTree(orb); 119 | } else { 120 | this.bruteForce(orb); 121 | } 122 | }, 123 | 124 | quadTree : function () { 125 | var n = EX1.elements.length, m, region, i, k, orb; 126 | // clear the quadtree 127 | EX1.tree.clear(); 128 | // fill the quadtree 129 | EX1.tree.insert(EX1.elements); 130 | // iterate all elements 131 | for (i = 0; i < n; i++) { 132 | orb = EX1.elements[i]; 133 | // get all elements in the same region as orb 134 | region = EX1.tree.retrieve(orb, function(item) { 135 | EX1.CD.detectCollision(orb, item); 136 | nChecks++; 137 | }); 138 | } 139 | }, 140 | 141 | bruteForce : function () { 142 | var n = EX1.elements.length, i, k, orb; 143 | // iterate all elements 144 | for (i = 0; i < n; i++) { 145 | orb = EX1.elements[i]; 146 | // iterate all elements and check for collision with the current element 147 | for (k = 0; k < n; k++) { 148 | this.detectCollision(orb, EX1.elements[k]); 149 | // increase check counter 150 | nChecks++; 151 | } 152 | } 153 | }, 154 | 155 | detectCollision : function (orb1, orb2) { 156 | if (orb1 === orb2) { 157 | return; 158 | } 159 | if (orb1.x + orb1.w < orb2.x) { 160 | return; 161 | } 162 | if (orb1.x > orb2.x + orb2.w) { 163 | return; 164 | } 165 | if (orb1.y + orb1.h < orb2.y) { 166 | return; 167 | } 168 | if (orb1.y > orb2.y + orb2.h) { 169 | return; 170 | } 171 | orb1.fill(); 172 | }, 173 | 174 | toggleDetection : function () { bruteForce = !bruteForce; }, 175 | 176 | getNChecks : function () { return nChecks; } 177 | }; 178 | }()); 179 | 180 | // ugly code 181 | EX1.init_controls = function () { 182 | var cRegions = document.getElementById("ex1-regions"), 183 | r250 = document.getElementById("250"), 184 | r500 = document.getElementById("500"), 185 | r1000 = document.getElementById("1000"), 186 | cType = document.getElementById("ex1-type"); 187 | cRegions.onclick = function () { EX1.simulation.toggleRegions(); }; 188 | r250.onclick = function () { EX1.init(250); }; 189 | r500.onclick = function () { EX1.init(500); }; 190 | r1000.onclick = function () { EX1.init(1000); }; 191 | cType.onclick = function () { EX1.CD.toggleDetection(); }; 192 | }; 193 | 194 | // allows us to draw the current quadtree regions to a specific canvas 195 | QUAD.drawRegions = function (anode, ctx) { 196 | var nodes = anode.getNodes(), i; 197 | if (nodes) { 198 | for (i = 0; i < nodes.length; i++) { 199 | QUAD.drawRegions(nodes[i], ctx); 200 | } 201 | } 202 | ctx.beginPath(); 203 | ctx.rect(anode.x, anode.y, anode.w, anode.h); 204 | ctx.stroke(); 205 | ctx.closePath(); 206 | }; 207 | 208 | // init the application 209 | EX1.init(250); -------------------------------------------------------------------------------- /quadtree.js: -------------------------------------------------------------------------------- 1 | /* 2 | * QuadTree Implementation in JavaScript 3 | * @author: silflow 4 | * 5 | * Usage: 6 | * To create a new empty Quadtree, do this: 7 | * var tree = QUAD.init(args) 8 | * 9 | * args = { 10 | * // mandatory fields 11 | * x : x coordinate 12 | * y : y coordinate 13 | * w : width 14 | * h : height 15 | * 16 | * // optional fields 17 | * maxChildren : max children per node 18 | * maxDepth : max depth of the tree 19 | *} 20 | * 21 | * API: 22 | * tree.insert() accepts arrays or single items 23 | * every item must have a .x, .y, .w, and .h property. if they don't, the tree will break. 24 | * 25 | * tree.retrieve(selector, callback) calls the callback for all objects that are in 26 | * the same region or overlapping. 27 | * 28 | * tree.clear() removes all items from the quadtree. 29 | */ 30 | 31 | var QUAD = {}; // global var for the quadtree 32 | 33 | QUAD.init = function (args) { 34 | 35 | var node; 36 | var TOP_LEFT = 0; 37 | var TOP_RIGHT = 1; 38 | var BOTTOM_LEFT = 2; 39 | var BOTTOM_RIGHT = 3; 40 | var PARENT = 4; 41 | 42 | // assign default values 43 | args.maxChildren = args.maxChildren || 2; 44 | args.maxDepth = args.maxDepth || 4; 45 | 46 | /** 47 | * Node creator. You should never create a node manually. the algorithm takes 48 | * care of that for you. 49 | */ 50 | node = function (x, y, w, h, depth, maxChildren, maxDepth) { 51 | 52 | var items = [], // holds all items 53 | nodes = []; // holds all child nodes 54 | 55 | // returns a fresh node object 56 | return { 57 | 58 | x : x, // top left point 59 | y : y, // top right point 60 | w : w, // width 61 | h : h, // height 62 | depth : depth, // depth level of the node 63 | 64 | /** 65 | * iterates all items that match the selector and invokes the supplied callback on them. 66 | */ 67 | retrieve: function(item, callback, instance) { 68 | for (var i = 0; i < items.length; ++i) { 69 | (instance) ? callback.call(instance, items[i]) : callback(items[i]); 70 | } 71 | // check if node has subnodes 72 | if (nodes.length) { 73 | // call retrieve on all matching subnodes 74 | this.findOverlappingNodes(item, function(dir) { 75 | nodes[dir].retrieve(item, callback, instance); 76 | }); 77 | } 78 | }, 79 | 80 | /** 81 | * Adds a new Item to the node. 82 | * 83 | * If the node already has subnodes, the item gets pushed down one level. 84 | * If the item does not fit into the subnodes, it gets saved in the 85 | * "children"-array. 86 | * 87 | * If the maxChildren limit is exceeded after inserting the item, 88 | * the node gets divided and all items inside the "children"-array get 89 | * pushed down to the new subnodes. 90 | */ 91 | insert : function (item) { 92 | 93 | var i; 94 | 95 | if (nodes.length) { 96 | // get the node in which the item fits best 97 | i = this.findInsertNode(item); 98 | if (i === PARENT) { 99 | // if the item does not fit, push it into the 100 | // children array 101 | items.push(item); 102 | } else { 103 | nodes[i].insert(item); 104 | } 105 | } else { 106 | items.push(item); 107 | //divide the node if maxChildren is exceeded and maxDepth is not reached 108 | if (items.length > maxChildren && this.depth < maxDepth) { 109 | this.divide(); 110 | } 111 | } 112 | }, 113 | 114 | /** 115 | * Find a node the item should be inserted in. 116 | */ 117 | findInsertNode : function (item) { 118 | // left 119 | if (item.x + item.w < x + (w / 2)) { 120 | if (item.y + item.h < y + (h / 2)) { 121 | return TOP_LEFT; 122 | } 123 | if (item.y >= y + (h / 2)) { 124 | return BOTTOM_LEFT; 125 | } 126 | return PARENT; 127 | } 128 | 129 | // right 130 | if (item.x >= x + (w / 2)) { 131 | if (item.y + item.h < y + (h / 2)) { 132 | return TOP_RIGHT; 133 | } 134 | if (item.y >= y + (h / 2)) { 135 | return BOTTOM_RIGHT; 136 | } 137 | return PARENT; 138 | } 139 | 140 | return PARENT; 141 | }, 142 | 143 | /** 144 | * Finds the regions the item overlaps with. See constants defined 145 | * above. The callback is called for every region the item overlaps. 146 | */ 147 | findOverlappingNodes : function (item, callback) { 148 | // left 149 | if (item.x < x + (w / 2)) { 150 | if (item.y < y + (h / 2)) { 151 | callback(TOP_LEFT); 152 | } 153 | if (item.y + item.h >= y + h / 2) { 154 | callback(BOTTOM_LEFT); 155 | } 156 | } 157 | // right 158 | if (item.x + item.w >= x + (w / 2)) { 159 | if (item.y < y + (h / 2)) { 160 | callback(TOP_RIGHT); 161 | } 162 | if (item.y + item.h >= y + h / 2) { 163 | callback(BOTTOM_RIGHT); 164 | } 165 | } 166 | }, 167 | 168 | /** 169 | * Divides the current node into four subnodes and adds them 170 | * to the nodes array of the current node. Then reinserts all 171 | * children. 172 | */ 173 | divide : function () { 174 | var width, height, i, oldChildren; 175 | var childrenDepth = this.depth + 1; 176 | // set dimensions of the new nodes 177 | width = (w / 2); 178 | height = (h / 2); 179 | // create top left node 180 | nodes.push(node(this.x, this.y, width, height, childrenDepth, maxChildren, maxDepth)); 181 | // create top right node 182 | nodes.push(node(this.x + width, this.y, width, height, childrenDepth, maxChildren, maxDepth)); 183 | // create bottom left node 184 | nodes.push(node(this.x, this.y + height, width, height, childrenDepth, maxChildren, maxDepth)); 185 | // create bottom right node 186 | nodes.push(node(this.x + width, this.y + height, width, height, childrenDepth, maxChildren, maxDepth)); 187 | 188 | oldChildren = items; 189 | items = []; 190 | for (i = 0; i < oldChildren.length; i++) { 191 | this.insert(oldChildren[i]); 192 | } 193 | }, 194 | 195 | /** 196 | * Clears the node and all its subnodes. 197 | */ 198 | clear : function () { 199 | var i; 200 | for (i = 0; i < nodes.length; i++) { 201 | nodes[i].clear(); 202 | } 203 | items.length = 0; 204 | nodes.length = 0; 205 | }, 206 | 207 | /* 208 | * convenience method: is not used in the core algorithm. 209 | * --------------------------------------------------------- 210 | * returns this nodes subnodes. this is usful if we want to do stuff 211 | * with the nodes, i.e. accessing the bounds of the nodes to draw them 212 | * on a canvas for debugging etc... 213 | */ 214 | getNodes : function () { 215 | return nodes.length ? nodes : false; 216 | } 217 | }; 218 | }; 219 | 220 | return { 221 | 222 | root : (function () { 223 | return node(args.x, args.y, args.w, args.h, 0, args.maxChildren, args.maxDepth); 224 | }()), 225 | 226 | insert : function (item) { 227 | 228 | var len, i; 229 | 230 | if (item instanceof Array) { 231 | len = item.length; 232 | for (i = 0; i < len; i++) { 233 | this.root.insert(item[i]); 234 | } 235 | 236 | } else { 237 | this.root.insert(item); 238 | } 239 | }, 240 | 241 | retrieve : function (selector, callback, instance) { 242 | return this.root.retrieve(selector, callback, instance); 243 | }, 244 | 245 | clear : function () { 246 | this.root.clear(); 247 | } 248 | }; 249 | }; 250 | -------------------------------------------------------------------------------- /test/qunit-1.11.0.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.11.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | (function( window ) { 12 | 13 | var QUnit, 14 | assert, 15 | config, 16 | onErrorFnPrev, 17 | testId = 0, 18 | fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), 19 | toString = Object.prototype.toString, 20 | hasOwn = Object.prototype.hasOwnProperty, 21 | // Keep a local reference to Date (GH-283) 22 | Date = window.Date, 23 | defined = { 24 | setTimeout: typeof window.setTimeout !== "undefined", 25 | sessionStorage: (function() { 26 | var x = "qunit-test-string"; 27 | try { 28 | sessionStorage.setItem( x, x ); 29 | sessionStorage.removeItem( x ); 30 | return true; 31 | } catch( e ) { 32 | return false; 33 | } 34 | }()) 35 | }, 36 | /** 37 | * Provides a normalized error string, correcting an issue 38 | * with IE 7 (and prior) where Error.prototype.toString is 39 | * not properly implemented 40 | * 41 | * Based on http://es5.github.com/#x15.11.4.4 42 | * 43 | * @param {String|Error} error 44 | * @return {String} error message 45 | */ 46 | errorString = function( error ) { 47 | var name, message, 48 | errorString = error.toString(); 49 | if ( errorString.substring( 0, 7 ) === "[object" ) { 50 | name = error.name ? error.name.toString() : "Error"; 51 | message = error.message ? error.message.toString() : ""; 52 | if ( name && message ) { 53 | return name + ": " + message; 54 | } else if ( name ) { 55 | return name; 56 | } else if ( message ) { 57 | return message; 58 | } else { 59 | return "Error"; 60 | } 61 | } else { 62 | return errorString; 63 | } 64 | }, 65 | /** 66 | * Makes a clone of an object using only Array or Object as base, 67 | * and copies over the own enumerable properties. 68 | * 69 | * @param {Object} obj 70 | * @return {Object} New object with only the own properties (recursively). 71 | */ 72 | objectValues = function( obj ) { 73 | // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. 74 | /*jshint newcap: false */ 75 | var key, val, 76 | vals = QUnit.is( "array", obj ) ? [] : {}; 77 | for ( key in obj ) { 78 | if ( hasOwn.call( obj, key ) ) { 79 | val = obj[key]; 80 | vals[key] = val === Object(val) ? objectValues(val) : val; 81 | } 82 | } 83 | return vals; 84 | }; 85 | 86 | function Test( settings ) { 87 | extend( this, settings ); 88 | this.assertions = []; 89 | this.testNumber = ++Test.count; 90 | } 91 | 92 | Test.count = 0; 93 | 94 | Test.prototype = { 95 | init: function() { 96 | var a, b, li, 97 | tests = id( "qunit-tests" ); 98 | 99 | if ( tests ) { 100 | b = document.createElement( "strong" ); 101 | b.innerHTML = this.nameHtml; 102 | 103 | // `a` initialized at top of scope 104 | a = document.createElement( "a" ); 105 | a.innerHTML = "Rerun"; 106 | a.href = QUnit.url({ testNumber: this.testNumber }); 107 | 108 | li = document.createElement( "li" ); 109 | li.appendChild( b ); 110 | li.appendChild( a ); 111 | li.className = "running"; 112 | li.id = this.id = "qunit-test-output" + testId++; 113 | 114 | tests.appendChild( li ); 115 | } 116 | }, 117 | setup: function() { 118 | if ( this.module !== config.previousModule ) { 119 | if ( config.previousModule ) { 120 | runLoggingCallbacks( "moduleDone", QUnit, { 121 | name: config.previousModule, 122 | failed: config.moduleStats.bad, 123 | passed: config.moduleStats.all - config.moduleStats.bad, 124 | total: config.moduleStats.all 125 | }); 126 | } 127 | config.previousModule = this.module; 128 | config.moduleStats = { all: 0, bad: 0 }; 129 | runLoggingCallbacks( "moduleStart", QUnit, { 130 | name: this.module 131 | }); 132 | } else if ( config.autorun ) { 133 | runLoggingCallbacks( "moduleStart", QUnit, { 134 | name: this.module 135 | }); 136 | } 137 | 138 | config.current = this; 139 | 140 | this.testEnvironment = extend({ 141 | setup: function() {}, 142 | teardown: function() {} 143 | }, this.moduleTestEnvironment ); 144 | 145 | this.started = +new Date(); 146 | runLoggingCallbacks( "testStart", QUnit, { 147 | name: this.testName, 148 | module: this.module 149 | }); 150 | 151 | // allow utility functions to access the current test environment 152 | QUnit.current_testEnvironment = this.testEnvironment; 153 | 154 | if ( !config.pollution ) { 155 | saveGlobal(); 156 | } 157 | if ( config.notrycatch ) { 158 | this.testEnvironment.setup.call( this.testEnvironment ); 159 | return; 160 | } 161 | try { 162 | this.testEnvironment.setup.call( this.testEnvironment ); 163 | } catch( e ) { 164 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 165 | } 166 | }, 167 | run: function() { 168 | config.current = this; 169 | 170 | var running = id( "qunit-testresult" ); 171 | 172 | if ( running ) { 173 | running.innerHTML = "Running:
" + this.nameHtml; 174 | } 175 | 176 | if ( this.async ) { 177 | QUnit.stop(); 178 | } 179 | 180 | this.callbackStarted = +new Date(); 181 | 182 | if ( config.notrycatch ) { 183 | this.callback.call( this.testEnvironment, QUnit.assert ); 184 | this.callbackRuntime = +new Date() - this.callbackStarted; 185 | return; 186 | } 187 | 188 | try { 189 | this.callback.call( this.testEnvironment, QUnit.assert ); 190 | this.callbackRuntime = +new Date() - this.callbackStarted; 191 | } catch( e ) { 192 | this.callbackRuntime = +new Date() - this.callbackStarted; 193 | 194 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 195 | // else next test will carry the responsibility 196 | saveGlobal(); 197 | 198 | // Restart the tests if they're blocking 199 | if ( config.blocking ) { 200 | QUnit.start(); 201 | } 202 | } 203 | }, 204 | teardown: function() { 205 | config.current = this; 206 | if ( config.notrycatch ) { 207 | if ( typeof this.callbackRuntime === "undefined" ) { 208 | this.callbackRuntime = +new Date() - this.callbackStarted; 209 | } 210 | this.testEnvironment.teardown.call( this.testEnvironment ); 211 | return; 212 | } else { 213 | try { 214 | this.testEnvironment.teardown.call( this.testEnvironment ); 215 | } catch( e ) { 216 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 217 | } 218 | } 219 | checkPollution(); 220 | }, 221 | finish: function() { 222 | config.current = this; 223 | if ( config.requireExpects && this.expected === null ) { 224 | QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); 225 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) { 226 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); 227 | } else if ( this.expected === null && !this.assertions.length ) { 228 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); 229 | } 230 | 231 | var i, assertion, a, b, time, li, ol, 232 | test = this, 233 | good = 0, 234 | bad = 0, 235 | tests = id( "qunit-tests" ); 236 | 237 | this.runtime = +new Date() - this.started; 238 | config.stats.all += this.assertions.length; 239 | config.moduleStats.all += this.assertions.length; 240 | 241 | if ( tests ) { 242 | ol = document.createElement( "ol" ); 243 | ol.className = "qunit-assert-list"; 244 | 245 | for ( i = 0; i < this.assertions.length; i++ ) { 246 | assertion = this.assertions[i]; 247 | 248 | li = document.createElement( "li" ); 249 | li.className = assertion.result ? "pass" : "fail"; 250 | li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); 251 | ol.appendChild( li ); 252 | 253 | if ( assertion.result ) { 254 | good++; 255 | } else { 256 | bad++; 257 | config.stats.bad++; 258 | config.moduleStats.bad++; 259 | } 260 | } 261 | 262 | // store result when possible 263 | if ( QUnit.config.reorder && defined.sessionStorage ) { 264 | if ( bad ) { 265 | sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); 266 | } else { 267 | sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); 268 | } 269 | } 270 | 271 | if ( bad === 0 ) { 272 | addClass( ol, "qunit-collapsed" ); 273 | } 274 | 275 | // `b` initialized at top of scope 276 | b = document.createElement( "strong" ); 277 | b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 278 | 279 | addEvent(b, "click", function() { 280 | var next = b.parentNode.lastChild, 281 | collapsed = hasClass( next, "qunit-collapsed" ); 282 | ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); 283 | }); 284 | 285 | addEvent(b, "dblclick", function( e ) { 286 | var target = e && e.target ? e.target : window.event.srcElement; 287 | if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { 288 | target = target.parentNode; 289 | } 290 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 291 | window.location = QUnit.url({ testNumber: test.testNumber }); 292 | } 293 | }); 294 | 295 | // `time` initialized at top of scope 296 | time = document.createElement( "span" ); 297 | time.className = "runtime"; 298 | time.innerHTML = this.runtime + " ms"; 299 | 300 | // `li` initialized at top of scope 301 | li = id( this.id ); 302 | li.className = bad ? "fail" : "pass"; 303 | li.removeChild( li.firstChild ); 304 | a = li.firstChild; 305 | li.appendChild( b ); 306 | li.appendChild( a ); 307 | li.appendChild( time ); 308 | li.appendChild( ol ); 309 | 310 | } else { 311 | for ( i = 0; i < this.assertions.length; i++ ) { 312 | if ( !this.assertions[i].result ) { 313 | bad++; 314 | config.stats.bad++; 315 | config.moduleStats.bad++; 316 | } 317 | } 318 | } 319 | 320 | runLoggingCallbacks( "testDone", QUnit, { 321 | name: this.testName, 322 | module: this.module, 323 | failed: bad, 324 | passed: this.assertions.length - bad, 325 | total: this.assertions.length, 326 | duration: this.runtime 327 | }); 328 | 329 | QUnit.reset(); 330 | 331 | config.current = undefined; 332 | }, 333 | 334 | queue: function() { 335 | var bad, 336 | test = this; 337 | 338 | synchronize(function() { 339 | test.init(); 340 | }); 341 | function run() { 342 | // each of these can by async 343 | synchronize(function() { 344 | test.setup(); 345 | }); 346 | synchronize(function() { 347 | test.run(); 348 | }); 349 | synchronize(function() { 350 | test.teardown(); 351 | }); 352 | synchronize(function() { 353 | test.finish(); 354 | }); 355 | } 356 | 357 | // `bad` initialized at top of scope 358 | // defer when previous test run passed, if storage is available 359 | bad = QUnit.config.reorder && defined.sessionStorage && 360 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); 361 | 362 | if ( bad ) { 363 | run(); 364 | } else { 365 | synchronize( run, true ); 366 | } 367 | } 368 | }; 369 | 370 | // Root QUnit object. 371 | // `QUnit` initialized at top of scope 372 | QUnit = { 373 | 374 | // call on start of module test to prepend name to all tests 375 | module: function( name, testEnvironment ) { 376 | config.currentModule = name; 377 | config.currentModuleTestEnvironment = testEnvironment; 378 | config.modules[name] = true; 379 | }, 380 | 381 | asyncTest: function( testName, expected, callback ) { 382 | if ( arguments.length === 2 ) { 383 | callback = expected; 384 | expected = null; 385 | } 386 | 387 | QUnit.test( testName, expected, callback, true ); 388 | }, 389 | 390 | test: function( testName, expected, callback, async ) { 391 | var test, 392 | nameHtml = "" + escapeText( testName ) + ""; 393 | 394 | if ( arguments.length === 2 ) { 395 | callback = expected; 396 | expected = null; 397 | } 398 | 399 | if ( config.currentModule ) { 400 | nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml; 401 | } 402 | 403 | test = new Test({ 404 | nameHtml: nameHtml, 405 | testName: testName, 406 | expected: expected, 407 | async: async, 408 | callback: callback, 409 | module: config.currentModule, 410 | moduleTestEnvironment: config.currentModuleTestEnvironment, 411 | stack: sourceFromStacktrace( 2 ) 412 | }); 413 | 414 | if ( !validTest( test ) ) { 415 | return; 416 | } 417 | 418 | test.queue(); 419 | }, 420 | 421 | // Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 422 | expect: function( asserts ) { 423 | if (arguments.length === 1) { 424 | config.current.expected = asserts; 425 | } else { 426 | return config.current.expected; 427 | } 428 | }, 429 | 430 | start: function( count ) { 431 | // QUnit hasn't been initialized yet. 432 | // Note: RequireJS (et al) may delay onLoad 433 | if ( config.semaphore === undefined ) { 434 | QUnit.begin(function() { 435 | // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first 436 | setTimeout(function() { 437 | QUnit.start( count ); 438 | }); 439 | }); 440 | return; 441 | } 442 | 443 | config.semaphore -= count || 1; 444 | // don't start until equal number of stop-calls 445 | if ( config.semaphore > 0 ) { 446 | return; 447 | } 448 | // ignore if start is called more often then stop 449 | if ( config.semaphore < 0 ) { 450 | config.semaphore = 0; 451 | QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); 452 | return; 453 | } 454 | // A slight delay, to avoid any current callbacks 455 | if ( defined.setTimeout ) { 456 | window.setTimeout(function() { 457 | if ( config.semaphore > 0 ) { 458 | return; 459 | } 460 | if ( config.timeout ) { 461 | clearTimeout( config.timeout ); 462 | } 463 | 464 | config.blocking = false; 465 | process( true ); 466 | }, 13); 467 | } else { 468 | config.blocking = false; 469 | process( true ); 470 | } 471 | }, 472 | 473 | stop: function( count ) { 474 | config.semaphore += count || 1; 475 | config.blocking = true; 476 | 477 | if ( config.testTimeout && defined.setTimeout ) { 478 | clearTimeout( config.timeout ); 479 | config.timeout = window.setTimeout(function() { 480 | QUnit.ok( false, "Test timed out" ); 481 | config.semaphore = 1; 482 | QUnit.start(); 483 | }, config.testTimeout ); 484 | } 485 | } 486 | }; 487 | 488 | // `assert` initialized at top of scope 489 | // Asssert helpers 490 | // All of these must either call QUnit.push() or manually do: 491 | // - runLoggingCallbacks( "log", .. ); 492 | // - config.current.assertions.push({ .. }); 493 | // We attach it to the QUnit object *after* we expose the public API, 494 | // otherwise `assert` will become a global variable in browsers (#341). 495 | assert = { 496 | /** 497 | * Asserts rough true-ish result. 498 | * @name ok 499 | * @function 500 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 501 | */ 502 | ok: function( result, msg ) { 503 | if ( !config.current ) { 504 | throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); 505 | } 506 | result = !!result; 507 | 508 | var source, 509 | details = { 510 | module: config.current.module, 511 | name: config.current.testName, 512 | result: result, 513 | message: msg 514 | }; 515 | 516 | msg = escapeText( msg || (result ? "okay" : "failed" ) ); 517 | msg = "" + msg + ""; 518 | 519 | if ( !result ) { 520 | source = sourceFromStacktrace( 2 ); 521 | if ( source ) { 522 | details.source = source; 523 | msg += "
Source:
" + escapeText( source ) + "
"; 524 | } 525 | } 526 | runLoggingCallbacks( "log", QUnit, details ); 527 | config.current.assertions.push({ 528 | result: result, 529 | message: msg 530 | }); 531 | }, 532 | 533 | /** 534 | * Assert that the first two arguments are equal, with an optional message. 535 | * Prints out both actual and expected values. 536 | * @name equal 537 | * @function 538 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); 539 | */ 540 | equal: function( actual, expected, message ) { 541 | /*jshint eqeqeq:false */ 542 | QUnit.push( expected == actual, actual, expected, message ); 543 | }, 544 | 545 | /** 546 | * @name notEqual 547 | * @function 548 | */ 549 | notEqual: function( actual, expected, message ) { 550 | /*jshint eqeqeq:false */ 551 | QUnit.push( expected != actual, actual, expected, message ); 552 | }, 553 | 554 | /** 555 | * @name propEqual 556 | * @function 557 | */ 558 | propEqual: function( actual, expected, message ) { 559 | actual = objectValues(actual); 560 | expected = objectValues(expected); 561 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 562 | }, 563 | 564 | /** 565 | * @name notPropEqual 566 | * @function 567 | */ 568 | notPropEqual: function( actual, expected, message ) { 569 | actual = objectValues(actual); 570 | expected = objectValues(expected); 571 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 572 | }, 573 | 574 | /** 575 | * @name deepEqual 576 | * @function 577 | */ 578 | deepEqual: function( actual, expected, message ) { 579 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 580 | }, 581 | 582 | /** 583 | * @name notDeepEqual 584 | * @function 585 | */ 586 | notDeepEqual: function( actual, expected, message ) { 587 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 588 | }, 589 | 590 | /** 591 | * @name strictEqual 592 | * @function 593 | */ 594 | strictEqual: function( actual, expected, message ) { 595 | QUnit.push( expected === actual, actual, expected, message ); 596 | }, 597 | 598 | /** 599 | * @name notStrictEqual 600 | * @function 601 | */ 602 | notStrictEqual: function( actual, expected, message ) { 603 | QUnit.push( expected !== actual, actual, expected, message ); 604 | }, 605 | 606 | "throws": function( block, expected, message ) { 607 | var actual, 608 | expectedOutput = expected, 609 | ok = false; 610 | 611 | // 'expected' is optional 612 | if ( typeof expected === "string" ) { 613 | message = expected; 614 | expected = null; 615 | } 616 | 617 | config.current.ignoreGlobalErrors = true; 618 | try { 619 | block.call( config.current.testEnvironment ); 620 | } catch (e) { 621 | actual = e; 622 | } 623 | config.current.ignoreGlobalErrors = false; 624 | 625 | if ( actual ) { 626 | // we don't want to validate thrown error 627 | if ( !expected ) { 628 | ok = true; 629 | expectedOutput = null; 630 | // expected is a regexp 631 | } else if ( QUnit.objectType( expected ) === "regexp" ) { 632 | ok = expected.test( errorString( actual ) ); 633 | // expected is a constructor 634 | } else if ( actual instanceof expected ) { 635 | ok = true; 636 | // expected is a validation function which returns true is validation passed 637 | } else if ( expected.call( {}, actual ) === true ) { 638 | expectedOutput = null; 639 | ok = true; 640 | } 641 | 642 | QUnit.push( ok, actual, expectedOutput, message ); 643 | } else { 644 | QUnit.pushFailure( message, null, 'No exception was thrown.' ); 645 | } 646 | } 647 | }; 648 | 649 | /** 650 | * @deprecate since 1.8.0 651 | * Kept assertion helpers in root for backwards compatibility. 652 | */ 653 | extend( QUnit, assert ); 654 | 655 | /** 656 | * @deprecated since 1.9.0 657 | * Kept root "raises()" for backwards compatibility. 658 | * (Note that we don't introduce assert.raises). 659 | */ 660 | QUnit.raises = assert[ "throws" ]; 661 | 662 | /** 663 | * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 664 | * Kept to avoid TypeErrors for undefined methods. 665 | */ 666 | QUnit.equals = function() { 667 | QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); 668 | }; 669 | QUnit.same = function() { 670 | QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); 671 | }; 672 | 673 | // We want access to the constructor's prototype 674 | (function() { 675 | function F() {} 676 | F.prototype = QUnit; 677 | QUnit = new F(); 678 | // Make F QUnit's constructor so that we can add to the prototype later 679 | QUnit.constructor = F; 680 | }()); 681 | 682 | /** 683 | * Config object: Maintain internal state 684 | * Later exposed as QUnit.config 685 | * `config` initialized at top of scope 686 | */ 687 | config = { 688 | // The queue of tests to run 689 | queue: [], 690 | 691 | // block until document ready 692 | blocking: true, 693 | 694 | // when enabled, show only failing tests 695 | // gets persisted through sessionStorage and can be changed in UI via checkbox 696 | hidepassed: false, 697 | 698 | // by default, run previously failed tests first 699 | // very useful in combination with "Hide passed tests" checked 700 | reorder: true, 701 | 702 | // by default, modify document.title when suite is done 703 | altertitle: true, 704 | 705 | // when enabled, all tests must call expect() 706 | requireExpects: false, 707 | 708 | // add checkboxes that are persisted in the query-string 709 | // when enabled, the id is set to `true` as a `QUnit.config` property 710 | urlConfig: [ 711 | { 712 | id: "noglobals", 713 | label: "Check for Globals", 714 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." 715 | }, 716 | { 717 | id: "notrycatch", 718 | label: "No try-catch", 719 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." 720 | } 721 | ], 722 | 723 | // Set of all modules. 724 | modules: {}, 725 | 726 | // logging callback queues 727 | begin: [], 728 | done: [], 729 | log: [], 730 | testStart: [], 731 | testDone: [], 732 | moduleStart: [], 733 | moduleDone: [] 734 | }; 735 | 736 | // Export global variables, unless an 'exports' object exists, 737 | // in that case we assume we're in CommonJS (dealt with on the bottom of the script) 738 | if ( typeof exports === "undefined" ) { 739 | extend( window, QUnit ); 740 | 741 | // Expose QUnit object 742 | window.QUnit = QUnit; 743 | } 744 | 745 | // Initialize more QUnit.config and QUnit.urlParams 746 | (function() { 747 | var i, 748 | location = window.location || { search: "", protocol: "file:" }, 749 | params = location.search.slice( 1 ).split( "&" ), 750 | length = params.length, 751 | urlParams = {}, 752 | current; 753 | 754 | if ( params[ 0 ] ) { 755 | for ( i = 0; i < length; i++ ) { 756 | current = params[ i ].split( "=" ); 757 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 758 | // allow just a key to turn on a flag, e.g., test.html?noglobals 759 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 760 | urlParams[ current[ 0 ] ] = current[ 1 ]; 761 | } 762 | } 763 | 764 | QUnit.urlParams = urlParams; 765 | 766 | // String search anywhere in moduleName+testName 767 | config.filter = urlParams.filter; 768 | 769 | // Exact match of the module name 770 | config.module = urlParams.module; 771 | 772 | config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; 773 | 774 | // Figure out if we're running the tests from a server or not 775 | QUnit.isLocal = location.protocol === "file:"; 776 | }()); 777 | 778 | // Extend QUnit object, 779 | // these after set here because they should not be exposed as global functions 780 | extend( QUnit, { 781 | assert: assert, 782 | 783 | config: config, 784 | 785 | // Initialize the configuration options 786 | init: function() { 787 | extend( config, { 788 | stats: { all: 0, bad: 0 }, 789 | moduleStats: { all: 0, bad: 0 }, 790 | started: +new Date(), 791 | updateRate: 1000, 792 | blocking: false, 793 | autostart: true, 794 | autorun: false, 795 | filter: "", 796 | queue: [], 797 | semaphore: 1 798 | }); 799 | 800 | var tests, banner, result, 801 | qunit = id( "qunit" ); 802 | 803 | if ( qunit ) { 804 | qunit.innerHTML = 805 | "

" + escapeText( document.title ) + "

" + 806 | "

" + 807 | "
" + 808 | "

" + 809 | "
    "; 810 | } 811 | 812 | tests = id( "qunit-tests" ); 813 | banner = id( "qunit-banner" ); 814 | result = id( "qunit-testresult" ); 815 | 816 | if ( tests ) { 817 | tests.innerHTML = ""; 818 | } 819 | 820 | if ( banner ) { 821 | banner.className = ""; 822 | } 823 | 824 | if ( result ) { 825 | result.parentNode.removeChild( result ); 826 | } 827 | 828 | if ( tests ) { 829 | result = document.createElement( "p" ); 830 | result.id = "qunit-testresult"; 831 | result.className = "result"; 832 | tests.parentNode.insertBefore( result, tests ); 833 | result.innerHTML = "Running...
     "; 834 | } 835 | }, 836 | 837 | // Resets the test setup. Useful for tests that modify the DOM. 838 | reset: function() { 839 | var fixture = id( "qunit-fixture" ); 840 | if ( fixture ) { 841 | fixture.innerHTML = config.fixture; 842 | } 843 | }, 844 | 845 | // Trigger an event on an element. 846 | // @example triggerEvent( document.body, "click" ); 847 | triggerEvent: function( elem, type, event ) { 848 | if ( document.createEvent ) { 849 | event = document.createEvent( "MouseEvents" ); 850 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 851 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 852 | 853 | elem.dispatchEvent( event ); 854 | } else if ( elem.fireEvent ) { 855 | elem.fireEvent( "on" + type ); 856 | } 857 | }, 858 | 859 | // Safe object type checking 860 | is: function( type, obj ) { 861 | return QUnit.objectType( obj ) === type; 862 | }, 863 | 864 | objectType: function( obj ) { 865 | if ( typeof obj === "undefined" ) { 866 | return "undefined"; 867 | // consider: typeof null === object 868 | } 869 | if ( obj === null ) { 870 | return "null"; 871 | } 872 | 873 | var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), 874 | type = match && match[1] || ""; 875 | 876 | switch ( type ) { 877 | case "Number": 878 | if ( isNaN(obj) ) { 879 | return "nan"; 880 | } 881 | return "number"; 882 | case "String": 883 | case "Boolean": 884 | case "Array": 885 | case "Date": 886 | case "RegExp": 887 | case "Function": 888 | return type.toLowerCase(); 889 | } 890 | if ( typeof obj === "object" ) { 891 | return "object"; 892 | } 893 | return undefined; 894 | }, 895 | 896 | push: function( result, actual, expected, message ) { 897 | if ( !config.current ) { 898 | throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); 899 | } 900 | 901 | var output, source, 902 | details = { 903 | module: config.current.module, 904 | name: config.current.testName, 905 | result: result, 906 | message: message, 907 | actual: actual, 908 | expected: expected 909 | }; 910 | 911 | message = escapeText( message ) || ( result ? "okay" : "failed" ); 912 | message = "" + message + ""; 913 | output = message; 914 | 915 | if ( !result ) { 916 | expected = escapeText( QUnit.jsDump.parse(expected) ); 917 | actual = escapeText( QUnit.jsDump.parse(actual) ); 918 | output += ""; 919 | 920 | if ( actual !== expected ) { 921 | output += ""; 922 | output += ""; 923 | } 924 | 925 | source = sourceFromStacktrace(); 926 | 927 | if ( source ) { 928 | details.source = source; 929 | output += ""; 930 | } 931 | 932 | output += "
    Expected:
    " + expected + "
    Result:
    " + actual + "
    Diff:
    " + QUnit.diff( expected, actual ) + "
    Source:
    " + escapeText( source ) + "
    "; 933 | } 934 | 935 | runLoggingCallbacks( "log", QUnit, details ); 936 | 937 | config.current.assertions.push({ 938 | result: !!result, 939 | message: output 940 | }); 941 | }, 942 | 943 | pushFailure: function( message, source, actual ) { 944 | if ( !config.current ) { 945 | throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); 946 | } 947 | 948 | var output, 949 | details = { 950 | module: config.current.module, 951 | name: config.current.testName, 952 | result: false, 953 | message: message 954 | }; 955 | 956 | message = escapeText( message ) || "error"; 957 | message = "" + message + ""; 958 | output = message; 959 | 960 | output += ""; 961 | 962 | if ( actual ) { 963 | output += ""; 964 | } 965 | 966 | if ( source ) { 967 | details.source = source; 968 | output += ""; 969 | } 970 | 971 | output += "
    Result:
    " + escapeText( actual ) + "
    Source:
    " + escapeText( source ) + "
    "; 972 | 973 | runLoggingCallbacks( "log", QUnit, details ); 974 | 975 | config.current.assertions.push({ 976 | result: false, 977 | message: output 978 | }); 979 | }, 980 | 981 | url: function( params ) { 982 | params = extend( extend( {}, QUnit.urlParams ), params ); 983 | var key, 984 | querystring = "?"; 985 | 986 | for ( key in params ) { 987 | if ( !hasOwn.call( params, key ) ) { 988 | continue; 989 | } 990 | querystring += encodeURIComponent( key ) + "=" + 991 | encodeURIComponent( params[ key ] ) + "&"; 992 | } 993 | return window.location.protocol + "//" + window.location.host + 994 | window.location.pathname + querystring.slice( 0, -1 ); 995 | }, 996 | 997 | extend: extend, 998 | id: id, 999 | addEvent: addEvent 1000 | // load, equiv, jsDump, diff: Attached later 1001 | }); 1002 | 1003 | /** 1004 | * @deprecated: Created for backwards compatibility with test runner that set the hook function 1005 | * into QUnit.{hook}, instead of invoking it and passing the hook function. 1006 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. 1007 | * Doing this allows us to tell if the following methods have been overwritten on the actual 1008 | * QUnit object. 1009 | */ 1010 | extend( QUnit.constructor.prototype, { 1011 | 1012 | // Logging callbacks; all receive a single argument with the listed properties 1013 | // run test/logs.html for any related changes 1014 | begin: registerLoggingCallback( "begin" ), 1015 | 1016 | // done: { failed, passed, total, runtime } 1017 | done: registerLoggingCallback( "done" ), 1018 | 1019 | // log: { result, actual, expected, message } 1020 | log: registerLoggingCallback( "log" ), 1021 | 1022 | // testStart: { name } 1023 | testStart: registerLoggingCallback( "testStart" ), 1024 | 1025 | // testDone: { name, failed, passed, total, duration } 1026 | testDone: registerLoggingCallback( "testDone" ), 1027 | 1028 | // moduleStart: { name } 1029 | moduleStart: registerLoggingCallback( "moduleStart" ), 1030 | 1031 | // moduleDone: { name, failed, passed, total } 1032 | moduleDone: registerLoggingCallback( "moduleDone" ) 1033 | }); 1034 | 1035 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 1036 | config.autorun = true; 1037 | } 1038 | 1039 | QUnit.load = function() { 1040 | runLoggingCallbacks( "begin", QUnit, {} ); 1041 | 1042 | // Initialize the config, saving the execution queue 1043 | var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, 1044 | urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter, 1045 | numModules = 0, 1046 | moduleFilterHtml = "", 1047 | urlConfigHtml = "", 1048 | oldconfig = extend( {}, config ); 1049 | 1050 | QUnit.init(); 1051 | extend(config, oldconfig); 1052 | 1053 | config.blocking = false; 1054 | 1055 | len = config.urlConfig.length; 1056 | 1057 | for ( i = 0; i < len; i++ ) { 1058 | val = config.urlConfig[i]; 1059 | if ( typeof val === "string" ) { 1060 | val = { 1061 | id: val, 1062 | label: val, 1063 | tooltip: "[no tooltip available]" 1064 | }; 1065 | } 1066 | config[ val.id ] = QUnit.urlParams[ val.id ]; 1067 | urlConfigHtml += ""; 1073 | } 1074 | 1075 | moduleFilterHtml += ""; 1088 | 1089 | // `userAgent` initialized at top of scope 1090 | userAgent = id( "qunit-userAgent" ); 1091 | if ( userAgent ) { 1092 | userAgent.innerHTML = navigator.userAgent; 1093 | } 1094 | 1095 | // `banner` initialized at top of scope 1096 | banner = id( "qunit-header" ); 1097 | if ( banner ) { 1098 | banner.innerHTML = "" + banner.innerHTML + " "; 1099 | } 1100 | 1101 | // `toolbar` initialized at top of scope 1102 | toolbar = id( "qunit-testrunner-toolbar" ); 1103 | if ( toolbar ) { 1104 | // `filter` initialized at top of scope 1105 | filter = document.createElement( "input" ); 1106 | filter.type = "checkbox"; 1107 | filter.id = "qunit-filter-pass"; 1108 | 1109 | addEvent( filter, "click", function() { 1110 | var tmp, 1111 | ol = document.getElementById( "qunit-tests" ); 1112 | 1113 | if ( filter.checked ) { 1114 | ol.className = ol.className + " hidepass"; 1115 | } else { 1116 | tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 1117 | ol.className = tmp.replace( / hidepass /, " " ); 1118 | } 1119 | if ( defined.sessionStorage ) { 1120 | if (filter.checked) { 1121 | sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); 1122 | } else { 1123 | sessionStorage.removeItem( "qunit-filter-passed-tests" ); 1124 | } 1125 | } 1126 | }); 1127 | 1128 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { 1129 | filter.checked = true; 1130 | // `ol` initialized at top of scope 1131 | ol = document.getElementById( "qunit-tests" ); 1132 | ol.className = ol.className + " hidepass"; 1133 | } 1134 | toolbar.appendChild( filter ); 1135 | 1136 | // `label` initialized at top of scope 1137 | label = document.createElement( "label" ); 1138 | label.setAttribute( "for", "qunit-filter-pass" ); 1139 | label.setAttribute( "title", "Only show tests and assertons that fail. Stored in sessionStorage." ); 1140 | label.innerHTML = "Hide passed tests"; 1141 | toolbar.appendChild( label ); 1142 | 1143 | urlConfigCheckboxesContainer = document.createElement("span"); 1144 | urlConfigCheckboxesContainer.innerHTML = urlConfigHtml; 1145 | urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input"); 1146 | // For oldIE support: 1147 | // * Add handlers to the individual elements instead of the container 1148 | // * Use "click" instead of "change" 1149 | // * Fallback from event.target to event.srcElement 1150 | addEvents( urlConfigCheckboxes, "click", function( event ) { 1151 | var params = {}, 1152 | target = event.target || event.srcElement; 1153 | params[ target.name ] = target.checked ? true : undefined; 1154 | window.location = QUnit.url( params ); 1155 | }); 1156 | toolbar.appendChild( urlConfigCheckboxesContainer ); 1157 | 1158 | if (numModules > 1) { 1159 | moduleFilter = document.createElement( 'span' ); 1160 | moduleFilter.setAttribute( 'id', 'qunit-modulefilter-container' ); 1161 | moduleFilter.innerHTML = moduleFilterHtml; 1162 | addEvent( moduleFilter.lastChild, "change", function() { 1163 | var selectBox = moduleFilter.getElementsByTagName("select")[0], 1164 | selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); 1165 | 1166 | window.location = QUnit.url( { module: ( selectedModule === "" ) ? undefined : selectedModule } ); 1167 | }); 1168 | toolbar.appendChild(moduleFilter); 1169 | } 1170 | } 1171 | 1172 | // `main` initialized at top of scope 1173 | main = id( "qunit-fixture" ); 1174 | if ( main ) { 1175 | config.fixture = main.innerHTML; 1176 | } 1177 | 1178 | if ( config.autostart ) { 1179 | QUnit.start(); 1180 | } 1181 | }; 1182 | 1183 | addEvent( window, "load", QUnit.load ); 1184 | 1185 | // `onErrorFnPrev` initialized at top of scope 1186 | // Preserve other handlers 1187 | onErrorFnPrev = window.onerror; 1188 | 1189 | // Cover uncaught exceptions 1190 | // Returning true will surpress the default browser handler, 1191 | // returning false will let it run. 1192 | window.onerror = function ( error, filePath, linerNr ) { 1193 | var ret = false; 1194 | if ( onErrorFnPrev ) { 1195 | ret = onErrorFnPrev( error, filePath, linerNr ); 1196 | } 1197 | 1198 | // Treat return value as window.onerror itself does, 1199 | // Only do our handling if not surpressed. 1200 | if ( ret !== true ) { 1201 | if ( QUnit.config.current ) { 1202 | if ( QUnit.config.current.ignoreGlobalErrors ) { 1203 | return true; 1204 | } 1205 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1206 | } else { 1207 | QUnit.test( "global failure", extend( function() { 1208 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1209 | }, { validTest: validTest } ) ); 1210 | } 1211 | return false; 1212 | } 1213 | 1214 | return ret; 1215 | }; 1216 | 1217 | function done() { 1218 | config.autorun = true; 1219 | 1220 | // Log the last module results 1221 | if ( config.currentModule ) { 1222 | runLoggingCallbacks( "moduleDone", QUnit, { 1223 | name: config.currentModule, 1224 | failed: config.moduleStats.bad, 1225 | passed: config.moduleStats.all - config.moduleStats.bad, 1226 | total: config.moduleStats.all 1227 | }); 1228 | } 1229 | 1230 | var i, key, 1231 | banner = id( "qunit-banner" ), 1232 | tests = id( "qunit-tests" ), 1233 | runtime = +new Date() - config.started, 1234 | passed = config.stats.all - config.stats.bad, 1235 | html = [ 1236 | "Tests completed in ", 1237 | runtime, 1238 | " milliseconds.
    ", 1239 | "", 1240 | passed, 1241 | " assertions of ", 1242 | config.stats.all, 1243 | " passed, ", 1244 | config.stats.bad, 1245 | " failed." 1246 | ].join( "" ); 1247 | 1248 | if ( banner ) { 1249 | banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); 1250 | } 1251 | 1252 | if ( tests ) { 1253 | id( "qunit-testresult" ).innerHTML = html; 1254 | } 1255 | 1256 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 1257 | // show ✖ for good, ✔ for bad suite result in title 1258 | // use escape sequences in case file gets loaded with non-utf-8-charset 1259 | document.title = [ 1260 | ( config.stats.bad ? "\u2716" : "\u2714" ), 1261 | document.title.replace( /^[\u2714\u2716] /i, "" ) 1262 | ].join( " " ); 1263 | } 1264 | 1265 | // clear own sessionStorage items if all tests passed 1266 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 1267 | // `key` & `i` initialized at top of scope 1268 | for ( i = 0; i < sessionStorage.length; i++ ) { 1269 | key = sessionStorage.key( i++ ); 1270 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 1271 | sessionStorage.removeItem( key ); 1272 | } 1273 | } 1274 | } 1275 | 1276 | // scroll back to top to show results 1277 | if ( window.scrollTo ) { 1278 | window.scrollTo(0, 0); 1279 | } 1280 | 1281 | runLoggingCallbacks( "done", QUnit, { 1282 | failed: config.stats.bad, 1283 | passed: passed, 1284 | total: config.stats.all, 1285 | runtime: runtime 1286 | }); 1287 | } 1288 | 1289 | /** @return Boolean: true if this test should be ran */ 1290 | function validTest( test ) { 1291 | var include, 1292 | filter = config.filter && config.filter.toLowerCase(), 1293 | module = config.module && config.module.toLowerCase(), 1294 | fullName = (test.module + ": " + test.testName).toLowerCase(); 1295 | 1296 | // Internally-generated tests are always valid 1297 | if ( test.callback && test.callback.validTest === validTest ) { 1298 | delete test.callback.validTest; 1299 | return true; 1300 | } 1301 | 1302 | if ( config.testNumber ) { 1303 | return test.testNumber === config.testNumber; 1304 | } 1305 | 1306 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { 1307 | return false; 1308 | } 1309 | 1310 | if ( !filter ) { 1311 | return true; 1312 | } 1313 | 1314 | include = filter.charAt( 0 ) !== "!"; 1315 | if ( !include ) { 1316 | filter = filter.slice( 1 ); 1317 | } 1318 | 1319 | // If the filter matches, we need to honour include 1320 | if ( fullName.indexOf( filter ) !== -1 ) { 1321 | return include; 1322 | } 1323 | 1324 | // Otherwise, do the opposite 1325 | return !include; 1326 | } 1327 | 1328 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) 1329 | // Later Safari and IE10 are supposed to support error.stack as well 1330 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 1331 | function extractStacktrace( e, offset ) { 1332 | offset = offset === undefined ? 3 : offset; 1333 | 1334 | var stack, include, i; 1335 | 1336 | if ( e.stacktrace ) { 1337 | // Opera 1338 | return e.stacktrace.split( "\n" )[ offset + 3 ]; 1339 | } else if ( e.stack ) { 1340 | // Firefox, Chrome 1341 | stack = e.stack.split( "\n" ); 1342 | if (/^error$/i.test( stack[0] ) ) { 1343 | stack.shift(); 1344 | } 1345 | if ( fileName ) { 1346 | include = []; 1347 | for ( i = offset; i < stack.length; i++ ) { 1348 | if ( stack[ i ].indexOf( fileName ) !== -1 ) { 1349 | break; 1350 | } 1351 | include.push( stack[ i ] ); 1352 | } 1353 | if ( include.length ) { 1354 | return include.join( "\n" ); 1355 | } 1356 | } 1357 | return stack[ offset ]; 1358 | } else if ( e.sourceURL ) { 1359 | // Safari, PhantomJS 1360 | // hopefully one day Safari provides actual stacktraces 1361 | // exclude useless self-reference for generated Error objects 1362 | if ( /qunit.js$/.test( e.sourceURL ) ) { 1363 | return; 1364 | } 1365 | // for actual exceptions, this is useful 1366 | return e.sourceURL + ":" + e.line; 1367 | } 1368 | } 1369 | function sourceFromStacktrace( offset ) { 1370 | try { 1371 | throw new Error(); 1372 | } catch ( e ) { 1373 | return extractStacktrace( e, offset ); 1374 | } 1375 | } 1376 | 1377 | /** 1378 | * Escape text for attribute or text content. 1379 | */ 1380 | function escapeText( s ) { 1381 | if ( !s ) { 1382 | return ""; 1383 | } 1384 | s = s + ""; 1385 | // Both single quotes and double quotes (for attributes) 1386 | return s.replace( /['"<>&]/g, function( s ) { 1387 | switch( s ) { 1388 | case '\'': 1389 | return '''; 1390 | case '"': 1391 | return '"'; 1392 | case '<': 1393 | return '<'; 1394 | case '>': 1395 | return '>'; 1396 | case '&': 1397 | return '&'; 1398 | } 1399 | }); 1400 | } 1401 | 1402 | function synchronize( callback, last ) { 1403 | config.queue.push( callback ); 1404 | 1405 | if ( config.autorun && !config.blocking ) { 1406 | process( last ); 1407 | } 1408 | } 1409 | 1410 | function process( last ) { 1411 | function next() { 1412 | process( last ); 1413 | } 1414 | var start = new Date().getTime(); 1415 | config.depth = config.depth ? config.depth + 1 : 1; 1416 | 1417 | while ( config.queue.length && !config.blocking ) { 1418 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 1419 | config.queue.shift()(); 1420 | } else { 1421 | window.setTimeout( next, 13 ); 1422 | break; 1423 | } 1424 | } 1425 | config.depth--; 1426 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 1427 | done(); 1428 | } 1429 | } 1430 | 1431 | function saveGlobal() { 1432 | config.pollution = []; 1433 | 1434 | if ( config.noglobals ) { 1435 | for ( var key in window ) { 1436 | // in Opera sometimes DOM element ids show up here, ignore them 1437 | if ( !hasOwn.call( window, key ) || /^qunit-test-output/.test( key ) ) { 1438 | continue; 1439 | } 1440 | config.pollution.push( key ); 1441 | } 1442 | } 1443 | } 1444 | 1445 | function checkPollution() { 1446 | var newGlobals, 1447 | deletedGlobals, 1448 | old = config.pollution; 1449 | 1450 | saveGlobal(); 1451 | 1452 | newGlobals = diff( config.pollution, old ); 1453 | if ( newGlobals.length > 0 ) { 1454 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 1455 | } 1456 | 1457 | deletedGlobals = diff( old, config.pollution ); 1458 | if ( deletedGlobals.length > 0 ) { 1459 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1460 | } 1461 | } 1462 | 1463 | // returns a new Array with the elements that are in a but not in b 1464 | function diff( a, b ) { 1465 | var i, j, 1466 | result = a.slice(); 1467 | 1468 | for ( i = 0; i < result.length; i++ ) { 1469 | for ( j = 0; j < b.length; j++ ) { 1470 | if ( result[i] === b[j] ) { 1471 | result.splice( i, 1 ); 1472 | i--; 1473 | break; 1474 | } 1475 | } 1476 | } 1477 | return result; 1478 | } 1479 | 1480 | function extend( a, b ) { 1481 | for ( var prop in b ) { 1482 | if ( b[ prop ] === undefined ) { 1483 | delete a[ prop ]; 1484 | 1485 | // Avoid "Member not found" error in IE8 caused by setting window.constructor 1486 | } else if ( prop !== "constructor" || a !== window ) { 1487 | a[ prop ] = b[ prop ]; 1488 | } 1489 | } 1490 | 1491 | return a; 1492 | } 1493 | 1494 | /** 1495 | * @param {HTMLElement} elem 1496 | * @param {string} type 1497 | * @param {Function} fn 1498 | */ 1499 | function addEvent( elem, type, fn ) { 1500 | // Standards-based browsers 1501 | if ( elem.addEventListener ) { 1502 | elem.addEventListener( type, fn, false ); 1503 | // IE 1504 | } else { 1505 | elem.attachEvent( "on" + type, fn ); 1506 | } 1507 | } 1508 | 1509 | /** 1510 | * @param {Array|NodeList} elems 1511 | * @param {string} type 1512 | * @param {Function} fn 1513 | */ 1514 | function addEvents( elems, type, fn ) { 1515 | var i = elems.length; 1516 | while ( i-- ) { 1517 | addEvent( elems[i], type, fn ); 1518 | } 1519 | } 1520 | 1521 | function hasClass( elem, name ) { 1522 | return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; 1523 | } 1524 | 1525 | function addClass( elem, name ) { 1526 | if ( !hasClass( elem, name ) ) { 1527 | elem.className += (elem.className ? " " : "") + name; 1528 | } 1529 | } 1530 | 1531 | function removeClass( elem, name ) { 1532 | var set = " " + elem.className + " "; 1533 | // Class name may appear multiple times 1534 | while ( set.indexOf(" " + name + " ") > -1 ) { 1535 | set = set.replace(" " + name + " " , " "); 1536 | } 1537 | // If possible, trim it for prettiness, but not neccecarily 1538 | elem.className = window.jQuery ? jQuery.trim( set ) : ( set.trim ? set.trim() : set ); 1539 | } 1540 | 1541 | function id( name ) { 1542 | return !!( typeof document !== "undefined" && document && document.getElementById ) && 1543 | document.getElementById( name ); 1544 | } 1545 | 1546 | function registerLoggingCallback( key ) { 1547 | return function( callback ) { 1548 | config[key].push( callback ); 1549 | }; 1550 | } 1551 | 1552 | // Supports deprecated method of completely overwriting logging callbacks 1553 | function runLoggingCallbacks( key, scope, args ) { 1554 | var i, callbacks; 1555 | if ( QUnit.hasOwnProperty( key ) ) { 1556 | QUnit[ key ].call(scope, args ); 1557 | } else { 1558 | callbacks = config[ key ]; 1559 | for ( i = 0; i < callbacks.length; i++ ) { 1560 | callbacks[ i ].call( scope, args ); 1561 | } 1562 | } 1563 | } 1564 | 1565 | // Test for equality any JavaScript type. 1566 | // Author: Philippe Rathé 1567 | QUnit.equiv = (function() { 1568 | 1569 | // Call the o related callback with the given arguments. 1570 | function bindCallbacks( o, callbacks, args ) { 1571 | var prop = QUnit.objectType( o ); 1572 | if ( prop ) { 1573 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { 1574 | return callbacks[ prop ].apply( callbacks, args ); 1575 | } else { 1576 | return callbacks[ prop ]; // or undefined 1577 | } 1578 | } 1579 | } 1580 | 1581 | // the real equiv function 1582 | var innerEquiv, 1583 | // stack to decide between skip/abort functions 1584 | callers = [], 1585 | // stack to avoiding loops from circular referencing 1586 | parents = [], 1587 | 1588 | getProto = Object.getPrototypeOf || function ( obj ) { 1589 | return obj.__proto__; 1590 | }, 1591 | callbacks = (function () { 1592 | 1593 | // for string, boolean, number and null 1594 | function useStrictEquality( b, a ) { 1595 | /*jshint eqeqeq:false */ 1596 | if ( b instanceof a.constructor || a instanceof b.constructor ) { 1597 | // to catch short annotaion VS 'new' annotation of a 1598 | // declaration 1599 | // e.g. var i = 1; 1600 | // var j = new Number(1); 1601 | return a == b; 1602 | } else { 1603 | return a === b; 1604 | } 1605 | } 1606 | 1607 | return { 1608 | "string": useStrictEquality, 1609 | "boolean": useStrictEquality, 1610 | "number": useStrictEquality, 1611 | "null": useStrictEquality, 1612 | "undefined": useStrictEquality, 1613 | 1614 | "nan": function( b ) { 1615 | return isNaN( b ); 1616 | }, 1617 | 1618 | "date": function( b, a ) { 1619 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); 1620 | }, 1621 | 1622 | "regexp": function( b, a ) { 1623 | return QUnit.objectType( b ) === "regexp" && 1624 | // the regex itself 1625 | a.source === b.source && 1626 | // and its modifers 1627 | a.global === b.global && 1628 | // (gmi) ... 1629 | a.ignoreCase === b.ignoreCase && 1630 | a.multiline === b.multiline && 1631 | a.sticky === b.sticky; 1632 | }, 1633 | 1634 | // - skip when the property is a method of an instance (OOP) 1635 | // - abort otherwise, 1636 | // initial === would have catch identical references anyway 1637 | "function": function() { 1638 | var caller = callers[callers.length - 1]; 1639 | return caller !== Object && typeof caller !== "undefined"; 1640 | }, 1641 | 1642 | "array": function( b, a ) { 1643 | var i, j, len, loop; 1644 | 1645 | // b could be an object literal here 1646 | if ( QUnit.objectType( b ) !== "array" ) { 1647 | return false; 1648 | } 1649 | 1650 | len = a.length; 1651 | if ( len !== b.length ) { 1652 | // safe and faster 1653 | return false; 1654 | } 1655 | 1656 | // track reference to avoid circular references 1657 | parents.push( a ); 1658 | for ( i = 0; i < len; i++ ) { 1659 | loop = false; 1660 | for ( j = 0; j < parents.length; j++ ) { 1661 | if ( parents[j] === a[i] ) { 1662 | loop = true;// dont rewalk array 1663 | } 1664 | } 1665 | if ( !loop && !innerEquiv(a[i], b[i]) ) { 1666 | parents.pop(); 1667 | return false; 1668 | } 1669 | } 1670 | parents.pop(); 1671 | return true; 1672 | }, 1673 | 1674 | "object": function( b, a ) { 1675 | var i, j, loop, 1676 | // Default to true 1677 | eq = true, 1678 | aProperties = [], 1679 | bProperties = []; 1680 | 1681 | // comparing constructors is more strict than using 1682 | // instanceof 1683 | if ( a.constructor !== b.constructor ) { 1684 | // Allow objects with no prototype to be equivalent to 1685 | // objects with Object as their constructor. 1686 | if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) || 1687 | ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) { 1688 | return false; 1689 | } 1690 | } 1691 | 1692 | // stack constructor before traversing properties 1693 | callers.push( a.constructor ); 1694 | // track reference to avoid circular references 1695 | parents.push( a ); 1696 | 1697 | for ( i in a ) { // be strict: don't ensures hasOwnProperty 1698 | // and go deep 1699 | loop = false; 1700 | for ( j = 0; j < parents.length; j++ ) { 1701 | if ( parents[j] === a[i] ) { 1702 | // don't go down the same path twice 1703 | loop = true; 1704 | } 1705 | } 1706 | aProperties.push(i); // collect a's properties 1707 | 1708 | if (!loop && !innerEquiv( a[i], b[i] ) ) { 1709 | eq = false; 1710 | break; 1711 | } 1712 | } 1713 | 1714 | callers.pop(); // unstack, we are done 1715 | parents.pop(); 1716 | 1717 | for ( i in b ) { 1718 | bProperties.push( i ); // collect b's properties 1719 | } 1720 | 1721 | // Ensures identical properties name 1722 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1723 | } 1724 | }; 1725 | }()); 1726 | 1727 | innerEquiv = function() { // can take multiple arguments 1728 | var args = [].slice.apply( arguments ); 1729 | if ( args.length < 2 ) { 1730 | return true; // end transition 1731 | } 1732 | 1733 | return (function( a, b ) { 1734 | if ( a === b ) { 1735 | return true; // catch the most you can 1736 | } else if ( a === null || b === null || typeof a === "undefined" || 1737 | typeof b === "undefined" || 1738 | QUnit.objectType(a) !== QUnit.objectType(b) ) { 1739 | return false; // don't lose time with error prone cases 1740 | } else { 1741 | return bindCallbacks(a, callbacks, [ b, a ]); 1742 | } 1743 | 1744 | // apply transition with (1..n) arguments 1745 | }( args[0], args[1] ) && arguments.callee.apply( this, args.splice(1, args.length - 1 )) ); 1746 | }; 1747 | 1748 | return innerEquiv; 1749 | }()); 1750 | 1751 | /** 1752 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1753 | * http://flesler.blogspot.com Licensed under BSD 1754 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1755 | * 1756 | * @projectDescription Advanced and extensible data dumping for Javascript. 1757 | * @version 1.0.0 1758 | * @author Ariel Flesler 1759 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1760 | */ 1761 | QUnit.jsDump = (function() { 1762 | function quote( str ) { 1763 | return '"' + str.toString().replace( /"/g, '\\"' ) + '"'; 1764 | } 1765 | function literal( o ) { 1766 | return o + ""; 1767 | } 1768 | function join( pre, arr, post ) { 1769 | var s = jsDump.separator(), 1770 | base = jsDump.indent(), 1771 | inner = jsDump.indent(1); 1772 | if ( arr.join ) { 1773 | arr = arr.join( "," + s + inner ); 1774 | } 1775 | if ( !arr ) { 1776 | return pre + post; 1777 | } 1778 | return [ pre, inner + arr, base + post ].join(s); 1779 | } 1780 | function array( arr, stack ) { 1781 | var i = arr.length, ret = new Array(i); 1782 | this.up(); 1783 | while ( i-- ) { 1784 | ret[i] = this.parse( arr[i] , undefined , stack); 1785 | } 1786 | this.down(); 1787 | return join( "[", ret, "]" ); 1788 | } 1789 | 1790 | var reName = /^function (\w+)/, 1791 | jsDump = { 1792 | // type is used mostly internally, you can fix a (custom)type in advance 1793 | parse: function( obj, type, stack ) { 1794 | stack = stack || [ ]; 1795 | var inStack, res, 1796 | parser = this.parsers[ type || this.typeOf(obj) ]; 1797 | 1798 | type = typeof parser; 1799 | inStack = inArray( obj, stack ); 1800 | 1801 | if ( inStack !== -1 ) { 1802 | return "recursion(" + (inStack - stack.length) + ")"; 1803 | } 1804 | if ( type === "function" ) { 1805 | stack.push( obj ); 1806 | res = parser.call( this, obj, stack ); 1807 | stack.pop(); 1808 | return res; 1809 | } 1810 | return ( type === "string" ) ? parser : this.parsers.error; 1811 | }, 1812 | typeOf: function( obj ) { 1813 | var type; 1814 | if ( obj === null ) { 1815 | type = "null"; 1816 | } else if ( typeof obj === "undefined" ) { 1817 | type = "undefined"; 1818 | } else if ( QUnit.is( "regexp", obj) ) { 1819 | type = "regexp"; 1820 | } else if ( QUnit.is( "date", obj) ) { 1821 | type = "date"; 1822 | } else if ( QUnit.is( "function", obj) ) { 1823 | type = "function"; 1824 | } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { 1825 | type = "window"; 1826 | } else if ( obj.nodeType === 9 ) { 1827 | type = "document"; 1828 | } else if ( obj.nodeType ) { 1829 | type = "node"; 1830 | } else if ( 1831 | // native arrays 1832 | toString.call( obj ) === "[object Array]" || 1833 | // NodeList objects 1834 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1835 | ) { 1836 | type = "array"; 1837 | } else if ( obj.constructor === Error.prototype.constructor ) { 1838 | type = "error"; 1839 | } else { 1840 | type = typeof obj; 1841 | } 1842 | return type; 1843 | }, 1844 | separator: function() { 1845 | return this.multiline ? this.HTML ? "
    " : "\n" : this.HTML ? " " : " "; 1846 | }, 1847 | // extra can be a number, shortcut for increasing-calling-decreasing 1848 | indent: function( extra ) { 1849 | if ( !this.multiline ) { 1850 | return ""; 1851 | } 1852 | var chr = this.indentChar; 1853 | if ( this.HTML ) { 1854 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1855 | } 1856 | return new Array( this._depth_ + (extra||0) ).join(chr); 1857 | }, 1858 | up: function( a ) { 1859 | this._depth_ += a || 1; 1860 | }, 1861 | down: function( a ) { 1862 | this._depth_ -= a || 1; 1863 | }, 1864 | setParser: function( name, parser ) { 1865 | this.parsers[name] = parser; 1866 | }, 1867 | // The next 3 are exposed so you can use them 1868 | quote: quote, 1869 | literal: literal, 1870 | join: join, 1871 | // 1872 | _depth_: 1, 1873 | // This is the list of parsers, to modify them, use jsDump.setParser 1874 | parsers: { 1875 | window: "[Window]", 1876 | document: "[Document]", 1877 | error: function(error) { 1878 | return "Error(\"" + error.message + "\")"; 1879 | }, 1880 | unknown: "[Unknown]", 1881 | "null": "null", 1882 | "undefined": "undefined", 1883 | "function": function( fn ) { 1884 | var ret = "function", 1885 | // functions never have name in IE 1886 | name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; 1887 | 1888 | if ( name ) { 1889 | ret += " " + name; 1890 | } 1891 | ret += "( "; 1892 | 1893 | ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); 1894 | return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); 1895 | }, 1896 | array: array, 1897 | nodelist: array, 1898 | "arguments": array, 1899 | object: function( map, stack ) { 1900 | var ret = [ ], keys, key, val, i; 1901 | QUnit.jsDump.up(); 1902 | keys = []; 1903 | for ( key in map ) { 1904 | keys.push( key ); 1905 | } 1906 | keys.sort(); 1907 | for ( i = 0; i < keys.length; i++ ) { 1908 | key = keys[ i ]; 1909 | val = map[ key ]; 1910 | ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); 1911 | } 1912 | QUnit.jsDump.down(); 1913 | return join( "{", ret, "}" ); 1914 | }, 1915 | node: function( node ) { 1916 | var len, i, val, 1917 | open = QUnit.jsDump.HTML ? "<" : "<", 1918 | close = QUnit.jsDump.HTML ? ">" : ">", 1919 | tag = node.nodeName.toLowerCase(), 1920 | ret = open + tag, 1921 | attrs = node.attributes; 1922 | 1923 | if ( attrs ) { 1924 | for ( i = 0, len = attrs.length; i < len; i++ ) { 1925 | val = attrs[i].nodeValue; 1926 | // IE6 includes all attributes in .attributes, even ones not explicitly set. 1927 | // Those have values like undefined, null, 0, false, "" or "inherit". 1928 | if ( val && val !== "inherit" ) { 1929 | ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); 1930 | } 1931 | } 1932 | } 1933 | ret += close; 1934 | 1935 | // Show content of TextNode or CDATASection 1936 | if ( node.nodeType === 3 || node.nodeType === 4 ) { 1937 | ret += node.nodeValue; 1938 | } 1939 | 1940 | return ret + open + "/" + tag + close; 1941 | }, 1942 | // function calls it internally, it's the arguments part of the function 1943 | functionArgs: function( fn ) { 1944 | var args, 1945 | l = fn.length; 1946 | 1947 | if ( !l ) { 1948 | return ""; 1949 | } 1950 | 1951 | args = new Array(l); 1952 | while ( l-- ) { 1953 | // 97 is 'a' 1954 | args[l] = String.fromCharCode(97+l); 1955 | } 1956 | return " " + args.join( ", " ) + " "; 1957 | }, 1958 | // object calls it internally, the key part of an item in a map 1959 | key: quote, 1960 | // function calls it internally, it's the content of the function 1961 | functionCode: "[code]", 1962 | // node calls it internally, it's an html attribute value 1963 | attribute: quote, 1964 | string: quote, 1965 | date: quote, 1966 | regexp: literal, 1967 | number: literal, 1968 | "boolean": literal 1969 | }, 1970 | // if true, entities are escaped ( <, >, \t, space and \n ) 1971 | HTML: false, 1972 | // indentation unit 1973 | indentChar: " ", 1974 | // if true, items in a collection, are separated by a \n, else just a space. 1975 | multiline: true 1976 | }; 1977 | 1978 | return jsDump; 1979 | }()); 1980 | 1981 | // from jquery.js 1982 | function inArray( elem, array ) { 1983 | if ( array.indexOf ) { 1984 | return array.indexOf( elem ); 1985 | } 1986 | 1987 | for ( var i = 0, length = array.length; i < length; i++ ) { 1988 | if ( array[ i ] === elem ) { 1989 | return i; 1990 | } 1991 | } 1992 | 1993 | return -1; 1994 | } 1995 | 1996 | /* 1997 | * Javascript Diff Algorithm 1998 | * By John Resig (http://ejohn.org/) 1999 | * Modified by Chu Alan "sprite" 2000 | * 2001 | * Released under the MIT license. 2002 | * 2003 | * More Info: 2004 | * http://ejohn.org/projects/javascript-diff-algorithm/ 2005 | * 2006 | * Usage: QUnit.diff(expected, actual) 2007 | * 2008 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" 2009 | */ 2010 | QUnit.diff = (function() { 2011 | /*jshint eqeqeq:false, eqnull:true */ 2012 | function diff( o, n ) { 2013 | var i, 2014 | ns = {}, 2015 | os = {}; 2016 | 2017 | for ( i = 0; i < n.length; i++ ) { 2018 | if ( !hasOwn.call( ns, n[i] ) ) { 2019 | ns[ n[i] ] = { 2020 | rows: [], 2021 | o: null 2022 | }; 2023 | } 2024 | ns[ n[i] ].rows.push( i ); 2025 | } 2026 | 2027 | for ( i = 0; i < o.length; i++ ) { 2028 | if ( !hasOwn.call( os, o[i] ) ) { 2029 | os[ o[i] ] = { 2030 | rows: [], 2031 | n: null 2032 | }; 2033 | } 2034 | os[ o[i] ].rows.push( i ); 2035 | } 2036 | 2037 | for ( i in ns ) { 2038 | if ( !hasOwn.call( ns, i ) ) { 2039 | continue; 2040 | } 2041 | if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) { 2042 | n[ ns[i].rows[0] ] = { 2043 | text: n[ ns[i].rows[0] ], 2044 | row: os[i].rows[0] 2045 | }; 2046 | o[ os[i].rows[0] ] = { 2047 | text: o[ os[i].rows[0] ], 2048 | row: ns[i].rows[0] 2049 | }; 2050 | } 2051 | } 2052 | 2053 | for ( i = 0; i < n.length - 1; i++ ) { 2054 | if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && 2055 | n[ i + 1 ] == o[ n[i].row + 1 ] ) { 2056 | 2057 | n[ i + 1 ] = { 2058 | text: n[ i + 1 ], 2059 | row: n[i].row + 1 2060 | }; 2061 | o[ n[i].row + 1 ] = { 2062 | text: o[ n[i].row + 1 ], 2063 | row: i + 1 2064 | }; 2065 | } 2066 | } 2067 | 2068 | for ( i = n.length - 1; i > 0; i-- ) { 2069 | if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && 2070 | n[ i - 1 ] == o[ n[i].row - 1 ]) { 2071 | 2072 | n[ i - 1 ] = { 2073 | text: n[ i - 1 ], 2074 | row: n[i].row - 1 2075 | }; 2076 | o[ n[i].row - 1 ] = { 2077 | text: o[ n[i].row - 1 ], 2078 | row: i - 1 2079 | }; 2080 | } 2081 | } 2082 | 2083 | return { 2084 | o: o, 2085 | n: n 2086 | }; 2087 | } 2088 | 2089 | return function( o, n ) { 2090 | o = o.replace( /\s+$/, "" ); 2091 | n = n.replace( /\s+$/, "" ); 2092 | 2093 | var i, pre, 2094 | str = "", 2095 | out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), 2096 | oSpace = o.match(/\s+/g), 2097 | nSpace = n.match(/\s+/g); 2098 | 2099 | if ( oSpace == null ) { 2100 | oSpace = [ " " ]; 2101 | } 2102 | else { 2103 | oSpace.push( " " ); 2104 | } 2105 | 2106 | if ( nSpace == null ) { 2107 | nSpace = [ " " ]; 2108 | } 2109 | else { 2110 | nSpace.push( " " ); 2111 | } 2112 | 2113 | if ( out.n.length === 0 ) { 2114 | for ( i = 0; i < out.o.length; i++ ) { 2115 | str += "" + out.o[i] + oSpace[i] + ""; 2116 | } 2117 | } 2118 | else { 2119 | if ( out.n[0].text == null ) { 2120 | for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { 2121 | str += "" + out.o[n] + oSpace[n] + ""; 2122 | } 2123 | } 2124 | 2125 | for ( i = 0; i < out.n.length; i++ ) { 2126 | if (out.n[i].text == null) { 2127 | str += "" + out.n[i] + nSpace[i] + ""; 2128 | } 2129 | else { 2130 | // `pre` initialized at top of scope 2131 | pre = ""; 2132 | 2133 | for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { 2134 | pre += "" + out.o[n] + oSpace[n] + ""; 2135 | } 2136 | str += " " + out.n[i].text + nSpace[i] + pre; 2137 | } 2138 | } 2139 | } 2140 | 2141 | return str; 2142 | }; 2143 | }()); 2144 | 2145 | // for CommonJS enviroments, export everything 2146 | if ( typeof exports !== "undefined" ) { 2147 | extend( exports, QUnit ); 2148 | } 2149 | 2150 | // get at whatever the global object is, like window in browsers 2151 | }( (function() {return this;}.call()) )); 2152 | --------------------------------------------------------------------------------