Merge "Added release notes for 'ContentHandler::runLegacyHooks' removal"
[mediawiki.git] / tests / qunit / suites / resources / mediawiki / mediawiki.loader.test.js
blob92ee7dd360105dbec7ae7a3d273d2532674f136d
1 ( function ( mw, $ ) {
2         QUnit.module( 'mediawiki (mw.loader)', QUnit.newMwEnvironment( {
3                 setup: function () {
4                         mw.loader.store.enabled = false;
5                 },
6                 teardown: function () {
7                         mw.loader.store.enabled = false;
8                 }
9         } ) );
11         mw.loader.addSource(
12                 'testloader',
13                 QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/load.mock.php' )
14         );
16         /**
17          * The sync style load test (for @import). This is, in a way, also an open bug for
18          * ResourceLoader ("execute js after styles are loaded"), but browsers don't offer a
19          * way to get a callback from when a stylesheet is loaded (that is, including any
20          * `@import` rules inside). To work around this, we'll have a little time loop to check
21          * if the styles apply.
22          *
23          * Note: This test originally used new Image() and onerror to get a callback
24          * when the url is loaded, but that is fragile since it doesn't monitor the
25          * same request as the css @import, and Safari 4 has issues with
26          * onerror/onload not being fired at all in weird cases like this.
27          */
28         function assertStyleAsync( assert, $element, prop, val, fn ) {
29                 var styleTestStart,
30                         el = $element.get( 0 ),
31                         styleTestTimeout = ( QUnit.config.testTimeout || 5000 ) - 200;
33                 function isCssImportApplied() {
34                         // Trigger reflow, repaint, redraw, whatever (cross-browser)
35                         var x = $element.css( 'height' );
36                         x = el.innerHTML;
37                         el.className = el.className;
38                         x = document.documentElement.clientHeight;
40                         return $element.css( prop ) === val;
41                 }
43                 function styleTestLoop() {
44                         var styleTestSince = new Date().getTime() - styleTestStart;
45                         // If it is passing or if we timed out, run the real test and stop the loop
46                         if ( isCssImportApplied() || styleTestSince > styleTestTimeout ) {
47                                 assert.equal( $element.css( prop ), val,
48                                         'style "' + prop + ': ' + val + '" from url is applied (after ' + styleTestSince + 'ms)'
49                                 );
51                                 if ( fn ) {
52                                         fn();
53                                 }
55                                 return;
56                         }
57                         // Otherwise, keep polling
58                         setTimeout( styleTestLoop );
59                 }
61                 // Start the loop
62                 styleTestStart = new Date().getTime();
63                 styleTestLoop();
64         }
66         function urlStyleTest( selector, prop, val ) {
67                 return QUnit.fixurl(
68                         mw.config.get( 'wgScriptPath' ) +
69                                 '/tests/qunit/data/styleTest.css.php?' +
70                                 $.param( {
71                                         selector: selector,
72                                         prop: prop,
73                                         val: val
74                                 } )
75                 );
76         }
78         QUnit.test( 'Basic', 2, function ( assert ) {
79                 var isAwesomeDone;
81                 mw.loader.testCallback = function () {
82                         assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' );
83                         isAwesomeDone = true;
84                 };
86                 mw.loader.implement( 'test.callback', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' ) ] );
88                 return mw.loader.using( 'test.callback', function () {
89                         assert.strictEqual( isAwesomeDone, true, 'test.callback module should\'ve caused isAwesomeDone to be true' );
90                         delete mw.loader.testCallback;
92                 }, function () {
93                         assert.ok( false, 'Error callback fired while loader.using "test.callback" module' );
94                 } );
95         } );
97         QUnit.test( 'Object method as module name', 2, function ( assert ) {
98                 var isAwesomeDone;
100                 mw.loader.testCallback = function () {
101                         assert.strictEqual( isAwesomeDone, undefined, 'Implementing module hasOwnProperty: isAwesomeDone should still be undefined' );
102                         isAwesomeDone = true;
103                 };
105                 mw.loader.implement( 'hasOwnProperty', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' ) ], {}, {} );
107                 return mw.loader.using( 'hasOwnProperty', function () {
108                         assert.strictEqual( isAwesomeDone, true, 'hasOwnProperty module should\'ve caused isAwesomeDone to be true' );
109                         delete mw.loader.testCallback;
111                 }, function () {
112                         assert.ok( false, 'Error callback fired while loader.using "hasOwnProperty" module' );
113                 } );
114         } );
116         QUnit.test( '.using( .. ) Promise', 2, function ( assert ) {
117                 var isAwesomeDone;
119                 mw.loader.testCallback = function () {
120                         assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' );
121                         isAwesomeDone = true;
122                 };
124                 mw.loader.implement( 'test.promise', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/callMwLoaderTestCallback.js' ) ] );
126                 return mw.loader.using( 'test.promise' )
127                 .done( function () {
128                         assert.strictEqual( isAwesomeDone, true, 'test.promise module should\'ve caused isAwesomeDone to be true' );
129                         delete mw.loader.testCallback;
130                 } )
131                 .fail( function () {
132                         assert.ok( false, 'Error callback fired while loader.using "test.promise" module' );
133                 } );
134         } );
136         QUnit.test( '.using() Error: Circular dependency', function ( assert ) {
137                 var done = assert.async();
139                 mw.loader.register( [
140                         [ 'test.circle1', '0', [ 'test.circle2' ] ],
141                         [ 'test.circle2', '0', [ 'test.circle3' ] ],
142                         [ 'test.circle3', '0', [ 'test.circle1' ] ]
143                 ] );
144                 mw.loader.using( 'test.circle3' ).then(
145                         function done() {
146                                 assert.ok( false, 'Unexpected resolution, expected error.' );
147                         },
148                         function fail( e ) {
149                                 assert.ok( /Circular/.test( String( e ) ), 'Detect circular dependency' );
150                         }
151                 )
152                 .always( done );
153         } );
155         QUnit.test( '.load() - Error: Circular dependency', function ( assert ) {
156                 mw.loader.register( [
157                         [ 'test.circleA', '0', [ 'test.circleB' ] ],
158                         [ 'test.circleB', '0', [ 'test.circleC' ] ],
159                         [ 'test.circleC', '0', [ 'test.circleA' ] ]
160                 ] );
161                 assert.throws( function () {
162                         mw.loader.load( 'test.circleC' );
163                 }, /Circular/, 'Detect circular dependency' );
164         } );
166         QUnit.test( '.using() - Error: Unregistered', function ( assert ) {
167                 var done = assert.async();
169                 mw.loader.using( 'test.using.unreg' ).then(
170                         function done() {
171                                 assert.ok( false, 'Unexpected resolution, expected error.' );
172                         },
173                         function fail( e ) {
174                                 assert.ok( /Unknown/.test( String( e ) ), 'Detect unknown dependency' );
175                         }
176                 ).always( done );
177         } );
179         QUnit.test( '.load() - Error: Unregistered (ignored)', 0, function ( assert ) {
180                 mw.loader.load( 'test.using.unreg2' );
181         } );
183         QUnit.test( '.implement( styles={ "css": [text, ..] } )', 2, function ( assert ) {
184                 var $element = $( '<div class="mw-test-implement-a"></div>' ).appendTo( '#qunit-fixture' );
186                 assert.notEqual(
187                         $element.css( 'float' ),
188                         'right',
189                         'style is clear'
190                 );
192                 mw.loader.implement(
193                         'test.implement.a',
194                         function () {
195                                 assert.equal(
196                                         $element.css( 'float' ),
197                                         'right',
198                                         'style is applied'
199                                 );
200                         },
201                         {
202                                 all: '.mw-test-implement-a { float: right; }'
203                         }
204                 );
206                 return mw.loader.using( 'test.implement.a' );
207         } );
209         QUnit.test( '.implement( styles={ "url": { <media>: [url, ..] } } )', 7, function ( assert ) {
210                 var $element1 = $( '<div class="mw-test-implement-b1"></div>' ).appendTo( '#qunit-fixture' ),
211                         $element2 = $( '<div class="mw-test-implement-b2"></div>' ).appendTo( '#qunit-fixture' ),
212                         $element3 = $( '<div class="mw-test-implement-b3"></div>' ).appendTo( '#qunit-fixture' ),
213                         done = assert.async();
215                 assert.notEqual(
216                         $element1.css( 'text-align' ),
217                         'center',
218                         'style is clear'
219                 );
220                 assert.notEqual(
221                         $element2.css( 'float' ),
222                         'left',
223                         'style is clear'
224                 );
225                 assert.notEqual(
226                         $element3.css( 'text-align' ),
227                         'right',
228                         'style is clear'
229                 );
231                 mw.loader.implement(
232                         'test.implement.b',
233                         function () {
234                                 // Note: done() must only be called when the entire test is
235                                 // complete. So, make sure that we don't start until *both*
236                                 // assertStyleAsync calls have completed.
237                                 var pending = 2;
238                                 assertStyleAsync( assert, $element2, 'float', 'left', function () {
239                                         assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' );
241                                         pending--;
242                                         if ( pending === 0 ) {
243                                                 done();
244                                         }
245                                 } );
246                                 assertStyleAsync( assert, $element3, 'float', 'right', function () {
247                                         assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' );
249                                         pending--;
250                                         if ( pending === 0 ) {
251                                                 done();
252                                         }
253                                 } );
254                         },
255                         {
256                                 url: {
257                                         print: [ urlStyleTest( '.mw-test-implement-b1', 'text-align', 'center' ) ],
258                                         screen: [
259                                                 // bug 40834: Make sure it actually works with more than 1 stylesheet reference
260                                                 urlStyleTest( '.mw-test-implement-b2', 'float', 'left' ),
261                                                 urlStyleTest( '.mw-test-implement-b3', 'float', 'right' )
262                                         ]
263                                 }
264                         }
265                 );
267                 mw.loader.load( 'test.implement.b' );
268         } );
270         // Backwards compatibility
271         QUnit.test( '.implement( styles={ <media>: text } ) (back-compat)', 2, function ( assert ) {
272                 var $element = $( '<div class="mw-test-implement-c"></div>' ).appendTo( '#qunit-fixture' );
274                 assert.notEqual(
275                         $element.css( 'float' ),
276                         'right',
277                         'style is clear'
278                 );
280                 mw.loader.implement(
281                         'test.implement.c',
282                         function () {
283                                 assert.equal(
284                                         $element.css( 'float' ),
285                                         'right',
286                                         'style is applied'
287                                 );
288                         },
289                         {
290                                 all: '.mw-test-implement-c { float: right; }'
291                         }
292                 );
294                 return mw.loader.using( 'test.implement.c' );
295         } );
297         // Backwards compatibility
298         QUnit.test( '.implement( styles={ <media>: [url, ..] } ) (back-compat)', 4, function ( assert ) {
299                 var $element = $( '<div class="mw-test-implement-d"></div>' ).appendTo( '#qunit-fixture' ),
300                         $element2 = $( '<div class="mw-test-implement-d2"></div>' ).appendTo( '#qunit-fixture' ),
301                         done = assert.async();
303                 assert.notEqual(
304                         $element.css( 'float' ),
305                         'right',
306                         'style is clear'
307                 );
308                 assert.notEqual(
309                         $element2.css( 'text-align' ),
310                         'center',
311                         'style is clear'
312                 );
314                 mw.loader.implement(
315                         'test.implement.d',
316                         function () {
317                                 assertStyleAsync( assert, $element, 'float', 'right', function () {
318                                         assert.notEqual( $element2.css( 'text-align' ), 'center', 'print style is not applied (bug 40500)' );
319                                         done();
320                                 } );
321                         },
322                         {
323                                 all: [ urlStyleTest( '.mw-test-implement-d', 'float', 'right' ) ],
324                                 print: [ urlStyleTest( '.mw-test-implement-d2', 'text-align', 'center' ) ]
325                         }
326                 );
328                 mw.loader.load( 'test.implement.d' );
329         } );
331         // @import (bug 31676)
332         QUnit.test( '.implement( styles has @import )', 7, function ( assert ) {
333                 var isJsExecuted, $element,
334                         done = assert.async();
336                 mw.loader.implement(
337                         'test.implement.import',
338                         function () {
339                                 assert.strictEqual( isJsExecuted, undefined, 'script not executed multiple times' );
340                                 isJsExecuted = true;
342                                 assert.equal( mw.loader.getState( 'test.implement.import' ), 'executing', 'module state during implement() script execution' );
344                                 $element = $( '<div class="mw-test-implement-import">Foo bar</div>' ).appendTo( '#qunit-fixture' );
346                                 assert.equal( mw.msg( 'test-foobar' ), 'Hello Foobar, $1!', 'messages load before script execution' );
348                                 assertStyleAsync( assert, $element, 'float', 'right', function () {
349                                         assert.equal( $element.css( 'text-align' ), 'center',
350                                                 'CSS styles after the @import rule are working'
351                                         );
353                                         done();
354                                 } );
355                         },
356                         {
357                                 css: [
358                                         '@import url(\''
359                                                 + urlStyleTest( '.mw-test-implement-import', 'float', 'right' )
360                                                 + '\');\n'
361                                                 + '.mw-test-implement-import { text-align: center; }'
362                                 ]
363                         },
364                         {
365                                 'test-foobar': 'Hello Foobar, $1!'
366                         }
367                 );
369                 mw.loader.using( 'test.implement.import' ).always( function () {
370                         assert.strictEqual( isJsExecuted, true, 'script executed' );
371                         assert.equal( mw.loader.getState( 'test.implement.import' ), 'ready', 'module state after script execution' );
372                 } );
373         } );
375         QUnit.test( '.implement( dependency with styles )', 4, function ( assert ) {
376                 var $element = $( '<div class="mw-test-implement-e"></div>' ).appendTo( '#qunit-fixture' ),
377                         $element2 = $( '<div class="mw-test-implement-e2"></div>' ).appendTo( '#qunit-fixture' );
379                 assert.notEqual(
380                         $element.css( 'float' ),
381                         'right',
382                         'style is clear'
383                 );
384                 assert.notEqual(
385                         $element2.css( 'float' ),
386                         'left',
387                         'style is clear'
388                 );
390                 mw.loader.register( [
391                         [ 'test.implement.e', '0', [ 'test.implement.e2' ] ],
392                         [ 'test.implement.e2', '0' ]
393                 ] );
395                 mw.loader.implement(
396                         'test.implement.e',
397                         function () {
398                                 assert.equal(
399                                         $element.css( 'float' ),
400                                         'right',
401                                         'Depending module\'s style is applied'
402                                 );
403                         },
404                         {
405                                 all: '.mw-test-implement-e { float: right; }'
406                         }
407                 );
409                 mw.loader.implement(
410                         'test.implement.e2',
411                         function () {
412                                 assert.equal(
413                                         $element2.css( 'float' ),
414                                         'left',
415                                         'Dependency\'s style is applied'
416                                 );
417                         },
418                         {
419                                 all: '.mw-test-implement-e2 { float: left; }'
420                         }
421                 );
423                 return mw.loader.using( 'test.implement.e' );
424         } );
426         QUnit.test( '.implement( only scripts )', 1, function ( assert ) {
427                 mw.loader.implement( 'test.onlyscripts', function () {} );
428                 assert.strictEqual( mw.loader.getState( 'test.onlyscripts' ), 'ready' );
429         } );
431         QUnit.test( '.implement( only messages )', 2, function ( assert ) {
432                 assert.assertFalse( mw.messages.exists( 'bug_29107' ), 'Verify that the test message doesn\'t exist yet' );
434                 // jscs: disable requireCamelCaseOrUpperCaseIdentifiers
435                 mw.loader.implement( 'test.implement.msgs', [], {}, { bug_29107: 'loaded' } );
436                 // jscs: enable requireCamelCaseOrUpperCaseIdentifiers
438                 return mw.loader.using( 'test.implement.msgs', function () {
439                         assert.ok( mw.messages.exists( 'bug_29107' ), 'Bug 29107: messages-only module should implement ok' );
440                 }, function () {
441                         assert.ok( false, 'Error callback fired while implementing "test.implement.msgs" module' );
442                 } );
443         } );
445         QUnit.test( '.implement( empty )', 1, function ( assert ) {
446                 mw.loader.implement( 'test.empty' );
447                 assert.strictEqual( mw.loader.getState( 'test.empty' ), 'ready' );
448         } );
450         QUnit.test( 'Broken indirect dependency', 4, function ( assert ) {
451                 // don't emit an error event
452                 this.sandbox.stub( mw, 'track' );
454                 mw.loader.register( [
455                         [ 'test.module1', '0' ],
456                         [ 'test.module2', '0', [ 'test.module1' ] ],
457                         [ 'test.module3', '0', [ 'test.module2' ] ]
458                 ] );
459                 mw.loader.implement( 'test.module1', function () {
460                         throw new Error( 'expected' );
461                 }, {}, {} );
462                 assert.strictEqual( mw.loader.getState( 'test.module1' ), 'error', 'Expected "error" state for test.module1' );
463                 assert.strictEqual( mw.loader.getState( 'test.module2' ), 'error', 'Expected "error" state for test.module2' );
464                 assert.strictEqual( mw.loader.getState( 'test.module3' ), 'error', 'Expected "error" state for test.module3' );
466                 assert.strictEqual( mw.track.callCount, 1 );
467         } );
469         QUnit.test( 'Out-of-order implementation', 9, function ( assert ) {
470                 mw.loader.register( [
471                         [ 'test.module4', '0' ],
472                         [ 'test.module5', '0', [ 'test.module4' ] ],
473                         [ 'test.module6', '0', [ 'test.module5' ] ]
474                 ] );
475                 mw.loader.implement( 'test.module4', function () {} );
476                 assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
477                 assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
478                 assert.strictEqual( mw.loader.getState( 'test.module6' ), 'registered', 'Expected "registered" state for test.module6' );
479                 mw.loader.implement( 'test.module6', function () {} );
480                 assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
481                 assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
482                 assert.strictEqual( mw.loader.getState( 'test.module6' ), 'loaded', 'Expected "loaded" state for test.module6' );
483                 mw.loader.implement( 'test.module5', function () {} );
484                 assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
485                 assert.strictEqual( mw.loader.getState( 'test.module5' ), 'ready', 'Expected "ready" state for test.module5' );
486                 assert.strictEqual( mw.loader.getState( 'test.module6' ), 'ready', 'Expected "ready" state for test.module6' );
487         } );
489         QUnit.test( 'Missing dependency', 13, function ( assert ) {
490                 mw.loader.register( [
491                         [ 'test.module7', '0' ],
492                         [ 'test.module8', '0', [ 'test.module7' ] ],
493                         [ 'test.module9', '0', [ 'test.module8' ] ]
494                 ] );
495                 mw.loader.implement( 'test.module8', function () {} );
496                 assert.strictEqual( mw.loader.getState( 'test.module7' ), 'registered', 'Expected "registered" state for test.module7' );
497                 assert.strictEqual( mw.loader.getState( 'test.module8' ), 'loaded', 'Expected "loaded" state for test.module8' );
498                 assert.strictEqual( mw.loader.getState( 'test.module9' ), 'registered', 'Expected "registered" state for test.module9' );
499                 mw.loader.state( 'test.module7', 'missing' );
500                 assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
501                 assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
502                 assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
503                 mw.loader.implement( 'test.module9', function () {} );
504                 assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
505                 assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
506                 assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
507                 mw.loader.using(
508                         [ 'test.module7' ],
509                         function () {
510                                 assert.ok( false, 'Success fired despite missing dependency' );
511                                 assert.ok( true, 'QUnit expected() count dummy' );
512                         },
513                         function ( e, dependencies ) {
514                                 assert.strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' );
515                                 assert.deepEqual( dependencies, [ 'test.module7' ], 'Error callback called with module test.module7' );
516                         }
517                 );
518                 mw.loader.using(
519                         [ 'test.module9' ],
520                         function () {
521                                 assert.ok( false, 'Success fired despite missing dependency' );
522                                 assert.ok( true, 'QUnit expected() count dummy' );
523                         },
524                         function ( e, dependencies ) {
525                                 assert.strictEqual( $.isArray( dependencies ), true, 'Expected array of dependencies' );
526                                 dependencies.sort();
527                                 assert.deepEqual(
528                                         dependencies,
529                                         [ 'test.module7', 'test.module8', 'test.module9' ],
530                                         'Error callback called with all three modules as dependencies'
531                                 );
532                         }
533                 );
534         } );
536         QUnit.test( 'Dependency handling', 5, function ( assert ) {
537                 var done = assert.async();
538                 mw.loader.register( [
539                         // [module, version, dependencies, group, source]
540                         [ 'testMissing', '1', [], null, 'testloader' ],
541                         [ 'testUsesMissing', '1', [ 'testMissing' ], null, 'testloader' ],
542                         [ 'testUsesNestedMissing', '1', [ 'testUsesMissing' ], null, 'testloader' ]
543                 ] );
545                 function verifyModuleStates() {
546                         assert.equal( mw.loader.getState( 'testMissing' ), 'missing', 'Module not known to server must have state "missing"' );
547                         assert.equal( mw.loader.getState( 'testUsesMissing' ), 'error', 'Module with missing dependency must have state "error"' );
548                         assert.equal( mw.loader.getState( 'testUsesNestedMissing' ), 'error', 'Module with indirect missing dependency must have state "error"' );
549                 }
551                 mw.loader.using( [ 'testUsesNestedMissing' ],
552                         function () {
553                                 assert.ok( false, 'Error handler should be invoked.' );
554                                 assert.ok( true ); // Dummy to reach QUnit expect()
556                                 verifyModuleStates();
558                                 done();
559                         },
560                         function ( e, badmodules ) {
561                                 assert.ok( true, 'Error handler should be invoked.' );
562                                 // As soon as server spits out state('testMissing', 'missing');
563                                 // it will bubble up and trigger the error callback.
564                                 // Therefor the badmodules array is not testUsesMissing or testUsesNestedMissing.
565                                 assert.deepEqual( badmodules, [ 'testMissing' ], 'Bad modules as expected.' );
567                                 verifyModuleStates();
569                                 done();
570                         }
571                 );
572         } );
574         QUnit.test( 'Skip-function handling', 5, function ( assert ) {
575                 mw.loader.register( [
576                         // [module, version, dependencies, group, source, skip]
577                         [ 'testSkipped', '1', [], null, 'testloader', 'return true;' ],
578                         [ 'testNotSkipped', '1', [], null, 'testloader', 'return false;' ],
579                         [ 'testUsesSkippable', '1', [ 'testSkipped', 'testNotSkipped' ], null, 'testloader' ]
580                 ] );
582                 function verifyModuleStates() {
583                         assert.equal( mw.loader.getState( 'testSkipped' ), 'ready', 'Module is ready when skipped' );
584                         assert.equal( mw.loader.getState( 'testNotSkipped' ), 'ready', 'Module is ready when not skipped but loaded' );
585                         assert.equal( mw.loader.getState( 'testUsesSkippable' ), 'ready', 'Module is ready when skippable dependencies are ready' );
586                 }
588                 return mw.loader.using( [ 'testUsesSkippable' ],
589                         function () {
590                                 assert.ok( true, 'Success handler should be invoked.' );
591                                 assert.ok( true ); // Dummy to match error handler and reach QUnit expect()
593                                 verifyModuleStates();
594                         },
595                         function ( e, badmodules ) {
596                                 assert.ok( false, 'Error handler should not be invoked.' );
597                                 assert.deepEqual( badmodules, [], 'Bad modules as expected.' );
599                                 verifyModuleStates();
600                         }
601                 );
602         } );
604         QUnit.asyncTest( '.load( "//protocol-relative" ) - T32825', 2, function ( assert ) {
605                 // This bug was actually already fixed in 1.18 and later when discovered in 1.17.
606                 // Test is for regressions!
608                 // Forge a URL to the test callback script
609                 var target = QUnit.fixurl(
610                         mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/qunitOkCall.js'
611                 );
613                 // Confirm that mw.loader.load() works with protocol-relative URLs
614                 target = target.replace( /https?:/, '' );
616                 assert.equal( target.slice( 0, 2 ), '//',
617                         'URL must be relative to test relative URLs!'
618                 );
620                 // Async!
621                 // The target calls QUnit.start
622                 mw.loader.load( target );
623         } );
625         QUnit.asyncTest( '.load( "/absolute-path" )', 2, function ( assert ) {
626                 // Forge a URL to the test callback script
627                 var target = QUnit.fixurl(
628                         mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/qunitOkCall.js'
629                 );
631                 // Confirm that mw.loader.load() works with absolute-paths (relative to current hostname)
632                 assert.equal( target.slice( 0, 1 ), '/', 'URL is relative to document root' );
634                 // Async!
635                 // The target calls QUnit.start
636                 mw.loader.load( target );
637         } );
639         QUnit.test( 'Empty string module name - T28804', function ( assert ) {
640                 var done = false;
642                 assert.strictEqual( mw.loader.getState( '' ), null, 'State (unregistered)' );
644                 mw.loader.register( '', 'v1' );
645                 assert.strictEqual( mw.loader.getState( '' ), 'registered', 'State (registered)' );
646                 assert.strictEqual( mw.loader.getVersion( '' ), 'v1', 'Version' );
648                 mw.loader.implement( '', function () {
649                         done = true;
650                 } );
652                 return mw.loader.using( '', function () {
653                         assert.strictEqual( done, true, 'script ran' );
654                         assert.strictEqual( mw.loader.getState( '' ), 'ready', 'State (ready)' );
655                 } );
656         } );
658         QUnit.test( 'Executing race - T112232', 2, function ( assert ) {
659                 var done = false;
661                 // The red herring schedules its CSS buffer first. In T112232, a bug in the
662                 // state machine would cause the job for testRaceLoadMe to run with an earlier job.
663                 mw.loader.implement(
664                         'testRaceRedHerring',
665                         function () {},
666                         { css: [ '.mw-testRaceRedHerring {}' ] }
667                 );
668                 mw.loader.implement(
669                         'testRaceLoadMe',
670                         function () {
671                                 done = true;
672                         },
673                         { css: [ '.mw-testRaceLoadMe { float: left; }' ] }
674                 );
676                 mw.loader.load( [ 'testRaceRedHerring', 'testRaceLoadMe' ] );
677                 return mw.loader.using( 'testRaceLoadMe', function () {
678                         assert.strictEqual( done, true, 'script ran' );
679                         assert.strictEqual( mw.loader.getState( 'testRaceLoadMe' ), 'ready', 'state' );
680                 } );
681         } );
683         QUnit.test( 'Stale response caching - T117587', function ( assert ) {
684                 var count = 0;
685                 mw.loader.store.enabled = true;
686                 mw.loader.register( 'test.stale', 'v2' );
687                 assert.strictEqual( mw.loader.store.get( 'test.stale' ), false, 'Not in store' );
689                 mw.loader.implement( 'test.stale@v1', function () {
690                         count++;
691                 } );
693                 return mw.loader.using( 'test.stale' )
694                         .then( function () {
695                                 assert.strictEqual( count, 1 );
696                                 // After implementing, registry contains version as implemented by the response.
697                                 assert.strictEqual( mw.loader.getVersion( 'test.stale' ), 'v1', 'Override version' );
698                                 assert.strictEqual( mw.loader.getState( 'test.stale' ), 'ready' );
699                                 assert.ok( mw.loader.store.get( 'test.stale' ), 'In store' );
700                         } )
701                         .then( function () {
702                                 // Reset run time, but keep mw.loader.store
703                                 mw.loader.moduleRegistry[ 'test.stale' ].script = undefined;
704                                 mw.loader.moduleRegistry[ 'test.stale' ].state = 'registered';
705                                 mw.loader.moduleRegistry[ 'test.stale' ].version = 'v2';
707                                 // Module was stored correctly as v1
708                                 // On future navigations, it will be ignored until evicted
709                                 assert.strictEqual( mw.loader.store.get( 'test.stale' ), false, 'Not in store' );
710                         } );
711         } );
713         QUnit.test( 'Stale response caching - backcompat', function ( assert ) {
714                 var count = 0;
715                 mw.loader.store.enabled = true;
716                 mw.loader.register( 'test.stalebc', 'v2' );
717                 assert.strictEqual( mw.loader.store.get( 'test.stalebc' ), false, 'Not in store' );
719                 mw.loader.implement( 'test.stalebc', function () {
720                         count++;
721                 } );
723                 return mw.loader.using( 'test.stalebc' )
724                         .then( function () {
725                                 assert.strictEqual( count, 1 );
726                                 assert.strictEqual( mw.loader.getState( 'test.stalebc' ), 'ready' );
727                                 assert.ok( mw.loader.store.get( 'test.stalebc' ), 'In store' );
728                         } )
729                         .then( function () {
730                                 // Reset run time, but keep mw.loader.store
731                                 mw.loader.moduleRegistry[ 'test.stalebc' ].script = undefined;
732                                 mw.loader.moduleRegistry[ 'test.stalebc' ].state = 'registered';
733                                 mw.loader.moduleRegistry[ 'test.stalebc' ].version = 'v2';
735                                 // Legacy behaviour is storing under the expected version,
736                                 // which woudl lead to whitewashing and stale values (T117587).
737                                 assert.ok( mw.loader.store.get( 'test.stalebc' ), 'In store' );
738                         } );
739         } );
741         QUnit.test( 'require()', 6, function ( assert ) {
742                 mw.loader.register( [
743                         [ 'test.require1', '0' ],
744                         [ 'test.require2', '0' ],
745                         [ 'test.require3', '0' ],
746                         [ 'test.require4', '0', [ 'test.require3' ] ]
747                 ] );
748                 mw.loader.implement( 'test.require1', function () {} );
749                 mw.loader.implement( 'test.require2', function ( $, jQuery, require, module ) {
750                         module.exports = 1;
751                 } );
752                 mw.loader.implement( 'test.require3', function ( $, jQuery, require, module ) {
753                         module.exports = function () {
754                                 return 'hello world';
755                         };
756                 } );
757                 mw.loader.implement( 'test.require4', function ( $, jQuery, require, module ) {
758                         var other = require( 'test.require3' );
759                         module.exports = {
760                                 pizza: function () {
761                                         return other();
762                                 }
763                         };
764                 } );
765                 return mw.loader.using( [ 'test.require1', 'test.require2', 'test.require3', 'test.require4' ] )
766                 .then( function ( require ) {
767                         var module1, module2, module3, module4;
769                         module1 = require( 'test.require1' );
770                         module2 = require( 'test.require2' );
771                         module3 = require( 'test.require3' );
772                         module4 = require( 'test.require4' );
774                         assert.strictEqual( typeof module1, 'object', 'export of module with no export' );
775                         assert.strictEqual( module2, 1, 'export a number' );
776                         assert.strictEqual( module3(), 'hello world', 'export a function' );
777                         assert.strictEqual( typeof module4.pizza, 'function', 'export an object' );
778                         assert.strictEqual( module4.pizza(), 'hello world', 'module can require other modules' );
780                         assert.throws( function () {
781                                 require( '_badmodule' );
782                         }, /is not loaded/, 'Requesting non-existent modules throws error.' );
783                 } );
784         } );
786         QUnit.test( 'require() in debug mode', function ( assert ) {
787                 var path = mw.config.get( 'wgScriptPath' );
788                 mw.loader.register( [
789                         [ 'test.require.define', '0' ],
790                         [ 'test.require.callback', '0', [ 'test.require.define' ] ]
791                 ] );
792                 mw.loader.implement( 'test.require.callback', [ QUnit.fixurl( path + '/tests/qunit/data/requireCallMwLoaderTestCallback.js' ) ] );
793                 mw.loader.implement( 'test.require.define', [ QUnit.fixurl( path + '/tests/qunit/data/defineCallMwLoaderTestCallback.js' ) ] );
795                 return mw.loader.using( 'test.require.callback' ).then( function ( require ) {
796                         var cb = require( 'test.require.callback' );
797                         assert.strictEqual( cb.immediate, 'Defined.', 'module.exports and require work in debug mode' );
798                         // Must use try-catch because cb.later() will throw if require is undefined,
799                         // which doesn't work well inside Deferred.then() when using jQuery 1.x with QUnit
800                         try {
801                                 assert.strictEqual( cb.later(), 'Defined.', 'require works asynchrously in debug mode' );
802                         } catch ( e ) {
803                                 assert.equal( null, String( e ), 'require works asynchrously in debug mode' );
804                         }
805                 }, function () {
806                         assert.ok( false, 'Error callback fired while loader.using "test.require.callback" module' );
807                 } );
808         } );
810 }( mediaWiki, jQuery ) );