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