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