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