]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - tests/qunit/suites/resources/mediawiki/mediawiki.loader.test.js
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / tests / qunit / suites / resources / mediawiki / mediawiki.loader.test.js
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                         // Teardown for StringSet shim test
9                         if ( this.nativeSet ) {
10                                 window.Set = this.nativeSet;
11                                 mw.redefineFallbacksForTest();
12                         }
13                 }
14         } ) );
15
16         mw.loader.addSource(
17                 'testloader',
18                 QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/load.mock.php' )
19         );
20
21         /**
22          * The sync style load test (for @import). This is, in a way, also an open bug for
23          * ResourceLoader ("execute js after styles are loaded"), but browsers don't offer a
24          * way to get a callback from when a stylesheet is loaded (that is, including any
25          * `@import` rules inside). To work around this, we'll have a little time loop to check
26          * if the styles apply.
27          *
28          * Note: This test originally used new Image() and onerror to get a callback
29          * when the url is loaded, but that is fragile since it doesn't monitor the
30          * same request as the css @import, and Safari 4 has issues with
31          * onerror/onload not being fired at all in weird cases like this.
32          */
33         function assertStyleAsync( assert, $element, prop, val, fn ) {
34                 var styleTestStart,
35                         el = $element.get( 0 ),
36                         styleTestTimeout = ( QUnit.config.testTimeout || 5000 ) - 200;
37
38                 function isCssImportApplied() {
39                         // Trigger reflow, repaint, redraw, whatever (cross-browser)
40                         $element.css( 'height' );
41                         // eslint-disable-next-line no-unused-expressions
42                         el.innerHTML;
43                         el.className = el.className;
44                         // eslint-disable-next-line no-unused-expressions
45                         document.documentElement.clientHeight;
46
47                         return $element.css( prop ) === val;
48                 }
49
50                 function styleTestLoop() {
51                         var styleTestSince = new Date().getTime() - styleTestStart;
52                         // If it is passing or if we timed out, run the real test and stop the loop
53                         if ( isCssImportApplied() || styleTestSince > styleTestTimeout ) {
54                                 assert.equal( $element.css( prop ), val,
55                                         'style "' + prop + ': ' + val + '" from url is applied (after ' + styleTestSince + 'ms)'
56                                 );
57
58                                 if ( fn ) {
59                                         fn();
60                                 }
61
62                                 return;
63                         }
64                         // Otherwise, keep polling
65                         setTimeout( styleTestLoop );
66                 }
67
68                 // Start the loop
69                 styleTestStart = new Date().getTime();
70                 styleTestLoop();
71         }
72
73         function urlStyleTest( selector, prop, val ) {
74                 return QUnit.fixurl(
75                         mw.config.get( 'wgScriptPath' ) +
76                                 '/tests/qunit/data/styleTest.css.php?' +
77                                 $.param( {
78                                         selector: selector,
79                                         prop: prop,
80                                         val: val
81                                 } )
82                 );
83         }
84
85         QUnit.test( 'Basic', function ( assert ) {
86                 var isAwesomeDone;
87
88                 mw.loader.testCallback = function () {
89                         assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' );
90                         isAwesomeDone = true;
91                 };
92
93                 mw.loader.implement( 'test.callback', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/mwLoaderTestCallback.js' ) ] );
94
95                 return mw.loader.using( 'test.callback', function () {
96                         assert.strictEqual( isAwesomeDone, true, 'test.callback module should\'ve caused isAwesomeDone to be true' );
97                         delete mw.loader.testCallback;
98
99                 }, function () {
100                         assert.ok( false, 'Error callback fired while loader.using "test.callback" module' );
101                 } );
102         } );
103
104         QUnit.test( 'Object method as module name', function ( assert ) {
105                 var isAwesomeDone;
106
107                 mw.loader.testCallback = function () {
108                         assert.strictEqual( isAwesomeDone, undefined, 'Implementing module hasOwnProperty: isAwesomeDone should still be undefined' );
109                         isAwesomeDone = true;
110                 };
111
112                 mw.loader.implement( 'hasOwnProperty', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/mwLoaderTestCallback.js' ) ], {}, {} );
113
114                 return mw.loader.using( 'hasOwnProperty', function () {
115                         assert.strictEqual( isAwesomeDone, true, 'hasOwnProperty module should\'ve caused isAwesomeDone to be true' );
116                         delete mw.loader.testCallback;
117
118                 }, function () {
119                         assert.ok( false, 'Error callback fired while loader.using "hasOwnProperty" module' );
120                 } );
121         } );
122
123         QUnit.test( '.using( .. ) Promise', function ( assert ) {
124                 var isAwesomeDone;
125
126                 mw.loader.testCallback = function () {
127                         assert.strictEqual( isAwesomeDone, undefined, 'Implementing module is.awesome: isAwesomeDone should still be undefined' );
128                         isAwesomeDone = true;
129                 };
130
131                 mw.loader.implement( 'test.promise', [ QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/mwLoaderTestCallback.js' ) ] );
132
133                 return mw.loader.using( 'test.promise' )
134                         .done( function () {
135                                 assert.strictEqual( isAwesomeDone, true, 'test.promise module should\'ve caused isAwesomeDone to be true' );
136                                 delete mw.loader.testCallback;
137                         } )
138                         .fail( function () {
139                                 assert.ok( false, 'Error callback fired while loader.using "test.promise" module' );
140                         } );
141         } );
142
143         // Covers mw.loader#sortDependencies (with native Set if available)
144         QUnit.test( '.using() Error: Circular dependency [StringSet default]', function ( assert ) {
145                 var done = assert.async();
146
147                 mw.loader.register( [
148                         [ 'test.circle1', '0', [ 'test.circle2' ] ],
149                         [ 'test.circle2', '0', [ 'test.circle3' ] ],
150                         [ 'test.circle3', '0', [ 'test.circle1' ] ]
151                 ] );
152                 mw.loader.using( 'test.circle3' ).then(
153                         function done() {
154                                 assert.ok( false, 'Unexpected resolution, expected error.' );
155                         },
156                         function fail( e ) {
157                                 assert.ok( /Circular/.test( String( e ) ), 'Detect circular dependency' );
158                         }
159                 )
160                         .always( done );
161         } );
162
163         // @covers mw.loader#sortDependencies (with fallback shim)
164         QUnit.test( '.using() Error: Circular dependency [StringSet shim]', function ( assert ) {
165                 var done = assert.async();
166
167                 if ( !window.Set ) {
168                         assert.expect( 0 );
169                         done();
170                         return;
171                 }
172
173                 this.nativeSet = window.Set;
174                 window.Set = undefined;
175                 mw.redefineFallbacksForTest();
176
177                 mw.loader.register( [
178                         [ 'test.shim.circle1', '0', [ 'test.shim.circle2' ] ],
179                         [ 'test.shim.circle2', '0', [ 'test.shim.circle3' ] ],
180                         [ 'test.shim.circle3', '0', [ 'test.shim.circle1' ] ]
181                 ] );
182                 mw.loader.using( 'test.shim.circle3' ).then(
183                         function done() {
184                                 assert.ok( false, 'Unexpected resolution, expected error.' );
185                         },
186                         function fail( e ) {
187                                 assert.ok( /Circular/.test( String( e ) ), 'Detect circular dependency' );
188                         }
189                 )
190                         .always( done );
191         } );
192
193         QUnit.test( '.load() - Error: Circular dependency', function ( assert ) {
194                 var capture = [];
195                 mw.loader.register( [
196                         [ 'test.circleA', '0', [ 'test.circleB' ] ],
197                         [ 'test.circleB', '0', [ 'test.circleC' ] ],
198                         [ 'test.circleC', '0', [ 'test.circleA' ] ]
199                 ] );
200                 this.sandbox.stub( mw, 'track', function ( topic, data ) {
201                         capture.push( {
202                                 topic: topic,
203                                 error: data.exception && data.exception.message,
204                                 source: data.source
205                         } );
206                 } );
207
208                 mw.loader.load( 'test.circleC' );
209                 assert.deepEqual(
210                         [ {
211                                 topic: 'resourceloader.exception',
212                                 error: 'Circular reference detected: test.circleB -> test.circleC',
213                                 source: 'resolve'
214                         } ],
215                         capture,
216                         'Detect circular dependency'
217                 );
218         } );
219
220         QUnit.test( '.using() - Error: Unregistered', function ( assert ) {
221                 var done = assert.async();
222
223                 mw.loader.using( 'test.using.unreg' ).then(
224                         function done() {
225                                 assert.ok( false, 'Unexpected resolution, expected error.' );
226                         },
227                         function fail( e ) {
228                                 assert.ok( /Unknown/.test( String( e ) ), 'Detect unknown dependency' );
229                         }
230                 ).always( done );
231         } );
232
233         QUnit.test( '.load() - Error: Unregistered', function ( assert ) {
234                 var capture = [];
235                 this.sandbox.stub( mw, 'track', function ( topic, data ) {
236                         capture.push( {
237                                 topic: topic,
238                                 error: data.exception && data.exception.message,
239                                 source: data.source
240                         } );
241                 } );
242
243                 mw.loader.load( 'test.load.unreg' );
244                 assert.deepEqual(
245                         [ {
246                                 topic: 'resourceloader.exception',
247                                 error: 'Unknown dependency: test.load.unreg',
248                                 source: 'resolve'
249                         } ],
250                         capture
251                 );
252         } );
253
254         // Regression test for T36853
255         QUnit.test( '.load() - Error: Missing dependency', function ( assert ) {
256                 var capture = [];
257                 this.sandbox.stub( mw, 'track', function ( topic, data ) {
258                         capture.push( {
259                                 topic: topic,
260                                 error: data.exception && data.exception.message,
261                                 source: data.source
262                         } );
263                 } );
264
265                 mw.loader.register( [
266                         [ 'test.load.missingdep1', '0', [ 'test.load.missingdep2' ] ],
267                         [ 'test.load.missingdep', '0', [ 'test.load.missingdep1' ] ]
268                 ] );
269                 mw.loader.load( 'test.load.missingdep' );
270                 assert.deepEqual(
271                         [ {
272                                 topic: 'resourceloader.exception',
273                                 error: 'Unknown dependency: test.load.missingdep2',
274                                 source: 'resolve'
275                         } ],
276                         capture
277                 );
278         } );
279
280         QUnit.test( '.implement( styles={ "css": [text, ..] } )', function ( assert ) {
281                 var $element = $( '<div class="mw-test-implement-a"></div>' ).appendTo( '#qunit-fixture' );
282
283                 assert.notEqual(
284                         $element.css( 'float' ),
285                         'right',
286                         'style is clear'
287                 );
288
289                 mw.loader.implement(
290                         'test.implement.a',
291                         function () {
292                                 assert.equal(
293                                         $element.css( 'float' ),
294                                         'right',
295                                         'style is applied'
296                                 );
297                         },
298                         {
299                                 all: '.mw-test-implement-a { float: right; }'
300                         }
301                 );
302
303                 return mw.loader.using( 'test.implement.a' );
304         } );
305
306         QUnit.test( '.implement( styles={ "url": { <media>: [url, ..] } } )', function ( assert ) {
307                 var $element1 = $( '<div class="mw-test-implement-b1"></div>' ).appendTo( '#qunit-fixture' ),
308                         $element2 = $( '<div class="mw-test-implement-b2"></div>' ).appendTo( '#qunit-fixture' ),
309                         $element3 = $( '<div class="mw-test-implement-b3"></div>' ).appendTo( '#qunit-fixture' ),
310                         done = assert.async();
311
312                 assert.notEqual(
313                         $element1.css( 'text-align' ),
314                         'center',
315                         'style is clear'
316                 );
317                 assert.notEqual(
318                         $element2.css( 'float' ),
319                         'left',
320                         'style is clear'
321                 );
322                 assert.notEqual(
323                         $element3.css( 'text-align' ),
324                         'right',
325                         'style is clear'
326                 );
327
328                 mw.loader.implement(
329                         'test.implement.b',
330                         function () {
331                                 // Note: done() must only be called when the entire test is
332                                 // complete. So, make sure that we don't start until *both*
333                                 // assertStyleAsync calls have completed.
334                                 var pending = 2;
335                                 assertStyleAsync( assert, $element2, 'float', 'left', function () {
336                                         assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' );
337
338                                         pending--;
339                                         if ( pending === 0 ) {
340                                                 done();
341                                         }
342                                 } );
343                                 assertStyleAsync( assert, $element3, 'float', 'right', function () {
344                                         assert.notEqual( $element1.css( 'text-align' ), 'center', 'print style is not applied' );
345
346                                         pending--;
347                                         if ( pending === 0 ) {
348                                                 done();
349                                         }
350                                 } );
351                         },
352                         {
353                                 url: {
354                                         print: [ urlStyleTest( '.mw-test-implement-b1', 'text-align', 'center' ) ],
355                                         screen: [
356                                                 // T42834: Make sure it actually works with more than 1 stylesheet reference
357                                                 urlStyleTest( '.mw-test-implement-b2', 'float', 'left' ),
358                                                 urlStyleTest( '.mw-test-implement-b3', 'float', 'right' )
359                                         ]
360                                 }
361                         }
362                 );
363
364                 mw.loader.load( 'test.implement.b' );
365         } );
366
367         // Backwards compatibility
368         QUnit.test( '.implement( styles={ <media>: text } ) (back-compat)', function ( assert ) {
369                 var $element = $( '<div class="mw-test-implement-c"></div>' ).appendTo( '#qunit-fixture' );
370
371                 assert.notEqual(
372                         $element.css( 'float' ),
373                         'right',
374                         'style is clear'
375                 );
376
377                 mw.loader.implement(
378                         'test.implement.c',
379                         function () {
380                                 assert.equal(
381                                         $element.css( 'float' ),
382                                         'right',
383                                         'style is applied'
384                                 );
385                         },
386                         {
387                                 all: '.mw-test-implement-c { float: right; }'
388                         }
389                 );
390
391                 return mw.loader.using( 'test.implement.c' );
392         } );
393
394         // Backwards compatibility
395         QUnit.test( '.implement( styles={ <media>: [url, ..] } ) (back-compat)', function ( assert ) {
396                 var $element = $( '<div class="mw-test-implement-d"></div>' ).appendTo( '#qunit-fixture' ),
397                         $element2 = $( '<div class="mw-test-implement-d2"></div>' ).appendTo( '#qunit-fixture' ),
398                         done = assert.async();
399
400                 assert.notEqual(
401                         $element.css( 'float' ),
402                         'right',
403                         'style is clear'
404                 );
405                 assert.notEqual(
406                         $element2.css( 'text-align' ),
407                         'center',
408                         'style is clear'
409                 );
410
411                 mw.loader.implement(
412                         'test.implement.d',
413                         function () {
414                                 assertStyleAsync( assert, $element, 'float', 'right', function () {
415                                         assert.notEqual( $element2.css( 'text-align' ), 'center', 'print style is not applied (T42500)' );
416                                         done();
417                                 } );
418                         },
419                         {
420                                 all: [ urlStyleTest( '.mw-test-implement-d', 'float', 'right' ) ],
421                                 print: [ urlStyleTest( '.mw-test-implement-d2', 'text-align', 'center' ) ]
422                         }
423                 );
424
425                 mw.loader.load( 'test.implement.d' );
426         } );
427
428         // @import (T33676)
429         QUnit.test( '.implement( styles has @import )', function ( assert ) {
430                 var isJsExecuted, $element,
431                         done = assert.async();
432
433                 mw.loader.implement(
434                         'test.implement.import',
435                         function () {
436                                 assert.strictEqual( isJsExecuted, undefined, 'script not executed multiple times' );
437                                 isJsExecuted = true;
438
439                                 assert.equal( mw.loader.getState( 'test.implement.import' ), 'executing', 'module state during implement() script execution' );
440
441                                 $element = $( '<div class="mw-test-implement-import">Foo bar</div>' ).appendTo( '#qunit-fixture' );
442
443                                 assert.equal( mw.msg( 'test-foobar' ), 'Hello Foobar, $1!', 'messages load before script execution' );
444
445                                 assertStyleAsync( assert, $element, 'float', 'right', function () {
446                                         assert.equal( $element.css( 'text-align' ), 'center',
447                                                 'CSS styles after the @import rule are working'
448                                         );
449
450                                         done();
451                                 } );
452                         },
453                         {
454                                 css: [
455                                         '@import url(\''
456                                                 + urlStyleTest( '.mw-test-implement-import', 'float', 'right' )
457                                                 + '\');\n'
458                                                 + '.mw-test-implement-import { text-align: center; }'
459                                 ]
460                         },
461                         {
462                                 'test-foobar': 'Hello Foobar, $1!'
463                         }
464                 );
465
466                 mw.loader.using( 'test.implement.import' ).always( function () {
467                         assert.strictEqual( isJsExecuted, true, 'script executed' );
468                         assert.equal( mw.loader.getState( 'test.implement.import' ), 'ready', 'module state after script execution' );
469                 } );
470         } );
471
472         QUnit.test( '.implement( dependency with styles )', function ( assert ) {
473                 var $element = $( '<div class="mw-test-implement-e"></div>' ).appendTo( '#qunit-fixture' ),
474                         $element2 = $( '<div class="mw-test-implement-e2"></div>' ).appendTo( '#qunit-fixture' );
475
476                 assert.notEqual(
477                         $element.css( 'float' ),
478                         'right',
479                         'style is clear'
480                 );
481                 assert.notEqual(
482                         $element2.css( 'float' ),
483                         'left',
484                         'style is clear'
485                 );
486
487                 mw.loader.register( [
488                         [ 'test.implement.e', '0', [ 'test.implement.e2' ] ],
489                         [ 'test.implement.e2', '0' ]
490                 ] );
491
492                 mw.loader.implement(
493                         'test.implement.e',
494                         function () {
495                                 assert.equal(
496                                         $element.css( 'float' ),
497                                         'right',
498                                         'Depending module\'s style is applied'
499                                 );
500                         },
501                         {
502                                 all: '.mw-test-implement-e { float: right; }'
503                         }
504                 );
505
506                 mw.loader.implement(
507                         'test.implement.e2',
508                         function () {
509                                 assert.equal(
510                                         $element2.css( 'float' ),
511                                         'left',
512                                         'Dependency\'s style is applied'
513                                 );
514                         },
515                         {
516                                 all: '.mw-test-implement-e2 { float: left; }'
517                         }
518                 );
519
520                 return mw.loader.using( 'test.implement.e' );
521         } );
522
523         QUnit.test( '.implement( only scripts )', function ( assert ) {
524                 mw.loader.implement( 'test.onlyscripts', function () {} );
525                 assert.strictEqual( mw.loader.getState( 'test.onlyscripts' ), 'ready' );
526         } );
527
528         QUnit.test( '.implement( only messages )', function ( assert ) {
529                 assert.assertFalse( mw.messages.exists( 'T31107' ), 'Verify that the test message doesn\'t exist yet' );
530
531                 mw.loader.implement( 'test.implement.msgs', [], {}, { T31107: 'loaded' } );
532
533                 return mw.loader.using( 'test.implement.msgs', function () {
534                         assert.ok( mw.messages.exists( 'T31107' ), 'T31107: messages-only module should implement ok' );
535                 }, function () {
536                         assert.ok( false, 'Error callback fired while implementing "test.implement.msgs" module' );
537                 } );
538         } );
539
540         QUnit.test( '.implement( empty )', function ( assert ) {
541                 mw.loader.implement( 'test.empty' );
542                 assert.strictEqual( mw.loader.getState( 'test.empty' ), 'ready' );
543         } );
544
545         QUnit.test( 'Broken indirect dependency', function ( assert ) {
546                 // don't emit an error event
547                 this.sandbox.stub( mw, 'track' );
548
549                 mw.loader.register( [
550                         [ 'test.module1', '0' ],
551                         [ 'test.module2', '0', [ 'test.module1' ] ],
552                         [ 'test.module3', '0', [ 'test.module2' ] ]
553                 ] );
554                 mw.loader.implement( 'test.module1', function () {
555                         throw new Error( 'expected' );
556                 }, {}, {} );
557                 assert.strictEqual( mw.loader.getState( 'test.module1' ), 'error', 'Expected "error" state for test.module1' );
558                 assert.strictEqual( mw.loader.getState( 'test.module2' ), 'error', 'Expected "error" state for test.module2' );
559                 assert.strictEqual( mw.loader.getState( 'test.module3' ), 'error', 'Expected "error" state for test.module3' );
560
561                 assert.strictEqual( mw.track.callCount, 1 );
562         } );
563
564         QUnit.test( 'Out-of-order implementation', function ( assert ) {
565                 mw.loader.register( [
566                         [ 'test.module4', '0' ],
567                         [ 'test.module5', '0', [ 'test.module4' ] ],
568                         [ 'test.module6', '0', [ 'test.module5' ] ]
569                 ] );
570                 mw.loader.implement( 'test.module4', function () {} );
571                 assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
572                 assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
573                 assert.strictEqual( mw.loader.getState( 'test.module6' ), 'registered', 'Expected "registered" state for test.module6' );
574                 mw.loader.implement( 'test.module6', function () {} );
575                 assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
576                 assert.strictEqual( mw.loader.getState( 'test.module5' ), 'registered', 'Expected "registered" state for test.module5' );
577                 assert.strictEqual( mw.loader.getState( 'test.module6' ), 'loaded', 'Expected "loaded" state for test.module6' );
578                 mw.loader.implement( 'test.module5', function () {} );
579                 assert.strictEqual( mw.loader.getState( 'test.module4' ), 'ready', 'Expected "ready" state for test.module4' );
580                 assert.strictEqual( mw.loader.getState( 'test.module5' ), 'ready', 'Expected "ready" state for test.module5' );
581                 assert.strictEqual( mw.loader.getState( 'test.module6' ), 'ready', 'Expected "ready" state for test.module6' );
582         } );
583
584         QUnit.test( 'Missing dependency', function ( assert ) {
585                 mw.loader.register( [
586                         [ 'test.module7', '0' ],
587                         [ 'test.module8', '0', [ 'test.module7' ] ],
588                         [ 'test.module9', '0', [ 'test.module8' ] ]
589                 ] );
590                 mw.loader.implement( 'test.module8', function () {} );
591                 assert.strictEqual( mw.loader.getState( 'test.module7' ), 'registered', 'Expected "registered" state for test.module7' );
592                 assert.strictEqual( mw.loader.getState( 'test.module8' ), 'loaded', 'Expected "loaded" state for test.module8' );
593                 assert.strictEqual( mw.loader.getState( 'test.module9' ), 'registered', 'Expected "registered" state for test.module9' );
594                 mw.loader.state( 'test.module7', 'missing' );
595                 assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
596                 assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
597                 assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
598                 mw.loader.implement( 'test.module9', function () {} );
599                 assert.strictEqual( mw.loader.getState( 'test.module7' ), 'missing', 'Expected "missing" state for test.module7' );
600                 assert.strictEqual( mw.loader.getState( 'test.module8' ), 'error', 'Expected "error" state for test.module8' );
601                 assert.strictEqual( mw.loader.getState( 'test.module9' ), 'error', 'Expected "error" state for test.module9' );
602                 mw.loader.using(
603                         [ 'test.module7' ],
604                         function () {
605                                 assert.ok( false, 'Success fired despite missing dependency' );
606                                 assert.ok( true, 'QUnit expected() count dummy' );
607                         },
608                         function ( e, dependencies ) {
609                                 assert.strictEqual( Array.isArray( dependencies ), true, 'Expected array of dependencies' );
610                                 assert.deepEqual( dependencies, [ 'test.module7' ], 'Error callback called with module test.module7' );
611                         }
612                 );
613                 mw.loader.using(
614                         [ 'test.module9' ],
615                         function () {
616                                 assert.ok( false, 'Success fired despite missing dependency' );
617                                 assert.ok( true, 'QUnit expected() count dummy' );
618                         },
619                         function ( e, dependencies ) {
620                                 assert.strictEqual( Array.isArray( dependencies ), true, 'Expected array of dependencies' );
621                                 dependencies.sort();
622                                 assert.deepEqual(
623                                         dependencies,
624                                         [ 'test.module7', 'test.module8', 'test.module9' ],
625                                         'Error callback called with all three modules as dependencies'
626                                 );
627                         }
628                 );
629         } );
630
631         QUnit.test( 'Dependency handling', function ( assert ) {
632                 var done = assert.async();
633                 mw.loader.register( [
634                         // [module, version, dependencies, group, source]
635                         [ 'testMissing', '1', [], null, 'testloader' ],
636                         [ 'testUsesMissing', '1', [ 'testMissing' ], null, 'testloader' ],
637                         [ 'testUsesNestedMissing', '1', [ 'testUsesMissing' ], null, 'testloader' ]
638                 ] );
639
640                 function verifyModuleStates() {
641                         assert.equal( mw.loader.getState( 'testMissing' ), 'missing', 'Module not known to server must have state "missing"' );
642                         assert.equal( mw.loader.getState( 'testUsesMissing' ), 'error', 'Module with missing dependency must have state "error"' );
643                         assert.equal( mw.loader.getState( 'testUsesNestedMissing' ), 'error', 'Module with indirect missing dependency must have state "error"' );
644                 }
645
646                 mw.loader.using( [ 'testUsesNestedMissing' ],
647                         function () {
648                                 assert.ok( false, 'Error handler should be invoked.' );
649                                 assert.ok( true ); // Dummy to reach QUnit expect()
650
651                                 verifyModuleStates();
652
653                                 done();
654                         },
655                         function ( e, badmodules ) {
656                                 assert.ok( true, 'Error handler should be invoked.' );
657                                 // As soon as server spits out state('testMissing', 'missing');
658                                 // it will bubble up and trigger the error callback.
659                                 // Therefor the badmodules array is not testUsesMissing or testUsesNestedMissing.
660                                 assert.deepEqual( badmodules, [ 'testMissing' ], 'Bad modules as expected.' );
661
662                                 verifyModuleStates();
663
664                                 done();
665                         }
666                 );
667         } );
668
669         QUnit.test( 'Skip-function handling', function ( assert ) {
670                 mw.loader.register( [
671                         // [module, version, dependencies, group, source, skip]
672                         [ 'testSkipped', '1', [], null, 'testloader', 'return true;' ],
673                         [ 'testNotSkipped', '1', [], null, 'testloader', 'return false;' ],
674                         [ 'testUsesSkippable', '1', [ 'testSkipped', 'testNotSkipped' ], null, 'testloader' ]
675                 ] );
676
677                 function verifyModuleStates() {
678                         assert.equal( mw.loader.getState( 'testSkipped' ), 'ready', 'Module is ready when skipped' );
679                         assert.equal( mw.loader.getState( 'testNotSkipped' ), 'ready', 'Module is ready when not skipped but loaded' );
680                         assert.equal( mw.loader.getState( 'testUsesSkippable' ), 'ready', 'Module is ready when skippable dependencies are ready' );
681                 }
682
683                 return mw.loader.using( [ 'testUsesSkippable' ],
684                         function () {
685                                 assert.ok( true, 'Success handler should be invoked.' );
686                                 assert.ok( true ); // Dummy to match error handler and reach QUnit expect()
687
688                                 verifyModuleStates();
689                         },
690                         function ( e, badmodules ) {
691                                 assert.ok( false, 'Error handler should not be invoked.' );
692                                 assert.deepEqual( badmodules, [], 'Bad modules as expected.' );
693
694                                 verifyModuleStates();
695                         }
696                 );
697         } );
698
699         // This bug was actually already fixed in 1.18 and later when discovered in 1.17.
700         QUnit.test( '.load( "//protocol-relative" ) - T32825', function ( assert ) {
701                 var target,
702                         done = assert.async();
703
704                 // URL to the callback script
705                 target = QUnit.fixurl(
706                         mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/mwLoaderTestCallback.js'
707                 );
708                 // Ensure a protocol-relative URL for this test
709                 target = target.replace( /https?:/, '' );
710                 assert.equal( target.slice( 0, 2 ), '//', 'URL is protocol-relative' );
711
712                 mw.loader.testCallback = function () {
713                         delete mw.loader.testCallback;
714                         assert.ok( true, 'callback' );
715                         done();
716                 };
717
718                 // Go!
719                 mw.loader.load( target );
720         } );
721
722         QUnit.test( '.load( "/absolute-path" )', function ( assert ) {
723                 var target,
724                         done = assert.async();
725
726                 // URL to the callback script
727                 target = QUnit.fixurl( mw.config.get( 'wgScriptPath' ) + '/tests/qunit/data/mwLoaderTestCallback.js' );
728                 assert.equal( target.slice( 0, 1 ), '/', 'URL is relative to document root' );
729
730                 mw.loader.testCallback = function () {
731                         delete mw.loader.testCallback;
732                         assert.ok( true, 'callback' );
733                         done();
734                 };
735
736                 // Go!
737                 mw.loader.load( target );
738         } );
739
740         QUnit.test( 'Empty string module name - T28804', function ( assert ) {
741                 var done = false;
742
743                 assert.strictEqual( mw.loader.getState( '' ), null, 'State (unregistered)' );
744
745                 mw.loader.register( '', 'v1' );
746                 assert.strictEqual( mw.loader.getState( '' ), 'registered', 'State (registered)' );
747                 assert.strictEqual( mw.loader.getVersion( '' ), 'v1', 'Version' );
748
749                 mw.loader.implement( '', function () {
750                         done = true;
751                 } );
752
753                 return mw.loader.using( '', function () {
754                         assert.strictEqual( done, true, 'script ran' );
755                         assert.strictEqual( mw.loader.getState( '' ), 'ready', 'State (ready)' );
756                 } );
757         } );
758
759         QUnit.test( 'Executing race - T112232', function ( assert ) {
760                 var done = false;
761
762                 // The red herring schedules its CSS buffer first. In T112232, a bug in the
763                 // state machine would cause the job for testRaceLoadMe to run with an earlier job.
764                 mw.loader.implement(
765                         'testRaceRedHerring',
766                         function () {},
767                         { css: [ '.mw-testRaceRedHerring {}' ] }
768                 );
769                 mw.loader.implement(
770                         'testRaceLoadMe',
771                         function () {
772                                 done = true;
773                         },
774                         { css: [ '.mw-testRaceLoadMe { float: left; }' ] }
775                 );
776
777                 mw.loader.load( [ 'testRaceRedHerring', 'testRaceLoadMe' ] );
778                 return mw.loader.using( 'testRaceLoadMe', function () {
779                         assert.strictEqual( done, true, 'script ran' );
780                         assert.strictEqual( mw.loader.getState( 'testRaceLoadMe' ), 'ready', 'state' );
781                 } );
782         } );
783
784         QUnit.test( 'Stale response caching - T117587', function ( assert ) {
785                 var count = 0;
786                 mw.loader.store.enabled = true;
787                 mw.loader.register( 'test.stale', 'v2' );
788                 assert.strictEqual( mw.loader.store.get( 'test.stale' ), false, 'Not in store' );
789
790                 mw.loader.implement( 'test.stale@v1', function () {
791                         count++;
792                 } );
793
794                 return mw.loader.using( 'test.stale' )
795                         .then( function () {
796                                 assert.strictEqual( count, 1 );
797                                 // After implementing, registry contains version as implemented by the response.
798                                 assert.strictEqual( mw.loader.getVersion( 'test.stale' ), 'v1', 'Override version' );
799                                 assert.strictEqual( mw.loader.getState( 'test.stale' ), 'ready' );
800                                 assert.ok( mw.loader.store.get( 'test.stale' ), 'In store' );
801                         } )
802                         .then( function () {
803                                 // Reset run time, but keep mw.loader.store
804                                 mw.loader.moduleRegistry[ 'test.stale' ].script = undefined;
805                                 mw.loader.moduleRegistry[ 'test.stale' ].state = 'registered';
806                                 mw.loader.moduleRegistry[ 'test.stale' ].version = 'v2';
807
808                                 // Module was stored correctly as v1
809                                 // On future navigations, it will be ignored until evicted
810                                 assert.strictEqual( mw.loader.store.get( 'test.stale' ), false, 'Not in store' );
811                         } );
812         } );
813
814         QUnit.test( 'Stale response caching - backcompat', function ( assert ) {
815                 var count = 0;
816                 mw.loader.store.enabled = true;
817                 mw.loader.register( 'test.stalebc', 'v2' );
818                 assert.strictEqual( mw.loader.store.get( 'test.stalebc' ), false, 'Not in store' );
819
820                 mw.loader.implement( 'test.stalebc', function () {
821                         count++;
822                 } );
823
824                 return mw.loader.using( 'test.stalebc' )
825                         .then( function () {
826                                 assert.strictEqual( count, 1 );
827                                 assert.strictEqual( mw.loader.getState( 'test.stalebc' ), 'ready' );
828                                 assert.ok( mw.loader.store.get( 'test.stalebc' ), 'In store' );
829                         } )
830                         .then( function () {
831                                 // Reset run time, but keep mw.loader.store
832                                 mw.loader.moduleRegistry[ 'test.stalebc' ].script = undefined;
833                                 mw.loader.moduleRegistry[ 'test.stalebc' ].state = 'registered';
834                                 mw.loader.moduleRegistry[ 'test.stalebc' ].version = 'v2';
835
836                                 // Legacy behaviour is storing under the expected version,
837                                 // which woudl lead to whitewashing and stale values (T117587).
838                                 assert.ok( mw.loader.store.get( 'test.stalebc' ), 'In store' );
839                         } );
840         } );
841
842         QUnit.test( 'require()', function ( assert ) {
843                 mw.loader.register( [
844                         [ 'test.require1', '0' ],
845                         [ 'test.require2', '0' ],
846                         [ 'test.require3', '0' ],
847                         [ 'test.require4', '0', [ 'test.require3' ] ]
848                 ] );
849                 mw.loader.implement( 'test.require1', function () {} );
850                 mw.loader.implement( 'test.require2', function ( $, jQuery, require, module ) {
851                         module.exports = 1;
852                 } );
853                 mw.loader.implement( 'test.require3', function ( $, jQuery, require, module ) {
854                         module.exports = function () {
855                                 return 'hello world';
856                         };
857                 } );
858                 mw.loader.implement( 'test.require4', function ( $, jQuery, require, module ) {
859                         var other = require( 'test.require3' );
860                         module.exports = {
861                                 pizza: function () {
862                                         return other();
863                                 }
864                         };
865                 } );
866                 return mw.loader.using( [ 'test.require1', 'test.require2', 'test.require3', 'test.require4' ] ).then( function ( require ) {
867                         var module1, module2, module3, module4;
868
869                         module1 = require( 'test.require1' );
870                         module2 = require( 'test.require2' );
871                         module3 = require( 'test.require3' );
872                         module4 = require( 'test.require4' );
873
874                         assert.strictEqual( typeof module1, 'object', 'export of module with no export' );
875                         assert.strictEqual( module2, 1, 'export a number' );
876                         assert.strictEqual( module3(), 'hello world', 'export a function' );
877                         assert.strictEqual( typeof module4.pizza, 'function', 'export an object' );
878                         assert.strictEqual( module4.pizza(), 'hello world', 'module can require other modules' );
879
880                         assert.throws( function () {
881                                 require( '_badmodule' );
882                         }, /is not loaded/, 'Requesting non-existent modules throws error.' );
883                 } );
884         } );
885
886         QUnit.test( 'require() in debug mode', function ( assert ) {
887                 var path = mw.config.get( 'wgScriptPath' );
888                 mw.loader.register( [
889                         [ 'test.require.define', '0' ],
890                         [ 'test.require.callback', '0', [ 'test.require.define' ] ]
891                 ] );
892                 mw.loader.implement( 'test.require.callback', [ QUnit.fixurl( path + '/tests/qunit/data/requireCallMwLoaderTestCallback.js' ) ] );
893                 mw.loader.implement( 'test.require.define', [ QUnit.fixurl( path + '/tests/qunit/data/defineCallMwLoaderTestCallback.js' ) ] );
894
895                 return mw.loader.using( 'test.require.callback' ).then( function ( require ) {
896                         var cb = require( 'test.require.callback' );
897                         assert.strictEqual( cb.immediate, 'Defined.', 'module.exports and require work in debug mode' );
898                         // Must use try-catch because cb.later() will throw if require is undefined,
899                         // which doesn't work well inside Deferred.then() when using jQuery 1.x with QUnit
900                         try {
901                                 assert.strictEqual( cb.later(), 'Defined.', 'require works asynchrously in debug mode' );
902                         } catch ( e ) {
903                                 assert.equal( null, String( e ), 'require works asynchrously in debug mode' );
904                         }
905                 }, function () {
906                         assert.ok( false, 'Error callback fired while loader.using "test.require.callback" module' );
907                 } );
908         } );
909
910         QUnit.test( 'Implicit dependencies', function ( assert ) {
911                 var ranUser = false,
912                         userSeesSite = false,
913                         ranSite = false;
914
915                 mw.loader.implement(
916                         'site',
917                         function () {
918                                 ranSite = true;
919                         }
920                 );
921                 mw.loader.implement(
922                         'user',
923                         function () {
924                                 userSeesSite = ranSite;
925                                 ranUser = true;
926                         }
927                 );
928
929                 assert.strictEqual( ranSite, false, 'verify site module not yet loaded' );
930                 assert.strictEqual( ranUser, false, 'verify user module not yet loaded' );
931                 return mw.loader.using( 'user', function () {
932                         assert.strictEqual( ranSite, true, 'ran site module' );
933                         assert.strictEqual( ranUser, true, 'ran user module' );
934                         assert.strictEqual( userSeesSite, true, 'ran site before user module' );
935
936                         // Reset
937                         mw.loader.moduleRegistry[ 'site' ].state = 'registered';
938                         mw.loader.moduleRegistry[ 'user' ].state = 'registered';
939                 } );
940         } );
941
942 }( mediaWiki, jQuery ) );