1 /*! |
|
2 * QUnit 1.18.0 |
|
3 * http://qunitjs.com/ |
|
4 * |
|
5 * Copyright jQuery Foundation and other contributors |
|
6 * Released under the MIT license |
|
7 * http://jquery.org/license |
|
8 * |
|
9 * Date: 2015-04-03T10:23Z |
|
10 */ |
|
11 |
|
12 (function( window ) { |
|
13 |
|
14 var QUnit, |
|
15 config, |
|
16 onErrorFnPrev, |
|
17 loggingCallbacks = {}, |
|
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 now = Date.now || function() { |
|
24 return new Date().getTime(); |
|
25 }, |
|
26 globalStartCalled = false, |
|
27 runStarted = false, |
|
28 setTimeout = window.setTimeout, |
|
29 clearTimeout = window.clearTimeout, |
|
30 defined = { |
|
31 document: window.document !== undefined, |
|
32 setTimeout: window.setTimeout !== undefined, |
|
33 sessionStorage: (function() { |
|
34 var x = "qunit-test-string"; |
|
35 try { |
|
36 sessionStorage.setItem( x, x ); |
|
37 sessionStorage.removeItem( x ); |
|
38 return true; |
|
39 } catch ( e ) { |
|
40 return false; |
|
41 } |
|
42 }()) |
|
43 }, |
|
44 /** |
|
45 * Provides a normalized error string, correcting an issue |
|
46 * with IE 7 (and prior) where Error.prototype.toString is |
|
47 * not properly implemented |
|
48 * |
|
49 * Based on http://es5.github.com/#x15.11.4.4 |
|
50 * |
|
51 * @param {String|Error} error |
|
52 * @return {String} error message |
|
53 */ |
|
54 errorString = function( error ) { |
|
55 var name, message, |
|
56 errorString = error.toString(); |
|
57 if ( errorString.substring( 0, 7 ) === "[object" ) { |
|
58 name = error.name ? error.name.toString() : "Error"; |
|
59 message = error.message ? error.message.toString() : ""; |
|
60 if ( name && message ) { |
|
61 return name + ": " + message; |
|
62 } else if ( name ) { |
|
63 return name; |
|
64 } else if ( message ) { |
|
65 return message; |
|
66 } else { |
|
67 return "Error"; |
|
68 } |
|
69 } else { |
|
70 return errorString; |
|
71 } |
|
72 }, |
|
73 /** |
|
74 * Makes a clone of an object using only Array or Object as base, |
|
75 * and copies over the own enumerable properties. |
|
76 * |
|
77 * @param {Object} obj |
|
78 * @return {Object} New object with only the own properties (recursively). |
|
79 */ |
|
80 objectValues = function( obj ) { |
|
81 var key, val, |
|
82 vals = QUnit.is( "array", obj ) ? [] : {}; |
|
83 for ( key in obj ) { |
|
84 if ( hasOwn.call( obj, key ) ) { |
|
85 val = obj[ key ]; |
|
86 vals[ key ] = val === Object( val ) ? objectValues( val ) : val; |
|
87 } |
|
88 } |
|
89 return vals; |
|
90 }; |
|
91 |
|
92 QUnit = {}; |
|
93 |
|
94 /** |
|
95 * Config object: Maintain internal state |
|
96 * Later exposed as QUnit.config |
|
97 * `config` initialized at top of scope |
|
98 */ |
|
99 config = { |
|
100 // The queue of tests to run |
|
101 queue: [], |
|
102 |
|
103 // block until document ready |
|
104 blocking: true, |
|
105 |
|
106 // by default, run previously failed tests first |
|
107 // very useful in combination with "Hide passed tests" checked |
|
108 reorder: true, |
|
109 |
|
110 // by default, modify document.title when suite is done |
|
111 altertitle: true, |
|
112 |
|
113 // by default, scroll to top of the page when suite is done |
|
114 scrolltop: true, |
|
115 |
|
116 // when enabled, all tests must call expect() |
|
117 requireExpects: false, |
|
118 |
|
119 // depth up-to which object will be dumped |
|
120 maxDepth: 5, |
|
121 |
|
122 // add checkboxes that are persisted in the query-string |
|
123 // when enabled, the id is set to `true` as a `QUnit.config` property |
|
124 urlConfig: [ |
|
125 { |
|
126 id: "hidepassed", |
|
127 label: "Hide passed tests", |
|
128 tooltip: "Only show tests and assertions that fail. Stored as query-strings." |
|
129 }, |
|
130 { |
|
131 id: "noglobals", |
|
132 label: "Check for Globals", |
|
133 tooltip: "Enabling this will test if any test introduces new properties on the " + |
|
134 "`window` object. Stored as query-strings." |
|
135 }, |
|
136 { |
|
137 id: "notrycatch", |
|
138 label: "No try-catch", |
|
139 tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " + |
|
140 "exceptions in IE reasonable. Stored as query-strings." |
|
141 } |
|
142 ], |
|
143 |
|
144 // Set of all modules. |
|
145 modules: [], |
|
146 |
|
147 // The first unnamed module |
|
148 currentModule: { |
|
149 name: "", |
|
150 tests: [] |
|
151 }, |
|
152 |
|
153 callbacks: {} |
|
154 }; |
|
155 |
|
156 // Push a loose unnamed module to the modules collection |
|
157 config.modules.push( config.currentModule ); |
|
158 |
|
159 // Initialize more QUnit.config and QUnit.urlParams |
|
160 (function() { |
|
161 var i, current, |
|
162 location = window.location || { search: "", protocol: "file:" }, |
|
163 params = location.search.slice( 1 ).split( "&" ), |
|
164 length = params.length, |
|
165 urlParams = {}; |
|
166 |
|
167 if ( params[ 0 ] ) { |
|
168 for ( i = 0; i < length; i++ ) { |
|
169 current = params[ i ].split( "=" ); |
|
170 current[ 0 ] = decodeURIComponent( current[ 0 ] ); |
|
171 |
|
172 // allow just a key to turn on a flag, e.g., test.html?noglobals |
|
173 current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; |
|
174 if ( urlParams[ current[ 0 ] ] ) { |
|
175 urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] ); |
|
176 } else { |
|
177 urlParams[ current[ 0 ] ] = current[ 1 ]; |
|
178 } |
|
179 } |
|
180 } |
|
181 |
|
182 if ( urlParams.filter === true ) { |
|
183 delete urlParams.filter; |
|
184 } |
|
185 |
|
186 QUnit.urlParams = urlParams; |
|
187 |
|
188 // String search anywhere in moduleName+testName |
|
189 config.filter = urlParams.filter; |
|
190 |
|
191 if ( urlParams.maxDepth ) { |
|
192 config.maxDepth = parseInt( urlParams.maxDepth, 10 ) === -1 ? |
|
193 Number.POSITIVE_INFINITY : |
|
194 urlParams.maxDepth; |
|
195 } |
|
196 |
|
197 config.testId = []; |
|
198 if ( urlParams.testId ) { |
|
199 |
|
200 // Ensure that urlParams.testId is an array |
|
201 urlParams.testId = decodeURIComponent( urlParams.testId ).split( "," ); |
|
202 for ( i = 0; i < urlParams.testId.length; i++ ) { |
|
203 config.testId.push( urlParams.testId[ i ] ); |
|
204 } |
|
205 } |
|
206 |
|
207 // Figure out if we're running the tests from a server or not |
|
208 QUnit.isLocal = location.protocol === "file:"; |
|
209 |
|
210 // Expose the current QUnit version |
|
211 QUnit.version = "1.18.0"; |
|
212 }()); |
|
213 |
|
214 // Root QUnit object. |
|
215 // `QUnit` initialized at top of scope |
|
216 extend( QUnit, { |
|
217 |
|
218 // call on start of module test to prepend name to all tests |
|
219 module: function( name, testEnvironment ) { |
|
220 var currentModule = { |
|
221 name: name, |
|
222 testEnvironment: testEnvironment, |
|
223 tests: [] |
|
224 }; |
|
225 |
|
226 // DEPRECATED: handles setup/teardown functions, |
|
227 // beforeEach and afterEach should be used instead |
|
228 if ( testEnvironment && testEnvironment.setup ) { |
|
229 testEnvironment.beforeEach = testEnvironment.setup; |
|
230 delete testEnvironment.setup; |
|
231 } |
|
232 if ( testEnvironment && testEnvironment.teardown ) { |
|
233 testEnvironment.afterEach = testEnvironment.teardown; |
|
234 delete testEnvironment.teardown; |
|
235 } |
|
236 |
|
237 config.modules.push( currentModule ); |
|
238 config.currentModule = currentModule; |
|
239 }, |
|
240 |
|
241 // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0. |
|
242 asyncTest: function( testName, expected, callback ) { |
|
243 if ( arguments.length === 2 ) { |
|
244 callback = expected; |
|
245 expected = null; |
|
246 } |
|
247 |
|
248 QUnit.test( testName, expected, callback, true ); |
|
249 }, |
|
250 |
|
251 test: function( testName, expected, callback, async ) { |
|
252 var test; |
|
253 |
|
254 if ( arguments.length === 2 ) { |
|
255 callback = expected; |
|
256 expected = null; |
|
257 } |
|
258 |
|
259 test = new Test({ |
|
260 testName: testName, |
|
261 expected: expected, |
|
262 async: async, |
|
263 callback: callback |
|
264 }); |
|
265 |
|
266 test.queue(); |
|
267 }, |
|
268 |
|
269 skip: function( testName ) { |
|
270 var test = new Test({ |
|
271 testName: testName, |
|
272 skip: true |
|
273 }); |
|
274 |
|
275 test.queue(); |
|
276 }, |
|
277 |
|
278 // DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0. |
|
279 // In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior. |
|
280 start: function( count ) { |
|
281 var globalStartAlreadyCalled = globalStartCalled; |
|
282 |
|
283 if ( !config.current ) { |
|
284 globalStartCalled = true; |
|
285 |
|
286 if ( runStarted ) { |
|
287 throw new Error( "Called start() outside of a test context while already started" ); |
|
288 } else if ( globalStartAlreadyCalled || count > 1 ) { |
|
289 throw new Error( "Called start() outside of a test context too many times" ); |
|
290 } else if ( config.autostart ) { |
|
291 throw new Error( "Called start() outside of a test context when " + |
|
292 "QUnit.config.autostart was true" ); |
|
293 } else if ( !config.pageLoaded ) { |
|
294 |
|
295 // The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it |
|
296 config.autostart = true; |
|
297 return; |
|
298 } |
|
299 } else { |
|
300 |
|
301 // If a test is running, adjust its semaphore |
|
302 config.current.semaphore -= count || 1; |
|
303 |
|
304 // Don't start until equal number of stop-calls |
|
305 if ( config.current.semaphore > 0 ) { |
|
306 return; |
|
307 } |
|
308 |
|
309 // throw an Error if start is called more often than stop |
|
310 if ( config.current.semaphore < 0 ) { |
|
311 config.current.semaphore = 0; |
|
312 |
|
313 QUnit.pushFailure( |
|
314 "Called start() while already started (test's semaphore was 0 already)", |
|
315 sourceFromStacktrace( 2 ) |
|
316 ); |
|
317 return; |
|
318 } |
|
319 } |
|
320 |
|
321 resumeProcessing(); |
|
322 }, |
|
323 |
|
324 // DEPRECATED: QUnit.stop() will be removed in QUnit 2.0. |
|
325 stop: function( count ) { |
|
326 |
|
327 // If there isn't a test running, don't allow QUnit.stop() to be called |
|
328 if ( !config.current ) { |
|
329 throw new Error( "Called stop() outside of a test context" ); |
|
330 } |
|
331 |
|
332 // If a test is running, adjust its semaphore |
|
333 config.current.semaphore += count || 1; |
|
334 |
|
335 pauseProcessing(); |
|
336 }, |
|
337 |
|
338 config: config, |
|
339 |
|
340 // Safe object type checking |
|
341 is: function( type, obj ) { |
|
342 return QUnit.objectType( obj ) === type; |
|
343 }, |
|
344 |
|
345 objectType: function( obj ) { |
|
346 if ( typeof obj === "undefined" ) { |
|
347 return "undefined"; |
|
348 } |
|
349 |
|
350 // Consider: typeof null === object |
|
351 if ( obj === null ) { |
|
352 return "null"; |
|
353 } |
|
354 |
|
355 var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ), |
|
356 type = match && match[ 1 ] || ""; |
|
357 |
|
358 switch ( type ) { |
|
359 case "Number": |
|
360 if ( isNaN( obj ) ) { |
|
361 return "nan"; |
|
362 } |
|
363 return "number"; |
|
364 case "String": |
|
365 case "Boolean": |
|
366 case "Array": |
|
367 case "Date": |
|
368 case "RegExp": |
|
369 case "Function": |
|
370 return type.toLowerCase(); |
|
371 } |
|
372 if ( typeof obj === "object" ) { |
|
373 return "object"; |
|
374 } |
|
375 return undefined; |
|
376 }, |
|
377 |
|
378 extend: extend, |
|
379 |
|
380 load: function() { |
|
381 config.pageLoaded = true; |
|
382 |
|
383 // Initialize the configuration options |
|
384 extend( config, { |
|
385 stats: { all: 0, bad: 0 }, |
|
386 moduleStats: { all: 0, bad: 0 }, |
|
387 started: 0, |
|
388 updateRate: 1000, |
|
389 autostart: true, |
|
390 filter: "" |
|
391 }, true ); |
|
392 |
|
393 config.blocking = false; |
|
394 |
|
395 if ( config.autostart ) { |
|
396 resumeProcessing(); |
|
397 } |
|
398 } |
|
399 }); |
|
400 |
|
401 // Register logging callbacks |
|
402 (function() { |
|
403 var i, l, key, |
|
404 callbacks = [ "begin", "done", "log", "testStart", "testDone", |
|
405 "moduleStart", "moduleDone" ]; |
|
406 |
|
407 function registerLoggingCallback( key ) { |
|
408 var loggingCallback = function( callback ) { |
|
409 if ( QUnit.objectType( callback ) !== "function" ) { |
|
410 throw new Error( |
|
411 "QUnit logging methods require a callback function as their first parameters." |
|
412 ); |
|
413 } |
|
414 |
|
415 config.callbacks[ key ].push( callback ); |
|
416 }; |
|
417 |
|
418 // DEPRECATED: This will be removed on QUnit 2.0.0+ |
|
419 // Stores the registered functions allowing restoring |
|
420 // at verifyLoggingCallbacks() if modified |
|
421 loggingCallbacks[ key ] = loggingCallback; |
|
422 |
|
423 return loggingCallback; |
|
424 } |
|
425 |
|
426 for ( i = 0, l = callbacks.length; i < l; i++ ) { |
|
427 key = callbacks[ i ]; |
|
428 |
|
429 // Initialize key collection of logging callback |
|
430 if ( QUnit.objectType( config.callbacks[ key ] ) === "undefined" ) { |
|
431 config.callbacks[ key ] = []; |
|
432 } |
|
433 |
|
434 QUnit[ key ] = registerLoggingCallback( key ); |
|
435 } |
|
436 })(); |
|
437 |
|
438 // `onErrorFnPrev` initialized at top of scope |
|
439 // Preserve other handlers |
|
440 onErrorFnPrev = window.onerror; |
|
441 |
|
442 // Cover uncaught exceptions |
|
443 // Returning true will suppress the default browser handler, |
|
444 // returning false will let it run. |
|
445 window.onerror = function( error, filePath, linerNr ) { |
|
446 var ret = false; |
|
447 if ( onErrorFnPrev ) { |
|
448 ret = onErrorFnPrev( error, filePath, linerNr ); |
|
449 } |
|
450 |
|
451 // Treat return value as window.onerror itself does, |
|
452 // Only do our handling if not suppressed. |
|
453 if ( ret !== true ) { |
|
454 if ( QUnit.config.current ) { |
|
455 if ( QUnit.config.current.ignoreGlobalErrors ) { |
|
456 return true; |
|
457 } |
|
458 QUnit.pushFailure( error, filePath + ":" + linerNr ); |
|
459 } else { |
|
460 QUnit.test( "global failure", extend(function() { |
|
461 QUnit.pushFailure( error, filePath + ":" + linerNr ); |
|
462 }, { validTest: true } ) ); |
|
463 } |
|
464 return false; |
|
465 } |
|
466 |
|
467 return ret; |
|
468 }; |
|
469 |
|
470 function done() { |
|
471 var runtime, passed; |
|
472 |
|
473 config.autorun = true; |
|
474 |
|
475 // Log the last module results |
|
476 if ( config.previousModule ) { |
|
477 runLoggingCallbacks( "moduleDone", { |
|
478 name: config.previousModule.name, |
|
479 tests: config.previousModule.tests, |
|
480 failed: config.moduleStats.bad, |
|
481 passed: config.moduleStats.all - config.moduleStats.bad, |
|
482 total: config.moduleStats.all, |
|
483 runtime: now() - config.moduleStats.started |
|
484 }); |
|
485 } |
|
486 delete config.previousModule; |
|
487 |
|
488 runtime = now() - config.started; |
|
489 passed = config.stats.all - config.stats.bad; |
|
490 |
|
491 runLoggingCallbacks( "done", { |
|
492 failed: config.stats.bad, |
|
493 passed: passed, |
|
494 total: config.stats.all, |
|
495 runtime: runtime |
|
496 }); |
|
497 } |
|
498 |
|
499 // Doesn't support IE6 to IE9, it will return undefined on these browsers |
|
500 // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack |
|
501 function extractStacktrace( e, offset ) { |
|
502 offset = offset === undefined ? 4 : offset; |
|
503 |
|
504 var stack, include, i; |
|
505 |
|
506 if ( e.stack ) { |
|
507 stack = e.stack.split( "\n" ); |
|
508 if ( /^error$/i.test( stack[ 0 ] ) ) { |
|
509 stack.shift(); |
|
510 } |
|
511 if ( fileName ) { |
|
512 include = []; |
|
513 for ( i = offset; i < stack.length; i++ ) { |
|
514 if ( stack[ i ].indexOf( fileName ) !== -1 ) { |
|
515 break; |
|
516 } |
|
517 include.push( stack[ i ] ); |
|
518 } |
|
519 if ( include.length ) { |
|
520 return include.join( "\n" ); |
|
521 } |
|
522 } |
|
523 return stack[ offset ]; |
|
524 |
|
525 // Support: Safari <=6 only |
|
526 } else if ( e.sourceURL ) { |
|
527 |
|
528 // exclude useless self-reference for generated Error objects |
|
529 if ( /qunit.js$/.test( e.sourceURL ) ) { |
|
530 return; |
|
531 } |
|
532 |
|
533 // for actual exceptions, this is useful |
|
534 return e.sourceURL + ":" + e.line; |
|
535 } |
|
536 } |
|
537 |
|
538 function sourceFromStacktrace( offset ) { |
|
539 var error = new Error(); |
|
540 |
|
541 // Support: Safari <=7 only, IE <=10 - 11 only |
|
542 // Not all browsers generate the `stack` property for `new Error()`, see also #636 |
|
543 if ( !error.stack ) { |
|
544 try { |
|
545 throw error; |
|
546 } catch ( err ) { |
|
547 error = err; |
|
548 } |
|
549 } |
|
550 |
|
551 return extractStacktrace( error, offset ); |
|
552 } |
|
553 |
|
554 function synchronize( callback, last ) { |
|
555 if ( QUnit.objectType( callback ) === "array" ) { |
|
556 while ( callback.length ) { |
|
557 synchronize( callback.shift() ); |
|
558 } |
|
559 return; |
|
560 } |
|
561 config.queue.push( callback ); |
|
562 |
|
563 if ( config.autorun && !config.blocking ) { |
|
564 process( last ); |
|
565 } |
|
566 } |
|
567 |
|
568 function process( last ) { |
|
569 function next() { |
|
570 process( last ); |
|
571 } |
|
572 var start = now(); |
|
573 config.depth = ( config.depth || 0 ) + 1; |
|
574 |
|
575 while ( config.queue.length && !config.blocking ) { |
|
576 if ( !defined.setTimeout || config.updateRate <= 0 || |
|
577 ( ( now() - start ) < config.updateRate ) ) { |
|
578 if ( config.current ) { |
|
579 |
|
580 // Reset async tracking for each phase of the Test lifecycle |
|
581 config.current.usedAsync = false; |
|
582 } |
|
583 config.queue.shift()(); |
|
584 } else { |
|
585 setTimeout( next, 13 ); |
|
586 break; |
|
587 } |
|
588 } |
|
589 config.depth--; |
|
590 if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { |
|
591 done(); |
|
592 } |
|
593 } |
|
594 |
|
595 function begin() { |
|
596 var i, l, |
|
597 modulesLog = []; |
|
598 |
|
599 // If the test run hasn't officially begun yet |
|
600 if ( !config.started ) { |
|
601 |
|
602 // Record the time of the test run's beginning |
|
603 config.started = now(); |
|
604 |
|
605 verifyLoggingCallbacks(); |
|
606 |
|
607 // Delete the loose unnamed module if unused. |
|
608 if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) { |
|
609 config.modules.shift(); |
|
610 } |
|
611 |
|
612 // Avoid unnecessary information by not logging modules' test environments |
|
613 for ( i = 0, l = config.modules.length; i < l; i++ ) { |
|
614 modulesLog.push({ |
|
615 name: config.modules[ i ].name, |
|
616 tests: config.modules[ i ].tests |
|
617 }); |
|
618 } |
|
619 |
|
620 // The test run is officially beginning now |
|
621 runLoggingCallbacks( "begin", { |
|
622 totalTests: Test.count, |
|
623 modules: modulesLog |
|
624 }); |
|
625 } |
|
626 |
|
627 config.blocking = false; |
|
628 process( true ); |
|
629 } |
|
630 |
|
631 function resumeProcessing() { |
|
632 runStarted = true; |
|
633 |
|
634 // A slight delay to allow this iteration of the event loop to finish (more assertions, etc.) |
|
635 if ( defined.setTimeout ) { |
|
636 setTimeout(function() { |
|
637 if ( config.current && config.current.semaphore > 0 ) { |
|
638 return; |
|
639 } |
|
640 if ( config.timeout ) { |
|
641 clearTimeout( config.timeout ); |
|
642 } |
|
643 |
|
644 begin(); |
|
645 }, 13 ); |
|
646 } else { |
|
647 begin(); |
|
648 } |
|
649 } |
|
650 |
|
651 function pauseProcessing() { |
|
652 config.blocking = true; |
|
653 |
|
654 if ( config.testTimeout && defined.setTimeout ) { |
|
655 clearTimeout( config.timeout ); |
|
656 config.timeout = setTimeout(function() { |
|
657 if ( config.current ) { |
|
658 config.current.semaphore = 0; |
|
659 QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) ); |
|
660 } else { |
|
661 throw new Error( "Test timed out" ); |
|
662 } |
|
663 resumeProcessing(); |
|
664 }, config.testTimeout ); |
|
665 } |
|
666 } |
|
667 |
|
668 function saveGlobal() { |
|
669 config.pollution = []; |
|
670 |
|
671 if ( config.noglobals ) { |
|
672 for ( var key in window ) { |
|
673 if ( hasOwn.call( window, key ) ) { |
|
674 // in Opera sometimes DOM element ids show up here, ignore them |
|
675 if ( /^qunit-test-output/.test( key ) ) { |
|
676 continue; |
|
677 } |
|
678 config.pollution.push( key ); |
|
679 } |
|
680 } |
|
681 } |
|
682 } |
|
683 |
|
684 function checkPollution() { |
|
685 var newGlobals, |
|
686 deletedGlobals, |
|
687 old = config.pollution; |
|
688 |
|
689 saveGlobal(); |
|
690 |
|
691 newGlobals = diff( config.pollution, old ); |
|
692 if ( newGlobals.length > 0 ) { |
|
693 QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) ); |
|
694 } |
|
695 |
|
696 deletedGlobals = diff( old, config.pollution ); |
|
697 if ( deletedGlobals.length > 0 ) { |
|
698 QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) ); |
|
699 } |
|
700 } |
|
701 |
|
702 // returns a new Array with the elements that are in a but not in b |
|
703 function diff( a, b ) { |
|
704 var i, j, |
|
705 result = a.slice(); |
|
706 |
|
707 for ( i = 0; i < result.length; i++ ) { |
|
708 for ( j = 0; j < b.length; j++ ) { |
|
709 if ( result[ i ] === b[ j ] ) { |
|
710 result.splice( i, 1 ); |
|
711 i--; |
|
712 break; |
|
713 } |
|
714 } |
|
715 } |
|
716 return result; |
|
717 } |
|
718 |
|
719 function extend( a, b, undefOnly ) { |
|
720 for ( var prop in b ) { |
|
721 if ( hasOwn.call( b, prop ) ) { |
|
722 |
|
723 // Avoid "Member not found" error in IE8 caused by messing with window.constructor |
|
724 if ( !( prop === "constructor" && a === window ) ) { |
|
725 if ( b[ prop ] === undefined ) { |
|
726 delete a[ prop ]; |
|
727 } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) { |
|
728 a[ prop ] = b[ prop ]; |
|
729 } |
|
730 } |
|
731 } |
|
732 } |
|
733 |
|
734 return a; |
|
735 } |
|
736 |
|
737 function runLoggingCallbacks( key, args ) { |
|
738 var i, l, callbacks; |
|
739 |
|
740 callbacks = config.callbacks[ key ]; |
|
741 for ( i = 0, l = callbacks.length; i < l; i++ ) { |
|
742 callbacks[ i ]( args ); |
|
743 } |
|
744 } |
|
745 |
|
746 // DEPRECATED: This will be removed on 2.0.0+ |
|
747 // This function verifies if the loggingCallbacks were modified by the user |
|
748 // If so, it will restore it, assign the given callback and print a console warning |
|
749 function verifyLoggingCallbacks() { |
|
750 var loggingCallback, userCallback; |
|
751 |
|
752 for ( loggingCallback in loggingCallbacks ) { |
|
753 if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) { |
|
754 |
|
755 userCallback = QUnit[ loggingCallback ]; |
|
756 |
|
757 // Restore the callback function |
|
758 QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ]; |
|
759 |
|
760 // Assign the deprecated given callback |
|
761 QUnit[ loggingCallback ]( userCallback ); |
|
762 |
|
763 if ( window.console && window.console.warn ) { |
|
764 window.console.warn( |
|
765 "QUnit." + loggingCallback + " was replaced with a new value.\n" + |
|
766 "Please, check out the documentation on how to apply logging callbacks.\n" + |
|
767 "Reference: http://api.qunitjs.com/category/callbacks/" |
|
768 ); |
|
769 } |
|
770 } |
|
771 } |
|
772 } |
|
773 |
|
774 // from jquery.js |
|
775 function inArray( elem, array ) { |
|
776 if ( array.indexOf ) { |
|
777 return array.indexOf( elem ); |
|
778 } |
|
779 |
|
780 for ( var i = 0, length = array.length; i < length; i++ ) { |
|
781 if ( array[ i ] === elem ) { |
|
782 return i; |
|
783 } |
|
784 } |
|
785 |
|
786 return -1; |
|
787 } |
|
788 |
|
789 function Test( settings ) { |
|
790 var i, l; |
|
791 |
|
792 ++Test.count; |
|
793 |
|
794 extend( this, settings ); |
|
795 this.assertions = []; |
|
796 this.semaphore = 0; |
|
797 this.usedAsync = false; |
|
798 this.module = config.currentModule; |
|
799 this.stack = sourceFromStacktrace( 3 ); |
|
800 |
|
801 // Register unique strings |
|
802 for ( i = 0, l = this.module.tests; i < l.length; i++ ) { |
|
803 if ( this.module.tests[ i ].name === this.testName ) { |
|
804 this.testName += " "; |
|
805 } |
|
806 } |
|
807 |
|
808 this.testId = generateHash( this.module.name, this.testName ); |
|
809 |
|
810 this.module.tests.push({ |
|
811 name: this.testName, |
|
812 testId: this.testId |
|
813 }); |
|
814 |
|
815 if ( settings.skip ) { |
|
816 |
|
817 // Skipped tests will fully ignore any sent callback |
|
818 this.callback = function() {}; |
|
819 this.async = false; |
|
820 this.expected = 0; |
|
821 } else { |
|
822 this.assert = new Assert( this ); |
|
823 } |
|
824 } |
|
825 |
|
826 Test.count = 0; |
|
827 |
|
828 Test.prototype = { |
|
829 before: function() { |
|
830 if ( |
|
831 |
|
832 // Emit moduleStart when we're switching from one module to another |
|
833 this.module !== config.previousModule || |
|
834 |
|
835 // They could be equal (both undefined) but if the previousModule property doesn't |
|
836 // yet exist it means this is the first test in a suite that isn't wrapped in a |
|
837 // module, in which case we'll just emit a moduleStart event for 'undefined'. |
|
838 // Without this, reporters can get testStart before moduleStart which is a problem. |
|
839 !hasOwn.call( config, "previousModule" ) |
|
840 ) { |
|
841 if ( hasOwn.call( config, "previousModule" ) ) { |
|
842 runLoggingCallbacks( "moduleDone", { |
|
843 name: config.previousModule.name, |
|
844 tests: config.previousModule.tests, |
|
845 failed: config.moduleStats.bad, |
|
846 passed: config.moduleStats.all - config.moduleStats.bad, |
|
847 total: config.moduleStats.all, |
|
848 runtime: now() - config.moduleStats.started |
|
849 }); |
|
850 } |
|
851 config.previousModule = this.module; |
|
852 config.moduleStats = { all: 0, bad: 0, started: now() }; |
|
853 runLoggingCallbacks( "moduleStart", { |
|
854 name: this.module.name, |
|
855 tests: this.module.tests |
|
856 }); |
|
857 } |
|
858 |
|
859 config.current = this; |
|
860 |
|
861 this.testEnvironment = extend( {}, this.module.testEnvironment ); |
|
862 delete this.testEnvironment.beforeEach; |
|
863 delete this.testEnvironment.afterEach; |
|
864 |
|
865 this.started = now(); |
|
866 runLoggingCallbacks( "testStart", { |
|
867 name: this.testName, |
|
868 module: this.module.name, |
|
869 testId: this.testId |
|
870 }); |
|
871 |
|
872 if ( !config.pollution ) { |
|
873 saveGlobal(); |
|
874 } |
|
875 }, |
|
876 |
|
877 run: function() { |
|
878 var promise; |
|
879 |
|
880 config.current = this; |
|
881 |
|
882 if ( this.async ) { |
|
883 QUnit.stop(); |
|
884 } |
|
885 |
|
886 this.callbackStarted = now(); |
|
887 |
|
888 if ( config.notrycatch ) { |
|
889 promise = this.callback.call( this.testEnvironment, this.assert ); |
|
890 this.resolvePromise( promise ); |
|
891 return; |
|
892 } |
|
893 |
|
894 try { |
|
895 promise = this.callback.call( this.testEnvironment, this.assert ); |
|
896 this.resolvePromise( promise ); |
|
897 } catch ( e ) { |
|
898 this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " + |
|
899 this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); |
|
900 |
|
901 // else next test will carry the responsibility |
|
902 saveGlobal(); |
|
903 |
|
904 // Restart the tests if they're blocking |
|
905 if ( config.blocking ) { |
|
906 QUnit.start(); |
|
907 } |
|
908 } |
|
909 }, |
|
910 |
|
911 after: function() { |
|
912 checkPollution(); |
|
913 }, |
|
914 |
|
915 queueHook: function( hook, hookName ) { |
|
916 var promise, |
|
917 test = this; |
|
918 return function runHook() { |
|
919 config.current = test; |
|
920 if ( config.notrycatch ) { |
|
921 promise = hook.call( test.testEnvironment, test.assert ); |
|
922 test.resolvePromise( promise, hookName ); |
|
923 return; |
|
924 } |
|
925 try { |
|
926 promise = hook.call( test.testEnvironment, test.assert ); |
|
927 test.resolvePromise( promise, hookName ); |
|
928 } catch ( error ) { |
|
929 test.pushFailure( hookName + " failed on " + test.testName + ": " + |
|
930 ( error.message || error ), extractStacktrace( error, 0 ) ); |
|
931 } |
|
932 }; |
|
933 }, |
|
934 |
|
935 // Currently only used for module level hooks, can be used to add global level ones |
|
936 hooks: function( handler ) { |
|
937 var hooks = []; |
|
938 |
|
939 // Hooks are ignored on skipped tests |
|
940 if ( this.skip ) { |
|
941 return hooks; |
|
942 } |
|
943 |
|
944 if ( this.module.testEnvironment && |
|
945 QUnit.objectType( this.module.testEnvironment[ handler ] ) === "function" ) { |
|
946 hooks.push( this.queueHook( this.module.testEnvironment[ handler ], handler ) ); |
|
947 } |
|
948 |
|
949 return hooks; |
|
950 }, |
|
951 |
|
952 finish: function() { |
|
953 config.current = this; |
|
954 if ( config.requireExpects && this.expected === null ) { |
|
955 this.pushFailure( "Expected number of assertions to be defined, but expect() was " + |
|
956 "not called.", this.stack ); |
|
957 } else if ( this.expected !== null && this.expected !== this.assertions.length ) { |
|
958 this.pushFailure( "Expected " + this.expected + " assertions, but " + |
|
959 this.assertions.length + " were run", this.stack ); |
|
960 } else if ( this.expected === null && !this.assertions.length ) { |
|
961 this.pushFailure( "Expected at least one assertion, but none were run - call " + |
|
962 "expect(0) to accept zero assertions.", this.stack ); |
|
963 } |
|
964 |
|
965 var i, |
|
966 bad = 0; |
|
967 |
|
968 this.runtime = now() - this.started; |
|
969 config.stats.all += this.assertions.length; |
|
970 config.moduleStats.all += this.assertions.length; |
|
971 |
|
972 for ( i = 0; i < this.assertions.length; i++ ) { |
|
973 if ( !this.assertions[ i ].result ) { |
|
974 bad++; |
|
975 config.stats.bad++; |
|
976 config.moduleStats.bad++; |
|
977 } |
|
978 } |
|
979 |
|
980 runLoggingCallbacks( "testDone", { |
|
981 name: this.testName, |
|
982 module: this.module.name, |
|
983 skipped: !!this.skip, |
|
984 failed: bad, |
|
985 passed: this.assertions.length - bad, |
|
986 total: this.assertions.length, |
|
987 runtime: this.runtime, |
|
988 |
|
989 // HTML Reporter use |
|
990 assertions: this.assertions, |
|
991 testId: this.testId, |
|
992 |
|
993 // DEPRECATED: this property will be removed in 2.0.0, use runtime instead |
|
994 duration: this.runtime |
|
995 }); |
|
996 |
|
997 // QUnit.reset() is deprecated and will be replaced for a new |
|
998 // fixture reset function on QUnit 2.0/2.1. |
|
999 // It's still called here for backwards compatibility handling |
|
1000 QUnit.reset(); |
|
1001 |
|
1002 config.current = undefined; |
|
1003 }, |
|
1004 |
|
1005 queue: function() { |
|
1006 var bad, |
|
1007 test = this; |
|
1008 |
|
1009 if ( !this.valid() ) { |
|
1010 return; |
|
1011 } |
|
1012 |
|
1013 function run() { |
|
1014 |
|
1015 // each of these can by async |
|
1016 synchronize([ |
|
1017 function() { |
|
1018 test.before(); |
|
1019 }, |
|
1020 |
|
1021 test.hooks( "beforeEach" ), |
|
1022 |
|
1023 function() { |
|
1024 test.run(); |
|
1025 }, |
|
1026 |
|
1027 test.hooks( "afterEach" ).reverse(), |
|
1028 |
|
1029 function() { |
|
1030 test.after(); |
|
1031 }, |
|
1032 function() { |
|
1033 test.finish(); |
|
1034 } |
|
1035 ]); |
|
1036 } |
|
1037 |
|
1038 // `bad` initialized at top of scope |
|
1039 // defer when previous test run passed, if storage is available |
|
1040 bad = QUnit.config.reorder && defined.sessionStorage && |
|
1041 +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName ); |
|
1042 |
|
1043 if ( bad ) { |
|
1044 run(); |
|
1045 } else { |
|
1046 synchronize( run, true ); |
|
1047 } |
|
1048 }, |
|
1049 |
|
1050 push: function( result, actual, expected, message ) { |
|
1051 var source, |
|
1052 details = { |
|
1053 module: this.module.name, |
|
1054 name: this.testName, |
|
1055 result: result, |
|
1056 message: message, |
|
1057 actual: actual, |
|
1058 expected: expected, |
|
1059 testId: this.testId, |
|
1060 runtime: now() - this.started |
|
1061 }; |
|
1062 |
|
1063 if ( !result ) { |
|
1064 source = sourceFromStacktrace(); |
|
1065 |
|
1066 if ( source ) { |
|
1067 details.source = source; |
|
1068 } |
|
1069 } |
|
1070 |
|
1071 runLoggingCallbacks( "log", details ); |
|
1072 |
|
1073 this.assertions.push({ |
|
1074 result: !!result, |
|
1075 message: message |
|
1076 }); |
|
1077 }, |
|
1078 |
|
1079 pushFailure: function( message, source, actual ) { |
|
1080 if ( !this instanceof Test ) { |
|
1081 throw new Error( "pushFailure() assertion outside test context, was " + |
|
1082 sourceFromStacktrace( 2 ) ); |
|
1083 } |
|
1084 |
|
1085 var details = { |
|
1086 module: this.module.name, |
|
1087 name: this.testName, |
|
1088 result: false, |
|
1089 message: message || "error", |
|
1090 actual: actual || null, |
|
1091 testId: this.testId, |
|
1092 runtime: now() - this.started |
|
1093 }; |
|
1094 |
|
1095 if ( source ) { |
|
1096 details.source = source; |
|
1097 } |
|
1098 |
|
1099 runLoggingCallbacks( "log", details ); |
|
1100 |
|
1101 this.assertions.push({ |
|
1102 result: false, |
|
1103 message: message |
|
1104 }); |
|
1105 }, |
|
1106 |
|
1107 resolvePromise: function( promise, phase ) { |
|
1108 var then, message, |
|
1109 test = this; |
|
1110 if ( promise != null ) { |
|
1111 then = promise.then; |
|
1112 if ( QUnit.objectType( then ) === "function" ) { |
|
1113 QUnit.stop(); |
|
1114 then.call( |
|
1115 promise, |
|
1116 QUnit.start, |
|
1117 function( error ) { |
|
1118 message = "Promise rejected " + |
|
1119 ( !phase ? "during" : phase.replace( /Each$/, "" ) ) + |
|
1120 " " + test.testName + ": " + ( error.message || error ); |
|
1121 test.pushFailure( message, extractStacktrace( error, 0 ) ); |
|
1122 |
|
1123 // else next test will carry the responsibility |
|
1124 saveGlobal(); |
|
1125 |
|
1126 // Unblock |
|
1127 QUnit.start(); |
|
1128 } |
|
1129 ); |
|
1130 } |
|
1131 } |
|
1132 }, |
|
1133 |
|
1134 valid: function() { |
|
1135 var include, |
|
1136 filter = config.filter && config.filter.toLowerCase(), |
|
1137 module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(), |
|
1138 fullName = ( this.module.name + ": " + this.testName ).toLowerCase(); |
|
1139 |
|
1140 // Internally-generated tests are always valid |
|
1141 if ( this.callback && this.callback.validTest ) { |
|
1142 return true; |
|
1143 } |
|
1144 |
|
1145 if ( config.testId.length > 0 && inArray( this.testId, config.testId ) < 0 ) { |
|
1146 return false; |
|
1147 } |
|
1148 |
|
1149 if ( module && ( !this.module.name || this.module.name.toLowerCase() !== module ) ) { |
|
1150 return false; |
|
1151 } |
|
1152 |
|
1153 if ( !filter ) { |
|
1154 return true; |
|
1155 } |
|
1156 |
|
1157 include = filter.charAt( 0 ) !== "!"; |
|
1158 if ( !include ) { |
|
1159 filter = filter.slice( 1 ); |
|
1160 } |
|
1161 |
|
1162 // If the filter matches, we need to honour include |
|
1163 if ( fullName.indexOf( filter ) !== -1 ) { |
|
1164 return include; |
|
1165 } |
|
1166 |
|
1167 // Otherwise, do the opposite |
|
1168 return !include; |
|
1169 } |
|
1170 |
|
1171 }; |
|
1172 |
|
1173 // Resets the test setup. Useful for tests that modify the DOM. |
|
1174 /* |
|
1175 DEPRECATED: Use multiple tests instead of resetting inside a test. |
|
1176 Use testStart or testDone for custom cleanup. |
|
1177 This method will throw an error in 2.0, and will be removed in 2.1 |
|
1178 */ |
|
1179 QUnit.reset = function() { |
|
1180 |
|
1181 // Return on non-browser environments |
|
1182 // This is necessary to not break on node tests |
|
1183 if ( typeof window === "undefined" ) { |
|
1184 return; |
|
1185 } |
|
1186 |
|
1187 var fixture = defined.document && document.getElementById && |
|
1188 document.getElementById( "qunit-fixture" ); |
|
1189 |
|
1190 if ( fixture ) { |
|
1191 fixture.innerHTML = config.fixture; |
|
1192 } |
|
1193 }; |
|
1194 |
|
1195 QUnit.pushFailure = function() { |
|
1196 if ( !QUnit.config.current ) { |
|
1197 throw new Error( "pushFailure() assertion outside test context, in " + |
|
1198 sourceFromStacktrace( 2 ) ); |
|
1199 } |
|
1200 |
|
1201 // Gets current test obj |
|
1202 var currentTest = QUnit.config.current; |
|
1203 |
|
1204 return currentTest.pushFailure.apply( currentTest, arguments ); |
|
1205 }; |
|
1206 |
|
1207 // Based on Java's String.hashCode, a simple but not |
|
1208 // rigorously collision resistant hashing function |
|
1209 function generateHash( module, testName ) { |
|
1210 var hex, |
|
1211 i = 0, |
|
1212 hash = 0, |
|
1213 str = module + "\x1C" + testName, |
|
1214 len = str.length; |
|
1215 |
|
1216 for ( ; i < len; i++ ) { |
|
1217 hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i ); |
|
1218 hash |= 0; |
|
1219 } |
|
1220 |
|
1221 // Convert the possibly negative integer hash code into an 8 character hex string, which isn't |
|
1222 // strictly necessary but increases user understanding that the id is a SHA-like hash |
|
1223 hex = ( 0x100000000 + hash ).toString( 16 ); |
|
1224 if ( hex.length < 8 ) { |
|
1225 hex = "0000000" + hex; |
|
1226 } |
|
1227 |
|
1228 return hex.slice( -8 ); |
|
1229 } |
|
1230 |
|
1231 function Assert( testContext ) { |
|
1232 this.test = testContext; |
|
1233 } |
|
1234 |
|
1235 // Assert helpers |
|
1236 QUnit.assert = Assert.prototype = { |
|
1237 |
|
1238 // Specify the number of expected assertions to guarantee that failed test |
|
1239 // (no assertions are run at all) don't slip through. |
|
1240 expect: function( asserts ) { |
|
1241 if ( arguments.length === 1 ) { |
|
1242 this.test.expected = asserts; |
|
1243 } else { |
|
1244 return this.test.expected; |
|
1245 } |
|
1246 }, |
|
1247 |
|
1248 // Increment this Test's semaphore counter, then return a single-use function that |
|
1249 // decrements that counter a maximum of once. |
|
1250 async: function() { |
|
1251 var test = this.test, |
|
1252 popped = false; |
|
1253 |
|
1254 test.semaphore += 1; |
|
1255 test.usedAsync = true; |
|
1256 pauseProcessing(); |
|
1257 |
|
1258 return function done() { |
|
1259 if ( !popped ) { |
|
1260 test.semaphore -= 1; |
|
1261 popped = true; |
|
1262 resumeProcessing(); |
|
1263 } else { |
|
1264 test.pushFailure( "Called the callback returned from `assert.async` more than once", |
|
1265 sourceFromStacktrace( 2 ) ); |
|
1266 } |
|
1267 }; |
|
1268 }, |
|
1269 |
|
1270 // Exports test.push() to the user API |
|
1271 push: function( /* result, actual, expected, message */ ) { |
|
1272 var assert = this, |
|
1273 currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current; |
|
1274 |
|
1275 // Backwards compatibility fix. |
|
1276 // Allows the direct use of global exported assertions and QUnit.assert.* |
|
1277 // Although, it's use is not recommended as it can leak assertions |
|
1278 // to other tests from async tests, because we only get a reference to the current test, |
|
1279 // not exactly the test where assertion were intended to be called. |
|
1280 if ( !currentTest ) { |
|
1281 throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) ); |
|
1282 } |
|
1283 |
|
1284 if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) { |
|
1285 currentTest.pushFailure( "Assertion after the final `assert.async` was resolved", |
|
1286 sourceFromStacktrace( 2 ) ); |
|
1287 |
|
1288 // Allow this assertion to continue running anyway... |
|
1289 } |
|
1290 |
|
1291 if ( !( assert instanceof Assert ) ) { |
|
1292 assert = currentTest.assert; |
|
1293 } |
|
1294 return assert.test.push.apply( assert.test, arguments ); |
|
1295 }, |
|
1296 |
|
1297 ok: function( result, message ) { |
|
1298 message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " + |
|
1299 QUnit.dump.parse( result ) ); |
|
1300 this.push( !!result, result, true, message ); |
|
1301 }, |
|
1302 |
|
1303 notOk: function( result, message ) { |
|
1304 message = message || ( !result ? "okay" : "failed, expected argument to be falsy, was: " + |
|
1305 QUnit.dump.parse( result ) ); |
|
1306 this.push( !result, result, false, message ); |
|
1307 }, |
|
1308 |
|
1309 equal: function( actual, expected, message ) { |
|
1310 /*jshint eqeqeq:false */ |
|
1311 this.push( expected == actual, actual, expected, message ); |
|
1312 }, |
|
1313 |
|
1314 notEqual: function( actual, expected, message ) { |
|
1315 /*jshint eqeqeq:false */ |
|
1316 this.push( expected != actual, actual, expected, message ); |
|
1317 }, |
|
1318 |
|
1319 propEqual: function( actual, expected, message ) { |
|
1320 actual = objectValues( actual ); |
|
1321 expected = objectValues( expected ); |
|
1322 this.push( QUnit.equiv( actual, expected ), actual, expected, message ); |
|
1323 }, |
|
1324 |
|
1325 notPropEqual: function( actual, expected, message ) { |
|
1326 actual = objectValues( actual ); |
|
1327 expected = objectValues( expected ); |
|
1328 this.push( !QUnit.equiv( actual, expected ), actual, expected, message ); |
|
1329 }, |
|
1330 |
|
1331 deepEqual: function( actual, expected, message ) { |
|
1332 this.push( QUnit.equiv( actual, expected ), actual, expected, message ); |
|
1333 }, |
|
1334 |
|
1335 notDeepEqual: function( actual, expected, message ) { |
|
1336 this.push( !QUnit.equiv( actual, expected ), actual, expected, message ); |
|
1337 }, |
|
1338 |
|
1339 strictEqual: function( actual, expected, message ) { |
|
1340 this.push( expected === actual, actual, expected, message ); |
|
1341 }, |
|
1342 |
|
1343 notStrictEqual: function( actual, expected, message ) { |
|
1344 this.push( expected !== actual, actual, expected, message ); |
|
1345 }, |
|
1346 |
|
1347 "throws": function( block, expected, message ) { |
|
1348 var actual, expectedType, |
|
1349 expectedOutput = expected, |
|
1350 ok = false, |
|
1351 currentTest = ( this instanceof Assert && this.test ) || QUnit.config.current; |
|
1352 |
|
1353 // 'expected' is optional unless doing string comparison |
|
1354 if ( message == null && typeof expected === "string" ) { |
|
1355 message = expected; |
|
1356 expected = null; |
|
1357 } |
|
1358 |
|
1359 currentTest.ignoreGlobalErrors = true; |
|
1360 try { |
|
1361 block.call( currentTest.testEnvironment ); |
|
1362 } catch (e) { |
|
1363 actual = e; |
|
1364 } |
|
1365 currentTest.ignoreGlobalErrors = false; |
|
1366 |
|
1367 if ( actual ) { |
|
1368 expectedType = QUnit.objectType( expected ); |
|
1369 |
|
1370 // we don't want to validate thrown error |
|
1371 if ( !expected ) { |
|
1372 ok = true; |
|
1373 expectedOutput = null; |
|
1374 |
|
1375 // expected is a regexp |
|
1376 } else if ( expectedType === "regexp" ) { |
|
1377 ok = expected.test( errorString( actual ) ); |
|
1378 |
|
1379 // expected is a string |
|
1380 } else if ( expectedType === "string" ) { |
|
1381 ok = expected === errorString( actual ); |
|
1382 |
|
1383 // expected is a constructor, maybe an Error constructor |
|
1384 } else if ( expectedType === "function" && actual instanceof expected ) { |
|
1385 ok = true; |
|
1386 |
|
1387 // expected is an Error object |
|
1388 } else if ( expectedType === "object" ) { |
|
1389 ok = actual instanceof expected.constructor && |
|
1390 actual.name === expected.name && |
|
1391 actual.message === expected.message; |
|
1392 |
|
1393 // expected is a validation function which returns true if validation passed |
|
1394 } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) { |
|
1395 expectedOutput = null; |
|
1396 ok = true; |
|
1397 } |
|
1398 } |
|
1399 |
|
1400 currentTest.assert.push( ok, actual, expectedOutput, message ); |
|
1401 } |
|
1402 }; |
|
1403 |
|
1404 // Provide an alternative to assert.throws(), for enviroments that consider throws a reserved word |
|
1405 // Known to us are: Closure Compiler, Narwhal |
|
1406 (function() { |
|
1407 /*jshint sub:true */ |
|
1408 Assert.prototype.raises = Assert.prototype[ "throws" ]; |
|
1409 }()); |
|
1410 |
|
1411 // Test for equality any JavaScript type. |
|
1412 // Author: Philippe Rathé <prathe@gmail.com> |
|
1413 QUnit.equiv = (function() { |
|
1414 |
|
1415 // Call the o related callback with the given arguments. |
|
1416 function bindCallbacks( o, callbacks, args ) { |
|
1417 var prop = QUnit.objectType( o ); |
|
1418 if ( prop ) { |
|
1419 if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { |
|
1420 return callbacks[ prop ].apply( callbacks, args ); |
|
1421 } else { |
|
1422 return callbacks[ prop ]; // or undefined |
|
1423 } |
|
1424 } |
|
1425 } |
|
1426 |
|
1427 // the real equiv function |
|
1428 var innerEquiv, |
|
1429 |
|
1430 // stack to decide between skip/abort functions |
|
1431 callers = [], |
|
1432 |
|
1433 // stack to avoiding loops from circular referencing |
|
1434 parents = [], |
|
1435 parentsB = [], |
|
1436 |
|
1437 getProto = Object.getPrototypeOf || function( obj ) { |
|
1438 /* jshint camelcase: false, proto: true */ |
|
1439 return obj.__proto__; |
|
1440 }, |
|
1441 callbacks = (function() { |
|
1442 |
|
1443 // for string, boolean, number and null |
|
1444 function useStrictEquality( b, a ) { |
|
1445 |
|
1446 /*jshint eqeqeq:false */ |
|
1447 if ( b instanceof a.constructor || a instanceof b.constructor ) { |
|
1448 |
|
1449 // to catch short annotation VS 'new' annotation of a |
|
1450 // declaration |
|
1451 // e.g. var i = 1; |
|
1452 // var j = new Number(1); |
|
1453 return a == b; |
|
1454 } else { |
|
1455 return a === b; |
|
1456 } |
|
1457 } |
|
1458 |
|
1459 return { |
|
1460 "string": useStrictEquality, |
|
1461 "boolean": useStrictEquality, |
|
1462 "number": useStrictEquality, |
|
1463 "null": useStrictEquality, |
|
1464 "undefined": useStrictEquality, |
|
1465 |
|
1466 "nan": function( b ) { |
|
1467 return isNaN( b ); |
|
1468 }, |
|
1469 |
|
1470 "date": function( b, a ) { |
|
1471 return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); |
|
1472 }, |
|
1473 |
|
1474 "regexp": function( b, a ) { |
|
1475 return QUnit.objectType( b ) === "regexp" && |
|
1476 |
|
1477 // the regex itself |
|
1478 a.source === b.source && |
|
1479 |
|
1480 // and its modifiers |
|
1481 a.global === b.global && |
|
1482 |
|
1483 // (gmi) ... |
|
1484 a.ignoreCase === b.ignoreCase && |
|
1485 a.multiline === b.multiline && |
|
1486 a.sticky === b.sticky; |
|
1487 }, |
|
1488 |
|
1489 // - skip when the property is a method of an instance (OOP) |
|
1490 // - abort otherwise, |
|
1491 // initial === would have catch identical references anyway |
|
1492 "function": function() { |
|
1493 var caller = callers[ callers.length - 1 ]; |
|
1494 return caller !== Object && typeof caller !== "undefined"; |
|
1495 }, |
|
1496 |
|
1497 "array": function( b, a ) { |
|
1498 var i, j, len, loop, aCircular, bCircular; |
|
1499 |
|
1500 // b could be an object literal here |
|
1501 if ( QUnit.objectType( b ) !== "array" ) { |
|
1502 return false; |
|
1503 } |
|
1504 |
|
1505 len = a.length; |
|
1506 if ( len !== b.length ) { |
|
1507 // safe and faster |
|
1508 return false; |
|
1509 } |
|
1510 |
|
1511 // track reference to avoid circular references |
|
1512 parents.push( a ); |
|
1513 parentsB.push( b ); |
|
1514 for ( i = 0; i < len; i++ ) { |
|
1515 loop = false; |
|
1516 for ( j = 0; j < parents.length; j++ ) { |
|
1517 aCircular = parents[ j ] === a[ i ]; |
|
1518 bCircular = parentsB[ j ] === b[ i ]; |
|
1519 if ( aCircular || bCircular ) { |
|
1520 if ( a[ i ] === b[ i ] || aCircular && bCircular ) { |
|
1521 loop = true; |
|
1522 } else { |
|
1523 parents.pop(); |
|
1524 parentsB.pop(); |
|
1525 return false; |
|
1526 } |
|
1527 } |
|
1528 } |
|
1529 if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { |
|
1530 parents.pop(); |
|
1531 parentsB.pop(); |
|
1532 return false; |
|
1533 } |
|
1534 } |
|
1535 parents.pop(); |
|
1536 parentsB.pop(); |
|
1537 return true; |
|
1538 }, |
|
1539 |
|
1540 "object": function( b, a ) { |
|
1541 |
|
1542 /*jshint forin:false */ |
|
1543 var i, j, loop, aCircular, bCircular, |
|
1544 // Default to true |
|
1545 eq = true, |
|
1546 aProperties = [], |
|
1547 bProperties = []; |
|
1548 |
|
1549 // comparing constructors is more strict than using |
|
1550 // instanceof |
|
1551 if ( a.constructor !== b.constructor ) { |
|
1552 |
|
1553 // Allow objects with no prototype to be equivalent to |
|
1554 // objects with Object as their constructor. |
|
1555 if ( !( ( getProto( a ) === null && getProto( b ) === Object.prototype ) || |
|
1556 ( getProto( b ) === null && getProto( a ) === Object.prototype ) ) ) { |
|
1557 return false; |
|
1558 } |
|
1559 } |
|
1560 |
|
1561 // stack constructor before traversing properties |
|
1562 callers.push( a.constructor ); |
|
1563 |
|
1564 // track reference to avoid circular references |
|
1565 parents.push( a ); |
|
1566 parentsB.push( b ); |
|
1567 |
|
1568 // be strict: don't ensure hasOwnProperty and go deep |
|
1569 for ( i in a ) { |
|
1570 loop = false; |
|
1571 for ( j = 0; j < parents.length; j++ ) { |
|
1572 aCircular = parents[ j ] === a[ i ]; |
|
1573 bCircular = parentsB[ j ] === b[ i ]; |
|
1574 if ( aCircular || bCircular ) { |
|
1575 if ( a[ i ] === b[ i ] || aCircular && bCircular ) { |
|
1576 loop = true; |
|
1577 } else { |
|
1578 eq = false; |
|
1579 break; |
|
1580 } |
|
1581 } |
|
1582 } |
|
1583 aProperties.push( i ); |
|
1584 if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { |
|
1585 eq = false; |
|
1586 break; |
|
1587 } |
|
1588 } |
|
1589 |
|
1590 parents.pop(); |
|
1591 parentsB.pop(); |
|
1592 callers.pop(); // unstack, we are done |
|
1593 |
|
1594 for ( i in b ) { |
|
1595 bProperties.push( i ); // collect b's properties |
|
1596 } |
|
1597 |
|
1598 // Ensures identical properties name |
|
1599 return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); |
|
1600 } |
|
1601 }; |
|
1602 }()); |
|
1603 |
|
1604 innerEquiv = function() { // can take multiple arguments |
|
1605 var args = [].slice.apply( arguments ); |
|
1606 if ( args.length < 2 ) { |
|
1607 return true; // end transition |
|
1608 } |
|
1609 |
|
1610 return ( (function( a, b ) { |
|
1611 if ( a === b ) { |
|
1612 return true; // catch the most you can |
|
1613 } else if ( a === null || b === null || typeof a === "undefined" || |
|
1614 typeof b === "undefined" || |
|
1615 QUnit.objectType( a ) !== QUnit.objectType( b ) ) { |
|
1616 |
|
1617 // don't lose time with error prone cases |
|
1618 return false; |
|
1619 } else { |
|
1620 return bindCallbacks( a, callbacks, [ b, a ] ); |
|
1621 } |
|
1622 |
|
1623 // apply transition with (1..n) arguments |
|
1624 }( args[ 0 ], args[ 1 ] ) ) && |
|
1625 innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) ); |
|
1626 }; |
|
1627 |
|
1628 return innerEquiv; |
|
1629 }()); |
|
1630 |
|
1631 // Based on jsDump by Ariel Flesler |
|
1632 // http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html |
|
1633 QUnit.dump = (function() { |
|
1634 function quote( str ) { |
|
1635 return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\""; |
|
1636 } |
|
1637 function literal( o ) { |
|
1638 return o + ""; |
|
1639 } |
|
1640 function join( pre, arr, post ) { |
|
1641 var s = dump.separator(), |
|
1642 base = dump.indent(), |
|
1643 inner = dump.indent( 1 ); |
|
1644 if ( arr.join ) { |
|
1645 arr = arr.join( "," + s + inner ); |
|
1646 } |
|
1647 if ( !arr ) { |
|
1648 return pre + post; |
|
1649 } |
|
1650 return [ pre, inner + arr, base + post ].join( s ); |
|
1651 } |
|
1652 function array( arr, stack ) { |
|
1653 var i = arr.length, |
|
1654 ret = new Array( i ); |
|
1655 |
|
1656 if ( dump.maxDepth && dump.depth > dump.maxDepth ) { |
|
1657 return "[object Array]"; |
|
1658 } |
|
1659 |
|
1660 this.up(); |
|
1661 while ( i-- ) { |
|
1662 ret[ i ] = this.parse( arr[ i ], undefined, stack ); |
|
1663 } |
|
1664 this.down(); |
|
1665 return join( "[", ret, "]" ); |
|
1666 } |
|
1667 |
|
1668 var reName = /^function (\w+)/, |
|
1669 dump = { |
|
1670 |
|
1671 // objType is used mostly internally, you can fix a (custom) type in advance |
|
1672 parse: function( obj, objType, stack ) { |
|
1673 stack = stack || []; |
|
1674 var res, parser, parserType, |
|
1675 inStack = inArray( obj, stack ); |
|
1676 |
|
1677 if ( inStack !== -1 ) { |
|
1678 return "recursion(" + ( inStack - stack.length ) + ")"; |
|
1679 } |
|
1680 |
|
1681 objType = objType || this.typeOf( obj ); |
|
1682 parser = this.parsers[ objType ]; |
|
1683 parserType = typeof parser; |
|
1684 |
|
1685 if ( parserType === "function" ) { |
|
1686 stack.push( obj ); |
|
1687 res = parser.call( this, obj, stack ); |
|
1688 stack.pop(); |
|
1689 return res; |
|
1690 } |
|
1691 return ( parserType === "string" ) ? parser : this.parsers.error; |
|
1692 }, |
|
1693 typeOf: function( obj ) { |
|
1694 var type; |
|
1695 if ( obj === null ) { |
|
1696 type = "null"; |
|
1697 } else if ( typeof obj === "undefined" ) { |
|
1698 type = "undefined"; |
|
1699 } else if ( QUnit.is( "regexp", obj ) ) { |
|
1700 type = "regexp"; |
|
1701 } else if ( QUnit.is( "date", obj ) ) { |
|
1702 type = "date"; |
|
1703 } else if ( QUnit.is( "function", obj ) ) { |
|
1704 type = "function"; |
|
1705 } else if ( obj.setInterval !== undefined && |
|
1706 obj.document !== undefined && |
|
1707 obj.nodeType === undefined ) { |
|
1708 type = "window"; |
|
1709 } else if ( obj.nodeType === 9 ) { |
|
1710 type = "document"; |
|
1711 } else if ( obj.nodeType ) { |
|
1712 type = "node"; |
|
1713 } else if ( |
|
1714 |
|
1715 // native arrays |
|
1716 toString.call( obj ) === "[object Array]" || |
|
1717 |
|
1718 // NodeList objects |
|
1719 ( typeof obj.length === "number" && obj.item !== undefined && |
|
1720 ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null && |
|
1721 obj[ 0 ] === undefined ) ) ) |
|
1722 ) { |
|
1723 type = "array"; |
|
1724 } else if ( obj.constructor === Error.prototype.constructor ) { |
|
1725 type = "error"; |
|
1726 } else { |
|
1727 type = typeof obj; |
|
1728 } |
|
1729 return type; |
|
1730 }, |
|
1731 separator: function() { |
|
1732 return this.multiline ? this.HTML ? "<br />" : "\n" : this.HTML ? " " : " "; |
|
1733 }, |
|
1734 // extra can be a number, shortcut for increasing-calling-decreasing |
|
1735 indent: function( extra ) { |
|
1736 if ( !this.multiline ) { |
|
1737 return ""; |
|
1738 } |
|
1739 var chr = this.indentChar; |
|
1740 if ( this.HTML ) { |
|
1741 chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); |
|
1742 } |
|
1743 return new Array( this.depth + ( extra || 0 ) ).join( chr ); |
|
1744 }, |
|
1745 up: function( a ) { |
|
1746 this.depth += a || 1; |
|
1747 }, |
|
1748 down: function( a ) { |
|
1749 this.depth -= a || 1; |
|
1750 }, |
|
1751 setParser: function( name, parser ) { |
|
1752 this.parsers[ name ] = parser; |
|
1753 }, |
|
1754 // The next 3 are exposed so you can use them |
|
1755 quote: quote, |
|
1756 literal: literal, |
|
1757 join: join, |
|
1758 // |
|
1759 depth: 1, |
|
1760 maxDepth: QUnit.config.maxDepth, |
|
1761 |
|
1762 // This is the list of parsers, to modify them, use dump.setParser |
|
1763 parsers: { |
|
1764 window: "[Window]", |
|
1765 document: "[Document]", |
|
1766 error: function( error ) { |
|
1767 return "Error(\"" + error.message + "\")"; |
|
1768 }, |
|
1769 unknown: "[Unknown]", |
|
1770 "null": "null", |
|
1771 "undefined": "undefined", |
|
1772 "function": function( fn ) { |
|
1773 var ret = "function", |
|
1774 |
|
1775 // functions never have name in IE |
|
1776 name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ]; |
|
1777 |
|
1778 if ( name ) { |
|
1779 ret += " " + name; |
|
1780 } |
|
1781 ret += "( "; |
|
1782 |
|
1783 ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" ); |
|
1784 return join( ret, dump.parse( fn, "functionCode" ), "}" ); |
|
1785 }, |
|
1786 array: array, |
|
1787 nodelist: array, |
|
1788 "arguments": array, |
|
1789 object: function( map, stack ) { |
|
1790 var keys, key, val, i, nonEnumerableProperties, |
|
1791 ret = []; |
|
1792 |
|
1793 if ( dump.maxDepth && dump.depth > dump.maxDepth ) { |
|
1794 return "[object Object]"; |
|
1795 } |
|
1796 |
|
1797 dump.up(); |
|
1798 keys = []; |
|
1799 for ( key in map ) { |
|
1800 keys.push( key ); |
|
1801 } |
|
1802 |
|
1803 // Some properties are not always enumerable on Error objects. |
|
1804 nonEnumerableProperties = [ "message", "name" ]; |
|
1805 for ( i in nonEnumerableProperties ) { |
|
1806 key = nonEnumerableProperties[ i ]; |
|
1807 if ( key in map && inArray( key, keys ) < 0 ) { |
|
1808 keys.push( key ); |
|
1809 } |
|
1810 } |
|
1811 keys.sort(); |
|
1812 for ( i = 0; i < keys.length; i++ ) { |
|
1813 key = keys[ i ]; |
|
1814 val = map[ key ]; |
|
1815 ret.push( dump.parse( key, "key" ) + ": " + |
|
1816 dump.parse( val, undefined, stack ) ); |
|
1817 } |
|
1818 dump.down(); |
|
1819 return join( "{", ret, "}" ); |
|
1820 }, |
|
1821 node: function( node ) { |
|
1822 var len, i, val, |
|
1823 open = dump.HTML ? "<" : "<", |
|
1824 close = dump.HTML ? ">" : ">", |
|
1825 tag = node.nodeName.toLowerCase(), |
|
1826 ret = open + tag, |
|
1827 attrs = node.attributes; |
|
1828 |
|
1829 if ( attrs ) { |
|
1830 for ( i = 0, len = attrs.length; i < len; i++ ) { |
|
1831 val = attrs[ i ].nodeValue; |
|
1832 |
|
1833 // IE6 includes all attributes in .attributes, even ones not explicitly |
|
1834 // set. Those have values like undefined, null, 0, false, "" or |
|
1835 // "inherit". |
|
1836 if ( val && val !== "inherit" ) { |
|
1837 ret += " " + attrs[ i ].nodeName + "=" + |
|
1838 dump.parse( val, "attribute" ); |
|
1839 } |
|
1840 } |
|
1841 } |
|
1842 ret += close; |
|
1843 |
|
1844 // Show content of TextNode or CDATASection |
|
1845 if ( node.nodeType === 3 || node.nodeType === 4 ) { |
|
1846 ret += node.nodeValue; |
|
1847 } |
|
1848 |
|
1849 return ret + open + "/" + tag + close; |
|
1850 }, |
|
1851 |
|
1852 // function calls it internally, it's the arguments part of the function |
|
1853 functionArgs: function( fn ) { |
|
1854 var args, |
|
1855 l = fn.length; |
|
1856 |
|
1857 if ( !l ) { |
|
1858 return ""; |
|
1859 } |
|
1860 |
|
1861 args = new Array( l ); |
|
1862 while ( l-- ) { |
|
1863 |
|
1864 // 97 is 'a' |
|
1865 args[ l ] = String.fromCharCode( 97 + l ); |
|
1866 } |
|
1867 return " " + args.join( ", " ) + " "; |
|
1868 }, |
|
1869 // object calls it internally, the key part of an item in a map |
|
1870 key: quote, |
|
1871 // function calls it internally, it's the content of the function |
|
1872 functionCode: "[code]", |
|
1873 // node calls it internally, it's an html attribute value |
|
1874 attribute: quote, |
|
1875 string: quote, |
|
1876 date: quote, |
|
1877 regexp: literal, |
|
1878 number: literal, |
|
1879 "boolean": literal |
|
1880 }, |
|
1881 // if true, entities are escaped ( <, >, \t, space and \n ) |
|
1882 HTML: false, |
|
1883 // indentation unit |
|
1884 indentChar: " ", |
|
1885 // if true, items in a collection, are separated by a \n, else just a space. |
|
1886 multiline: true |
|
1887 }; |
|
1888 |
|
1889 return dump; |
|
1890 }()); |
|
1891 |
|
1892 // back compat |
|
1893 QUnit.jsDump = QUnit.dump; |
|
1894 |
|
1895 // For browser, export only select globals |
|
1896 if ( typeof window !== "undefined" ) { |
|
1897 |
|
1898 // Deprecated |
|
1899 // Extend assert methods to QUnit and Global scope through Backwards compatibility |
|
1900 (function() { |
|
1901 var i, |
|
1902 assertions = Assert.prototype; |
|
1903 |
|
1904 function applyCurrent( current ) { |
|
1905 return function() { |
|
1906 var assert = new Assert( QUnit.config.current ); |
|
1907 current.apply( assert, arguments ); |
|
1908 }; |
|
1909 } |
|
1910 |
|
1911 for ( i in assertions ) { |
|
1912 QUnit[ i ] = applyCurrent( assertions[ i ] ); |
|
1913 } |
|
1914 })(); |
|
1915 |
|
1916 (function() { |
|
1917 var i, l, |
|
1918 keys = [ |
|
1919 "test", |
|
1920 "module", |
|
1921 "expect", |
|
1922 "asyncTest", |
|
1923 "start", |
|
1924 "stop", |
|
1925 "ok", |
|
1926 "notOk", |
|
1927 "equal", |
|
1928 "notEqual", |
|
1929 "propEqual", |
|
1930 "notPropEqual", |
|
1931 "deepEqual", |
|
1932 "notDeepEqual", |
|
1933 "strictEqual", |
|
1934 "notStrictEqual", |
|
1935 "throws" |
|
1936 ]; |
|
1937 |
|
1938 for ( i = 0, l = keys.length; i < l; i++ ) { |
|
1939 window[ keys[ i ] ] = QUnit[ keys[ i ] ]; |
|
1940 } |
|
1941 })(); |
|
1942 |
|
1943 window.QUnit = QUnit; |
|
1944 } |
|
1945 |
|
1946 // For nodejs |
|
1947 if ( typeof module !== "undefined" && module && module.exports ) { |
|
1948 module.exports = QUnit; |
|
1949 |
|
1950 // For consistency with CommonJS environments' exports |
|
1951 module.exports.QUnit = QUnit; |
|
1952 } |
|
1953 |
|
1954 // For CommonJS with exports, but without module.exports, like Rhino |
|
1955 if ( typeof exports !== "undefined" && exports ) { |
|
1956 exports.QUnit = QUnit; |
|
1957 } |
|
1958 |
|
1959 if ( typeof define === "function" && define.amd ) { |
|
1960 define( function() { |
|
1961 return QUnit; |
|
1962 } ); |
|
1963 QUnit.config.autostart = false; |
|
1964 } |
|
1965 |
|
1966 // Get a reference to the global object, like window in browsers |
|
1967 }( (function() { |
|
1968 return this; |
|
1969 })() )); |
|
1970 |
|
1971 /*istanbul ignore next */ |
|
1972 // jscs:disable maximumLineLength |
|
1973 /* |
|
1974 * This file is a modified version of google-diff-match-patch's JavaScript implementation |
|
1975 * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), |
|
1976 * modifications are licensed as more fully set forth in LICENSE.txt. |
|
1977 * |
|
1978 * The original source of google-diff-match-patch is attributable and licensed as follows: |
|
1979 * |
|
1980 * Copyright 2006 Google Inc. |
|
1981 * http://code.google.com/p/google-diff-match-patch/ |
|
1982 * |
|
1983 * Licensed under the Apache License, Version 2.0 (the "License"); |
|
1984 * you may not use this file except in compliance with the License. |
|
1985 * You may obtain a copy of the License at |
|
1986 * |
|
1987 * http://www.apache.org/licenses/LICENSE-2.0 |
|
1988 * |
|
1989 * Unless required by applicable law or agreed to in writing, software |
|
1990 * distributed under the License is distributed on an "AS IS" BASIS, |
|
1991 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
1992 * See the License for the specific language governing permissions and |
|
1993 * limitations under the License. |
|
1994 * |
|
1995 * More Info: |
|
1996 * https://code.google.com/p/google-diff-match-patch/ |
|
1997 * |
|
1998 * Usage: QUnit.diff(expected, actual) |
|
1999 * |
|
2000 * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) === "the quick <del>brown </del> fox jump<ins>s</ins><del>ed</del over" |
|
2001 */ |
|
2002 QUnit.diff = (function() { |
|
2003 |
|
2004 function DiffMatchPatch() { |
|
2005 |
|
2006 // Defaults. |
|
2007 // Redefine these in your program to override the defaults. |
|
2008 |
|
2009 // Number of seconds to map a diff before giving up (0 for infinity). |
|
2010 this.DiffTimeout = 1.0; |
|
2011 // Cost of an empty edit operation in terms of edit characters. |
|
2012 this.DiffEditCost = 4; |
|
2013 } |
|
2014 |
|
2015 // DIFF FUNCTIONS |
|
2016 |
|
2017 /** |
|
2018 * The data structure representing a diff is an array of tuples: |
|
2019 * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] |
|
2020 * which means: delete 'Hello', add 'Goodbye' and keep ' world.' |
|
2021 */ |
|
2022 var DIFF_DELETE = -1, |
|
2023 DIFF_INSERT = 1, |
|
2024 DIFF_EQUAL = 0; |
|
2025 |
|
2026 /** |
|
2027 * Find the differences between two texts. Simplifies the problem by stripping |
|
2028 * any common prefix or suffix off the texts before diffing. |
|
2029 * @param {string} text1 Old string to be diffed. |
|
2030 * @param {string} text2 New string to be diffed. |
|
2031 * @param {boolean=} optChecklines Optional speedup flag. If present and false, |
|
2032 * then don't run a line-level diff first to identify the changed areas. |
|
2033 * Defaults to true, which does a faster, slightly less optimal diff. |
|
2034 * @param {number} optDeadline Optional time when the diff should be complete |
|
2035 * by. Used internally for recursive calls. Users should set DiffTimeout |
|
2036 * instead. |
|
2037 * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples. |
|
2038 */ |
|
2039 DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines, optDeadline ) { |
|
2040 var deadline, checklines, commonlength, |
|
2041 commonprefix, commonsuffix, diffs; |
|
2042 // Set a deadline by which time the diff must be complete. |
|
2043 if ( typeof optDeadline === "undefined" ) { |
|
2044 if ( this.DiffTimeout <= 0 ) { |
|
2045 optDeadline = Number.MAX_VALUE; |
|
2046 } else { |
|
2047 optDeadline = ( new Date() ).getTime() + this.DiffTimeout * 1000; |
|
2048 } |
|
2049 } |
|
2050 deadline = optDeadline; |
|
2051 |
|
2052 // Check for null inputs. |
|
2053 if ( text1 === null || text2 === null ) { |
|
2054 throw new Error( "Null input. (DiffMain)" ); |
|
2055 } |
|
2056 |
|
2057 // Check for equality (speedup). |
|
2058 if ( text1 === text2 ) { |
|
2059 if ( text1 ) { |
|
2060 return [ |
|
2061 [ DIFF_EQUAL, text1 ] |
|
2062 ]; |
|
2063 } |
|
2064 return []; |
|
2065 } |
|
2066 |
|
2067 if ( typeof optChecklines === "undefined" ) { |
|
2068 optChecklines = true; |
|
2069 } |
|
2070 |
|
2071 checklines = optChecklines; |
|
2072 |
|
2073 // Trim off common prefix (speedup). |
|
2074 commonlength = this.diffCommonPrefix( text1, text2 ); |
|
2075 commonprefix = text1.substring( 0, commonlength ); |
|
2076 text1 = text1.substring( commonlength ); |
|
2077 text2 = text2.substring( commonlength ); |
|
2078 |
|
2079 // Trim off common suffix (speedup). |
|
2080 ///////// |
|
2081 commonlength = this.diffCommonSuffix( text1, text2 ); |
|
2082 commonsuffix = text1.substring( text1.length - commonlength ); |
|
2083 text1 = text1.substring( 0, text1.length - commonlength ); |
|
2084 text2 = text2.substring( 0, text2.length - commonlength ); |
|
2085 |
|
2086 // Compute the diff on the middle block. |
|
2087 diffs = this.diffCompute( text1, text2, checklines, deadline ); |
|
2088 |
|
2089 // Restore the prefix and suffix. |
|
2090 if ( commonprefix ) { |
|
2091 diffs.unshift( [ DIFF_EQUAL, commonprefix ] ); |
|
2092 } |
|
2093 if ( commonsuffix ) { |
|
2094 diffs.push( [ DIFF_EQUAL, commonsuffix ] ); |
|
2095 } |
|
2096 this.diffCleanupMerge( diffs ); |
|
2097 return diffs; |
|
2098 }; |
|
2099 |
|
2100 /** |
|
2101 * Reduce the number of edits by eliminating operationally trivial equalities. |
|
2102 * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples. |
|
2103 */ |
|
2104 DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) { |
|
2105 var changes, equalities, equalitiesLength, lastequality, |
|
2106 pointer, preIns, preDel, postIns, postDel; |
|
2107 changes = false; |
|
2108 equalities = []; // Stack of indices where equalities are found. |
|
2109 equalitiesLength = 0; // Keeping our own length var is faster in JS. |
|
2110 /** @type {?string} */ |
|
2111 lastequality = null; |
|
2112 // Always equal to diffs[equalities[equalitiesLength - 1]][1] |
|
2113 pointer = 0; // Index of current position. |
|
2114 // Is there an insertion operation before the last equality. |
|
2115 preIns = false; |
|
2116 // Is there a deletion operation before the last equality. |
|
2117 preDel = false; |
|
2118 // Is there an insertion operation after the last equality. |
|
2119 postIns = false; |
|
2120 // Is there a deletion operation after the last equality. |
|
2121 postDel = false; |
|
2122 while ( pointer < diffs.length ) { |
|
2123 if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found. |
|
2124 if ( diffs[ pointer ][ 1 ].length < this.DiffEditCost && ( postIns || postDel ) ) { |
|
2125 // Candidate found. |
|
2126 equalities[ equalitiesLength++ ] = pointer; |
|
2127 preIns = postIns; |
|
2128 preDel = postDel; |
|
2129 lastequality = diffs[ pointer ][ 1 ]; |
|
2130 } else { |
|
2131 // Not a candidate, and can never become one. |
|
2132 equalitiesLength = 0; |
|
2133 lastequality = null; |
|
2134 } |
|
2135 postIns = postDel = false; |
|
2136 } else { // An insertion or deletion. |
|
2137 if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) { |
|
2138 postDel = true; |
|
2139 } else { |
|
2140 postIns = true; |
|
2141 } |
|
2142 /* |
|
2143 * Five types to be split: |
|
2144 * <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del> |
|
2145 * <ins>A</ins>X<ins>C</ins><del>D</del> |
|
2146 * <ins>A</ins><del>B</del>X<ins>C</ins> |
|
2147 * <ins>A</del>X<ins>C</ins><del>D</del> |
|
2148 * <ins>A</ins><del>B</del>X<del>C</del> |
|
2149 */ |
|
2150 if ( lastequality && ( ( preIns && preDel && postIns && postDel ) || |
|
2151 ( ( lastequality.length < this.DiffEditCost / 2 ) && |
|
2152 ( preIns + preDel + postIns + postDel ) === 3 ) ) ) { |
|
2153 // Duplicate record. |
|
2154 diffs.splice( equalities[equalitiesLength - 1], 0, [ DIFF_DELETE, lastequality ] ); |
|
2155 // Change second copy to insert. |
|
2156 diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; |
|
2157 equalitiesLength--; // Throw away the equality we just deleted; |
|
2158 lastequality = null; |
|
2159 if (preIns && preDel) { |
|
2160 // No changes made which could affect previous entry, keep going. |
|
2161 postIns = postDel = true; |
|
2162 equalitiesLength = 0; |
|
2163 } else { |
|
2164 equalitiesLength--; // Throw away the previous equality. |
|
2165 pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; |
|
2166 postIns = postDel = false; |
|
2167 } |
|
2168 changes = true; |
|
2169 } |
|
2170 } |
|
2171 pointer++; |
|
2172 } |
|
2173 |
|
2174 if ( changes ) { |
|
2175 this.diffCleanupMerge( diffs ); |
|
2176 } |
|
2177 }; |
|
2178 |
|
2179 /** |
|
2180 * Convert a diff array into a pretty HTML report. |
|
2181 * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples. |
|
2182 * @param {integer} string to be beautified. |
|
2183 * @return {string} HTML representation. |
|
2184 */ |
|
2185 DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) { |
|
2186 var op, data, x, html = []; |
|
2187 for ( x = 0; x < diffs.length; x++ ) { |
|
2188 op = diffs[x][0]; // Operation (insert, delete, equal) |
|
2189 data = diffs[x][1]; // Text of change. |
|
2190 switch ( op ) { |
|
2191 case DIFF_INSERT: |
|
2192 html[x] = "<ins>" + data + "</ins>"; |
|
2193 break; |
|
2194 case DIFF_DELETE: |
|
2195 html[x] = "<del>" + data + "</del>"; |
|
2196 break; |
|
2197 case DIFF_EQUAL: |
|
2198 html[x] = "<span>" + data + "</span>"; |
|
2199 break; |
|
2200 } |
|
2201 } |
|
2202 return html.join(""); |
|
2203 }; |
|
2204 |
|
2205 /** |
|
2206 * Determine the common prefix of two strings. |
|
2207 * @param {string} text1 First string. |
|
2208 * @param {string} text2 Second string. |
|
2209 * @return {number} The number of characters common to the start of each |
|
2210 * string. |
|
2211 */ |
|
2212 DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) { |
|
2213 var pointermid, pointermax, pointermin, pointerstart; |
|
2214 // Quick check for common null cases. |
|
2215 if ( !text1 || !text2 || text1.charAt(0) !== text2.charAt(0) ) { |
|
2216 return 0; |
|
2217 } |
|
2218 // Binary search. |
|
2219 // Performance analysis: http://neil.fraser.name/news/2007/10/09/ |
|
2220 pointermin = 0; |
|
2221 pointermax = Math.min( text1.length, text2.length ); |
|
2222 pointermid = pointermax; |
|
2223 pointerstart = 0; |
|
2224 while ( pointermin < pointermid ) { |
|
2225 if ( text1.substring( pointerstart, pointermid ) === text2.substring( pointerstart, pointermid ) ) { |
|
2226 pointermin = pointermid; |
|
2227 pointerstart = pointermin; |
|
2228 } else { |
|
2229 pointermax = pointermid; |
|
2230 } |
|
2231 pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); |
|
2232 } |
|
2233 return pointermid; |
|
2234 }; |
|
2235 |
|
2236 /** |
|
2237 * Determine the common suffix of two strings. |
|
2238 * @param {string} text1 First string. |
|
2239 * @param {string} text2 Second string. |
|
2240 * @return {number} The number of characters common to the end of each string. |
|
2241 */ |
|
2242 DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) { |
|
2243 var pointermid, pointermax, pointermin, pointerend; |
|
2244 // Quick check for common null cases. |
|
2245 if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) { |
|
2246 return 0; |
|
2247 } |
|
2248 // Binary search. |
|
2249 // Performance analysis: http://neil.fraser.name/news/2007/10/09/ |
|
2250 pointermin = 0; |
|
2251 pointermax = Math.min(text1.length, text2.length); |
|
2252 pointermid = pointermax; |
|
2253 pointerend = 0; |
|
2254 while ( pointermin < pointermid ) { |
|
2255 if (text1.substring( text1.length - pointermid, text1.length - pointerend ) === |
|
2256 text2.substring( text2.length - pointermid, text2.length - pointerend ) ) { |
|
2257 pointermin = pointermid; |
|
2258 pointerend = pointermin; |
|
2259 } else { |
|
2260 pointermax = pointermid; |
|
2261 } |
|
2262 pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); |
|
2263 } |
|
2264 return pointermid; |
|
2265 }; |
|
2266 |
|
2267 /** |
|
2268 * Find the differences between two texts. Assumes that the texts do not |
|
2269 * have any common prefix or suffix. |
|
2270 * @param {string} text1 Old string to be diffed. |
|
2271 * @param {string} text2 New string to be diffed. |
|
2272 * @param {boolean} checklines Speedup flag. If false, then don't run a |
|
2273 * line-level diff first to identify the changed areas. |
|
2274 * If true, then run a faster, slightly less optimal diff. |
|
2275 * @param {number} deadline Time when the diff should be complete by. |
|
2276 * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples. |
|
2277 * @private |
|
2278 */ |
|
2279 DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) { |
|
2280 var diffs, longtext, shorttext, i, hm, |
|
2281 text1A, text2A, text1B, text2B, |
|
2282 midCommon, diffsA, diffsB; |
|
2283 |
|
2284 if ( !text1 ) { |
|
2285 // Just add some text (speedup). |
|
2286 return [ |
|
2287 [ DIFF_INSERT, text2 ] |
|
2288 ]; |
|
2289 } |
|
2290 |
|
2291 if (!text2) { |
|
2292 // Just delete some text (speedup). |
|
2293 return [ |
|
2294 [ DIFF_DELETE, text1 ] |
|
2295 ]; |
|
2296 } |
|
2297 |
|
2298 longtext = text1.length > text2.length ? text1 : text2; |
|
2299 shorttext = text1.length > text2.length ? text2 : text1; |
|
2300 i = longtext.indexOf( shorttext ); |
|
2301 if ( i !== -1 ) { |
|
2302 // Shorter text is inside the longer text (speedup). |
|
2303 diffs = [ |
|
2304 [ DIFF_INSERT, longtext.substring( 0, i ) ], |
|
2305 [ DIFF_EQUAL, shorttext ], |
|
2306 [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ] |
|
2307 ]; |
|
2308 // Swap insertions for deletions if diff is reversed. |
|
2309 if ( text1.length > text2.length ) { |
|
2310 diffs[0][0] = diffs[2][0] = DIFF_DELETE; |
|
2311 } |
|
2312 return diffs; |
|
2313 } |
|
2314 |
|
2315 if ( shorttext.length === 1 ) { |
|
2316 // Single character string. |
|
2317 // After the previous speedup, the character can't be an equality. |
|
2318 return [ |
|
2319 [ DIFF_DELETE, text1 ], |
|
2320 [ DIFF_INSERT, text2 ] |
|
2321 ]; |
|
2322 } |
|
2323 |
|
2324 // Check to see if the problem can be split in two. |
|
2325 hm = this.diffHalfMatch(text1, text2); |
|
2326 if (hm) { |
|
2327 // A half-match was found, sort out the return data. |
|
2328 text1A = hm[0]; |
|
2329 text1B = hm[1]; |
|
2330 text2A = hm[2]; |
|
2331 text2B = hm[3]; |
|
2332 midCommon = hm[4]; |
|
2333 // Send both pairs off for separate processing. |
|
2334 diffsA = this.DiffMain(text1A, text2A, checklines, deadline); |
|
2335 diffsB = this.DiffMain(text1B, text2B, checklines, deadline); |
|
2336 // Merge the results. |
|
2337 return diffsA.concat([ |
|
2338 [ DIFF_EQUAL, midCommon ] |
|
2339 ], diffsB); |
|
2340 } |
|
2341 |
|
2342 if (checklines && text1.length > 100 && text2.length > 100) { |
|
2343 return this.diffLineMode(text1, text2, deadline); |
|
2344 } |
|
2345 |
|
2346 return this.diffBisect(text1, text2, deadline); |
|
2347 }; |
|
2348 |
|
2349 /** |
|
2350 * Do the two texts share a substring which is at least half the length of the |
|
2351 * longer text? |
|
2352 * This speedup can produce non-minimal diffs. |
|
2353 * @param {string} text1 First string. |
|
2354 * @param {string} text2 Second string. |
|
2355 * @return {Array.<string>} Five element Array, containing the prefix of |
|
2356 * text1, the suffix of text1, the prefix of text2, the suffix of |
|
2357 * text2 and the common middle. Or null if there was no match. |
|
2358 * @private |
|
2359 */ |
|
2360 DiffMatchPatch.prototype.diffHalfMatch = function(text1, text2) { |
|
2361 var longtext, shorttext, dmp, |
|
2362 text1A, text2B, text2A, text1B, midCommon, |
|
2363 hm1, hm2, hm; |
|
2364 if (this.DiffTimeout <= 0) { |
|
2365 // Don't risk returning a non-optimal diff if we have unlimited time. |
|
2366 return null; |
|
2367 } |
|
2368 longtext = text1.length > text2.length ? text1 : text2; |
|
2369 shorttext = text1.length > text2.length ? text2 : text1; |
|
2370 if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { |
|
2371 return null; // Pointless. |
|
2372 } |
|
2373 dmp = this; // 'this' becomes 'window' in a closure. |
|
2374 |
|
2375 /** |
|
2376 * Does a substring of shorttext exist within longtext such that the substring |
|
2377 * is at least half the length of longtext? |
|
2378 * Closure, but does not reference any external variables. |
|
2379 * @param {string} longtext Longer string. |
|
2380 * @param {string} shorttext Shorter string. |
|
2381 * @param {number} i Start index of quarter length substring within longtext. |
|
2382 * @return {Array.<string>} Five element Array, containing the prefix of |
|
2383 * longtext, the suffix of longtext, the prefix of shorttext, the suffix |
|
2384 * of shorttext and the common middle. Or null if there was no match. |
|
2385 * @private |
|
2386 */ |
|
2387 function diffHalfMatchI(longtext, shorttext, i) { |
|
2388 var seed, j, bestCommon, prefixLength, suffixLength, |
|
2389 bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; |
|
2390 // Start with a 1/4 length substring at position i as a seed. |
|
2391 seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); |
|
2392 j = -1; |
|
2393 bestCommon = ""; |
|
2394 while ((j = shorttext.indexOf(seed, j + 1)) !== -1) { |
|
2395 prefixLength = dmp.diffCommonPrefix(longtext.substring(i), |
|
2396 shorttext.substring(j)); |
|
2397 suffixLength = dmp.diffCommonSuffix(longtext.substring(0, i), |
|
2398 shorttext.substring(0, j)); |
|
2399 if (bestCommon.length < suffixLength + prefixLength) { |
|
2400 bestCommon = shorttext.substring(j - suffixLength, j) + |
|
2401 shorttext.substring(j, j + prefixLength); |
|
2402 bestLongtextA = longtext.substring(0, i - suffixLength); |
|
2403 bestLongtextB = longtext.substring(i + prefixLength); |
|
2404 bestShorttextA = shorttext.substring(0, j - suffixLength); |
|
2405 bestShorttextB = shorttext.substring(j + prefixLength); |
|
2406 } |
|
2407 } |
|
2408 if (bestCommon.length * 2 >= longtext.length) { |
|
2409 return [ bestLongtextA, bestLongtextB, |
|
2410 bestShorttextA, bestShorttextB, bestCommon |
|
2411 ]; |
|
2412 } else { |
|
2413 return null; |
|
2414 } |
|
2415 } |
|
2416 |
|
2417 // First check if the second quarter is the seed for a half-match. |
|
2418 hm1 = diffHalfMatchI(longtext, shorttext, |
|
2419 Math.ceil(longtext.length / 4)); |
|
2420 // Check again based on the third quarter. |
|
2421 hm2 = diffHalfMatchI(longtext, shorttext, |
|
2422 Math.ceil(longtext.length / 2)); |
|
2423 if (!hm1 && !hm2) { |
|
2424 return null; |
|
2425 } else if (!hm2) { |
|
2426 hm = hm1; |
|
2427 } else if (!hm1) { |
|
2428 hm = hm2; |
|
2429 } else { |
|
2430 // Both matched. Select the longest. |
|
2431 hm = hm1[4].length > hm2[4].length ? hm1 : hm2; |
|
2432 } |
|
2433 |
|
2434 // A half-match was found, sort out the return data. |
|
2435 text1A, text1B, text2A, text2B; |
|
2436 if (text1.length > text2.length) { |
|
2437 text1A = hm[0]; |
|
2438 text1B = hm[1]; |
|
2439 text2A = hm[2]; |
|
2440 text2B = hm[3]; |
|
2441 } else { |
|
2442 text2A = hm[0]; |
|
2443 text2B = hm[1]; |
|
2444 text1A = hm[2]; |
|
2445 text1B = hm[3]; |
|
2446 } |
|
2447 midCommon = hm[4]; |
|
2448 return [ text1A, text1B, text2A, text2B, midCommon ]; |
|
2449 }; |
|
2450 |
|
2451 /** |
|
2452 * Do a quick line-level diff on both strings, then rediff the parts for |
|
2453 * greater accuracy. |
|
2454 * This speedup can produce non-minimal diffs. |
|
2455 * @param {string} text1 Old string to be diffed. |
|
2456 * @param {string} text2 New string to be diffed. |
|
2457 * @param {number} deadline Time when the diff should be complete by. |
|
2458 * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples. |
|
2459 * @private |
|
2460 */ |
|
2461 DiffMatchPatch.prototype.diffLineMode = function(text1, text2, deadline) { |
|
2462 var a, diffs, linearray, pointer, countInsert, |
|
2463 countDelete, textInsert, textDelete, j; |
|
2464 // Scan the text on a line-by-line basis first. |
|
2465 a = this.diffLinesToChars(text1, text2); |
|
2466 text1 = a.chars1; |
|
2467 text2 = a.chars2; |
|
2468 linearray = a.lineArray; |
|
2469 |
|
2470 diffs = this.DiffMain(text1, text2, false, deadline); |
|
2471 |
|
2472 // Convert the diff back to original text. |
|
2473 this.diffCharsToLines(diffs, linearray); |
|
2474 // Eliminate freak matches (e.g. blank lines) |
|
2475 this.diffCleanupSemantic(diffs); |
|
2476 |
|
2477 // Rediff any replacement blocks, this time character-by-character. |
|
2478 // Add a dummy entry at the end. |
|
2479 diffs.push( [ DIFF_EQUAL, "" ] ); |
|
2480 pointer = 0; |
|
2481 countDelete = 0; |
|
2482 countInsert = 0; |
|
2483 textDelete = ""; |
|
2484 textInsert = ""; |
|
2485 while (pointer < diffs.length) { |
|
2486 switch ( diffs[pointer][0] ) { |
|
2487 case DIFF_INSERT: |
|
2488 countInsert++; |
|
2489 textInsert += diffs[pointer][1]; |
|
2490 break; |
|
2491 case DIFF_DELETE: |
|
2492 countDelete++; |
|
2493 textDelete += diffs[pointer][1]; |
|
2494 break; |
|
2495 case DIFF_EQUAL: |
|
2496 // Upon reaching an equality, check for prior redundancies. |
|
2497 if (countDelete >= 1 && countInsert >= 1) { |
|
2498 // Delete the offending records and add the merged ones. |
|
2499 diffs.splice(pointer - countDelete - countInsert, |
|
2500 countDelete + countInsert); |
|
2501 pointer = pointer - countDelete - countInsert; |
|
2502 a = this.DiffMain(textDelete, textInsert, false, deadline); |
|
2503 for (j = a.length - 1; j >= 0; j--) { |
|
2504 diffs.splice( pointer, 0, a[j] ); |
|
2505 } |
|
2506 pointer = pointer + a.length; |
|
2507 } |
|
2508 countInsert = 0; |
|
2509 countDelete = 0; |
|
2510 textDelete = ""; |
|
2511 textInsert = ""; |
|
2512 break; |
|
2513 } |
|
2514 pointer++; |
|
2515 } |
|
2516 diffs.pop(); // Remove the dummy entry at the end. |
|
2517 |
|
2518 return diffs; |
|
2519 }; |
|
2520 |
|
2521 /** |
|
2522 * Find the 'middle snake' of a diff, split the problem in two |
|
2523 * and return the recursively constructed diff. |
|
2524 * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. |
|
2525 * @param {string} text1 Old string to be diffed. |
|
2526 * @param {string} text2 New string to be diffed. |
|
2527 * @param {number} deadline Time at which to bail if not yet complete. |
|
2528 * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples. |
|
2529 * @private |
|
2530 */ |
|
2531 DiffMatchPatch.prototype.diffBisect = function(text1, text2, deadline) { |
|
2532 var text1Length, text2Length, maxD, vOffset, vLength, |
|
2533 v1, v2, x, delta, front, k1start, k1end, k2start, |
|
2534 k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; |
|
2535 // Cache the text lengths to prevent multiple calls. |
|
2536 text1Length = text1.length; |
|
2537 text2Length = text2.length; |
|
2538 maxD = Math.ceil((text1Length + text2Length) / 2); |
|
2539 vOffset = maxD; |
|
2540 vLength = 2 * maxD; |
|
2541 v1 = new Array(vLength); |
|
2542 v2 = new Array(vLength); |
|
2543 // Setting all elements to -1 is faster in Chrome & Firefox than mixing |
|
2544 // integers and undefined. |
|
2545 for (x = 0; x < vLength; x++) { |
|
2546 v1[x] = -1; |
|
2547 v2[x] = -1; |
|
2548 } |
|
2549 v1[vOffset + 1] = 0; |
|
2550 v2[vOffset + 1] = 0; |
|
2551 delta = text1Length - text2Length; |
|
2552 // If the total number of characters is odd, then the front path will collide |
|
2553 // with the reverse path. |
|
2554 front = (delta % 2 !== 0); |
|
2555 // Offsets for start and end of k loop. |
|
2556 // Prevents mapping of space beyond the grid. |
|
2557 k1start = 0; |
|
2558 k1end = 0; |
|
2559 k2start = 0; |
|
2560 k2end = 0; |
|
2561 for (d = 0; d < maxD; d++) { |
|
2562 // Bail out if deadline is reached. |
|
2563 if ((new Date()).getTime() > deadline) { |
|
2564 break; |
|
2565 } |
|
2566 |
|
2567 // Walk the front path one step. |
|
2568 for (k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { |
|
2569 k1Offset = vOffset + k1; |
|
2570 if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) { |
|
2571 x1 = v1[k1Offset + 1]; |
|
2572 } else { |
|
2573 x1 = v1[k1Offset - 1] + 1; |
|
2574 } |
|
2575 y1 = x1 - k1; |
|
2576 while (x1 < text1Length && y1 < text2Length && |
|
2577 text1.charAt(x1) === text2.charAt(y1)) { |
|
2578 x1++; |
|
2579 y1++; |
|
2580 } |
|
2581 v1[k1Offset] = x1; |
|
2582 if (x1 > text1Length) { |
|
2583 // Ran off the right of the graph. |
|
2584 k1end += 2; |
|
2585 } else if (y1 > text2Length) { |
|
2586 // Ran off the bottom of the graph. |
|
2587 k1start += 2; |
|
2588 } else if (front) { |
|
2589 k2Offset = vOffset + delta - k1; |
|
2590 if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) { |
|
2591 // Mirror x2 onto top-left coordinate system. |
|
2592 x2 = text1Length - v2[k2Offset]; |
|
2593 if (x1 >= x2) { |
|
2594 // Overlap detected. |
|
2595 return this.diffBisectSplit(text1, text2, x1, y1, deadline); |
|
2596 } |
|
2597 } |
|
2598 } |
|
2599 } |
|
2600 |
|
2601 // Walk the reverse path one step. |
|
2602 for (k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { |
|
2603 k2Offset = vOffset + k2; |
|
2604 if ( k2 === -d || (k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) { |
|
2605 x2 = v2[k2Offset + 1]; |
|
2606 } else { |
|
2607 x2 = v2[k2Offset - 1] + 1; |
|
2608 } |
|
2609 y2 = x2 - k2; |
|
2610 while (x2 < text1Length && y2 < text2Length && |
|
2611 text1.charAt(text1Length - x2 - 1) === |
|
2612 text2.charAt(text2Length - y2 - 1)) { |
|
2613 x2++; |
|
2614 y2++; |
|
2615 } |
|
2616 v2[k2Offset] = x2; |
|
2617 if (x2 > text1Length) { |
|
2618 // Ran off the left of the graph. |
|
2619 k2end += 2; |
|
2620 } else if (y2 > text2Length) { |
|
2621 // Ran off the top of the graph. |
|
2622 k2start += 2; |
|
2623 } else if (!front) { |
|
2624 k1Offset = vOffset + delta - k2; |
|
2625 if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) { |
|
2626 x1 = v1[k1Offset]; |
|
2627 y1 = vOffset + x1 - k1Offset; |
|
2628 // Mirror x2 onto top-left coordinate system. |
|
2629 x2 = text1Length - x2; |
|
2630 if (x1 >= x2) { |
|
2631 // Overlap detected. |
|
2632 return this.diffBisectSplit(text1, text2, x1, y1, deadline); |
|
2633 } |
|
2634 } |
|
2635 } |
|
2636 } |
|
2637 } |
|
2638 // Diff took too long and hit the deadline or |
|
2639 // number of diffs equals number of characters, no commonality at all. |
|
2640 return [ |
|
2641 [ DIFF_DELETE, text1 ], |
|
2642 [ DIFF_INSERT, text2 ] |
|
2643 ]; |
|
2644 }; |
|
2645 |
|
2646 /** |
|
2647 * Given the location of the 'middle snake', split the diff in two parts |
|
2648 * and recurse. |
|
2649 * @param {string} text1 Old string to be diffed. |
|
2650 * @param {string} text2 New string to be diffed. |
|
2651 * @param {number} x Index of split point in text1. |
|
2652 * @param {number} y Index of split point in text2. |
|
2653 * @param {number} deadline Time at which to bail if not yet complete. |
|
2654 * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples. |
|
2655 * @private |
|
2656 */ |
|
2657 DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) { |
|
2658 var text1a, text1b, text2a, text2b, diffs, diffsb; |
|
2659 text1a = text1.substring(0, x); |
|
2660 text2a = text2.substring(0, y); |
|
2661 text1b = text1.substring(x); |
|
2662 text2b = text2.substring(y); |
|
2663 |
|
2664 // Compute both diffs serially. |
|
2665 diffs = this.DiffMain(text1a, text2a, false, deadline); |
|
2666 diffsb = this.DiffMain(text1b, text2b, false, deadline); |
|
2667 |
|
2668 return diffs.concat(diffsb); |
|
2669 }; |
|
2670 |
|
2671 /** |
|
2672 * Reduce the number of edits by eliminating semantically trivial equalities. |
|
2673 * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples. |
|
2674 */ |
|
2675 DiffMatchPatch.prototype.diffCleanupSemantic = function(diffs) { |
|
2676 var changes, equalities, equalitiesLength, lastequality, |
|
2677 pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1, |
|
2678 lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2; |
|
2679 changes = false; |
|
2680 equalities = []; // Stack of indices where equalities are found. |
|
2681 equalitiesLength = 0; // Keeping our own length var is faster in JS. |
|
2682 /** @type {?string} */ |
|
2683 lastequality = null; |
|
2684 // Always equal to diffs[equalities[equalitiesLength - 1]][1] |
|
2685 pointer = 0; // Index of current position. |
|
2686 // Number of characters that changed prior to the equality. |
|
2687 lengthInsertions1 = 0; |
|
2688 lengthDeletions1 = 0; |
|
2689 // Number of characters that changed after the equality. |
|
2690 lengthInsertions2 = 0; |
|
2691 lengthDeletions2 = 0; |
|
2692 while (pointer < diffs.length) { |
|
2693 if (diffs[pointer][0] === DIFF_EQUAL) { // Equality found. |
|
2694 equalities[equalitiesLength++] = pointer; |
|
2695 lengthInsertions1 = lengthInsertions2; |
|
2696 lengthDeletions1 = lengthDeletions2; |
|
2697 lengthInsertions2 = 0; |
|
2698 lengthDeletions2 = 0; |
|
2699 lastequality = diffs[pointer][1]; |
|
2700 } else { // An insertion or deletion. |
|
2701 if (diffs[pointer][0] === DIFF_INSERT) { |
|
2702 lengthInsertions2 += diffs[pointer][1].length; |
|
2703 } else { |
|
2704 lengthDeletions2 += diffs[pointer][1].length; |
|
2705 } |
|
2706 // Eliminate an equality that is smaller or equal to the edits on both |
|
2707 // sides of it. |
|
2708 if (lastequality && (lastequality.length <= |
|
2709 Math.max(lengthInsertions1, lengthDeletions1)) && |
|
2710 (lastequality.length <= Math.max(lengthInsertions2, |
|
2711 lengthDeletions2))) { |
|
2712 // Duplicate record. |
|
2713 diffs.splice( equalities[ equalitiesLength - 1 ], 0, [ DIFF_DELETE, lastequality ] ); |
|
2714 // Change second copy to insert. |
|
2715 diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; |
|
2716 // Throw away the equality we just deleted. |
|
2717 equalitiesLength--; |
|
2718 // Throw away the previous equality (it needs to be reevaluated). |
|
2719 equalitiesLength--; |
|
2720 pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; |
|
2721 lengthInsertions1 = 0; // Reset the counters. |
|
2722 lengthDeletions1 = 0; |
|
2723 lengthInsertions2 = 0; |
|
2724 lengthDeletions2 = 0; |
|
2725 lastequality = null; |
|
2726 changes = true; |
|
2727 } |
|
2728 } |
|
2729 pointer++; |
|
2730 } |
|
2731 |
|
2732 // Normalize the diff. |
|
2733 if (changes) { |
|
2734 this.diffCleanupMerge(diffs); |
|
2735 } |
|
2736 |
|
2737 // Find any overlaps between deletions and insertions. |
|
2738 // e.g: <del>abcxxx</del><ins>xxxdef</ins> |
|
2739 // -> <del>abc</del>xxx<ins>def</ins> |
|
2740 // e.g: <del>xxxabc</del><ins>defxxx</ins> |
|
2741 // -> <ins>def</ins>xxx<del>abc</del> |
|
2742 // Only extract an overlap if it is as big as the edit ahead or behind it. |
|
2743 pointer = 1; |
|
2744 while (pointer < diffs.length) { |
|
2745 if (diffs[pointer - 1][0] === DIFF_DELETE && |
|
2746 diffs[pointer][0] === DIFF_INSERT) { |
|
2747 deletion = diffs[pointer - 1][1]; |
|
2748 insertion = diffs[pointer][1]; |
|
2749 overlapLength1 = this.diffCommonOverlap(deletion, insertion); |
|
2750 overlapLength2 = this.diffCommonOverlap(insertion, deletion); |
|
2751 if (overlapLength1 >= overlapLength2) { |
|
2752 if (overlapLength1 >= deletion.length / 2 || |
|
2753 overlapLength1 >= insertion.length / 2) { |
|
2754 // Overlap found. Insert an equality and trim the surrounding edits. |
|
2755 diffs.splice( pointer, 0, [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ] ); |
|
2756 diffs[pointer - 1][1] = |
|
2757 deletion.substring(0, deletion.length - overlapLength1); |
|
2758 diffs[pointer + 1][1] = insertion.substring(overlapLength1); |
|
2759 pointer++; |
|
2760 } |
|
2761 } else { |
|
2762 if (overlapLength2 >= deletion.length / 2 || |
|
2763 overlapLength2 >= insertion.length / 2) { |
|
2764 // Reverse overlap found. |
|
2765 // Insert an equality and swap and trim the surrounding edits. |
|
2766 diffs.splice( pointer, 0, [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ] ); |
|
2767 diffs[pointer - 1][0] = DIFF_INSERT; |
|
2768 diffs[pointer - 1][1] = |
|
2769 insertion.substring(0, insertion.length - overlapLength2); |
|
2770 diffs[pointer + 1][0] = DIFF_DELETE; |
|
2771 diffs[pointer + 1][1] = |
|
2772 deletion.substring(overlapLength2); |
|
2773 pointer++; |
|
2774 } |
|
2775 } |
|
2776 pointer++; |
|
2777 } |
|
2778 pointer++; |
|
2779 } |
|
2780 }; |
|
2781 |
|
2782 /** |
|
2783 * Determine if the suffix of one string is the prefix of another. |
|
2784 * @param {string} text1 First string. |
|
2785 * @param {string} text2 Second string. |
|
2786 * @return {number} The number of characters common to the end of the first |
|
2787 * string and the start of the second string. |
|
2788 * @private |
|
2789 */ |
|
2790 DiffMatchPatch.prototype.diffCommonOverlap = function(text1, text2) { |
|
2791 var text1Length, text2Length, textLength, |
|
2792 best, length, pattern, found; |
|
2793 // Cache the text lengths to prevent multiple calls. |
|
2794 text1Length = text1.length; |
|
2795 text2Length = text2.length; |
|
2796 // Eliminate the null case. |
|
2797 if (text1Length === 0 || text2Length === 0) { |
|
2798 return 0; |
|
2799 } |
|
2800 // Truncate the longer string. |
|
2801 if (text1Length > text2Length) { |
|
2802 text1 = text1.substring(text1Length - text2Length); |
|
2803 } else if (text1Length < text2Length) { |
|
2804 text2 = text2.substring(0, text1Length); |
|
2805 } |
|
2806 textLength = Math.min(text1Length, text2Length); |
|
2807 // Quick check for the worst case. |
|
2808 if (text1 === text2) { |
|
2809 return textLength; |
|
2810 } |
|
2811 |
|
2812 // Start by looking for a single character match |
|
2813 // and increase length until no match is found. |
|
2814 // Performance analysis: http://neil.fraser.name/news/2010/11/04/ |
|
2815 best = 0; |
|
2816 length = 1; |
|
2817 while (true) { |
|
2818 pattern = text1.substring(textLength - length); |
|
2819 found = text2.indexOf(pattern); |
|
2820 if (found === -1) { |
|
2821 return best; |
|
2822 } |
|
2823 length += found; |
|
2824 if (found === 0 || text1.substring(textLength - length) === |
|
2825 text2.substring(0, length)) { |
|
2826 best = length; |
|
2827 length++; |
|
2828 } |
|
2829 } |
|
2830 }; |
|
2831 |
|
2832 /** |
|
2833 * Split two texts into an array of strings. Reduce the texts to a string of |
|
2834 * hashes where each Unicode character represents one line. |
|
2835 * @param {string} text1 First string. |
|
2836 * @param {string} text2 Second string. |
|
2837 * @return {{chars1: string, chars2: string, lineArray: !Array.<string>}} |
|
2838 * An object containing the encoded text1, the encoded text2 and |
|
2839 * the array of unique strings. |
|
2840 * The zeroth element of the array of unique strings is intentionally blank. |
|
2841 * @private |
|
2842 */ |
|
2843 DiffMatchPatch.prototype.diffLinesToChars = function(text1, text2) { |
|
2844 var lineArray, lineHash, chars1, chars2; |
|
2845 lineArray = []; // e.g. lineArray[4] === 'Hello\n' |
|
2846 lineHash = {}; // e.g. lineHash['Hello\n'] === 4 |
|
2847 |
|
2848 // '\x00' is a valid character, but various debuggers don't like it. |
|
2849 // So we'll insert a junk entry to avoid generating a null character. |
|
2850 lineArray[0] = ""; |
|
2851 |
|
2852 /** |
|
2853 * Split a text into an array of strings. Reduce the texts to a string of |
|
2854 * hashes where each Unicode character represents one line. |
|
2855 * Modifies linearray and linehash through being a closure. |
|
2856 * @param {string} text String to encode. |
|
2857 * @return {string} Encoded string. |
|
2858 * @private |
|
2859 */ |
|
2860 function diffLinesToCharsMunge(text) { |
|
2861 var chars, lineStart, lineEnd, lineArrayLength, line; |
|
2862 chars = ""; |
|
2863 // Walk the text, pulling out a substring for each line. |
|
2864 // text.split('\n') would would temporarily double our memory footprint. |
|
2865 // Modifying text would create many large strings to garbage collect. |
|
2866 lineStart = 0; |
|
2867 lineEnd = -1; |
|
2868 // Keeping our own length variable is faster than looking it up. |
|
2869 lineArrayLength = lineArray.length; |
|
2870 while (lineEnd < text.length - 1) { |
|
2871 lineEnd = text.indexOf("\n", lineStart); |
|
2872 if (lineEnd === -1) { |
|
2873 lineEnd = text.length - 1; |
|
2874 } |
|
2875 line = text.substring(lineStart, lineEnd + 1); |
|
2876 lineStart = lineEnd + 1; |
|
2877 |
|
2878 if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : |
|
2879 (lineHash[line] !== undefined)) { |
|
2880 chars += String.fromCharCode( lineHash[ line ] ); |
|
2881 } else { |
|
2882 chars += String.fromCharCode(lineArrayLength); |
|
2883 lineHash[line] = lineArrayLength; |
|
2884 lineArray[lineArrayLength++] = line; |
|
2885 } |
|
2886 } |
|
2887 return chars; |
|
2888 } |
|
2889 |
|
2890 chars1 = diffLinesToCharsMunge(text1); |
|
2891 chars2 = diffLinesToCharsMunge(text2); |
|
2892 return { |
|
2893 chars1: chars1, |
|
2894 chars2: chars2, |
|
2895 lineArray: lineArray |
|
2896 }; |
|
2897 }; |
|
2898 |
|
2899 /** |
|
2900 * Rehydrate the text in a diff from a string of line hashes to real lines of |
|
2901 * text. |
|
2902 * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples. |
|
2903 * @param {!Array.<string>} lineArray Array of unique strings. |
|
2904 * @private |
|
2905 */ |
|
2906 DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) { |
|
2907 var x, chars, text, y; |
|
2908 for ( x = 0; x < diffs.length; x++ ) { |
|
2909 chars = diffs[x][1]; |
|
2910 text = []; |
|
2911 for ( y = 0; y < chars.length; y++ ) { |
|
2912 text[y] = lineArray[chars.charCodeAt(y)]; |
|
2913 } |
|
2914 diffs[x][1] = text.join(""); |
|
2915 } |
|
2916 }; |
|
2917 |
|
2918 /** |
|
2919 * Reorder and merge like edit sections. Merge equalities. |
|
2920 * Any edit section can move as long as it doesn't cross an equality. |
|
2921 * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples. |
|
2922 */ |
|
2923 DiffMatchPatch.prototype.diffCleanupMerge = function(diffs) { |
|
2924 var pointer, countDelete, countInsert, textInsert, textDelete, |
|
2925 commonlength, changes; |
|
2926 diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end. |
|
2927 pointer = 0; |
|
2928 countDelete = 0; |
|
2929 countInsert = 0; |
|
2930 textDelete = ""; |
|
2931 textInsert = ""; |
|
2932 commonlength; |
|
2933 while (pointer < diffs.length) { |
|
2934 switch ( diffs[ pointer ][ 0 ] ) { |
|
2935 case DIFF_INSERT: |
|
2936 countInsert++; |
|
2937 textInsert += diffs[pointer][1]; |
|
2938 pointer++; |
|
2939 break; |
|
2940 case DIFF_DELETE: |
|
2941 countDelete++; |
|
2942 textDelete += diffs[pointer][1]; |
|
2943 pointer++; |
|
2944 break; |
|
2945 case DIFF_EQUAL: |
|
2946 // Upon reaching an equality, check for prior redundancies. |
|
2947 if (countDelete + countInsert > 1) { |
|
2948 if (countDelete !== 0 && countInsert !== 0) { |
|
2949 // Factor out any common prefixies. |
|
2950 commonlength = this.diffCommonPrefix(textInsert, textDelete); |
|
2951 if (commonlength !== 0) { |
|
2952 if ((pointer - countDelete - countInsert) > 0 && |
|
2953 diffs[pointer - countDelete - countInsert - 1][0] === |
|
2954 DIFF_EQUAL) { |
|
2955 diffs[pointer - countDelete - countInsert - 1][1] += |
|
2956 textInsert.substring(0, commonlength); |
|
2957 } else { |
|
2958 diffs.splice( 0, 0, [ DIFF_EQUAL, |
|
2959 textInsert.substring( 0, commonlength ) |
|
2960 ] ); |
|
2961 pointer++; |
|
2962 } |
|
2963 textInsert = textInsert.substring(commonlength); |
|
2964 textDelete = textDelete.substring(commonlength); |
|
2965 } |
|
2966 // Factor out any common suffixies. |
|
2967 commonlength = this.diffCommonSuffix(textInsert, textDelete); |
|
2968 if (commonlength !== 0) { |
|
2969 diffs[pointer][1] = textInsert.substring(textInsert.length - |
|
2970 commonlength) + diffs[pointer][1]; |
|
2971 textInsert = textInsert.substring(0, textInsert.length - |
|
2972 commonlength); |
|
2973 textDelete = textDelete.substring(0, textDelete.length - |
|
2974 commonlength); |
|
2975 } |
|
2976 } |
|
2977 // Delete the offending records and add the merged ones. |
|
2978 if (countDelete === 0) { |
|
2979 diffs.splice( pointer - countInsert, |
|
2980 countDelete + countInsert, [ DIFF_INSERT, textInsert ] ); |
|
2981 } else if (countInsert === 0) { |
|
2982 diffs.splice( pointer - countDelete, |
|
2983 countDelete + countInsert, [ DIFF_DELETE, textDelete ] ); |
|
2984 } else { |
|
2985 diffs.splice( pointer - countDelete - countInsert, |
|
2986 countDelete + countInsert, [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ] ); |
|
2987 } |
|
2988 pointer = pointer - countDelete - countInsert + |
|
2989 (countDelete ? 1 : 0) + (countInsert ? 1 : 0) + 1; |
|
2990 } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) { |
|
2991 // Merge this equality with the previous one. |
|
2992 diffs[pointer - 1][1] += diffs[pointer][1]; |
|
2993 diffs.splice(pointer, 1); |
|
2994 } else { |
|
2995 pointer++; |
|
2996 } |
|
2997 countInsert = 0; |
|
2998 countDelete = 0; |
|
2999 textDelete = ""; |
|
3000 textInsert = ""; |
|
3001 break; |
|
3002 } |
|
3003 } |
|
3004 if (diffs[diffs.length - 1][1] === "") { |
|
3005 diffs.pop(); // Remove the dummy entry at the end. |
|
3006 } |
|
3007 |
|
3008 // Second pass: look for single edits surrounded on both sides by equalities |
|
3009 // which can be shifted sideways to eliminate an equality. |
|
3010 // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC |
|
3011 changes = false; |
|
3012 pointer = 1; |
|
3013 // Intentionally ignore the first and last element (don't need checking). |
|
3014 while (pointer < diffs.length - 1) { |
|
3015 if (diffs[pointer - 1][0] === DIFF_EQUAL && |
|
3016 diffs[pointer + 1][0] === DIFF_EQUAL) { |
|
3017 // This is a single edit surrounded by equalities. |
|
3018 if ( diffs[ pointer ][ 1 ].substring( diffs[ pointer ][ 1 ].length - |
|
3019 diffs[ pointer - 1 ][ 1 ].length ) === diffs[ pointer - 1 ][ 1 ] ) { |
|
3020 // Shift the edit over the previous equality. |
|
3021 diffs[pointer][1] = diffs[pointer - 1][1] + |
|
3022 diffs[pointer][1].substring(0, diffs[pointer][1].length - |
|
3023 diffs[pointer - 1][1].length); |
|
3024 diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; |
|
3025 diffs.splice(pointer - 1, 1); |
|
3026 changes = true; |
|
3027 } else if ( diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer + 1 ][ 1 ].length ) === |
|
3028 diffs[ pointer + 1 ][ 1 ] ) { |
|
3029 // Shift the edit over the next equality. |
|
3030 diffs[pointer - 1][1] += diffs[pointer + 1][1]; |
|
3031 diffs[pointer][1] = |
|
3032 diffs[pointer][1].substring(diffs[pointer + 1][1].length) + |
|
3033 diffs[pointer + 1][1]; |
|
3034 diffs.splice(pointer + 1, 1); |
|
3035 changes = true; |
|
3036 } |
|
3037 } |
|
3038 pointer++; |
|
3039 } |
|
3040 // If shifts were made, the diff needs reordering and another shift sweep. |
|
3041 if (changes) { |
|
3042 this.diffCleanupMerge(diffs); |
|
3043 } |
|
3044 }; |
|
3045 |
|
3046 return function(o, n) { |
|
3047 var diff, output, text; |
|
3048 diff = new DiffMatchPatch(); |
|
3049 output = diff.DiffMain(o, n); |
|
3050 //console.log(output); |
|
3051 diff.diffCleanupEfficiency(output); |
|
3052 text = diff.diffPrettyHtml(output); |
|
3053 |
|
3054 return text; |
|
3055 }; |
|
3056 }()); |
|
3057 // jscs:enable |
|
3058 |
|
3059 (function() { |
|
3060 |
|
3061 // Deprecated QUnit.init - Ref #530 |
|
3062 // Re-initialize the configuration options |
|
3063 QUnit.init = function() { |
|
3064 var tests, banner, result, qunit, |
|
3065 config = QUnit.config; |
|
3066 |
|
3067 config.stats = { all: 0, bad: 0 }; |
|
3068 config.moduleStats = { all: 0, bad: 0 }; |
|
3069 config.started = 0; |
|
3070 config.updateRate = 1000; |
|
3071 config.blocking = false; |
|
3072 config.autostart = true; |
|
3073 config.autorun = false; |
|
3074 config.filter = ""; |
|
3075 config.queue = []; |
|
3076 |
|
3077 // Return on non-browser environments |
|
3078 // This is necessary to not break on node tests |
|
3079 if ( typeof window === "undefined" ) { |
|
3080 return; |
|
3081 } |
|
3082 |
|
3083 qunit = id( "qunit" ); |
|
3084 if ( qunit ) { |
|
3085 qunit.innerHTML = |
|
3086 "<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" + |
|
3087 "<h2 id='qunit-banner'></h2>" + |
|
3088 "<div id='qunit-testrunner-toolbar'></div>" + |
|
3089 "<h2 id='qunit-userAgent'></h2>" + |
|
3090 "<ol id='qunit-tests'></ol>"; |
|
3091 } |
|
3092 |
|
3093 tests = id( "qunit-tests" ); |
|
3094 banner = id( "qunit-banner" ); |
|
3095 result = id( "qunit-testresult" ); |
|
3096 |
|
3097 if ( tests ) { |
|
3098 tests.innerHTML = ""; |
|
3099 } |
|
3100 |
|
3101 if ( banner ) { |
|
3102 banner.className = ""; |
|
3103 } |
|
3104 |
|
3105 if ( result ) { |
|
3106 result.parentNode.removeChild( result ); |
|
3107 } |
|
3108 |
|
3109 if ( tests ) { |
|
3110 result = document.createElement( "p" ); |
|
3111 result.id = "qunit-testresult"; |
|
3112 result.className = "result"; |
|
3113 tests.parentNode.insertBefore( result, tests ); |
|
3114 result.innerHTML = "Running...<br /> "; |
|
3115 } |
|
3116 }; |
|
3117 |
|
3118 // Don't load the HTML Reporter on non-Browser environments |
|
3119 if ( typeof window === "undefined" ) { |
|
3120 return; |
|
3121 } |
|
3122 |
|
3123 var config = QUnit.config, |
|
3124 hasOwn = Object.prototype.hasOwnProperty, |
|
3125 defined = { |
|
3126 document: window.document !== undefined, |
|
3127 sessionStorage: (function() { |
|
3128 var x = "qunit-test-string"; |
|
3129 try { |
|
3130 sessionStorage.setItem( x, x ); |
|
3131 sessionStorage.removeItem( x ); |
|
3132 return true; |
|
3133 } catch ( e ) { |
|
3134 return false; |
|
3135 } |
|
3136 }()) |
|
3137 }, |
|
3138 modulesList = []; |
|
3139 |
|
3140 /** |
|
3141 * Escape text for attribute or text content. |
|
3142 */ |
|
3143 function escapeText( s ) { |
|
3144 if ( !s ) { |
|
3145 return ""; |
|
3146 } |
|
3147 s = s + ""; |
|
3148 |
|
3149 // Both single quotes and double quotes (for attributes) |
|
3150 return s.replace( /['"<>&]/g, function( s ) { |
|
3151 switch ( s ) { |
|
3152 case "'": |
|
3153 return "'"; |
|
3154 case "\"": |
|
3155 return """; |
|
3156 case "<": |
|
3157 return "<"; |
|
3158 case ">": |
|
3159 return ">"; |
|
3160 case "&": |
|
3161 return "&"; |
|
3162 } |
|
3163 }); |
|
3164 } |
|
3165 |
|
3166 /** |
|
3167 * @param {HTMLElement} elem |
|
3168 * @param {string} type |
|
3169 * @param {Function} fn |
|
3170 */ |
|
3171 function addEvent( elem, type, fn ) { |
|
3172 if ( elem.addEventListener ) { |
|
3173 |
|
3174 // Standards-based browsers |
|
3175 elem.addEventListener( type, fn, false ); |
|
3176 } else if ( elem.attachEvent ) { |
|
3177 |
|
3178 // support: IE <9 |
|
3179 elem.attachEvent( "on" + type, function() { |
|
3180 var event = window.event; |
|
3181 if ( !event.target ) { |
|
3182 event.target = event.srcElement || document; |
|
3183 } |
|
3184 |
|
3185 fn.call( elem, event ); |
|
3186 }); |
|
3187 } |
|
3188 } |
|
3189 |
|
3190 /** |
|
3191 * @param {Array|NodeList} elems |
|
3192 * @param {string} type |
|
3193 * @param {Function} fn |
|
3194 */ |
|
3195 function addEvents( elems, type, fn ) { |
|
3196 var i = elems.length; |
|
3197 while ( i-- ) { |
|
3198 addEvent( elems[ i ], type, fn ); |
|
3199 } |
|
3200 } |
|
3201 |
|
3202 function hasClass( elem, name ) { |
|
3203 return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0; |
|
3204 } |
|
3205 |
|
3206 function addClass( elem, name ) { |
|
3207 if ( !hasClass( elem, name ) ) { |
|
3208 elem.className += ( elem.className ? " " : "" ) + name; |
|
3209 } |
|
3210 } |
|
3211 |
|
3212 function toggleClass( elem, name ) { |
|
3213 if ( hasClass( elem, name ) ) { |
|
3214 removeClass( elem, name ); |
|
3215 } else { |
|
3216 addClass( elem, name ); |
|
3217 } |
|
3218 } |
|
3219 |
|
3220 function removeClass( elem, name ) { |
|
3221 var set = " " + elem.className + " "; |
|
3222 |
|
3223 // Class name may appear multiple times |
|
3224 while ( set.indexOf( " " + name + " " ) >= 0 ) { |
|
3225 set = set.replace( " " + name + " ", " " ); |
|
3226 } |
|
3227 |
|
3228 // trim for prettiness |
|
3229 elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" ); |
|
3230 } |
|
3231 |
|
3232 function id( name ) { |
|
3233 return defined.document && document.getElementById && document.getElementById( name ); |
|
3234 } |
|
3235 |
|
3236 function getUrlConfigHtml() { |
|
3237 var i, j, val, |
|
3238 escaped, escapedTooltip, |
|
3239 selection = false, |
|
3240 len = config.urlConfig.length, |
|
3241 urlConfigHtml = ""; |
|
3242 |
|
3243 for ( i = 0; i < len; i++ ) { |
|
3244 val = config.urlConfig[ i ]; |
|
3245 if ( typeof val === "string" ) { |
|
3246 val = { |
|
3247 id: val, |
|
3248 label: val |
|
3249 }; |
|
3250 } |
|
3251 |
|
3252 escaped = escapeText( val.id ); |
|
3253 escapedTooltip = escapeText( val.tooltip ); |
|
3254 |
|
3255 if ( config[ val.id ] === undefined ) { |
|
3256 config[ val.id ] = QUnit.urlParams[ val.id ]; |
|
3257 } |
|
3258 |
|
3259 if ( !val.value || typeof val.value === "string" ) { |
|
3260 urlConfigHtml += "<input id='qunit-urlconfig-" + escaped + |
|
3261 "' name='" + escaped + "' type='checkbox'" + |
|
3262 ( val.value ? " value='" + escapeText( val.value ) + "'" : "" ) + |
|
3263 ( config[ val.id ] ? " checked='checked'" : "" ) + |
|
3264 " title='" + escapedTooltip + "' /><label for='qunit-urlconfig-" + escaped + |
|
3265 "' title='" + escapedTooltip + "'>" + val.label + "</label>"; |
|
3266 } else { |
|
3267 urlConfigHtml += "<label for='qunit-urlconfig-" + escaped + |
|
3268 "' title='" + escapedTooltip + "'>" + val.label + |
|
3269 ": </label><select id='qunit-urlconfig-" + escaped + |
|
3270 "' name='" + escaped + "' title='" + escapedTooltip + "'><option></option>"; |
|
3271 |
|
3272 if ( QUnit.is( "array", val.value ) ) { |
|
3273 for ( j = 0; j < val.value.length; j++ ) { |
|
3274 escaped = escapeText( val.value[ j ] ); |
|
3275 urlConfigHtml += "<option value='" + escaped + "'" + |
|
3276 ( config[ val.id ] === val.value[ j ] ? |
|
3277 ( selection = true ) && " selected='selected'" : "" ) + |
|
3278 ">" + escaped + "</option>"; |
|
3279 } |
|
3280 } else { |
|
3281 for ( j in val.value ) { |
|
3282 if ( hasOwn.call( val.value, j ) ) { |
|
3283 urlConfigHtml += "<option value='" + escapeText( j ) + "'" + |
|
3284 ( config[ val.id ] === j ? |
|
3285 ( selection = true ) && " selected='selected'" : "" ) + |
|
3286 ">" + escapeText( val.value[ j ] ) + "</option>"; |
|
3287 } |
|
3288 } |
|
3289 } |
|
3290 if ( config[ val.id ] && !selection ) { |
|
3291 escaped = escapeText( config[ val.id ] ); |
|
3292 urlConfigHtml += "<option value='" + escaped + |
|
3293 "' selected='selected' disabled='disabled'>" + escaped + "</option>"; |
|
3294 } |
|
3295 urlConfigHtml += "</select>"; |
|
3296 } |
|
3297 } |
|
3298 |
|
3299 return urlConfigHtml; |
|
3300 } |
|
3301 |
|
3302 // Handle "click" events on toolbar checkboxes and "change" for select menus. |
|
3303 // Updates the URL with the new state of `config.urlConfig` values. |
|
3304 function toolbarChanged() { |
|
3305 var updatedUrl, value, |
|
3306 field = this, |
|
3307 params = {}; |
|
3308 |
|
3309 // Detect if field is a select menu or a checkbox |
|
3310 if ( "selectedIndex" in field ) { |
|
3311 value = field.options[ field.selectedIndex ].value || undefined; |
|
3312 } else { |
|
3313 value = field.checked ? ( field.defaultValue || true ) : undefined; |
|
3314 } |
|
3315 |
|
3316 params[ field.name ] = value; |
|
3317 updatedUrl = setUrl( params ); |
|
3318 |
|
3319 if ( "hidepassed" === field.name && "replaceState" in window.history ) { |
|
3320 config[ field.name ] = value || false; |
|
3321 if ( value ) { |
|
3322 addClass( id( "qunit-tests" ), "hidepass" ); |
|
3323 } else { |
|
3324 removeClass( id( "qunit-tests" ), "hidepass" ); |
|
3325 } |
|
3326 |
|
3327 // It is not necessary to refresh the whole page |
|
3328 window.history.replaceState( null, "", updatedUrl ); |
|
3329 } else { |
|
3330 window.location = updatedUrl; |
|
3331 } |
|
3332 } |
|
3333 |
|
3334 function setUrl( params ) { |
|
3335 var key, |
|
3336 querystring = "?"; |
|
3337 |
|
3338 params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params ); |
|
3339 |
|
3340 for ( key in params ) { |
|
3341 if ( hasOwn.call( params, key ) ) { |
|
3342 if ( params[ key ] === undefined ) { |
|
3343 continue; |
|
3344 } |
|
3345 querystring += encodeURIComponent( key ); |
|
3346 if ( params[ key ] !== true ) { |
|
3347 querystring += "=" + encodeURIComponent( params[ key ] ); |
|
3348 } |
|
3349 querystring += "&"; |
|
3350 } |
|
3351 } |
|
3352 return location.protocol + "//" + location.host + |
|
3353 location.pathname + querystring.slice( 0, -1 ); |
|
3354 } |
|
3355 |
|
3356 function applyUrlParams() { |
|
3357 var selectedModule, |
|
3358 modulesList = id( "qunit-modulefilter" ), |
|
3359 filter = id( "qunit-filter-input" ).value; |
|
3360 |
|
3361 selectedModule = modulesList ? |
|
3362 decodeURIComponent( modulesList.options[ modulesList.selectedIndex ].value ) : |
|
3363 undefined; |
|
3364 |
|
3365 window.location = setUrl({ |
|
3366 module: ( selectedModule === "" ) ? undefined : selectedModule, |
|
3367 filter: ( filter === "" ) ? undefined : filter, |
|
3368 |
|
3369 // Remove testId filter |
|
3370 testId: undefined |
|
3371 }); |
|
3372 } |
|
3373 |
|
3374 function toolbarUrlConfigContainer() { |
|
3375 var urlConfigContainer = document.createElement( "span" ); |
|
3376 |
|
3377 urlConfigContainer.innerHTML = getUrlConfigHtml(); |
|
3378 addClass( urlConfigContainer, "qunit-url-config" ); |
|
3379 |
|
3380 // For oldIE support: |
|
3381 // * Add handlers to the individual elements instead of the container |
|
3382 // * Use "click" instead of "change" for checkboxes |
|
3383 addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", toolbarChanged ); |
|
3384 addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged ); |
|
3385 |
|
3386 return urlConfigContainer; |
|
3387 } |
|
3388 |
|
3389 function toolbarLooseFilter() { |
|
3390 var filter = document.createElement( "form" ), |
|
3391 label = document.createElement( "label" ), |
|
3392 input = document.createElement( "input" ), |
|
3393 button = document.createElement( "button" ); |
|
3394 |
|
3395 addClass( filter, "qunit-filter" ); |
|
3396 |
|
3397 label.innerHTML = "Filter: "; |
|
3398 |
|
3399 input.type = "text"; |
|
3400 input.value = config.filter || ""; |
|
3401 input.name = "filter"; |
|
3402 input.id = "qunit-filter-input"; |
|
3403 |
|
3404 button.innerHTML = "Go"; |
|
3405 |
|
3406 label.appendChild( input ); |
|
3407 |
|
3408 filter.appendChild( label ); |
|
3409 filter.appendChild( button ); |
|
3410 addEvent( filter, "submit", function( ev ) { |
|
3411 applyUrlParams(); |
|
3412 |
|
3413 if ( ev && ev.preventDefault ) { |
|
3414 ev.preventDefault(); |
|
3415 } |
|
3416 |
|
3417 return false; |
|
3418 }); |
|
3419 |
|
3420 return filter; |
|
3421 } |
|
3422 |
|
3423 function toolbarModuleFilterHtml() { |
|
3424 var i, |
|
3425 moduleFilterHtml = ""; |
|
3426 |
|
3427 if ( !modulesList.length ) { |
|
3428 return false; |
|
3429 } |
|
3430 |
|
3431 modulesList.sort(function( a, b ) { |
|
3432 return a.localeCompare( b ); |
|
3433 }); |
|
3434 |
|
3435 moduleFilterHtml += "<label for='qunit-modulefilter'>Module: </label>" + |
|
3436 "<select id='qunit-modulefilter' name='modulefilter'><option value='' " + |
|
3437 ( QUnit.urlParams.module === undefined ? "selected='selected'" : "" ) + |
|
3438 ">< All Modules ></option>"; |
|
3439 |
|
3440 for ( i = 0; i < modulesList.length; i++ ) { |
|
3441 moduleFilterHtml += "<option value='" + |
|
3442 escapeText( encodeURIComponent( modulesList[ i ] ) ) + "' " + |
|
3443 ( QUnit.urlParams.module === modulesList[ i ] ? "selected='selected'" : "" ) + |
|
3444 ">" + escapeText( modulesList[ i ] ) + "</option>"; |
|
3445 } |
|
3446 moduleFilterHtml += "</select>"; |
|
3447 |
|
3448 return moduleFilterHtml; |
|
3449 } |
|
3450 |
|
3451 function toolbarModuleFilter() { |
|
3452 var toolbar = id( "qunit-testrunner-toolbar" ), |
|
3453 moduleFilter = document.createElement( "span" ), |
|
3454 moduleFilterHtml = toolbarModuleFilterHtml(); |
|
3455 |
|
3456 if ( !toolbar || !moduleFilterHtml ) { |
|
3457 return false; |
|
3458 } |
|
3459 |
|
3460 moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); |
|
3461 moduleFilter.innerHTML = moduleFilterHtml; |
|
3462 |
|
3463 addEvent( moduleFilter.lastChild, "change", applyUrlParams ); |
|
3464 |
|
3465 toolbar.appendChild( moduleFilter ); |
|
3466 } |
|
3467 |
|
3468 function appendToolbar() { |
|
3469 var toolbar = id( "qunit-testrunner-toolbar" ); |
|
3470 |
|
3471 if ( toolbar ) { |
|
3472 toolbar.appendChild( toolbarUrlConfigContainer() ); |
|
3473 toolbar.appendChild( toolbarLooseFilter() ); |
|
3474 } |
|
3475 } |
|
3476 |
|
3477 function appendHeader() { |
|
3478 var header = id( "qunit-header" ); |
|
3479 |
|
3480 if ( header ) { |
|
3481 header.innerHTML = "<a href='" + |
|
3482 setUrl({ filter: undefined, module: undefined, testId: undefined }) + |
|
3483 "'>" + header.innerHTML + "</a> "; |
|
3484 } |
|
3485 } |
|
3486 |
|
3487 function appendBanner() { |
|
3488 var banner = id( "qunit-banner" ); |
|
3489 |
|
3490 if ( banner ) { |
|
3491 banner.className = ""; |
|
3492 } |
|
3493 } |
|
3494 |
|
3495 function appendTestResults() { |
|
3496 var tests = id( "qunit-tests" ), |
|
3497 result = id( "qunit-testresult" ); |
|
3498 |
|
3499 if ( result ) { |
|
3500 result.parentNode.removeChild( result ); |
|
3501 } |
|
3502 |
|
3503 if ( tests ) { |
|
3504 tests.innerHTML = ""; |
|
3505 result = document.createElement( "p" ); |
|
3506 result.id = "qunit-testresult"; |
|
3507 result.className = "result"; |
|
3508 tests.parentNode.insertBefore( result, tests ); |
|
3509 result.innerHTML = "Running...<br /> "; |
|
3510 } |
|
3511 } |
|
3512 |
|
3513 function storeFixture() { |
|
3514 var fixture = id( "qunit-fixture" ); |
|
3515 if ( fixture ) { |
|
3516 config.fixture = fixture.innerHTML; |
|
3517 } |
|
3518 } |
|
3519 |
|
3520 function appendUserAgent() { |
|
3521 var userAgent = id( "qunit-userAgent" ); |
|
3522 |
|
3523 if ( userAgent ) { |
|
3524 userAgent.innerHTML = ""; |
|
3525 userAgent.appendChild( |
|
3526 document.createTextNode( |
|
3527 "QUnit " + QUnit.version + "; " + navigator.userAgent |
|
3528 ) |
|
3529 ); |
|
3530 } |
|
3531 } |
|
3532 |
|
3533 function appendTestsList( modules ) { |
|
3534 var i, l, x, z, test, moduleObj; |
|
3535 |
|
3536 for ( i = 0, l = modules.length; i < l; i++ ) { |
|
3537 moduleObj = modules[ i ]; |
|
3538 |
|
3539 if ( moduleObj.name ) { |
|
3540 modulesList.push( moduleObj.name ); |
|
3541 } |
|
3542 |
|
3543 for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) { |
|
3544 test = moduleObj.tests[ x ]; |
|
3545 |
|
3546 appendTest( test.name, test.testId, moduleObj.name ); |
|
3547 } |
|
3548 } |
|
3549 } |
|
3550 |
|
3551 function appendTest( name, testId, moduleName ) { |
|
3552 var title, rerunTrigger, testBlock, assertList, |
|
3553 tests = id( "qunit-tests" ); |
|
3554 |
|
3555 if ( !tests ) { |
|
3556 return; |
|
3557 } |
|
3558 |
|
3559 title = document.createElement( "strong" ); |
|
3560 title.innerHTML = getNameHtml( name, moduleName ); |
|
3561 |
|
3562 rerunTrigger = document.createElement( "a" ); |
|
3563 rerunTrigger.innerHTML = "Rerun"; |
|
3564 rerunTrigger.href = setUrl({ testId: testId }); |
|
3565 |
|
3566 testBlock = document.createElement( "li" ); |
|
3567 testBlock.appendChild( title ); |
|
3568 testBlock.appendChild( rerunTrigger ); |
|
3569 testBlock.id = "qunit-test-output-" + testId; |
|
3570 |
|
3571 assertList = document.createElement( "ol" ); |
|
3572 assertList.className = "qunit-assert-list"; |
|
3573 |
|
3574 testBlock.appendChild( assertList ); |
|
3575 |
|
3576 tests.appendChild( testBlock ); |
|
3577 } |
|
3578 |
|
3579 // HTML Reporter initialization and load |
|
3580 QUnit.begin(function( details ) { |
|
3581 var qunit = id( "qunit" ); |
|
3582 |
|
3583 // Fixture is the only one necessary to run without the #qunit element |
|
3584 storeFixture(); |
|
3585 |
|
3586 if ( qunit ) { |
|
3587 qunit.innerHTML = |
|
3588 "<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" + |
|
3589 "<h2 id='qunit-banner'></h2>" + |
|
3590 "<div id='qunit-testrunner-toolbar'></div>" + |
|
3591 "<h2 id='qunit-userAgent'></h2>" + |
|
3592 "<ol id='qunit-tests'></ol>"; |
|
3593 } |
|
3594 |
|
3595 appendHeader(); |
|
3596 appendBanner(); |
|
3597 appendTestResults(); |
|
3598 appendUserAgent(); |
|
3599 appendToolbar(); |
|
3600 appendTestsList( details.modules ); |
|
3601 toolbarModuleFilter(); |
|
3602 |
|
3603 if ( qunit && config.hidepassed ) { |
|
3604 addClass( qunit.lastChild, "hidepass" ); |
|
3605 } |
|
3606 }); |
|
3607 |
|
3608 QUnit.done(function( details ) { |
|
3609 var i, key, |
|
3610 banner = id( "qunit-banner" ), |
|
3611 tests = id( "qunit-tests" ), |
|
3612 html = [ |
|
3613 "Tests completed in ", |
|
3614 details.runtime, |
|
3615 " milliseconds.<br />", |
|
3616 "<span class='passed'>", |
|
3617 details.passed, |
|
3618 "</span> assertions of <span class='total'>", |
|
3619 details.total, |
|
3620 "</span> passed, <span class='failed'>", |
|
3621 details.failed, |
|
3622 "</span> failed." |
|
3623 ].join( "" ); |
|
3624 |
|
3625 if ( banner ) { |
|
3626 banner.className = details.failed ? "qunit-fail" : "qunit-pass"; |
|
3627 } |
|
3628 |
|
3629 if ( tests ) { |
|
3630 id( "qunit-testresult" ).innerHTML = html; |
|
3631 } |
|
3632 |
|
3633 if ( config.altertitle && defined.document && document.title ) { |
|
3634 |
|
3635 // show ✖ for good, ✔ for bad suite result in title |
|
3636 // use escape sequences in case file gets loaded with non-utf-8-charset |
|
3637 document.title = [ |
|
3638 ( details.failed ? "\u2716" : "\u2714" ), |
|
3639 document.title.replace( /^[\u2714\u2716] /i, "" ) |
|
3640 ].join( " " ); |
|
3641 } |
|
3642 |
|
3643 // clear own sessionStorage items if all tests passed |
|
3644 if ( config.reorder && defined.sessionStorage && details.failed === 0 ) { |
|
3645 for ( i = 0; i < sessionStorage.length; i++ ) { |
|
3646 key = sessionStorage.key( i++ ); |
|
3647 if ( key.indexOf( "qunit-test-" ) === 0 ) { |
|
3648 sessionStorage.removeItem( key ); |
|
3649 } |
|
3650 } |
|
3651 } |
|
3652 |
|
3653 // scroll back to top to show results |
|
3654 if ( config.scrolltop && window.scrollTo ) { |
|
3655 window.scrollTo( 0, 0 ); |
|
3656 } |
|
3657 }); |
|
3658 |
|
3659 function getNameHtml( name, module ) { |
|
3660 var nameHtml = ""; |
|
3661 |
|
3662 if ( module ) { |
|
3663 nameHtml = "<span class='module-name'>" + escapeText( module ) + "</span>: "; |
|
3664 } |
|
3665 |
|
3666 nameHtml += "<span class='test-name'>" + escapeText( name ) + "</span>"; |
|
3667 |
|
3668 return nameHtml; |
|
3669 } |
|
3670 |
|
3671 QUnit.testStart(function( details ) { |
|
3672 var running, testBlock, bad; |
|
3673 |
|
3674 testBlock = id( "qunit-test-output-" + details.testId ); |
|
3675 if ( testBlock ) { |
|
3676 testBlock.className = "running"; |
|
3677 } else { |
|
3678 |
|
3679 // Report later registered tests |
|
3680 appendTest( details.name, details.testId, details.module ); |
|
3681 } |
|
3682 |
|
3683 running = id( "qunit-testresult" ); |
|
3684 if ( running ) { |
|
3685 bad = QUnit.config.reorder && defined.sessionStorage && |
|
3686 +sessionStorage.getItem( "qunit-test-" + details.module + "-" + details.name ); |
|
3687 |
|
3688 running.innerHTML = ( bad ? |
|
3689 "Rerunning previously failed test: <br />" : |
|
3690 "Running: <br />" ) + |
|
3691 getNameHtml( details.name, details.module ); |
|
3692 } |
|
3693 |
|
3694 }); |
|
3695 |
|
3696 QUnit.log(function( details ) { |
|
3697 var assertList, assertLi, |
|
3698 message, expected, actual, |
|
3699 testItem = id( "qunit-test-output-" + details.testId ); |
|
3700 |
|
3701 if ( !testItem ) { |
|
3702 return; |
|
3703 } |
|
3704 |
|
3705 message = escapeText( details.message ) || ( details.result ? "okay" : "failed" ); |
|
3706 message = "<span class='test-message'>" + message + "</span>"; |
|
3707 message += "<span class='runtime'>@ " + details.runtime + " ms</span>"; |
|
3708 |
|
3709 // pushFailure doesn't provide details.expected |
|
3710 // when it calls, it's implicit to also not show expected and diff stuff |
|
3711 // Also, we need to check details.expected existence, as it can exist and be undefined |
|
3712 if ( !details.result && hasOwn.call( details, "expected" ) ) { |
|
3713 expected = escapeText( QUnit.dump.parse( details.expected ) ); |
|
3714 actual = escapeText( QUnit.dump.parse( details.actual ) ); |
|
3715 message += "<table><tr class='test-expected'><th>Expected: </th><td><pre>" + |
|
3716 expected + |
|
3717 "</pre></td></tr>"; |
|
3718 |
|
3719 if ( actual !== expected ) { |
|
3720 message += "<tr class='test-actual'><th>Result: </th><td><pre>" + |
|
3721 actual + "</pre></td></tr>" + |
|
3722 "<tr class='test-diff'><th>Diff: </th><td><pre>" + |
|
3723 QUnit.diff( expected, actual ) + "</pre></td></tr>"; |
|
3724 } else { |
|
3725 if ( expected.indexOf( "[object Array]" ) !== -1 || |
|
3726 expected.indexOf( "[object Object]" ) !== -1 ) { |
|
3727 message += "<tr class='test-message'><th>Message: </th><td>" + |
|
3728 "Diff suppressed as the depth of object is more than current max depth (" + |
|
3729 QUnit.config.maxDepth + ").<p>Hint: Use <code>QUnit.dump.maxDepth</code> to " + |
|
3730 " run with a higher max depth or <a href='" + setUrl({ maxDepth: -1 }) + "'>" + |
|
3731 "Rerun</a> without max depth.</p></td></tr>"; |
|
3732 } |
|
3733 } |
|
3734 |
|
3735 if ( details.source ) { |
|
3736 message += "<tr class='test-source'><th>Source: </th><td><pre>" + |
|
3737 escapeText( details.source ) + "</pre></td></tr>"; |
|
3738 } |
|
3739 |
|
3740 message += "</table>"; |
|
3741 |
|
3742 // this occours when pushFailure is set and we have an extracted stack trace |
|
3743 } else if ( !details.result && details.source ) { |
|
3744 message += "<table>" + |
|
3745 "<tr class='test-source'><th>Source: </th><td><pre>" + |
|
3746 escapeText( details.source ) + "</pre></td></tr>" + |
|
3747 "</table>"; |
|
3748 } |
|
3749 |
|
3750 assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; |
|
3751 |
|
3752 assertLi = document.createElement( "li" ); |
|
3753 assertLi.className = details.result ? "pass" : "fail"; |
|
3754 assertLi.innerHTML = message; |
|
3755 assertList.appendChild( assertLi ); |
|
3756 }); |
|
3757 |
|
3758 QUnit.testDone(function( details ) { |
|
3759 var testTitle, time, testItem, assertList, |
|
3760 good, bad, testCounts, skipped, |
|
3761 tests = id( "qunit-tests" ); |
|
3762 |
|
3763 if ( !tests ) { |
|
3764 return; |
|
3765 } |
|
3766 |
|
3767 testItem = id( "qunit-test-output-" + details.testId ); |
|
3768 |
|
3769 assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; |
|
3770 |
|
3771 good = details.passed; |
|
3772 bad = details.failed; |
|
3773 |
|
3774 // store result when possible |
|
3775 if ( config.reorder && defined.sessionStorage ) { |
|
3776 if ( bad ) { |
|
3777 sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad ); |
|
3778 } else { |
|
3779 sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name ); |
|
3780 } |
|
3781 } |
|
3782 |
|
3783 if ( bad === 0 ) { |
|
3784 addClass( assertList, "qunit-collapsed" ); |
|
3785 } |
|
3786 |
|
3787 // testItem.firstChild is the test name |
|
3788 testTitle = testItem.firstChild; |
|
3789 |
|
3790 testCounts = bad ? |
|
3791 "<b class='failed'>" + bad + "</b>, " + "<b class='passed'>" + good + "</b>, " : |
|
3792 ""; |
|
3793 |
|
3794 testTitle.innerHTML += " <b class='counts'>(" + testCounts + |
|
3795 details.assertions.length + ")</b>"; |
|
3796 |
|
3797 if ( details.skipped ) { |
|
3798 testItem.className = "skipped"; |
|
3799 skipped = document.createElement( "em" ); |
|
3800 skipped.className = "qunit-skipped-label"; |
|
3801 skipped.innerHTML = "skipped"; |
|
3802 testItem.insertBefore( skipped, testTitle ); |
|
3803 } else { |
|
3804 addEvent( testTitle, "click", function() { |
|
3805 toggleClass( assertList, "qunit-collapsed" ); |
|
3806 }); |
|
3807 |
|
3808 testItem.className = bad ? "fail" : "pass"; |
|
3809 |
|
3810 time = document.createElement( "span" ); |
|
3811 time.className = "runtime"; |
|
3812 time.innerHTML = details.runtime + " ms"; |
|
3813 testItem.insertBefore( time, assertList ); |
|
3814 } |
|
3815 }); |
|
3816 |
|
3817 if ( defined.document ) { |
|
3818 if ( document.readyState === "complete" ) { |
|
3819 QUnit.load(); |
|
3820 } else { |
|
3821 addEvent( window, "load", QUnit.load ); |
|
3822 } |
|
3823 } else { |
|
3824 config.pageLoaded = true; |
|
3825 config.autorun = true; |
|
3826 } |
|
3827 |
|
3828 })(); |
|