Merge "Remove Title->getSquidURLs, deprecated in 1.27, unused"
[lhc/web/wiklou.git] / tests / phpunit / includes / resourceloader / ResourceLoaderTest.php
1 <?php
2
3 use Wikimedia\TestingAccessWrapper;
4
5 class ResourceLoaderTest extends ResourceLoaderTestCase {
6
7 protected function setUp() {
8 parent::setUp();
9
10 $this->setMwGlobals( [
11 'wgShowExceptionDetails' => true,
12 ] );
13 }
14
15 /**
16 * Ensure the ResourceLoaderRegisterModules hook is called.
17 *
18 * @covers ResourceLoader::__construct
19 */
20 public function testConstructRegistrationHook() {
21 $resourceLoaderRegisterModulesHook = false;
22
23 $this->setMwGlobals( 'wgHooks', [
24 'ResourceLoaderRegisterModules' => [
25 function ( &$resourceLoader ) use ( &$resourceLoaderRegisterModulesHook ) {
26 $resourceLoaderRegisterModulesHook = true;
27 }
28 ]
29 ] );
30
31 $unused = new ResourceLoader();
32 $this->assertTrue(
33 $resourceLoaderRegisterModulesHook,
34 'Hook ResourceLoaderRegisterModules called'
35 );
36 }
37
38 /**
39 * @covers ResourceLoader::register
40 * @covers ResourceLoader::getModule
41 */
42 public function testRegisterValidObject() {
43 $module = new ResourceLoaderTestModule();
44 $resourceLoader = new EmptyResourceLoader();
45 $resourceLoader->register( 'test', $module );
46 $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
47 }
48
49 /**
50 * @covers ResourceLoader::register
51 * @covers ResourceLoader::getModule
52 */
53 public function testRegisterValidArray() {
54 $module = new ResourceLoaderTestModule();
55 $resourceLoader = new EmptyResourceLoader();
56 // Covers case of register() setting $rl->moduleInfos,
57 // but $rl->modules lazy-populated by getModule()
58 $resourceLoader->register( 'test', [ 'object' => $module ] );
59 $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
60 }
61
62 /**
63 * @covers ResourceLoader::register
64 */
65 public function testRegisterEmptyString() {
66 $module = new ResourceLoaderTestModule();
67 $resourceLoader = new EmptyResourceLoader();
68 $resourceLoader->register( '', $module );
69 $this->assertEquals( $module, $resourceLoader->getModule( '' ) );
70 }
71
72 /**
73 * @covers ResourceLoader::register
74 */
75 public function testRegisterInvalidName() {
76 $resourceLoader = new EmptyResourceLoader();
77 $this->setExpectedException( MWException::class, "name 'test!invalid' is invalid" );
78 $resourceLoader->register( 'test!invalid', new ResourceLoaderTestModule() );
79 }
80
81 /**
82 * @covers ResourceLoader::register
83 */
84 public function testRegisterInvalidType() {
85 $resourceLoader = new EmptyResourceLoader();
86 $this->setExpectedException( MWException::class, 'ResourceLoader module info type error' );
87 $resourceLoader->register( 'test', new stdClass() );
88 }
89
90 /**
91 * @covers ResourceLoader::register
92 */
93 public function testRegisterDuplicate() {
94 $logger = $this->getMockBuilder( Psr\Log\LoggerInterface::class )->getMock();
95 $logger->expects( $this->once() )
96 ->method( 'warning' );
97 $resourceLoader = new EmptyResourceLoader( null, $logger );
98
99 $module1 = new ResourceLoaderTestModule();
100 $module2 = new ResourceLoaderTestModule();
101 $resourceLoader->register( 'test', $module1 );
102 $resourceLoader->register( 'test', $module2 );
103 $this->assertSame( $module2, $resourceLoader->getModule( 'test' ) );
104 }
105
106 /**
107 * @covers ResourceLoader::getModuleNames
108 */
109 public function testGetModuleNames() {
110 // Use an empty one so that core and extension modules don't get in.
111 $resourceLoader = new EmptyResourceLoader();
112 $resourceLoader->register( 'test.foo', new ResourceLoaderTestModule() );
113 $resourceLoader->register( 'test.bar', new ResourceLoaderTestModule() );
114 $this->assertEquals(
115 [ 'test.foo', 'test.bar' ],
116 $resourceLoader->getModuleNames()
117 );
118 }
119
120 public function provideTestIsFileModule() {
121 $fileModuleObj = $this->getMockBuilder( ResourceLoaderFileModule::class )
122 ->disableOriginalConstructor()
123 ->getMock();
124 return [
125 'object' => [ false,
126 new ResourceLoaderTestModule()
127 ],
128 'FileModule object' => [ false,
129 $fileModuleObj
130 ],
131 'simple empty' => [ true,
132 []
133 ],
134 'simple scripts' => [ true,
135 [ 'scripts' => 'example.js' ]
136 ],
137 'simple scripts, raw and targets' => [ true, [
138 'scripts' => [ 'a.js', 'b.js' ],
139 'raw' => true,
140 'targets' => [ 'desktop', 'mobile' ],
141 ] ],
142 'FileModule' => [ true,
143 [ 'class' => ResourceLoaderFileModule::class, 'scripts' => 'example.js' ]
144 ],
145 'TestModule' => [ false,
146 [ 'class' => ResourceLoaderTestModule::class, 'scripts' => 'example.js' ]
147 ],
148 'SkinModule (FileModule subclass)' => [ true,
149 [ 'class' => ResourceLoaderSkinModule::class, 'scripts' => 'example.js' ]
150 ],
151 'JqueryMsgModule (FileModule subclass)' => [ true, [
152 'class' => ResourceLoaderJqueryMsgModule::class,
153 'scripts' => 'example.js',
154 ] ],
155 'WikiModule' => [ false, [
156 'class' => ResourceLoaderWikiModule::class,
157 'scripts' => [ 'MediaWiki:Example.js' ],
158 ] ],
159 ];
160 }
161
162 /**
163 * @dataProvider provideTestIsFileModule
164 * @covers ResourceLoader::isFileModule
165 */
166 public function testIsFileModule( $expected, $module ) {
167 $rl = TestingAccessWrapper::newFromObject( new EmptyResourceLoader() );
168 $rl->register( 'test', $module );
169 $this->assertSame( $expected, $rl->isFileModule( 'test' ) );
170 }
171
172 /**
173 * @covers ResourceLoader::isFileModule
174 */
175 public function testIsFileModuleUnknown() {
176 $rl = TestingAccessWrapper::newFromObject( new EmptyResourceLoader() );
177 $this->assertSame( false, $rl->isFileModule( 'unknown' ) );
178 }
179
180 /**
181 * @covers ResourceLoader::isModuleRegistered
182 */
183 public function testIsModuleRegistered() {
184 $rl = new EmptyResourceLoader();
185 $rl->register( 'test', new ResourceLoaderTestModule() );
186 $this->assertTrue( $rl->isModuleRegistered( 'test' ) );
187 $this->assertFalse( $rl->isModuleRegistered( 'test.unknown' ) );
188 }
189
190 /**
191 * @covers ResourceLoader::getModule
192 */
193 public function testGetModuleUnknown() {
194 $rl = new EmptyResourceLoader();
195 $this->assertSame( null, $rl->getModule( 'test' ) );
196 }
197
198 /**
199 * @covers ResourceLoader::getModule
200 */
201 public function testGetModuleClass() {
202 $rl = new EmptyResourceLoader();
203 $rl->register( 'test', [ 'class' => ResourceLoaderTestModule::class ] );
204 $this->assertInstanceOf(
205 ResourceLoaderTestModule::class,
206 $rl->getModule( 'test' )
207 );
208 }
209
210 /**
211 * @covers ResourceLoader::getModule
212 */
213 public function testGetModuleFactory() {
214 $factory = function ( array $info ) {
215 $this->assertArrayHasKey( 'kitten', $info );
216 return new ResourceLoaderTestModule( $info );
217 };
218
219 $rl = new EmptyResourceLoader();
220 $rl->register( 'test', [ 'factory' => $factory, 'kitten' => 'little ball of fur' ] );
221 $this->assertInstanceOf(
222 ResourceLoaderTestModule::class,
223 $rl->getModule( 'test' )
224 );
225 }
226
227 /**
228 * @covers ResourceLoader::getModule
229 */
230 public function testGetModuleClassDefault() {
231 $rl = new EmptyResourceLoader();
232 $rl->register( 'test', [] );
233 $this->assertInstanceOf(
234 ResourceLoaderFileModule::class,
235 $rl->getModule( 'test' ),
236 'Array-style module registrations default to FileModule'
237 );
238 }
239
240 /**
241 * @covers ResourceLoader::getLessCompiler
242 */
243 public function testLessImportDirs() {
244 $rl = new EmptyResourceLoader();
245 $lc = $rl->getLessCompiler( [ 'foo' => '2px', 'Foo' => '#eeeeee' ] );
246 $basePath = dirname( dirname( __DIR__ ) ) . '/data/less';
247 $lc->SetImportDirs( [
248 "$basePath/common" => '',
249 ] );
250 $css = $lc->parseFile( "$basePath/module/use-import-dir.less" )->getCss();
251 $this->assertStringEqualsFile( "$basePath/module/styles.css", $css );
252 }
253
254 public static function providePackedModules() {
255 return [
256 [
257 'Example from makePackedModulesString doc comment',
258 [ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ],
259 'foo.bar,baz|bar.baz,quux',
260 ],
261 [
262 'Example from expandModuleNames doc comment',
263 [ 'jquery.foo', 'jquery.bar', 'jquery.ui.baz', 'jquery.ui.quux' ],
264 'jquery.foo,bar|jquery.ui.baz,quux',
265 ],
266 [
267 'Regression fixed in r87497 (7fee86c38e) with dotless names',
268 [ 'foo', 'bar', 'baz' ],
269 'foo,bar,baz',
270 ],
271 [
272 'Prefixless modules after a prefixed module',
273 [ 'single.module', 'foobar', 'foobaz' ],
274 'single.module|foobar,foobaz',
275 ],
276 [
277 'Ordering',
278 [ 'foo', 'foo.baz', 'baz.quux', 'foo.bar' ],
279 'foo|foo.baz,bar|baz.quux',
280 [ 'foo', 'foo.baz', 'foo.bar', 'baz.quux' ],
281 ]
282 ];
283 }
284
285 /**
286 * @dataProvider providePackedModules
287 * @covers ResourceLoader::makePackedModulesString
288 */
289 public function testMakePackedModulesString( $desc, $modules, $packed ) {
290 $this->assertEquals( $packed, ResourceLoader::makePackedModulesString( $modules ), $desc );
291 }
292
293 /**
294 * @dataProvider providePackedModules
295 * @covers ResourceLoaderContext::expandModuleNames
296 */
297 public function testExpandModuleNames( $desc, $modules, $packed, $unpacked = null ) {
298 $this->assertEquals(
299 $unpacked ?: $modules,
300 ResourceLoaderContext::expandModuleNames( $packed ),
301 $desc
302 );
303 }
304
305 public static function provideAddSource() {
306 return [
307 [ 'foowiki', 'https://example.org/w/load.php', 'foowiki' ],
308 [ 'foowiki', [ 'loadScript' => 'https://example.org/w/load.php' ], 'foowiki' ],
309 [
310 [
311 'foowiki' => 'https://example.org/w/load.php',
312 'bazwiki' => 'https://example.com/w/load.php',
313 ],
314 null,
315 [ 'foowiki', 'bazwiki' ]
316 ]
317 ];
318 }
319
320 /**
321 * @dataProvider provideAddSource
322 * @covers ResourceLoader::addSource
323 * @covers ResourceLoader::getSources
324 */
325 public function testAddSource( $name, $info, $expected ) {
326 $rl = new ResourceLoader;
327 $rl->addSource( $name, $info );
328 if ( is_array( $expected ) ) {
329 foreach ( $expected as $source ) {
330 $this->assertArrayHasKey( $source, $rl->getSources() );
331 }
332 } else {
333 $this->assertArrayHasKey( $expected, $rl->getSources() );
334 }
335 }
336
337 /**
338 * @covers ResourceLoader::addSource
339 */
340 public function testAddSourceDupe() {
341 $rl = new ResourceLoader;
342 $this->setExpectedException(
343 MWException::class, 'ResourceLoader duplicate source addition error'
344 );
345 $rl->addSource( 'foo', 'https://example.org/w/load.php' );
346 $rl->addSource( 'foo', 'https://example.com/w/load.php' );
347 }
348
349 /**
350 * @covers ResourceLoader::addSource
351 */
352 public function testAddSourceInvalid() {
353 $rl = new ResourceLoader;
354 $this->setExpectedException( MWException::class, 'with no "loadScript" key' );
355 $rl->addSource( 'foo', [ 'x' => 'https://example.org/w/load.php' ] );
356 }
357
358 public static function provideLoaderImplement() {
359 return [
360 [ [
361 'title' => 'Implement scripts, styles and messages',
362
363 'name' => 'test.example',
364 'scripts' => 'mw.example();',
365 'styles' => [ 'css' => [ '.mw-example {}' ] ],
366 'messages' => [ 'example' => '' ],
367 'templates' => [],
368
369 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
370 mw.example();
371 }, {
372 "css": [
373 ".mw-example {}"
374 ]
375 }, {
376 "example": ""
377 } );',
378 ] ],
379 [ [
380 'title' => 'Implement scripts',
381
382 'name' => 'test.example',
383 'scripts' => 'mw.example();',
384 'styles' => [],
385
386 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
387 mw.example();
388 } );',
389 ] ],
390 [ [
391 'title' => 'Implement styles',
392
393 'name' => 'test.example',
394 'scripts' => [],
395 'styles' => [ 'css' => [ '.mw-example {}' ] ],
396
397 'expected' => 'mw.loader.implement( "test.example", [], {
398 "css": [
399 ".mw-example {}"
400 ]
401 } );',
402 ] ],
403 [ [
404 'title' => 'Implement scripts and messages',
405
406 'name' => 'test.example',
407 'scripts' => 'mw.example();',
408 'messages' => [ 'example' => '' ],
409
410 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
411 mw.example();
412 }, {}, {
413 "example": ""
414 } );',
415 ] ],
416 [ [
417 'title' => 'Implement scripts and templates',
418
419 'name' => 'test.example',
420 'scripts' => 'mw.example();',
421 'templates' => [ 'example.html' => '' ],
422
423 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
424 mw.example();
425 }, {}, {}, {
426 "example.html": ""
427 } );',
428 ] ],
429 [ [
430 'title' => 'Implement unwrapped user script',
431
432 'name' => 'user',
433 'scripts' => 'mw.example( 1 );',
434 'wrap' => false,
435
436 'expected' => 'mw.loader.implement( "user", "mw.example( 1 );" );',
437 ] ],
438 [ [
439 'title' => 'Implement multi-file script',
440
441 'name' => 'test.multifile',
442 'scripts' => [
443 'files' => [
444 'one.js' => [
445 'type' => 'script',
446 'content' => 'mw.example( 1 );',
447 ],
448 'two.json' => [
449 'type' => 'data',
450 'content' => [ 'n' => 2 ],
451 ],
452 'three.js' => [
453 'type' => 'script',
454 'content' => 'mw.example( 3 );'
455 ],
456 ],
457 'main' => 'three.js',
458 ],
459
460 'expected' => <<<END
461 mw.loader.implement( "test.multifile", {
462 "main": "three.js",
463 "files": {
464 "one.js": function ( require, module ) {
465 mw.example( 1 );
466 },
467 "two.json": {
468 "n": 2
469 },
470 "three.js": function ( require, module ) {
471 mw.example( 3 );
472 }
473 }
474 } );
475 END
476 ] ],
477 ];
478 }
479
480 /**
481 * @dataProvider provideLoaderImplement
482 * @covers ResourceLoader::makeLoaderImplementScript
483 * @covers ResourceLoader::trimArray
484 */
485 public function testMakeLoaderImplementScript( $case ) {
486 $case += [
487 'wrap' => true,
488 'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' ), 'packageFiles' => [],
489 ];
490 ResourceLoader::clearCache();
491 $this->setMwGlobals( 'wgResourceLoaderDebug', true );
492
493 $rl = TestingAccessWrapper::newFromClass( ResourceLoader::class );
494 $this->assertEquals(
495 $case['expected'],
496 $rl->makeLoaderImplementScript(
497 $case['name'],
498 ( $case['wrap'] && is_string( $case['scripts'] ) )
499 ? new XmlJsCode( $case['scripts'] )
500 : $case['scripts'],
501 $case['styles'],
502 $case['messages'],
503 $case['templates'],
504 $case['packageFiles']
505 )
506 );
507 }
508
509 /**
510 * @covers ResourceLoader::makeLoaderImplementScript
511 */
512 public function testMakeLoaderImplementScriptInvalid() {
513 $this->setExpectedException( MWException::class, 'Invalid scripts error' );
514 $rl = TestingAccessWrapper::newFromClass( ResourceLoader::class );
515 $rl->makeLoaderImplementScript(
516 'test', // name
517 123, // scripts
518 null, // styles
519 null, // messages
520 null, // templates
521 null // package files
522 );
523 }
524
525 /**
526 * @covers ResourceLoader::makeLoaderRegisterScript
527 */
528 public function testMakeLoaderRegisterScript() {
529 $this->assertEquals(
530 'mw.loader.register( [
531 [
532 "test.name",
533 "1234567"
534 ]
535 ] );',
536 ResourceLoader::makeLoaderRegisterScript( [
537 [ 'test.name', '1234567' ],
538 ] ),
539 'Nested array parameter'
540 );
541
542 $this->assertEquals(
543 'mw.loader.register( [
544 [
545 "test.foo",
546 "100"
547 ],
548 [
549 "test.bar",
550 "200",
551 [
552 "test.unknown"
553 ]
554 ],
555 [
556 "test.baz",
557 "300",
558 [
559 3,
560 0
561 ]
562 ],
563 [
564 "test.quux",
565 "400",
566 [],
567 null,
568 null,
569 "return true;"
570 ]
571 ] );',
572 ResourceLoader::makeLoaderRegisterScript( [
573 [ 'test.foo', '100' , [], null, null ],
574 [ 'test.bar', '200', [ 'test.unknown' ], null ],
575 [ 'test.baz', '300', [ 'test.quux', 'test.foo' ], null ],
576 [ 'test.quux', '400', [], null, null, 'return true;' ],
577 ] ),
578 'Compact dependency indexes'
579 );
580 }
581
582 /**
583 * @covers ResourceLoader::makeLoaderSourcesScript
584 */
585 public function testMakeLoaderSourcesScript() {
586 $this->assertEquals(
587 'mw.loader.addSource( {
588 "local": "/w/load.php"
589 } );',
590 ResourceLoader::makeLoaderSourcesScript( 'local', '/w/load.php' )
591 );
592 $this->assertEquals(
593 'mw.loader.addSource( {
594 "local": "/w/load.php"
595 } );',
596 ResourceLoader::makeLoaderSourcesScript( [ 'local' => '/w/load.php' ] )
597 );
598 $this->assertEquals(
599 'mw.loader.addSource( {
600 "local": "/w/load.php",
601 "example": "https://example.org/w/load.php"
602 } );',
603 ResourceLoader::makeLoaderSourcesScript( [
604 'local' => '/w/load.php',
605 'example' => 'https://example.org/w/load.php'
606 ] )
607 );
608 $this->assertEquals(
609 'mw.loader.addSource( [] );',
610 ResourceLoader::makeLoaderSourcesScript( [] )
611 );
612 }
613
614 private static function fakeSources() {
615 return [
616 'examplewiki' => [
617 'loadScript' => '//example.org/w/load.php',
618 'apiScript' => '//example.org/w/api.php',
619 ],
620 'example2wiki' => [
621 'loadScript' => '//example.com/w/load.php',
622 'apiScript' => '//example.com/w/api.php',
623 ],
624 ];
625 }
626
627 /**
628 * @covers ResourceLoader::getLoadScript
629 */
630 public function testGetLoadScript() {
631 $this->setMwGlobals( 'wgResourceLoaderSources', [] );
632 $rl = new ResourceLoader();
633 $sources = self::fakeSources();
634 $rl->addSource( $sources );
635 foreach ( [ 'examplewiki', 'example2wiki' ] as $name ) {
636 $this->assertEquals( $rl->getLoadScript( $name ), $sources[$name]['loadScript'] );
637 }
638
639 try {
640 $rl->getLoadScript( 'thiswasneverreigstered' );
641 $this->assertTrue( false, 'ResourceLoader::getLoadScript should have thrown an exception' );
642 } catch ( MWException $e ) {
643 $this->assertTrue( true );
644 }
645 }
646
647 protected function getFailFerryMock( $getter = 'getScript' ) {
648 $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
649 ->setMethods( [ $getter ] )
650 ->getMock();
651 $mock->method( $getter )->will( $this->throwException(
652 new Exception( 'Ferry not found' )
653 ) );
654 return $mock;
655 }
656
657 protected function getSimpleModuleMock( $script = '' ) {
658 $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
659 ->setMethods( [ 'getScript' ] )
660 ->getMock();
661 $mock->method( 'getScript' )->willReturn( $script );
662 return $mock;
663 }
664
665 protected function getSimpleStyleModuleMock( $styles = '' ) {
666 $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
667 ->setMethods( [ 'getStyles' ] )
668 ->getMock();
669 $mock->method( 'getStyles' )->willReturn( [ '' => $styles ] );
670 return $mock;
671 }
672
673 /**
674 * @covers ResourceLoader::getCombinedVersion
675 */
676 public function testGetCombinedVersion() {
677 $rl = $this->getMockBuilder( EmptyResourceLoader::class )
678 // Disable log from outputErrorAndLog
679 ->setMethods( [ 'outputErrorAndLog' ] )->getMock();
680 $rl->register( [
681 'foo' => self::getSimpleModuleMock(),
682 'ferry' => self::getFailFerryMock(),
683 'bar' => self::getSimpleModuleMock(),
684 ] );
685 $context = $this->getResourceLoaderContext( [], $rl );
686
687 $this->assertEquals(
688 '',
689 $rl->getCombinedVersion( $context, [] ),
690 'empty list'
691 );
692
693 $this->assertEquals(
694 ResourceLoader::makeHash( self::BLANK_VERSION ),
695 $rl->getCombinedVersion( $context, [ 'foo' ] ),
696 'compute foo'
697 );
698
699 // Verify that getCombinedVersion() does not throw when ferry fails.
700 // Instead it gracefully continues to combine the remaining modules.
701 $this->assertEquals(
702 ResourceLoader::makeHash( self::BLANK_VERSION . self::BLANK_VERSION ),
703 $rl->getCombinedVersion( $context, [ 'foo', 'ferry', 'bar' ] ),
704 'compute foo+ferry+bar (T152266)'
705 );
706 }
707
708 public static function provideMakeModuleResponseConcat() {
709 $testcases = [
710 [
711 'modules' => [
712 'foo' => 'foo()',
713 ],
714 'expected' => "foo()\n" . 'mw.loader.state( {
715 "foo": "ready"
716 } );',
717 'minified' => "foo()\n" . 'mw.loader.state({"foo":"ready"});',
718 'message' => 'Script without semi-colon',
719 ],
720 [
721 'modules' => [
722 'foo' => 'foo()',
723 'bar' => 'bar()',
724 ],
725 'expected' => "foo()\nbar()\n" . 'mw.loader.state( {
726 "foo": "ready",
727 "bar": "ready"
728 } );',
729 'minified' => "foo()\nbar()\n" . 'mw.loader.state({"foo":"ready","bar":"ready"});',
730 'message' => 'Two scripts without semi-colon',
731 ],
732 [
733 'modules' => [
734 'foo' => "foo()\n// bar();"
735 ],
736 'expected' => "foo()\n// bar();\n" . 'mw.loader.state( {
737 "foo": "ready"
738 } );',
739 'minified' => "foo()\n" . 'mw.loader.state({"foo":"ready"});',
740 'message' => 'Script with semi-colon in comment (T162719)',
741 ],
742 ];
743 $ret = [];
744 foreach ( $testcases as $i => $case ) {
745 $ret["#$i"] = [
746 $case['modules'],
747 $case['expected'],
748 true, // debug
749 $case['message'],
750 ];
751 $ret["#$i (minified)"] = [
752 $case['modules'],
753 $case['minified'],
754 false, // debug
755 $case['message'],
756 ];
757 }
758 return $ret;
759 }
760
761 /**
762 * Verify how multiple scripts and mw.loader.state() calls are concatenated.
763 *
764 * @dataProvider provideMakeModuleResponseConcat
765 * @covers ResourceLoader::makeModuleResponse
766 */
767 public function testMakeModuleResponseConcat( $scripts, $expected, $debug, $message = null ) {
768 $rl = new EmptyResourceLoader();
769 $modules = array_map( function ( $script ) {
770 return self::getSimpleModuleMock( $script );
771 }, $scripts );
772 $rl->register( $modules );
773
774 $this->setMwGlobals( 'wgResourceLoaderDebug', $debug );
775 $context = $this->getResourceLoaderContext(
776 [
777 'modules' => implode( '|', array_keys( $modules ) ),
778 'only' => 'scripts',
779 ],
780 $rl
781 );
782
783 $response = $rl->makeModuleResponse( $context, $modules );
784 $this->assertSame( [], $rl->getErrors(), 'Errors' );
785 $this->assertEquals( $expected, $response, $message ?: 'Response' );
786 }
787
788 /**
789 * @covers ResourceLoader::makeModuleResponse
790 */
791 public function testMakeModuleResponseEmpty() {
792 $rl = new EmptyResourceLoader();
793 $context = $this->getResourceLoaderContext(
794 [ 'modules' => '', 'only' => 'scripts' ],
795 $rl
796 );
797
798 $response = $rl->makeModuleResponse( $context, [] );
799 $this->assertSame( [], $rl->getErrors(), 'Errors' );
800 $this->assertRegExp( '/^\/\*.+no modules were requested.+\*\/$/ms', $response );
801 }
802
803 /**
804 * Verify that when building module content in a load.php response,
805 * an exception from one module will not break script output from
806 * other modules.
807 *
808 * @covers ResourceLoader::makeModuleResponse
809 */
810 public function testMakeModuleResponseError() {
811 $modules = [
812 'foo' => self::getSimpleModuleMock( 'foo();' ),
813 'ferry' => self::getFailFerryMock(),
814 'bar' => self::getSimpleModuleMock( 'bar();' ),
815 ];
816 $rl = new EmptyResourceLoader();
817 $rl->register( $modules );
818 $context = $this->getResourceLoaderContext(
819 [
820 'modules' => 'foo|ferry|bar',
821 'only' => 'scripts',
822 ],
823 $rl
824 );
825
826 // Disable log from makeModuleResponse via outputErrorAndLog
827 $this->setLogger( 'exception', new Psr\Log\NullLogger() );
828
829 $response = $rl->makeModuleResponse( $context, $modules );
830 $errors = $rl->getErrors();
831
832 $this->assertCount( 1, $errors );
833 $this->assertRegExp( '/Ferry not found/', $errors[0] );
834 $this->assertEquals(
835 "foo();\nbar();\n" . 'mw.loader.state( {
836 "ferry": "error",
837 "foo": "ready",
838 "bar": "ready"
839 } );',
840 $response
841 );
842 }
843
844 /**
845 * Verify that exceptions in PHP for one module will not break others
846 * (stylesheet response).
847 *
848 * @covers ResourceLoader::makeModuleResponse
849 */
850 public function testMakeModuleResponseErrorCSS() {
851 $modules = [
852 'foo' => self::getSimpleStyleModuleMock( '.foo{}' ),
853 'ferry' => self::getFailFerryMock( 'getStyles' ),
854 'bar' => self::getSimpleStyleModuleMock( '.bar{}' ),
855 ];
856 $rl = new EmptyResourceLoader();
857 $rl->register( $modules );
858 $context = $this->getResourceLoaderContext(
859 [
860 'modules' => 'foo|ferry|bar',
861 'only' => 'styles',
862 'debug' => 'false',
863 ],
864 $rl
865 );
866
867 // Disable log from makeModuleResponse via outputErrorAndLog
868 $this->setLogger( 'exception', new Psr\Log\NullLogger() );
869
870 $response = $rl->makeModuleResponse( $context, $modules );
871 $errors = $rl->getErrors();
872
873 $this->assertCount( 2, $errors );
874 $this->assertRegExp( '/Ferry not found/', $errors[0] );
875 $this->assertRegExp( '/Problem.+"ferry":\s*"error"/ms', $errors[1] );
876 $this->assertEquals(
877 '.foo{}.bar{}',
878 $response
879 );
880 }
881
882 /**
883 * Verify that when building the startup module response,
884 * an exception from one module class will not break the entire
885 * startup module response. See T152266.
886 *
887 * @covers ResourceLoader::makeModuleResponse
888 */
889 public function testMakeModuleResponseStartupError() {
890 $rl = new EmptyResourceLoader();
891 $rl->register( [
892 'foo' => self::getSimpleModuleMock( 'foo();' ),
893 'ferry' => self::getFailFerryMock(),
894 'bar' => self::getSimpleModuleMock( 'bar();' ),
895 'startup' => [ 'class' => ResourceLoaderStartUpModule::class ],
896 ] );
897 $context = $this->getResourceLoaderContext(
898 [
899 'modules' => 'startup',
900 'only' => 'scripts',
901 ],
902 $rl
903 );
904
905 $this->assertEquals(
906 [ 'foo', 'ferry', 'bar', 'startup' ],
907 $rl->getModuleNames(),
908 'getModuleNames'
909 );
910
911 // Disable log from makeModuleResponse via outputErrorAndLog
912 $this->setLogger( 'exception', new Psr\Log\NullLogger() );
913
914 $modules = [ 'startup' => $rl->getModule( 'startup' ) ];
915 $response = $rl->makeModuleResponse( $context, $modules );
916 $errors = $rl->getErrors();
917
918 $this->assertRegExp( '/Ferry not found/', $errors[0] );
919 $this->assertCount( 1, $errors );
920 $this->assertRegExp(
921 '/isCompatible.*window\.RLQ/s',
922 $response,
923 'startup response undisrupted (T152266)'
924 );
925 $this->assertRegExp(
926 '/register\([^)]+"ferry",\s*""/s',
927 $response,
928 'startup response registers broken module'
929 );
930 $this->assertRegExp(
931 '/state\([^)]+"ferry":\s*"error"/s',
932 $response,
933 'startup response sets state to error'
934 );
935 }
936
937 /**
938 * Integration test for modules sending extra HTTP response headers.
939 *
940 * @covers ResourceLoaderModule::getHeaders
941 * @covers ResourceLoaderModule::buildContent
942 * @covers ResourceLoader::makeModuleResponse
943 */
944 public function testMakeModuleResponseExtraHeaders() {
945 $module = $this->getMockBuilder( ResourceLoaderTestModule::class )
946 ->setMethods( [ 'getPreloadLinks' ] )->getMock();
947 $module->method( 'getPreloadLinks' )->willReturn( [
948 'https://example.org/script.js' => [ 'as' => 'script' ],
949 ] );
950
951 $rl = new EmptyResourceLoader();
952 $rl->register( [
953 'foo' => $module,
954 ] );
955 $context = $this->getResourceLoaderContext(
956 [ 'modules' => 'foo', 'only' => 'scripts' ],
957 $rl
958 );
959
960 $modules = [ 'foo' => $rl->getModule( 'foo' ) ];
961 $response = $rl->makeModuleResponse( $context, $modules );
962 $extraHeaders = TestingAccessWrapper::newFromObject( $rl )->extraHeaders;
963
964 $this->assertEquals(
965 [
966 'Link: <https://example.org/script.js>;rel=preload;as=script'
967 ],
968 $extraHeaders,
969 'Extra headers'
970 );
971 }
972
973 /**
974 * @covers ResourceLoaderModule::getHeaders
975 * @covers ResourceLoaderModule::buildContent
976 * @covers ResourceLoader::makeModuleResponse
977 */
978 public function testMakeModuleResponseExtraHeadersMulti() {
979 $foo = $this->getMockBuilder( ResourceLoaderTestModule::class )
980 ->setMethods( [ 'getPreloadLinks' ] )->getMock();
981 $foo->method( 'getPreloadLinks' )->willReturn( [
982 'https://example.org/script.js' => [ 'as' => 'script' ],
983 ] );
984
985 $bar = $this->getMockBuilder( ResourceLoaderTestModule::class )
986 ->setMethods( [ 'getPreloadLinks' ] )->getMock();
987 $bar->method( 'getPreloadLinks' )->willReturn( [
988 '/example.png' => [ 'as' => 'image' ],
989 '/example.jpg' => [ 'as' => 'image' ],
990 ] );
991
992 $rl = new EmptyResourceLoader();
993 $rl->register( [ 'foo' => $foo, 'bar' => $bar ] );
994 $context = $this->getResourceLoaderContext(
995 [ 'modules' => 'foo|bar', 'only' => 'scripts' ],
996 $rl
997 );
998
999 $modules = [ 'foo' => $rl->getModule( 'foo' ), 'bar' => $rl->getModule( 'bar' ) ];
1000 $response = $rl->makeModuleResponse( $context, $modules );
1001 $extraHeaders = TestingAccessWrapper::newFromObject( $rl )->extraHeaders;
1002 $this->assertEquals(
1003 [
1004 'Link: <https://example.org/script.js>;rel=preload;as=script',
1005 'Link: </example.png>;rel=preload;as=image,</example.jpg>;rel=preload;as=image'
1006 ],
1007 $extraHeaders,
1008 'Extra headers'
1009 );
1010 }
1011
1012 /**
1013 * @covers ResourceLoader::respond
1014 */
1015 public function testRespondEmpty() {
1016 $rl = $this->getMockBuilder( EmptyResourceLoader::class )
1017 ->setMethods( [
1018 'tryRespondNotModified',
1019 'sendResponseHeaders',
1020 'measureResponseTime',
1021 ] )
1022 ->getMock();
1023 $context = $this->getResourceLoaderContext( [ 'modules' => '' ], $rl );
1024
1025 $rl->expects( $this->once() )->method( 'measureResponseTime' );
1026 $this->expectOutputRegex( '/no modules were requested/' );
1027
1028 $rl->respond( $context );
1029 }
1030
1031 /**
1032 * @covers ResourceLoader::respond
1033 */
1034 public function testRespondSimple() {
1035 $module = new ResourceLoaderTestModule( [ 'script' => 'foo();' ] );
1036 $rl = $this->getMockBuilder( EmptyResourceLoader::class )
1037 ->setMethods( [
1038 'measureResponseTime',
1039 'tryRespondNotModified',
1040 'sendResponseHeaders',
1041 'makeModuleResponse',
1042 ] )
1043 ->getMock();
1044 $rl->register( 'test', $module );
1045 $context = $this->getResourceLoaderContext(
1046 [ 'modules' => 'test', 'only' => null ],
1047 $rl
1048 );
1049
1050 $rl->expects( $this->once() )->method( 'makeModuleResponse' )
1051 ->with( $context, [ 'test' => $module ] )
1052 ->willReturn( 'implement_foo;' );
1053 $this->expectOutputRegex( '/^implement_foo;/' );
1054
1055 $rl->respond( $context );
1056 }
1057
1058 /**
1059 * @covers ResourceLoader::respond
1060 */
1061 public function testRespondInternalFailures() {
1062 $module = new ResourceLoaderTestModule( [ 'script' => 'foo();' ] );
1063 $rl = $this->getMockBuilder( EmptyResourceLoader::class )
1064 ->setMethods( [
1065 'measureResponseTime',
1066 'preloadModuleInfo',
1067 'getCombinedVersion',
1068 'tryRespondNotModified',
1069 'makeModuleResponse',
1070 'sendResponseHeaders',
1071 ] )
1072 ->getMock();
1073 $rl->register( 'test', $module );
1074 $context = $this->getResourceLoaderContext( [ 'modules' => 'test' ], $rl );
1075 // Disable logging from outputErrorAndLog
1076 $this->setLogger( 'exception', new Psr\Log\NullLogger() );
1077
1078 $rl->expects( $this->once() )->method( 'preloadModuleInfo' )
1079 ->willThrowException( new Exception( 'Preload error' ) );
1080 $rl->expects( $this->once() )->method( 'getCombinedVersion' )
1081 ->willThrowException( new Exception( 'Version error' ) );
1082 $rl->expects( $this->once() )->method( 'makeModuleResponse' )
1083 ->with( $context, [ 'test' => $module ] )
1084 ->willReturn( 'foo;' );
1085 // Internal errors should be caught and logged without affecting module output
1086 $this->expectOutputRegex( '/^\/\*.+Preload error.+Version error.+\*\/.*foo;/ms' );
1087
1088 $rl->respond( $context );
1089 }
1090
1091 /**
1092 * @covers ResourceLoader::measureResponseTime
1093 */
1094 public function testMeasureResponseTime() {
1095 $stats = $this->getMockBuilder( NullStatsdDataFactory::class )
1096 ->setMethods( [ 'timing' ] )->getMock();
1097 $this->setService( 'StatsdDataFactory', $stats );
1098
1099 $stats->expects( $this->once() )->method( 'timing' )
1100 ->with( 'resourceloader.responseTime', $this->anything() );
1101
1102 $timing = new Timing();
1103 $timing->mark( 'requestShutdown' );
1104 $rl = TestingAccessWrapper::newFromObject( new EmptyResourceLoader );
1105 $rl->measureResponseTime( $timing );
1106 DeferredUpdates::doUpdates();
1107 }
1108 }