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