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