Mercurial > wikked
comparison static/bootstrap/js/tests/vendor/qunit.js @ 88:a5a3d454eac9
Updated Bootstrap.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Fri, 05 Apr 2013 08:08:12 -0700 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
87:c0cf67362fb1 | 88:a5a3d454eac9 |
---|---|
1 /** | |
2 * QUnit - A JavaScript Unit Testing Framework | |
3 * | |
4 * http://docs.jquery.com/QUnit | |
5 * | |
6 * Copyright (c) 2012 John Resig, Jörn Zaefferer | |
7 * Dual licensed under the MIT (MIT-LICENSE.txt) | |
8 * or GPL (GPL-LICENSE.txt) licenses. | |
9 */ | |
10 | |
11 (function(window) { | |
12 | |
13 var defined = { | |
14 setTimeout: typeof window.setTimeout !== "undefined", | |
15 sessionStorage: (function() { | |
16 try { | |
17 return !!sessionStorage.getItem; | |
18 } catch(e) { | |
19 return false; | |
20 } | |
21 })() | |
22 }; | |
23 | |
24 var testId = 0; | |
25 | |
26 var Test = function(name, testName, expected, testEnvironmentArg, async, callback) { | |
27 this.name = name; | |
28 this.testName = testName; | |
29 this.expected = expected; | |
30 this.testEnvironmentArg = testEnvironmentArg; | |
31 this.async = async; | |
32 this.callback = callback; | |
33 this.assertions = []; | |
34 }; | |
35 Test.prototype = { | |
36 init: function() { | |
37 var tests = id("qunit-tests"); | |
38 if (tests) { | |
39 var b = document.createElement("strong"); | |
40 b.innerHTML = "Running " + this.name; | |
41 var li = document.createElement("li"); | |
42 li.appendChild( b ); | |
43 li.className = "running"; | |
44 li.id = this.id = "test-output" + testId++; | |
45 tests.appendChild( li ); | |
46 } | |
47 }, | |
48 setup: function() { | |
49 if (this.module != config.previousModule) { | |
50 if ( config.previousModule ) { | |
51 QUnit.moduleDone( { | |
52 name: config.previousModule, | |
53 failed: config.moduleStats.bad, | |
54 passed: config.moduleStats.all - config.moduleStats.bad, | |
55 total: config.moduleStats.all | |
56 } ); | |
57 } | |
58 config.previousModule = this.module; | |
59 config.moduleStats = { all: 0, bad: 0 }; | |
60 QUnit.moduleStart( { | |
61 name: this.module | |
62 } ); | |
63 } | |
64 | |
65 config.current = this; | |
66 this.testEnvironment = extend({ | |
67 setup: function() {}, | |
68 teardown: function() {} | |
69 }, this.moduleTestEnvironment); | |
70 if (this.testEnvironmentArg) { | |
71 extend(this.testEnvironment, this.testEnvironmentArg); | |
72 } | |
73 | |
74 QUnit.testStart( { | |
75 name: this.testName | |
76 } ); | |
77 | |
78 // allow utility functions to access the current test environment | |
79 // TODO why?? | |
80 QUnit.current_testEnvironment = this.testEnvironment; | |
81 | |
82 try { | |
83 if ( !config.pollution ) { | |
84 saveGlobal(); | |
85 } | |
86 | |
87 this.testEnvironment.setup.call(this.testEnvironment); | |
88 } catch(e) { | |
89 QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message ); | |
90 } | |
91 }, | |
92 run: function() { | |
93 if ( this.async ) { | |
94 QUnit.stop(); | |
95 } | |
96 | |
97 if ( config.notrycatch ) { | |
98 this.callback.call(this.testEnvironment); | |
99 return; | |
100 } | |
101 try { | |
102 this.callback.call(this.testEnvironment); | |
103 } catch(e) { | |
104 fail("Test " + this.testName + " died, exception and test follows", e, this.callback); | |
105 QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) ); | |
106 // else next test will carry the responsibility | |
107 saveGlobal(); | |
108 | |
109 // Restart the tests if they're blocking | |
110 if ( config.blocking ) { | |
111 start(); | |
112 } | |
113 } | |
114 }, | |
115 teardown: function() { | |
116 try { | |
117 this.testEnvironment.teardown.call(this.testEnvironment); | |
118 checkPollution(); | |
119 } catch(e) { | |
120 QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message ); | |
121 } | |
122 }, | |
123 finish: function() { | |
124 if ( this.expected && this.expected != this.assertions.length ) { | |
125 QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); | |
126 } | |
127 | |
128 var good = 0, bad = 0, | |
129 tests = id("qunit-tests"); | |
130 | |
131 config.stats.all += this.assertions.length; | |
132 config.moduleStats.all += this.assertions.length; | |
133 | |
134 if ( tests ) { | |
135 var ol = document.createElement("ol"); | |
136 | |
137 for ( var i = 0; i < this.assertions.length; i++ ) { | |
138 var assertion = this.assertions[i]; | |
139 | |
140 var li = document.createElement("li"); | |
141 li.className = assertion.result ? "pass" : "fail"; | |
142 li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); | |
143 ol.appendChild( li ); | |
144 | |
145 if ( assertion.result ) { | |
146 good++; | |
147 } else { | |
148 bad++; | |
149 config.stats.bad++; | |
150 config.moduleStats.bad++; | |
151 } | |
152 } | |
153 | |
154 // store result when possible | |
155 if ( QUnit.config.reorder && defined.sessionStorage ) { | |
156 if (bad) { | |
157 sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad); | |
158 } else { | |
159 sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName); | |
160 } | |
161 } | |
162 | |
163 if (bad == 0) { | |
164 ol.style.display = "none"; | |
165 } | |
166 | |
167 var b = document.createElement("strong"); | |
168 b.innerHTML = this.name + " <b class='counts'>(<b class='failed'>" + bad + "</b>, <b class='passed'>" + good + "</b>, " + this.assertions.length + ")</b>"; | |
169 | |
170 var a = document.createElement("a"); | |
171 a.innerHTML = "Rerun"; | |
172 a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); | |
173 | |
174 addEvent(b, "click", function() { | |
175 var next = b.nextSibling.nextSibling, | |
176 display = next.style.display; | |
177 next.style.display = display === "none" ? "block" : "none"; | |
178 }); | |
179 | |
180 addEvent(b, "dblclick", function(e) { | |
181 var target = e && e.target ? e.target : window.event.srcElement; | |
182 if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { | |
183 target = target.parentNode; | |
184 } | |
185 if ( window.location && target.nodeName.toLowerCase() === "strong" ) { | |
186 window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); | |
187 } | |
188 }); | |
189 | |
190 var li = id(this.id); | |
191 li.className = bad ? "fail" : "pass"; | |
192 li.removeChild( li.firstChild ); | |
193 li.appendChild( b ); | |
194 li.appendChild( a ); | |
195 li.appendChild( ol ); | |
196 | |
197 } else { | |
198 for ( var i = 0; i < this.assertions.length; i++ ) { | |
199 if ( !this.assertions[i].result ) { | |
200 bad++; | |
201 config.stats.bad++; | |
202 config.moduleStats.bad++; | |
203 } | |
204 } | |
205 } | |
206 | |
207 try { | |
208 QUnit.reset(); | |
209 } catch(e) { | |
210 fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); | |
211 } | |
212 | |
213 QUnit.testDone( { | |
214 name: this.testName, | |
215 failed: bad, | |
216 passed: this.assertions.length - bad, | |
217 total: this.assertions.length | |
218 } ); | |
219 }, | |
220 | |
221 queue: function() { | |
222 var test = this; | |
223 synchronize(function() { | |
224 test.init(); | |
225 }); | |
226 function run() { | |
227 // each of these can by async | |
228 synchronize(function() { | |
229 test.setup(); | |
230 }); | |
231 synchronize(function() { | |
232 test.run(); | |
233 }); | |
234 synchronize(function() { | |
235 test.teardown(); | |
236 }); | |
237 synchronize(function() { | |
238 test.finish(); | |
239 }); | |
240 } | |
241 // defer when previous test run passed, if storage is available | |
242 var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName); | |
243 if (bad) { | |
244 run(); | |
245 } else { | |
246 synchronize(run); | |
247 }; | |
248 } | |
249 | |
250 }; | |
251 | |
252 var QUnit = { | |
253 | |
254 // call on start of module test to prepend name to all tests | |
255 module: function(name, testEnvironment) { | |
256 config.currentModule = name; | |
257 config.currentModuleTestEnviroment = testEnvironment; | |
258 }, | |
259 | |
260 asyncTest: function(testName, expected, callback) { | |
261 if ( arguments.length === 2 ) { | |
262 callback = expected; | |
263 expected = 0; | |
264 } | |
265 | |
266 QUnit.test(testName, expected, callback, true); | |
267 }, | |
268 | |
269 test: function(testName, expected, callback, async) { | |
270 var name = '<span class="test-name">' + testName + '</span>', testEnvironmentArg; | |
271 | |
272 if ( arguments.length === 2 ) { | |
273 callback = expected; | |
274 expected = null; | |
275 } | |
276 // is 2nd argument a testEnvironment? | |
277 if ( expected && typeof expected === 'object') { | |
278 testEnvironmentArg = expected; | |
279 expected = null; | |
280 } | |
281 | |
282 if ( config.currentModule ) { | |
283 name = '<span class="module-name">' + config.currentModule + "</span>: " + name; | |
284 } | |
285 | |
286 if ( !validTest(config.currentModule + ": " + testName) ) { | |
287 return; | |
288 } | |
289 | |
290 var test = new Test(name, testName, expected, testEnvironmentArg, async, callback); | |
291 test.module = config.currentModule; | |
292 test.moduleTestEnvironment = config.currentModuleTestEnviroment; | |
293 test.queue(); | |
294 }, | |
295 | |
296 /** | |
297 * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. | |
298 */ | |
299 expect: function(asserts) { | |
300 config.current.expected = asserts; | |
301 }, | |
302 | |
303 /** | |
304 * Asserts true. | |
305 * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); | |
306 */ | |
307 ok: function(a, msg) { | |
308 a = !!a; | |
309 var details = { | |
310 result: a, | |
311 message: msg | |
312 }; | |
313 msg = escapeHtml(msg); | |
314 QUnit.log(details); | |
315 config.current.assertions.push({ | |
316 result: a, | |
317 message: msg | |
318 }); | |
319 }, | |
320 | |
321 /** | |
322 * Checks that the first two arguments are equal, with an optional message. | |
323 * Prints out both actual and expected values. | |
324 * | |
325 * Prefered to ok( actual == expected, message ) | |
326 * | |
327 * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); | |
328 * | |
329 * @param Object actual | |
330 * @param Object expected | |
331 * @param String message (optional) | |
332 */ | |
333 equal: function(actual, expected, message) { | |
334 QUnit.push(expected == actual, actual, expected, message); | |
335 }, | |
336 | |
337 notEqual: function(actual, expected, message) { | |
338 QUnit.push(expected != actual, actual, expected, message); | |
339 }, | |
340 | |
341 deepEqual: function(actual, expected, message) { | |
342 QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); | |
343 }, | |
344 | |
345 notDeepEqual: function(actual, expected, message) { | |
346 QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); | |
347 }, | |
348 | |
349 strictEqual: function(actual, expected, message) { | |
350 QUnit.push(expected === actual, actual, expected, message); | |
351 }, | |
352 | |
353 notStrictEqual: function(actual, expected, message) { | |
354 QUnit.push(expected !== actual, actual, expected, message); | |
355 }, | |
356 | |
357 raises: function(block, expected, message) { | |
358 var actual, ok = false; | |
359 | |
360 if (typeof expected === 'string') { | |
361 message = expected; | |
362 expected = null; | |
363 } | |
364 | |
365 try { | |
366 block(); | |
367 } catch (e) { | |
368 actual = e; | |
369 } | |
370 | |
371 if (actual) { | |
372 // we don't want to validate thrown error | |
373 if (!expected) { | |
374 ok = true; | |
375 // expected is a regexp | |
376 } else if (QUnit.objectType(expected) === "regexp") { | |
377 ok = expected.test(actual); | |
378 // expected is a constructor | |
379 } else if (actual instanceof expected) { | |
380 ok = true; | |
381 // expected is a validation function which returns true is validation passed | |
382 } else if (expected.call({}, actual) === true) { | |
383 ok = true; | |
384 } | |
385 } | |
386 | |
387 QUnit.ok(ok, message); | |
388 }, | |
389 | |
390 start: function() { | |
391 config.semaphore--; | |
392 if (config.semaphore > 0) { | |
393 // don't start until equal number of stop-calls | |
394 return; | |
395 } | |
396 if (config.semaphore < 0) { | |
397 // ignore if start is called more often then stop | |
398 config.semaphore = 0; | |
399 } | |
400 // A slight delay, to avoid any current callbacks | |
401 if ( defined.setTimeout ) { | |
402 window.setTimeout(function() { | |
403 if (config.semaphore > 0) { | |
404 return; | |
405 } | |
406 if ( config.timeout ) { | |
407 clearTimeout(config.timeout); | |
408 } | |
409 | |
410 config.blocking = false; | |
411 process(); | |
412 }, 13); | |
413 } else { | |
414 config.blocking = false; | |
415 process(); | |
416 } | |
417 }, | |
418 | |
419 stop: function(timeout) { | |
420 config.semaphore++; | |
421 config.blocking = true; | |
422 | |
423 if ( timeout && defined.setTimeout ) { | |
424 clearTimeout(config.timeout); | |
425 config.timeout = window.setTimeout(function() { | |
426 QUnit.ok( false, "Test timed out" ); | |
427 QUnit.start(); | |
428 }, timeout); | |
429 } | |
430 } | |
431 }; | |
432 | |
433 // Backwards compatibility, deprecated | |
434 QUnit.equals = QUnit.equal; | |
435 QUnit.same = QUnit.deepEqual; | |
436 | |
437 // Maintain internal state | |
438 var config = { | |
439 // The queue of tests to run | |
440 queue: [], | |
441 | |
442 // block until document ready | |
443 blocking: true, | |
444 | |
445 // when enabled, show only failing tests | |
446 // gets persisted through sessionStorage and can be changed in UI via checkbox | |
447 hidepassed: false, | |
448 | |
449 // by default, run previously failed tests first | |
450 // very useful in combination with "Hide passed tests" checked | |
451 reorder: true, | |
452 | |
453 // by default, modify document.title when suite is done | |
454 altertitle: true, | |
455 | |
456 urlConfig: ['noglobals', 'notrycatch'] | |
457 }; | |
458 | |
459 // Load paramaters | |
460 (function() { | |
461 var location = window.location || { search: "", protocol: "file:" }, | |
462 params = location.search.slice( 1 ).split( "&" ), | |
463 length = params.length, | |
464 urlParams = {}, | |
465 current; | |
466 | |
467 if ( params[ 0 ] ) { | |
468 for ( var i = 0; i < length; i++ ) { | |
469 current = params[ i ].split( "=" ); | |
470 current[ 0 ] = decodeURIComponent( current[ 0 ] ); | |
471 // allow just a key to turn on a flag, e.g., test.html?noglobals | |
472 current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; | |
473 urlParams[ current[ 0 ] ] = current[ 1 ]; | |
474 } | |
475 } | |
476 | |
477 QUnit.urlParams = urlParams; | |
478 config.filter = urlParams.filter; | |
479 | |
480 // Figure out if we're running the tests from a server or not | |
481 QUnit.isLocal = !!(location.protocol === 'file:'); | |
482 })(); | |
483 | |
484 // Expose the API as global variables, unless an 'exports' | |
485 // object exists, in that case we assume we're in CommonJS | |
486 if ( typeof exports === "undefined" || typeof require === "undefined" ) { | |
487 extend(window, QUnit); | |
488 window.QUnit = QUnit; | |
489 } else { | |
490 extend(exports, QUnit); | |
491 exports.QUnit = QUnit; | |
492 } | |
493 | |
494 // define these after exposing globals to keep them in these QUnit namespace only | |
495 extend(QUnit, { | |
496 config: config, | |
497 | |
498 // Initialize the configuration options | |
499 init: function() { | |
500 extend(config, { | |
501 stats: { all: 0, bad: 0 }, | |
502 moduleStats: { all: 0, bad: 0 }, | |
503 started: +new Date, | |
504 updateRate: 1000, | |
505 blocking: false, | |
506 autostart: true, | |
507 autorun: false, | |
508 filter: "", | |
509 queue: [], | |
510 semaphore: 0 | |
511 }); | |
512 | |
513 var tests = id( "qunit-tests" ), | |
514 banner = id( "qunit-banner" ), | |
515 result = id( "qunit-testresult" ); | |
516 | |
517 if ( tests ) { | |
518 tests.innerHTML = ""; | |
519 } | |
520 | |
521 if ( banner ) { | |
522 banner.className = ""; | |
523 } | |
524 | |
525 if ( result ) { | |
526 result.parentNode.removeChild( result ); | |
527 } | |
528 | |
529 if ( tests ) { | |
530 result = document.createElement( "p" ); | |
531 result.id = "qunit-testresult"; | |
532 result.className = "result"; | |
533 tests.parentNode.insertBefore( result, tests ); | |
534 result.innerHTML = 'Running...<br/> '; | |
535 } | |
536 }, | |
537 | |
538 /** | |
539 * Resets the test setup. Useful for tests that modify the DOM. | |
540 * | |
541 * If jQuery is available, uses jQuery's html(), otherwise just innerHTML. | |
542 */ | |
543 reset: function() { | |
544 if ( window.jQuery ) { | |
545 jQuery( "#qunit-fixture" ).html( config.fixture ); | |
546 } else { | |
547 var main = id( 'qunit-fixture' ); | |
548 if ( main ) { | |
549 main.innerHTML = config.fixture; | |
550 } | |
551 } | |
552 }, | |
553 | |
554 /** | |
555 * Trigger an event on an element. | |
556 * | |
557 * @example triggerEvent( document.body, "click" ); | |
558 * | |
559 * @param DOMElement elem | |
560 * @param String type | |
561 */ | |
562 triggerEvent: function( elem, type, event ) { | |
563 if ( document.createEvent ) { | |
564 event = document.createEvent("MouseEvents"); | |
565 event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, | |
566 0, 0, 0, 0, 0, false, false, false, false, 0, null); | |
567 elem.dispatchEvent( event ); | |
568 | |
569 } else if ( elem.fireEvent ) { | |
570 elem.fireEvent("on"+type); | |
571 } | |
572 }, | |
573 | |
574 // Safe object type checking | |
575 is: function( type, obj ) { | |
576 return QUnit.objectType( obj ) == type; | |
577 }, | |
578 | |
579 objectType: function( obj ) { | |
580 if (typeof obj === "undefined") { | |
581 return "undefined"; | |
582 | |
583 // consider: typeof null === object | |
584 } | |
585 if (obj === null) { | |
586 return "null"; | |
587 } | |
588 | |
589 var type = Object.prototype.toString.call( obj ) | |
590 .match(/^\[object\s(.*)\]$/)[1] || ''; | |
591 | |
592 switch (type) { | |
593 case 'Number': | |
594 if (isNaN(obj)) { | |
595 return "nan"; | |
596 } else { | |
597 return "number"; | |
598 } | |
599 case 'String': | |
600 case 'Boolean': | |
601 case 'Array': | |
602 case 'Date': | |
603 case 'RegExp': | |
604 case 'Function': | |
605 return type.toLowerCase(); | |
606 } | |
607 if (typeof obj === "object") { | |
608 return "object"; | |
609 } | |
610 return undefined; | |
611 }, | |
612 | |
613 push: function(result, actual, expected, message) { | |
614 var details = { | |
615 result: result, | |
616 message: message, | |
617 actual: actual, | |
618 expected: expected | |
619 }; | |
620 | |
621 message = escapeHtml(message) || (result ? "okay" : "failed"); | |
622 message = '<span class="test-message">' + message + "</span>"; | |
623 expected = escapeHtml(QUnit.jsDump.parse(expected)); | |
624 actual = escapeHtml(QUnit.jsDump.parse(actual)); | |
625 var output = message + '<table><tr class="test-expected"><th>Expected: </th><td><pre>' + expected + '</pre></td></tr>'; | |
626 if (actual != expected) { | |
627 output += '<tr class="test-actual"><th>Result: </th><td><pre>' + actual + '</pre></td></tr>'; | |
628 output += '<tr class="test-diff"><th>Diff: </th><td><pre>' + QUnit.diff(expected, actual) +'</pre></td></tr>'; | |
629 } | |
630 if (!result) { | |
631 var source = sourceFromStacktrace(); | |
632 if (source) { | |
633 details.source = source; | |
634 output += '<tr class="test-source"><th>Source: </th><td><pre>' + escapeHtml(source) + '</pre></td></tr>'; | |
635 } | |
636 } | |
637 output += "</table>"; | |
638 | |
639 QUnit.log(details); | |
640 | |
641 config.current.assertions.push({ | |
642 result: !!result, | |
643 message: output | |
644 }); | |
645 }, | |
646 | |
647 url: function( params ) { | |
648 params = extend( extend( {}, QUnit.urlParams ), params ); | |
649 var querystring = "?", | |
650 key; | |
651 for ( key in params ) { | |
652 querystring += encodeURIComponent( key ) + "=" + | |
653 encodeURIComponent( params[ key ] ) + "&"; | |
654 } | |
655 return window.location.pathname + querystring.slice( 0, -1 ); | |
656 }, | |
657 | |
658 extend: extend, | |
659 id: id, | |
660 addEvent: addEvent, | |
661 | |
662 // Logging callbacks; all receive a single argument with the listed properties | |
663 // run test/logs.html for any related changes | |
664 begin: function() {}, | |
665 // done: { failed, passed, total, runtime } | |
666 done: function() {}, | |
667 // log: { result, actual, expected, message } | |
668 log: function() {}, | |
669 // testStart: { name } | |
670 testStart: function() {}, | |
671 // testDone: { name, failed, passed, total } | |
672 testDone: function() {}, | |
673 // moduleStart: { name } | |
674 moduleStart: function() {}, | |
675 // moduleDone: { name, failed, passed, total } | |
676 moduleDone: function() {} | |
677 }); | |
678 | |
679 if ( typeof document === "undefined" || document.readyState === "complete" ) { | |
680 config.autorun = true; | |
681 } | |
682 | |
683 QUnit.load = function() { | |
684 QUnit.begin({}); | |
685 | |
686 // Initialize the config, saving the execution queue | |
687 var oldconfig = extend({}, config); | |
688 QUnit.init(); | |
689 extend(config, oldconfig); | |
690 | |
691 config.blocking = false; | |
692 | |
693 var urlConfigHtml = '', len = config.urlConfig.length; | |
694 for ( var i = 0, val; i < len, val = config.urlConfig[i]; i++ ) { | |
695 config[val] = QUnit.urlParams[val]; | |
696 urlConfigHtml += '<label><input name="' + val + '" type="checkbox"' + ( config[val] ? ' checked="checked"' : '' ) + '>' + val + '</label>'; | |
697 } | |
698 | |
699 var userAgent = id("qunit-userAgent"); | |
700 if ( userAgent ) { | |
701 userAgent.innerHTML = navigator.userAgent; | |
702 } | |
703 var banner = id("qunit-header"); | |
704 if ( banner ) { | |
705 banner.innerHTML = '<a href="' + QUnit.url({ filter: undefined }) + '"> ' + banner.innerHTML + '</a> ' + urlConfigHtml; | |
706 addEvent( banner, "change", function( event ) { | |
707 var params = {}; | |
708 params[ event.target.name ] = event.target.checked ? true : undefined; | |
709 window.location = QUnit.url( params ); | |
710 }); | |
711 } | |
712 | |
713 var toolbar = id("qunit-testrunner-toolbar"); | |
714 if ( toolbar ) { | |
715 var filter = document.createElement("input"); | |
716 filter.type = "checkbox"; | |
717 filter.id = "qunit-filter-pass"; | |
718 addEvent( filter, "click", function() { | |
719 var ol = document.getElementById("qunit-tests"); | |
720 if ( filter.checked ) { | |
721 ol.className = ol.className + " hidepass"; | |
722 } else { | |
723 var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; | |
724 ol.className = tmp.replace(/ hidepass /, " "); | |
725 } | |
726 if ( defined.sessionStorage ) { | |
727 if (filter.checked) { | |
728 sessionStorage.setItem("qunit-filter-passed-tests", "true"); | |
729 } else { | |
730 sessionStorage.removeItem("qunit-filter-passed-tests"); | |
731 } | |
732 } | |
733 }); | |
734 if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { | |
735 filter.checked = true; | |
736 var ol = document.getElementById("qunit-tests"); | |
737 ol.className = ol.className + " hidepass"; | |
738 } | |
739 toolbar.appendChild( filter ); | |
740 | |
741 var label = document.createElement("label"); | |
742 label.setAttribute("for", "qunit-filter-pass"); | |
743 label.innerHTML = "Hide passed tests"; | |
744 toolbar.appendChild( label ); | |
745 } | |
746 | |
747 var main = id('qunit-fixture'); | |
748 if ( main ) { | |
749 config.fixture = main.innerHTML; | |
750 } | |
751 | |
752 if (config.autostart) { | |
753 QUnit.start(); | |
754 } | |
755 }; | |
756 | |
757 addEvent(window, "load", QUnit.load); | |
758 | |
759 function done() { | |
760 config.autorun = true; | |
761 | |
762 // Log the last module results | |
763 if ( config.currentModule ) { | |
764 QUnit.moduleDone( { | |
765 name: config.currentModule, | |
766 failed: config.moduleStats.bad, | |
767 passed: config.moduleStats.all - config.moduleStats.bad, | |
768 total: config.moduleStats.all | |
769 } ); | |
770 } | |
771 | |
772 var banner = id("qunit-banner"), | |
773 tests = id("qunit-tests"), | |
774 runtime = +new Date - config.started, | |
775 passed = config.stats.all - config.stats.bad, | |
776 html = [ | |
777 'Tests completed in ', | |
778 runtime, | |
779 ' milliseconds.<br/>', | |
780 '<span class="passed">', | |
781 passed, | |
782 '</span> tests of <span class="total">', | |
783 config.stats.all, | |
784 '</span> passed, <span class="failed">', | |
785 config.stats.bad, | |
786 '</span> failed.' | |
787 ].join(''); | |
788 | |
789 if ( banner ) { | |
790 banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); | |
791 } | |
792 | |
793 if ( tests ) { | |
794 id( "qunit-testresult" ).innerHTML = html; | |
795 } | |
796 | |
797 if ( config.altertitle && typeof document !== "undefined" && document.title ) { | |
798 // show ✖ for good, ✔ for bad suite result in title | |
799 // use escape sequences in case file gets loaded with non-utf-8-charset | |
800 document.title = [ | |
801 (config.stats.bad ? "\u2716" : "\u2714"), | |
802 document.title.replace(/^[\u2714\u2716] /i, "") | |
803 ].join(" "); | |
804 } | |
805 | |
806 QUnit.done( { | |
807 failed: config.stats.bad, | |
808 passed: passed, | |
809 total: config.stats.all, | |
810 runtime: runtime | |
811 } ); | |
812 } | |
813 | |
814 function validTest( name ) { | |
815 var filter = config.filter, | |
816 run = false; | |
817 | |
818 if ( !filter ) { | |
819 return true; | |
820 } | |
821 | |
822 var not = filter.charAt( 0 ) === "!"; | |
823 if ( not ) { | |
824 filter = filter.slice( 1 ); | |
825 } | |
826 | |
827 if ( name.indexOf( filter ) !== -1 ) { | |
828 return !not; | |
829 } | |
830 | |
831 if ( not ) { | |
832 run = true; | |
833 } | |
834 | |
835 return run; | |
836 } | |
837 | |
838 // so far supports only Firefox, Chrome and Opera (buggy) | |
839 // could be extended in the future to use something like https://github.com/csnover/TraceKit | |
840 function sourceFromStacktrace() { | |
841 try { | |
842 throw new Error(); | |
843 } catch ( e ) { | |
844 if (e.stacktrace) { | |
845 // Opera | |
846 return e.stacktrace.split("\n")[6]; | |
847 } else if (e.stack) { | |
848 // Firefox, Chrome | |
849 return e.stack.split("\n")[4]; | |
850 } else if (e.sourceURL) { | |
851 // Safari, PhantomJS | |
852 // TODO sourceURL points at the 'throw new Error' line above, useless | |
853 //return e.sourceURL + ":" + e.line; | |
854 } | |
855 } | |
856 } | |
857 | |
858 function escapeHtml(s) { | |
859 if (!s) { | |
860 return ""; | |
861 } | |
862 s = s + ""; | |
863 return s.replace(/[\&"<>\\]/g, function(s) { | |
864 switch(s) { | |
865 case "&": return "&"; | |
866 case "\\": return "\\\\"; | |
867 case '"': return '\"'; | |
868 case "<": return "<"; | |
869 case ">": return ">"; | |
870 default: return s; | |
871 } | |
872 }); | |
873 } | |
874 | |
875 function synchronize( callback ) { | |
876 config.queue.push( callback ); | |
877 | |
878 if ( config.autorun && !config.blocking ) { | |
879 process(); | |
880 } | |
881 } | |
882 | |
883 function process() { | |
884 var start = (new Date()).getTime(); | |
885 | |
886 while ( config.queue.length && !config.blocking ) { | |
887 if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { | |
888 config.queue.shift()(); | |
889 } else { | |
890 window.setTimeout( process, 13 ); | |
891 break; | |
892 } | |
893 } | |
894 if (!config.blocking && !config.queue.length) { | |
895 done(); | |
896 } | |
897 } | |
898 | |
899 function saveGlobal() { | |
900 config.pollution = []; | |
901 | |
902 if ( config.noglobals ) { | |
903 for ( var key in window ) { | |
904 config.pollution.push( key ); | |
905 } | |
906 } | |
907 } | |
908 | |
909 function checkPollution( name ) { | |
910 var old = config.pollution; | |
911 saveGlobal(); | |
912 | |
913 var newGlobals = diff( config.pollution, old ); | |
914 if ( newGlobals.length > 0 ) { | |
915 ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); | |
916 } | |
917 | |
918 var deletedGlobals = diff( old, config.pollution ); | |
919 if ( deletedGlobals.length > 0 ) { | |
920 ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); | |
921 } | |
922 } | |
923 | |
924 // returns a new Array with the elements that are in a but not in b | |
925 function diff( a, b ) { | |
926 var result = a.slice(); | |
927 for ( var i = 0; i < result.length; i++ ) { | |
928 for ( var j = 0; j < b.length; j++ ) { | |
929 if ( result[i] === b[j] ) { | |
930 result.splice(i, 1); | |
931 i--; | |
932 break; | |
933 } | |
934 } | |
935 } | |
936 return result; | |
937 } | |
938 | |
939 function fail(message, exception, callback) { | |
940 if ( typeof console !== "undefined" && console.error && console.warn ) { | |
941 console.error(message); | |
942 console.error(exception); | |
943 console.warn(callback.toString()); | |
944 | |
945 } else if ( window.opera && opera.postError ) { | |
946 opera.postError(message, exception, callback.toString); | |
947 } | |
948 } | |
949 | |
950 function extend(a, b) { | |
951 for ( var prop in b ) { | |
952 if ( b[prop] === undefined ) { | |
953 delete a[prop]; | |
954 } else { | |
955 a[prop] = b[prop]; | |
956 } | |
957 } | |
958 | |
959 return a; | |
960 } | |
961 | |
962 function addEvent(elem, type, fn) { | |
963 if ( elem.addEventListener ) { | |
964 elem.addEventListener( type, fn, false ); | |
965 } else if ( elem.attachEvent ) { | |
966 elem.attachEvent( "on" + type, fn ); | |
967 } else { | |
968 fn(); | |
969 } | |
970 } | |
971 | |
972 function id(name) { | |
973 return !!(typeof document !== "undefined" && document && document.getElementById) && | |
974 document.getElementById( name ); | |
975 } | |
976 | |
977 // Test for equality any JavaScript type. | |
978 // Discussions and reference: http://philrathe.com/articles/equiv | |
979 // Test suites: http://philrathe.com/tests/equiv | |
980 // Author: Philippe Rathé <prathe@gmail.com> | |
981 QUnit.equiv = function () { | |
982 | |
983 var innerEquiv; // the real equiv function | |
984 var callers = []; // stack to decide between skip/abort functions | |
985 var parents = []; // stack to avoiding loops from circular referencing | |
986 | |
987 // Call the o related callback with the given arguments. | |
988 function bindCallbacks(o, callbacks, args) { | |
989 var prop = QUnit.objectType(o); | |
990 if (prop) { | |
991 if (QUnit.objectType(callbacks[prop]) === "function") { | |
992 return callbacks[prop].apply(callbacks, args); | |
993 } else { | |
994 return callbacks[prop]; // or undefined | |
995 } | |
996 } | |
997 } | |
998 | |
999 var callbacks = function () { | |
1000 | |
1001 // for string, boolean, number and null | |
1002 function useStrictEquality(b, a) { | |
1003 if (b instanceof a.constructor || a instanceof b.constructor) { | |
1004 // to catch short annotaion VS 'new' annotation of a | |
1005 // declaration | |
1006 // e.g. var i = 1; | |
1007 // var j = new Number(1); | |
1008 return a == b; | |
1009 } else { | |
1010 return a === b; | |
1011 } | |
1012 } | |
1013 | |
1014 return { | |
1015 "string" : useStrictEquality, | |
1016 "boolean" : useStrictEquality, | |
1017 "number" : useStrictEquality, | |
1018 "null" : useStrictEquality, | |
1019 "undefined" : useStrictEquality, | |
1020 | |
1021 "nan" : function(b) { | |
1022 return isNaN(b); | |
1023 }, | |
1024 | |
1025 "date" : function(b, a) { | |
1026 return QUnit.objectType(b) === "date" | |
1027 && a.valueOf() === b.valueOf(); | |
1028 }, | |
1029 | |
1030 "regexp" : function(b, a) { | |
1031 return QUnit.objectType(b) === "regexp" | |
1032 && a.source === b.source && // the regex itself | |
1033 a.global === b.global && // and its modifers | |
1034 // (gmi) ... | |
1035 a.ignoreCase === b.ignoreCase | |
1036 && a.multiline === b.multiline; | |
1037 }, | |
1038 | |
1039 // - skip when the property is a method of an instance (OOP) | |
1040 // - abort otherwise, | |
1041 // initial === would have catch identical references anyway | |
1042 "function" : function() { | |
1043 var caller = callers[callers.length - 1]; | |
1044 return caller !== Object && typeof caller !== "undefined"; | |
1045 }, | |
1046 | |
1047 "array" : function(b, a) { | |
1048 var i, j, loop; | |
1049 var len; | |
1050 | |
1051 // b could be an object literal here | |
1052 if (!(QUnit.objectType(b) === "array")) { | |
1053 return false; | |
1054 } | |
1055 | |
1056 len = a.length; | |
1057 if (len !== b.length) { // safe and faster | |
1058 return false; | |
1059 } | |
1060 | |
1061 // track reference to avoid circular references | |
1062 parents.push(a); | |
1063 for (i = 0; i < len; i++) { | |
1064 loop = false; | |
1065 for (j = 0; j < parents.length; j++) { | |
1066 if (parents[j] === a[i]) { | |
1067 loop = true;// dont rewalk array | |
1068 } | |
1069 } | |
1070 if (!loop && !innerEquiv(a[i], b[i])) { | |
1071 parents.pop(); | |
1072 return false; | |
1073 } | |
1074 } | |
1075 parents.pop(); | |
1076 return true; | |
1077 }, | |
1078 | |
1079 "object" : function(b, a) { | |
1080 var i, j, loop; | |
1081 var eq = true; // unless we can proove it | |
1082 var aProperties = [], bProperties = []; // collection of | |
1083 // strings | |
1084 | |
1085 // comparing constructors is more strict than using | |
1086 // instanceof | |
1087 if (a.constructor !== b.constructor) { | |
1088 return false; | |
1089 } | |
1090 | |
1091 // stack constructor before traversing properties | |
1092 callers.push(a.constructor); | |
1093 // track reference to avoid circular references | |
1094 parents.push(a); | |
1095 | |
1096 for (i in a) { // be strict: don't ensures hasOwnProperty | |
1097 // and go deep | |
1098 loop = false; | |
1099 for (j = 0; j < parents.length; j++) { | |
1100 if (parents[j] === a[i]) | |
1101 loop = true; // don't go down the same path | |
1102 // twice | |
1103 } | |
1104 aProperties.push(i); // collect a's properties | |
1105 | |
1106 if (!loop && !innerEquiv(a[i], b[i])) { | |
1107 eq = false; | |
1108 break; | |
1109 } | |
1110 } | |
1111 | |
1112 callers.pop(); // unstack, we are done | |
1113 parents.pop(); | |
1114 | |
1115 for (i in b) { | |
1116 bProperties.push(i); // collect b's properties | |
1117 } | |
1118 | |
1119 // Ensures identical properties name | |
1120 return eq | |
1121 && innerEquiv(aProperties.sort(), bProperties | |
1122 .sort()); | |
1123 } | |
1124 }; | |
1125 }(); | |
1126 | |
1127 innerEquiv = function() { // can take multiple arguments | |
1128 var args = Array.prototype.slice.apply(arguments); | |
1129 if (args.length < 2) { | |
1130 return true; // end transition | |
1131 } | |
1132 | |
1133 return (function(a, b) { | |
1134 if (a === b) { | |
1135 return true; // catch the most you can | |
1136 } else if (a === null || b === null || typeof a === "undefined" | |
1137 || typeof b === "undefined" | |
1138 || QUnit.objectType(a) !== QUnit.objectType(b)) { | |
1139 return false; // don't lose time with error prone cases | |
1140 } else { | |
1141 return bindCallbacks(a, callbacks, [ b, a ]); | |
1142 } | |
1143 | |
1144 // apply transition with (1..n) arguments | |
1145 })(args[0], args[1]) | |
1146 && arguments.callee.apply(this, args.splice(1, | |
1147 args.length - 1)); | |
1148 }; | |
1149 | |
1150 return innerEquiv; | |
1151 | |
1152 }(); | |
1153 | |
1154 /** | |
1155 * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | | |
1156 * http://flesler.blogspot.com Licensed under BSD | |
1157 * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 | |
1158 * | |
1159 * @projectDescription Advanced and extensible data dumping for Javascript. | |
1160 * @version 1.0.0 | |
1161 * @author Ariel Flesler | |
1162 * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} | |
1163 */ | |
1164 QUnit.jsDump = (function() { | |
1165 function quote( str ) { | |
1166 return '"' + str.toString().replace(/"/g, '\\"') + '"'; | |
1167 }; | |
1168 function literal( o ) { | |
1169 return o + ''; | |
1170 }; | |
1171 function join( pre, arr, post ) { | |
1172 var s = jsDump.separator(), | |
1173 base = jsDump.indent(), | |
1174 inner = jsDump.indent(1); | |
1175 if ( arr.join ) | |
1176 arr = arr.join( ',' + s + inner ); | |
1177 if ( !arr ) | |
1178 return pre + post; | |
1179 return [ pre, inner + arr, base + post ].join(s); | |
1180 }; | |
1181 function array( arr, stack ) { | |
1182 var i = arr.length, ret = Array(i); | |
1183 this.up(); | |
1184 while ( i-- ) | |
1185 ret[i] = this.parse( arr[i] , undefined , stack); | |
1186 this.down(); | |
1187 return join( '[', ret, ']' ); | |
1188 }; | |
1189 | |
1190 var reName = /^function (\w+)/; | |
1191 | |
1192 var jsDump = { | |
1193 parse:function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance | |
1194 stack = stack || [ ]; | |
1195 var parser = this.parsers[ type || this.typeOf(obj) ]; | |
1196 type = typeof parser; | |
1197 var inStack = inArray(obj, stack); | |
1198 if (inStack != -1) { | |
1199 return 'recursion('+(inStack - stack.length)+')'; | |
1200 } | |
1201 //else | |
1202 if (type == 'function') { | |
1203 stack.push(obj); | |
1204 var res = parser.call( this, obj, stack ); | |
1205 stack.pop(); | |
1206 return res; | |
1207 } | |
1208 // else | |
1209 return (type == 'string') ? parser : this.parsers.error; | |
1210 }, | |
1211 typeOf:function( obj ) { | |
1212 var type; | |
1213 if ( obj === null ) { | |
1214 type = "null"; | |
1215 } else if (typeof obj === "undefined") { | |
1216 type = "undefined"; | |
1217 } else if (QUnit.is("RegExp", obj)) { | |
1218 type = "regexp"; | |
1219 } else if (QUnit.is("Date", obj)) { | |
1220 type = "date"; | |
1221 } else if (QUnit.is("Function", obj)) { | |
1222 type = "function"; | |
1223 } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { | |
1224 type = "window"; | |
1225 } else if (obj.nodeType === 9) { | |
1226 type = "document"; | |
1227 } else if (obj.nodeType) { | |
1228 type = "node"; | |
1229 } else if (typeof obj === "object" && typeof obj.length === "number" && obj.length >= 0) { | |
1230 type = "array"; | |
1231 } else { | |
1232 type = typeof obj; | |
1233 } | |
1234 return type; | |
1235 }, | |
1236 separator:function() { | |
1237 return this.multiline ? this.HTML ? '<br />' : '\n' : this.HTML ? ' ' : ' '; | |
1238 }, | |
1239 indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing | |
1240 if ( !this.multiline ) | |
1241 return ''; | |
1242 var chr = this.indentChar; | |
1243 if ( this.HTML ) | |
1244 chr = chr.replace(/\t/g,' ').replace(/ /g,' '); | |
1245 return Array( this._depth_ + (extra||0) ).join(chr); | |
1246 }, | |
1247 up:function( a ) { | |
1248 this._depth_ += a || 1; | |
1249 }, | |
1250 down:function( a ) { | |
1251 this._depth_ -= a || 1; | |
1252 }, | |
1253 setParser:function( name, parser ) { | |
1254 this.parsers[name] = parser; | |
1255 }, | |
1256 // The next 3 are exposed so you can use them | |
1257 quote:quote, | |
1258 literal:literal, | |
1259 join:join, | |
1260 // | |
1261 _depth_: 1, | |
1262 // This is the list of parsers, to modify them, use jsDump.setParser | |
1263 parsers:{ | |
1264 window: '[Window]', | |
1265 document: '[Document]', | |
1266 error:'[ERROR]', //when no parser is found, shouldn't happen | |
1267 unknown: '[Unknown]', | |
1268 'null':'null', | |
1269 'undefined':'undefined', | |
1270 'function':function( fn ) { | |
1271 var ret = 'function', | |
1272 name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE | |
1273 if ( name ) | |
1274 ret += ' ' + name; | |
1275 ret += '('; | |
1276 | |
1277 ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); | |
1278 return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); | |
1279 }, | |
1280 array: array, | |
1281 nodelist: array, | |
1282 arguments: array, | |
1283 object:function( map, stack ) { | |
1284 var ret = [ ]; | |
1285 QUnit.jsDump.up(); | |
1286 for ( var key in map ) { | |
1287 var val = map[key]; | |
1288 ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(val, undefined, stack)); | |
1289 } | |
1290 QUnit.jsDump.down(); | |
1291 return join( '{', ret, '}' ); | |
1292 }, | |
1293 node:function( node ) { | |
1294 var open = QUnit.jsDump.HTML ? '<' : '<', | |
1295 close = QUnit.jsDump.HTML ? '>' : '>'; | |
1296 | |
1297 var tag = node.nodeName.toLowerCase(), | |
1298 ret = open + tag; | |
1299 | |
1300 for ( var a in QUnit.jsDump.DOMAttrs ) { | |
1301 var val = node[QUnit.jsDump.DOMAttrs[a]]; | |
1302 if ( val ) | |
1303 ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); | |
1304 } | |
1305 return ret + close + open + '/' + tag + close; | |
1306 }, | |
1307 functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function | |
1308 var l = fn.length; | |
1309 if ( !l ) return ''; | |
1310 | |
1311 var args = Array(l); | |
1312 while ( l-- ) | |
1313 args[l] = String.fromCharCode(97+l);//97 is 'a' | |
1314 return ' ' + args.join(', ') + ' '; | |
1315 }, | |
1316 key:quote, //object calls it internally, the key part of an item in a map | |
1317 functionCode:'[code]', //function calls it internally, it's the content of the function | |
1318 attribute:quote, //node calls it internally, it's an html attribute value | |
1319 string:quote, | |
1320 date:quote, | |
1321 regexp:literal, //regex | |
1322 number:literal, | |
1323 'boolean':literal | |
1324 }, | |
1325 DOMAttrs:{//attributes to dump from nodes, name=>realName | |
1326 id:'id', | |
1327 name:'name', | |
1328 'class':'className' | |
1329 }, | |
1330 HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) | |
1331 indentChar:' ',//indentation unit | |
1332 multiline:true //if true, items in a collection, are separated by a \n, else just a space. | |
1333 }; | |
1334 | |
1335 return jsDump; | |
1336 })(); | |
1337 | |
1338 // from Sizzle.js | |
1339 function getText( elems ) { | |
1340 var ret = "", elem; | |
1341 | |
1342 for ( var i = 0; elems[i]; i++ ) { | |
1343 elem = elems[i]; | |
1344 | |
1345 // Get the text from text nodes and CDATA nodes | |
1346 if ( elem.nodeType === 3 || elem.nodeType === 4 ) { | |
1347 ret += elem.nodeValue; | |
1348 | |
1349 // Traverse everything else, except comment nodes | |
1350 } else if ( elem.nodeType !== 8 ) { | |
1351 ret += getText( elem.childNodes ); | |
1352 } | |
1353 } | |
1354 | |
1355 return ret; | |
1356 }; | |
1357 | |
1358 //from jquery.js | |
1359 function inArray( elem, array ) { | |
1360 if ( array.indexOf ) { | |
1361 return array.indexOf( elem ); | |
1362 } | |
1363 | |
1364 for ( var i = 0, length = array.length; i < length; i++ ) { | |
1365 if ( array[ i ] === elem ) { | |
1366 return i; | |
1367 } | |
1368 } | |
1369 | |
1370 return -1; | |
1371 } | |
1372 | |
1373 /* | |
1374 * Javascript Diff Algorithm | |
1375 * By John Resig (http://ejohn.org/) | |
1376 * Modified by Chu Alan "sprite" | |
1377 * | |
1378 * Released under the MIT license. | |
1379 * | |
1380 * More Info: | |
1381 * http://ejohn.org/projects/javascript-diff-algorithm/ | |
1382 * | |
1383 * Usage: QUnit.diff(expected, actual) | |
1384 * | |
1385 * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick <del>brown </del> fox <del>jumped </del><ins>jumps </ins> over" | |
1386 */ | |
1387 QUnit.diff = (function() { | |
1388 function diff(o, n) { | |
1389 var ns = {}; | |
1390 var os = {}; | |
1391 | |
1392 for (var i = 0; i < n.length; i++) { | |
1393 if (ns[n[i]] == null) | |
1394 ns[n[i]] = { | |
1395 rows: [], | |
1396 o: null | |
1397 }; | |
1398 ns[n[i]].rows.push(i); | |
1399 } | |
1400 | |
1401 for (var i = 0; i < o.length; i++) { | |
1402 if (os[o[i]] == null) | |
1403 os[o[i]] = { | |
1404 rows: [], | |
1405 n: null | |
1406 }; | |
1407 os[o[i]].rows.push(i); | |
1408 } | |
1409 | |
1410 for (var i in ns) { | |
1411 if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { | |
1412 n[ns[i].rows[0]] = { | |
1413 text: n[ns[i].rows[0]], | |
1414 row: os[i].rows[0] | |
1415 }; | |
1416 o[os[i].rows[0]] = { | |
1417 text: o[os[i].rows[0]], | |
1418 row: ns[i].rows[0] | |
1419 }; | |
1420 } | |
1421 } | |
1422 | |
1423 for (var i = 0; i < n.length - 1; i++) { | |
1424 if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && | |
1425 n[i + 1] == o[n[i].row + 1]) { | |
1426 n[i + 1] = { | |
1427 text: n[i + 1], | |
1428 row: n[i].row + 1 | |
1429 }; | |
1430 o[n[i].row + 1] = { | |
1431 text: o[n[i].row + 1], | |
1432 row: i + 1 | |
1433 }; | |
1434 } | |
1435 } | |
1436 | |
1437 for (var i = n.length - 1; i > 0; i--) { | |
1438 if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && | |
1439 n[i - 1] == o[n[i].row - 1]) { | |
1440 n[i - 1] = { | |
1441 text: n[i - 1], | |
1442 row: n[i].row - 1 | |
1443 }; | |
1444 o[n[i].row - 1] = { | |
1445 text: o[n[i].row - 1], | |
1446 row: i - 1 | |
1447 }; | |
1448 } | |
1449 } | |
1450 | |
1451 return { | |
1452 o: o, | |
1453 n: n | |
1454 }; | |
1455 } | |
1456 | |
1457 return function(o, n) { | |
1458 o = o.replace(/\s+$/, ''); | |
1459 n = n.replace(/\s+$/, ''); | |
1460 var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); | |
1461 | |
1462 var str = ""; | |
1463 | |
1464 var oSpace = o.match(/\s+/g); | |
1465 if (oSpace == null) { | |
1466 oSpace = [" "]; | |
1467 } | |
1468 else { | |
1469 oSpace.push(" "); | |
1470 } | |
1471 var nSpace = n.match(/\s+/g); | |
1472 if (nSpace == null) { | |
1473 nSpace = [" "]; | |
1474 } | |
1475 else { | |
1476 nSpace.push(" "); | |
1477 } | |
1478 | |
1479 if (out.n.length == 0) { | |
1480 for (var i = 0; i < out.o.length; i++) { | |
1481 str += '<del>' + out.o[i] + oSpace[i] + "</del>"; | |
1482 } | |
1483 } | |
1484 else { | |
1485 if (out.n[0].text == null) { | |
1486 for (n = 0; n < out.o.length && out.o[n].text == null; n++) { | |
1487 str += '<del>' + out.o[n] + oSpace[n] + "</del>"; | |
1488 } | |
1489 } | |
1490 | |
1491 for (var i = 0; i < out.n.length; i++) { | |
1492 if (out.n[i].text == null) { | |
1493 str += '<ins>' + out.n[i] + nSpace[i] + "</ins>"; | |
1494 } | |
1495 else { | |
1496 var pre = ""; | |
1497 | |
1498 for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { | |
1499 pre += '<del>' + out.o[n] + oSpace[n] + "</del>"; | |
1500 } | |
1501 str += " " + out.n[i].text + nSpace[i] + pre; | |
1502 } | |
1503 } | |
1504 } | |
1505 | |
1506 return str; | |
1507 }; | |
1508 })(); | |
1509 | |
1510 })(this); |