2 ( function ( $, mw
, QUnit
) {
8 * Make a safe copy of localEnv:
9 * - Creates a new object that inherits, instead of modifying the original.
10 * This prevents recursion in the event that a test suite stores inherits
11 * hooks object statically and passes it to multiple QUnit.module() calls.
12 * - Supporting QUnit 1.x 'setup' and 'teardown' hooks
13 * (deprecated in QUnit 1.16, removed in QUnit 2).
15 function makeSafeEnv( localEnv
) {
16 var wrap
= localEnv
? Object
.create( localEnv
) : {};
18 wrap
.beforeEach
= wrap
.beforeEach
|| wrap
.setup
;
20 if ( wrap
.teardown
) {
21 wrap
.afterEach
= wrap
.afterEach
|| wrap
.teardown
;
27 * Add bogus to url to prevent IE crazy caching
29 * @param {string} value a relative path (eg. 'data/foo.js'
30 * or 'data/test.php?foo=bar').
31 * @return {string} Such as 'data/foo.js?131031765087663960'
33 QUnit
.fixurl = function ( value
) {
34 return value
+ ( /\?/.test( value
) ? '&' : '?' )
35 + String( new Date().getTime() )
36 + String( parseInt( Math
.random() * 100000, 10 ) );
43 // For each test() that is asynchronous, allow this time to pass before
44 // killing the test and assuming timeout failure.
45 QUnit
.config
.testTimeout
= 60 * 1000;
47 // Reduce default animation duration from 400ms to 0ms for unit tests
48 // eslint-disable-next-line no-underscore-dangle
49 $.fx
.speeds
._default
= 0;
51 // Add a checkbox to QUnit header to toggle MediaWiki ResourceLoader debug mode.
52 QUnit
.config
.urlConfig
.push( {
54 label
: 'Enable ResourceLoaderDebug',
55 tooltip
: 'Enable debug mode in ResourceLoader',
62 * Glue code for nicer integration with QUnit setup/teardown
63 * Inspired by http://sinonjs.org/releases/sinon-qunit-1.0.0.js
65 sinon
.assert
.fail = function ( msg
) {
66 QUnit
.assert
.ok( false, msg
);
68 sinon
.assert
.pass = function ( msg
) {
69 QUnit
.assert
.ok( true, msg
);
74 properties
: [ 'spy', 'stub', 'mock', 'sandbox' ],
75 // Don't fake timers by default
79 // Extend QUnit.module with:
80 // - Add support for QUnit 1.x 'setup' and 'teardown' hooks
81 // - Add a Sinon sandbox to the test context.
82 // - Add a test fixture to the test context.
84 var orgModule
= QUnit
.module
;
85 QUnit
.module = function ( name
, localEnv
, executeNow
) {
86 var orgExecute
, orgBeforeEach
, orgAfterEach
;
88 // In a nested module, don't re-add our hooks, QUnit does that already.
89 return orgModule
.apply( this, arguments
);
91 if ( arguments
.length
=== 2 && typeof localEnv
=== 'function' ) {
92 executeNow
= localEnv
;
96 // Wrap executeNow() so that we can detect nested modules
97 orgExecute
= executeNow
;
98 executeNow = function () {
101 ret
= orgExecute
.apply( this, arguments
);
107 localEnv
= makeSafeEnv( localEnv
);
108 orgBeforeEach
= localEnv
.beforeEach
;
109 orgAfterEach
= localEnv
.afterEach
;
111 localEnv
.beforeEach = function () {
113 var config
= sinon
.getConfig( sinon
.config
);
114 config
.injectInto
= this;
115 sinon
.sandbox
.create( config
);
118 this.fixture
= document
.createElement( 'div' );
119 this.fixture
.id
= 'qunit-fixture';
120 document
.body
.appendChild( this.fixture
);
122 if ( orgBeforeEach
) {
123 return orgBeforeEach
.apply( this, arguments
);
126 localEnv
.afterEach = function () {
128 if ( orgAfterEach
) {
129 ret
= orgAfterEach
.apply( this, arguments
);
131 this.sandbox
.verifyAndRestore();
132 this.fixture
.parentNode
.removeChild( this.fixture
);
136 return orgModule( name
, localEnv
, executeNow
);
141 * Reset mw.config and others to a fresh copy of the live config for each test(),
142 * and restore it back to the live one afterwards.
144 * @param {Object} [localEnv]
145 * @example (see test suite at the bottom of this file)
148 QUnit
.newMwEnvironment
= ( function () {
149 var warn
, error
, liveConfig
, liveMessages
,
150 MwMap
= mw
.config
.constructor, // internal use only
153 liveConfig
= mw
.config
;
154 liveMessages
= mw
.messages
;
156 function suppressWarnings() {
157 if ( warn
=== undefined ) {
159 error
= mw
.log
.error
;
160 mw
.log
.warn
= mw
.log
.error
= $.noop
;
164 function restoreWarnings() {
165 // Guard against calls not balanced with suppressWarnings()
166 if ( warn
!== undefined ) {
168 mw
.log
.error
= error
;
169 warn
= error
= undefined;
173 function freshConfigCopy( custom
) {
175 // Tests should mock all factors that directly influence the tested code.
176 // For backwards compatibility though we set mw.config to a fresh copy of the live
177 // config. This way any modifications made to mw.config during the test will not
178 // affect other tests, nor the global scope outside the test runner.
179 // This is a shallow copy, since overriding an array or object value via "custom"
180 // should replace it. Setting a config property means you override it, not extend it.
181 // NOTE: It is important that we suppress warnings because extend() will also access
182 // deprecated properties and trigger deprecation warnings from mw.log#deprecate.
184 copy
= $.extend( {}, liveConfig
.get(), custom
);
190 function freshMessagesCopy( custom
) {
191 return $.extend( /* deep */true, {}, liveMessages
.get(), custom
);
195 * @param {jQuery.Event} event
196 * @param {jqXHR} jqXHR
197 * @param {Object} ajaxOptions
199 function trackAjax( event
, jqXHR
, ajaxOptions
) {
200 ajaxRequests
.push( { xhr
: jqXHR
, options
: ajaxOptions
} );
203 return function ( orgEnv
) {
204 var localEnv
, orgBeforeEach
, orgAfterEach
;
206 localEnv
= makeSafeEnv( orgEnv
);
207 // MediaWiki env testing
208 localEnv
.config
= localEnv
.config
|| {};
209 localEnv
.messages
= localEnv
.messages
|| {};
211 orgBeforeEach
= localEnv
.beforeEach
;
212 orgAfterEach
= localEnv
.afterEach
;
214 localEnv
.beforeEach = function () {
215 // Greetings, mock environment!
216 mw
.config
= new MwMap();
217 mw
.config
.set( freshConfigCopy( localEnv
.config
) );
218 mw
.messages
= new MwMap();
219 mw
.messages
.set( freshMessagesCopy( localEnv
.messages
) );
220 // Update reference to mw.messages
221 mw
.jqueryMsg
.setParserDefaults( {
222 messages
: mw
.messages
225 this.suppressWarnings
= suppressWarnings
;
226 this.restoreWarnings
= restoreWarnings
;
228 // Start tracking ajax requests
229 $( document
).on( 'ajaxSend', trackAjax
);
231 if ( orgBeforeEach
) {
232 return orgBeforeEach
.apply( this, arguments
);
235 localEnv
.afterEach = function () {
236 var timers
, pending
, $activeLen
, ret
;
238 if ( orgAfterEach
) {
239 ret
= orgAfterEach
.apply( this, arguments
);
242 // Stop tracking ajax requests
243 $( document
).off( 'ajaxSend', trackAjax
);
245 // As a convenience feature, automatically restore warnings if they're
246 // still suppressed by the end of the test.
249 // Farewell, mock environment!
250 mw
.config
= liveConfig
;
251 mw
.messages
= liveMessages
;
252 // Restore reference to mw.messages
253 mw
.jqueryMsg
.setParserDefaults( {
254 messages
: liveMessages
257 // Tests should use fake timers or wait for animations to complete
258 // Check for incomplete animations/requests/etc and throw if there are any.
259 if ( $.timers
&& $.timers
.length
!== 0 ) {
260 timers
= $.timers
.length
;
261 $.each( $.timers
, function ( i
, timer
) {
262 var node
= timer
.elem
;
263 mw
.log
.warn( 'Unfinished animation #' + i
+ ' in ' + timer
.queue
+ ' queue on ' +
264 mw
.html
.element( node
.nodeName
.toLowerCase(), $( node
).getAttrs() )
267 // Force animations to stop to give the next test a clean start
271 throw new Error( 'Unfinished animations: ' + timers
);
274 // Test should use fake XHR, wait for requests, or call abort()
275 $activeLen
= $.active
;
276 if ( $activeLen
!== undefined && $activeLen
!== 0 ) {
277 pending
= ajaxRequests
.filter( function ( ajax
) {
278 return ajax
.xhr
.state() === 'pending';
280 if ( pending
.length
!== $activeLen
) {
281 mw
.log
.warn( 'Pending requests does not match jQuery.active count' );
283 // Force requests to stop to give the next test a clean start
284 ajaxRequests
.forEach( function ( ajax
, i
) {
286 'AJAX request #' + i
+ ' (state: ' + ajax
.xhr
.state() + ')',
293 throw new Error( 'Pending AJAX requests: ' + pending
.length
+ ' (active: ' + $activeLen
+ ')' );
302 // $.when stops as soon as one fails, which makes sense in most
303 // practical scenarios, but not in a unit test where we really do
304 // need to wait until all of them are finished.
305 QUnit
.whenPromisesComplete = function () {
306 var altPromises
= [];
308 $.each( arguments
, function ( i
, arg
) {
309 var alt
= $.Deferred();
310 altPromises
.push( alt
);
312 // Whether this one fails or not, forwards it to
313 // the 'done' (resolve) callback of the alternative promise.
314 arg
.always( alt
.resolve
);
317 return $.when
.apply( $, altPromises
);
321 * Recursively convert a node to a plain object representing its structure.
322 * Only considers attributes and contents (elements and text nodes).
323 * Attribute values are compared strictly and not normalised.
326 * @return {Object|string} Plain JavaScript value representing the node.
328 function getDomStructure( node
) {
329 var $node
, children
, processedChildren
, i
, len
, el
;
331 if ( node
.nodeType
=== Node
.ELEMENT_NODE
) {
332 children
= $node
.contents();
333 processedChildren
= [];
334 for ( i
= 0, len
= children
.length
; i
< len
; i
++ ) {
336 if ( el
.nodeType
=== Node
.ELEMENT_NODE
|| el
.nodeType
=== Node
.TEXT_NODE
) {
337 processedChildren
.push( getDomStructure( el
) );
342 tagName
: node
.tagName
,
343 attributes
: $node
.getAttrs(),
344 contents
: processedChildren
347 // Should be text node
353 * Gets structure of node for this HTML.
355 * @param {string} html HTML markup for one or more nodes.
357 function getHtmlStructure( html
) {
358 var el
= $( '<div>' ).append( html
)[ 0 ];
359 return getDomStructure( el
);
363 * Add-on assertion helpers
365 // Define the add-ons
368 // Expect boolean true
369 assertTrue: function ( actual
, message
) {
371 result
: actual
=== true,
378 // Expect boolean false
379 assertFalse: function ( actual
, message
) {
381 result
: actual
=== false,
388 // Expect numerical value less than X
389 lt: function ( actual
, expected
, message
) {
391 result
: actual
< expected
,
393 expected
: 'less than ' + expected
,
398 // Expect numerical value less than or equal to X
399 ltOrEq: function ( actual
, expected
, message
) {
401 result
: actual
<= expected
,
403 expected
: 'less than or equal to ' + expected
,
408 // Expect numerical value greater than X
409 gt: function ( actual
, expected
, message
) {
411 result
: actual
> expected
,
413 expected
: 'greater than ' + expected
,
418 // Expect numerical value greater than or equal to X
419 gtOrEq: function ( actual
, expected
, message
) {
421 result
: actual
>= true,
423 expected
: 'greater than or equal to ' + expected
,
429 * Asserts that two HTML strings are structurally equivalent.
431 * @param {string} actualHtml Actual HTML markup.
432 * @param {string} expectedHtml Expected HTML markup
433 * @param {string} message Assertion message.
435 htmlEqual: function ( actualHtml
, expectedHtml
, message
) {
436 var actual
= getHtmlStructure( actualHtml
),
437 expected
= getHtmlStructure( expectedHtml
);
439 result
: QUnit
.equiv( actual
, expected
),
447 * Asserts that two HTML strings are not structurally equivalent.
449 * @param {string} actualHtml Actual HTML markup.
450 * @param {string} expectedHtml Expected HTML markup.
451 * @param {string} message Assertion message.
453 notHtmlEqual: function ( actualHtml
, expectedHtml
, message
) {
454 var actual
= getHtmlStructure( actualHtml
),
455 expected
= getHtmlStructure( expectedHtml
);
458 result
: !QUnit
.equiv( actual
, expected
),
467 $.extend( QUnit
.assert
, addons
);
470 * Small test suite to confirm proper functionality of the utilities and
471 * initializations defined above in this file.
473 QUnit
.module( 'testrunner', QUnit
.newMwEnvironment( {
475 this.mwHtmlLive
= mw
.html
;
477 escape: function () {
482 teardown: function () {
483 mw
.html
= this.mwHtmlLive
;
493 QUnit
.test( 'Setup', function ( assert
) {
494 assert
.equal( mw
.html
.escape( 'foo' ), 'mocked', 'setup() callback was ran.' );
495 assert
.equal( mw
.config
.get( 'testVar' ), 'foo', 'config object applied' );
496 assert
.equal( mw
.messages
.get( 'testMsg' ), 'Foo.', 'messages object applied' );
498 mw
.config
.set( 'testVar', 'bar' );
499 mw
.messages
.set( 'testMsg', 'Bar.' );
502 QUnit
.test( 'Teardown', function ( assert
) {
503 assert
.equal( mw
.config
.get( 'testVar' ), 'foo', 'config object restored and re-applied after test()' );
504 assert
.equal( mw
.messages
.get( 'testMsg' ), 'Foo.', 'messages object restored and re-applied after test()' );
507 QUnit
.test( 'Loader status', function ( assert
) {
509 modules
= mw
.loader
.getModuleNames(),
513 for ( i
= 0, len
= modules
.length
; i
< len
; i
++ ) {
514 state
= mw
.loader
.getState( modules
[ i
] );
515 if ( state
=== 'error' ) {
516 error
.push( modules
[ i
] );
517 } else if ( state
=== 'missing' ) {
518 missing
.push( modules
[ i
] );
522 assert
.deepEqual( error
, [], 'Modules in error state' );
523 assert
.deepEqual( missing
, [], 'Modules in missing state' );
526 QUnit
.test( 'assert.htmlEqual', function ( assert
) {
528 '<div><p class="some classes" data-length="10">Child paragraph with <a href="http://example.com">A link</a></p>Regular text<span>A span</span></div>',
529 '<div><p data-length=\'10\' class=\'some classes\'>Child paragraph with <a href=\'http://example.com\' >A link</a></p>Regular text<span>A span</span></div>',
530 'Attribute order, spacing and quotation marks (equal)'
534 '<div><p class="some classes" data-length="10">Child paragraph with <a href="http://example.com">A link</a></p>Regular text<span>A span</span></div>',
535 '<div><p data-length=\'10\' class=\'some more classes\'>Child paragraph with <a href=\'http://example.com\' >A link</a></p>Regular text<span>A span</span></div>',
536 'Attribute order, spacing and quotation marks (not equal)'
540 '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
541 '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
542 'Multiple root nodes (equal)'
546 '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
547 '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="important" >Last</label><input id="lastname" />',
548 'Multiple root nodes (not equal, last label node is different)'
552 'fo"o<br/>b>ar',
554 'Extra escaping is equal'
559 'Text escaping (not equal)'
563 'foo<a href="http://example.com">example</a>bar',
564 'foo<a href="http://example.com">example</a>bar',
565 'Outer text nodes are compared (equal)'
569 'foo<a href="http://example.com">example</a>bar',
570 'foo<a href="http://example.com">example</a>quux',
571 'Outer text nodes are compared (last text node different)'
575 QUnit
.module( 'testrunner-after', QUnit
.newMwEnvironment() );
577 QUnit
.test( 'Teardown', function ( assert
) {
578 assert
.equal( mw
.html
.escape( '<' ), '<', 'teardown() callback was ran.' );
579 assert
.equal( mw
.config
.get( 'testVar' ), null, 'config object restored to live in next module()' );
580 assert
.equal( mw
.messages
.get( 'testMsg' ), null, 'messages object restored to live in next module()' );
583 QUnit
.module( 'testrunner-each', {
584 beforeEach: function () {
585 this.mwHtmlLive
= mw
.html
;
587 afterEach: function () {
588 mw
.html
= this.mwHtmlLive
;
591 QUnit
.test( 'beforeEach', function ( assert
) {
592 assert
.ok( this.mwHtmlLive
, 'setup() ran' );
595 QUnit
.test( 'afterEach', function ( assert
) {
596 assert
.equal( mw
.html
.escape( '<' ), '<', 'afterEach() ran' );
599 QUnit
.module( 'testrunner-each-compat', {
601 this.mwHtmlLive
= mw
.html
;
603 teardown: function () {
604 mw
.html
= this.mwHtmlLive
;
607 QUnit
.test( 'setup', function ( assert
) {
608 assert
.ok( this.mwHtmlLive
, 'setup() ran' );
611 QUnit
.test( 'teardown', function ( assert
) {
612 assert
.equal( mw
.html
.escape( '<' ), '<', 'teardown() ran' );
615 // Regression test for 'this.sandbox undefined' error, fixed by
616 // ensuring Sinon setup/teardown is not re-run on inner module.
617 QUnit
.module( 'testrunner-nested', function () {
618 QUnit
.module( 'testrunner-nested-inner', function () {
619 QUnit
.test( 'Dummy', function ( assert
) {
620 assert
.ok( true, 'Nested modules supported' );
625 QUnit
.module( 'testrunner-hooks-outer', function () {
626 var beforeHookWasExecuted
= false,
627 afterHookWasExecuted
= false;
628 QUnit
.module( 'testrunner-hooks', {
629 before: function () {
630 beforeHookWasExecuted
= true;
632 // This way we can be sure that module `testrunner-hook-after` will always
633 // be executed after module `testrunner-hooks`
634 QUnit
.module( 'testrunner-hooks-after' );
636 '`after` hook for module `testrunner-hooks` was executed',
637 function ( assert
) {
638 assert
.ok( afterHookWasExecuted
);
643 afterHookWasExecuted
= true;
647 QUnit
.test( '`before` hook was executed', function ( assert
) {
648 assert
.ok( beforeHookWasExecuted
);
652 }( jQuery
, mediaWiki
, QUnit
) );