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