743660a7753942f1b0f4667155bb0a8a138f6659
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 // eslint-disable-next-line jquery/no-each-util
262 $.each( $.timers
, function ( i
, timer
) {
263 var node
= timer
.elem
;
264 mw
.log
.warn( 'Unfinished animation #' + i
+ ' in ' + timer
.queue
+ ' queue on ' +
265 mw
.html
.element( node
.nodeName
.toLowerCase(), $( node
).getAttrs() )
268 // Force animations to stop to give the next test a clean start
272 throw new Error( 'Unfinished animations: ' + timers
);
275 // Test should use fake XHR, wait for requests, or call abort()
276 $activeLen
= $.active
;
277 if ( $activeLen
!== undefined && $activeLen
!== 0 ) {
278 pending
= ajaxRequests
.filter( function ( ajax
) {
279 return ajax
.xhr
.state() === 'pending';
281 if ( pending
.length
!== $activeLen
) {
282 mw
.log
.warn( 'Pending requests does not match jQuery.active count' );
284 // Force requests to stop to give the next test a clean start
285 ajaxRequests
.forEach( function ( ajax
, i
) {
287 'AJAX request #' + i
+ ' (state: ' + ajax
.xhr
.state() + ')',
294 throw new Error( 'Pending AJAX requests: ' + pending
.length
+ ' (active: ' + $activeLen
+ ')' );
303 // $.when stops as soon as one fails, which makes sense in most
304 // practical scenarios, but not in a unit test where we really do
305 // need to wait until all of them are finished.
306 QUnit
.whenPromisesComplete = function () {
307 var altPromises
= [];
309 // When we have ES6 support we'll be able to use Array.from here
310 // eslint-disable-next-line jquery/no-each-util
311 $.each( arguments
, function ( i
, arg
) {
312 var alt
= $.Deferred();
313 altPromises
.push( alt
);
315 // Whether this one fails or not, forwards it to
316 // the 'done' (resolve) callback of the alternative promise.
317 arg
.always( alt
.resolve
);
320 return $.when
.apply( $, altPromises
);
324 * Recursively convert a node to a plain object representing its structure.
325 * Only considers attributes and contents (elements and text nodes).
326 * Attribute values are compared strictly and not normalised.
329 * @return {Object|string} Plain JavaScript value representing the node.
331 function getDomStructure( node
) {
332 var $node
, children
, processedChildren
, i
, len
, el
;
334 if ( node
.nodeType
=== Node
.ELEMENT_NODE
) {
335 children
= $node
.contents();
336 processedChildren
= [];
337 for ( i
= 0, len
= children
.length
; i
< len
; i
++ ) {
339 if ( el
.nodeType
=== Node
.ELEMENT_NODE
|| el
.nodeType
=== Node
.TEXT_NODE
) {
340 processedChildren
.push( getDomStructure( el
) );
345 tagName
: node
.tagName
,
346 attributes
: $node
.getAttrs(),
347 contents
: processedChildren
350 // Should be text node
356 * Gets structure of node for this HTML.
358 * @param {string} html HTML markup for one or more nodes.
360 function getHtmlStructure( html
) {
361 var el
= $( '<div>' ).append( html
)[ 0 ];
362 return getDomStructure( el
);
366 * Add-on assertion helpers
368 // Define the add-ons
371 // Expect boolean true
372 assertTrue: function ( actual
, message
) {
374 result
: actual
=== true,
381 // Expect boolean false
382 assertFalse: function ( actual
, message
) {
384 result
: actual
=== false,
391 // Expect numerical value less than X
392 lt: function ( actual
, expected
, message
) {
394 result
: actual
< expected
,
396 expected
: 'less than ' + expected
,
401 // Expect numerical value less than or equal to X
402 ltOrEq: function ( actual
, expected
, message
) {
404 result
: actual
<= expected
,
406 expected
: 'less than or equal to ' + expected
,
411 // Expect numerical value greater than X
412 gt: function ( actual
, expected
, message
) {
414 result
: actual
> expected
,
416 expected
: 'greater than ' + expected
,
421 // Expect numerical value greater than or equal to X
422 gtOrEq: function ( actual
, expected
, message
) {
424 result
: actual
>= true,
426 expected
: 'greater than or equal to ' + expected
,
432 * Asserts that two HTML strings are structurally equivalent.
434 * @param {string} actualHtml Actual HTML markup.
435 * @param {string} expectedHtml Expected HTML markup
436 * @param {string} message Assertion message.
438 htmlEqual: function ( actualHtml
, expectedHtml
, message
) {
439 var actual
= getHtmlStructure( actualHtml
),
440 expected
= getHtmlStructure( expectedHtml
);
442 result
: QUnit
.equiv( actual
, expected
),
450 * Asserts that two HTML strings are not structurally equivalent.
452 * @param {string} actualHtml Actual HTML markup.
453 * @param {string} expectedHtml Expected HTML markup.
454 * @param {string} message Assertion message.
456 notHtmlEqual: function ( actualHtml
, expectedHtml
, message
) {
457 var actual
= getHtmlStructure( actualHtml
),
458 expected
= getHtmlStructure( expectedHtml
);
461 result
: !QUnit
.equiv( actual
, expected
),
470 $.extend( QUnit
.assert
, addons
);
473 * Small test suite to confirm proper functionality of the utilities and
474 * initializations defined above in this file.
476 QUnit
.module( 'testrunner', QUnit
.newMwEnvironment( {
478 this.mwHtmlLive
= mw
.html
;
480 escape: function () {
485 teardown: function () {
486 mw
.html
= this.mwHtmlLive
;
496 QUnit
.test( 'Setup', function ( assert
) {
497 assert
.strictEqual( mw
.html
.escape( 'foo' ), 'mocked', 'setup() callback was ran.' );
498 assert
.strictEqual( mw
.config
.get( 'testVar' ), 'foo', 'config object applied' );
499 assert
.strictEqual( mw
.messages
.get( 'testMsg' ), 'Foo.', 'messages object applied' );
501 mw
.config
.set( 'testVar', 'bar' );
502 mw
.messages
.set( 'testMsg', 'Bar.' );
505 QUnit
.test( 'Teardown', function ( assert
) {
506 assert
.strictEqual( mw
.config
.get( 'testVar' ), 'foo', 'config object restored and re-applied after test()' );
507 assert
.strictEqual( mw
.messages
.get( 'testMsg' ), 'Foo.', 'messages object restored and re-applied after test()' );
510 QUnit
.test( 'Loader status', function ( assert
) {
512 modules
= mw
.loader
.getModuleNames(),
516 for ( i
= 0, len
= modules
.length
; i
< len
; i
++ ) {
517 state
= mw
.loader
.getState( modules
[ i
] );
518 if ( state
=== 'error' ) {
519 error
.push( modules
[ i
] );
520 } else if ( state
=== 'missing' ) {
521 missing
.push( modules
[ i
] );
525 assert
.deepEqual( error
, [], 'Modules in error state' );
526 assert
.deepEqual( missing
, [], 'Modules in missing state' );
529 QUnit
.test( 'assert.htmlEqual', function ( assert
) {
531 '<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>',
532 '<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>',
533 'Attribute order, spacing and quotation marks (equal)'
537 '<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>',
538 '<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>',
539 'Attribute order, spacing and quotation marks (not equal)'
543 '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
544 '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
545 'Multiple root nodes (equal)'
549 '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="minor">Last</label><input id="lastname" />',
550 '<label for="firstname" accesskey="f" class="important">First</label><input id="firstname" /><label for="lastname" accesskey="l" class="important" >Last</label><input id="lastname" />',
551 'Multiple root nodes (not equal, last label node is different)'
555 'fo"o<br/>b>ar',
557 'Extra escaping is equal'
562 'Text escaping (not equal)'
566 'foo<a href="http://example.com">example</a>bar',
567 'foo<a href="http://example.com">example</a>bar',
568 'Outer text nodes are compared (equal)'
572 'foo<a href="http://example.com">example</a>bar',
573 'foo<a href="http://example.com">example</a>quux',
574 'Outer text nodes are compared (last text node different)'
578 QUnit
.module( 'testrunner-after', QUnit
.newMwEnvironment() );
580 QUnit
.test( 'Teardown', function ( assert
) {
581 assert
.strictEqual( mw
.html
.escape( '<' ), '<', 'teardown() callback was ran.' );
582 assert
.strictEqual( mw
.config
.get( 'testVar' ), null, 'config object restored to live in next module()' );
583 assert
.strictEqual( mw
.messages
.get( 'testMsg' ), null, 'messages object restored to live in next module()' );
586 QUnit
.module( 'testrunner-each', {
587 beforeEach: function () {
588 this.mwHtmlLive
= mw
.html
;
590 afterEach: function () {
591 mw
.html
= this.mwHtmlLive
;
594 QUnit
.test( 'beforeEach', function ( assert
) {
595 assert
.ok( this.mwHtmlLive
, 'setup() ran' );
598 QUnit
.test( 'afterEach', function ( assert
) {
599 assert
.strictEqual( mw
.html
.escape( '<' ), '<', 'afterEach() ran' );
602 QUnit
.module( 'testrunner-each-compat', {
603 // eslint-disable-next-line qunit/no-setup-teardown
605 this.mwHtmlLive
= mw
.html
;
607 // eslint-disable-next-line qunit/no-setup-teardown
608 teardown: function () {
609 mw
.html
= this.mwHtmlLive
;
612 QUnit
.test( 'setup', function ( assert
) {
613 assert
.ok( this.mwHtmlLive
, 'setup() ran' );
616 QUnit
.test( 'teardown', function ( assert
) {
617 assert
.strictEqual( mw
.html
.escape( '<' ), '<', 'teardown() ran' );
620 // Regression test for 'this.sandbox undefined' error, fixed by
621 // ensuring Sinon setup/teardown is not re-run on inner module.
622 QUnit
.module( 'testrunner-nested', function () {
623 QUnit
.module( 'testrunner-nested-inner', function () {
624 QUnit
.test( 'Dummy', function ( assert
) {
625 assert
.ok( true, 'Nested modules supported' );
630 QUnit
.module( 'testrunner-hooks-outer', function () {
631 var beforeHookWasExecuted
= false,
632 afterHookWasExecuted
= false;
633 QUnit
.module( 'testrunner-hooks', {
634 before: function () {
635 beforeHookWasExecuted
= true;
637 // This way we can be sure that module `testrunner-hook-after` will always
638 // be executed after module `testrunner-hooks`
639 QUnit
.module( 'testrunner-hooks-after' );
641 '`after` hook for module `testrunner-hooks` was executed',
642 function ( assert
) {
643 assert
.ok( afterHookWasExecuted
);
648 afterHookWasExecuted
= true;
652 QUnit
.test( '`before` hook was executed', function ( assert
) {
653 assert
.ok( beforeHookWasExecuted
);