3 use Wikimedia\TestingAccessWrapper
;
4 use MediaWiki\MediaWikiServices
;
6 class ResourceLoaderTest
extends ResourceLoaderTestCase
{
8 protected function setUp() {
11 $this->setMwGlobals( [
12 'wgShowExceptionDetails' => true,
17 * Ensure the ResourceLoaderRegisterModules hook is called.
20 public function testServiceWiring() {
22 $this->setMwGlobals( 'wgHooks', [
23 'ResourceLoaderRegisterModules' => [
24 function ( &$resourceLoader ) use ( &$ranHook ) {
30 MediaWikiServices
::getInstance()->getResourceLoader();
32 $this->assertSame( 1, $ranHook, 'Hook was called' );
35 public static function provideInvalidModuleName() {
37 'name with 300 chars' => [ str_repeat( 'x', 300 ) ],
38 'name with bang' => [ 'this!that' ],
39 'name with comma' => [ 'this,that' ],
40 'name with pipe' => [ 'this|that' ],
44 public static function provideValidModuleName() {
46 'empty string' => [ '' ],
47 'simple name' => [ 'this.and-that2' ],
48 'name with 100 chars' => [ str_repeat( 'x', 100 ) ],
49 'name with hash' => [ 'this#that' ],
50 'name with slash' => [ 'this/that' ],
51 'name with at' => [ 'this@that' ],
56 * @dataProvider provideInvalidModuleName
57 * @covers ResourceLoader
59 public function testIsValidModuleName_invalid( $name ) {
60 $this->assertFalse( ResourceLoader
::isValidModuleName( $name ) );
64 * @dataProvider provideValidModuleName
65 * @covers ResourceLoader
67 public function testIsValidModuleName_valid( $name ) {
68 $this->assertTrue( ResourceLoader
::isValidModuleName( $name ) );
72 * @covers ResourceLoader::register
73 * @covers ResourceLoader::getModule
75 public function testRegisterValidArray() {
76 $resourceLoader = new EmptyResourceLoader();
77 // Covers case of register() setting $rl->moduleInfos,
78 // but $rl->modules lazy-populated by getModule()
79 $resourceLoader->register( 'test', [ 'class' => ResourceLoaderTestModule
::class ] );
80 $this->assertInstanceOf(
81 ResourceLoaderTestModule
::class,
82 $resourceLoader->getModule( 'test' )
87 * @covers ResourceLoader::register
90 public function testRegisterEmptyString() {
91 $resourceLoader = new EmptyResourceLoader();
92 $resourceLoader->register( '', [ 'class' => ResourceLoaderTestModule
::class ] );
93 $this->assertInstanceOf(
94 ResourceLoaderTestModule
::class,
95 $resourceLoader->getModule( '' )
100 * @covers ResourceLoader::register
103 public function testRegisterInvalidName() {
104 $resourceLoader = new EmptyResourceLoader();
105 $this->setExpectedException( MWException
::class, "name 'test!invalid' is invalid" );
106 $resourceLoader->register( 'test!invalid', [] );
110 * @covers ResourceLoader::register
112 public function testRegisterInvalidType() {
113 $resourceLoader = new EmptyResourceLoader();
114 $this->setExpectedException( InvalidArgumentException
::class, 'Invalid module info' );
115 $resourceLoader->register( 'test', new stdClass() );
119 * @covers ResourceLoader::register
121 public function testRegisterDuplicate() {
122 $logger = $this->getMockBuilder( Psr\Log\LoggerInterface
::class )->getMock();
123 $logger->expects( $this->once() )
124 ->method( 'warning' );
125 $resourceLoader = new EmptyResourceLoader( null, $logger );
127 $resourceLoader->register( 'test', [ 'class' => ResourceLoaderSkinModule
::class ] );
128 $resourceLoader->register( 'test', [ 'class' => ResourceLoaderStartUpModule
::class ] );
129 $this->assertInstanceOf(
130 ResourceLoaderStartUpModule
::class,
131 $resourceLoader->getModule( 'test' ),
137 * @covers ResourceLoader::getModuleNames
139 public function testGetModuleNames() {
140 // Use an empty one so that core and extension modules don't get in.
141 $resourceLoader = new EmptyResourceLoader();
142 $resourceLoader->register( 'test.foo', [] );
143 $resourceLoader->register( 'test.bar', [] );
145 [ 'startup', 'test.foo', 'test.bar' ],
146 $resourceLoader->getModuleNames()
150 public function provideTestIsFileModule() {
151 $fileModuleObj = $this->createMock( ResourceLoaderFileModule
::class );
153 'factory ignored' => [ false,
155 'factory' => function () {
156 return new ResourceLoaderTestModule();
160 'factory ignored (actual FileModule)' => [ false,
162 'factory' => function () use ( $fileModuleObj ) {
163 return $fileModuleObj;
167 'simple empty' => [ true,
170 'simple scripts' => [ true,
171 [ 'scripts' => 'example.js' ]
173 'simple scripts with targets' => [ true, [
174 'scripts' => [ 'a.js', 'b.js' ],
175 'targets' => [ 'desktop', 'mobile' ],
177 'FileModule' => [ true,
178 [ 'class' => ResourceLoaderFileModule
::class, 'scripts' => 'example.js' ]
180 'TestModule' => [ false,
181 [ 'class' => ResourceLoaderTestModule
::class, 'scripts' => 'example.js' ]
183 'SkinModule (FileModule subclass)' => [ true,
184 [ 'class' => ResourceLoaderSkinModule
::class, 'scripts' => 'example.js' ]
186 'WikiModule' => [ false, [
187 'class' => ResourceLoaderWikiModule
::class,
188 'scripts' => [ 'MediaWiki:Example.js' ],
194 * @dataProvider provideTestIsFileModule
195 * @covers ResourceLoader::isFileModule
197 public function testIsFileModule( $expected, $module ) {
198 $rl = TestingAccessWrapper
::newFromObject( new EmptyResourceLoader() );
199 $rl->register( 'test', $module );
200 $this->assertSame( $expected, $rl->isFileModule( 'test' ) );
204 * @covers ResourceLoader::isFileModule
206 public function testIsFileModuleUnknown() {
207 $rl = TestingAccessWrapper
::newFromObject( new EmptyResourceLoader() );
208 $this->assertSame( false, $rl->isFileModule( 'unknown' ) );
212 * @covers ResourceLoader::isModuleRegistered
214 public function testIsModuleRegistered() {
215 $rl = new EmptyResourceLoader();
216 $rl->register( 'test', [] );
217 $this->assertTrue( $rl->isModuleRegistered( 'test' ) );
218 $this->assertFalse( $rl->isModuleRegistered( 'test.unknown' ) );
222 * @covers ResourceLoader::getModule
224 public function testGetModuleUnknown() {
225 $rl = new EmptyResourceLoader();
226 $this->assertSame( null, $rl->getModule( 'test' ) );
230 * @covers ResourceLoader::getModule
232 public function testGetModuleClass() {
233 $rl = new EmptyResourceLoader();
234 $rl->register( 'test', [ 'class' => ResourceLoaderTestModule
::class ] );
235 $this->assertInstanceOf(
236 ResourceLoaderTestModule
::class,
237 $rl->getModule( 'test' )
242 * @covers ResourceLoader::getModule
244 public function testGetModuleFactory() {
245 $factory = function ( array $info ) {
246 $this->assertArrayHasKey( 'kitten', $info );
247 return new ResourceLoaderTestModule( $info );
250 $rl = new EmptyResourceLoader();
251 $rl->register( 'test', [ 'factory' => $factory, 'kitten' => 'little ball of fur' ] );
252 $this->assertInstanceOf(
253 ResourceLoaderTestModule
::class,
254 $rl->getModule( 'test' )
259 * @covers ResourceLoader::getModule
261 public function testGetModuleClassDefault() {
262 $rl = new EmptyResourceLoader();
263 $rl->register( 'test', [] );
264 $this->assertInstanceOf(
265 ResourceLoaderFileModule
::class,
266 $rl->getModule( 'test' ),
267 'Array-style module registrations default to FileModule'
272 * @covers ResourceLoader::getLessCompiler
274 public function testLessImportDirs() {
275 $rl = new EmptyResourceLoader();
276 $lc = $rl->getLessCompiler( [ 'foo' => '2px', 'Foo' => '#eeeeee' ] );
277 $basePath = dirname( dirname( __DIR__
) ) . '/data/less';
278 $lc->SetImportDirs( [
279 "$basePath/common" => '',
281 $css = $lc->parseFile( "$basePath/module/use-import-dir.less" )->getCss();
282 $this->assertStringEqualsFile( "$basePath/module/styles.css", $css );
285 public static function providePackedModules() {
288 'Example from makePackedModulesString doc comment',
289 [ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ],
290 'foo.bar,baz|bar.baz,quux',
293 'Example from expandModuleNames doc comment',
294 [ 'jquery.foo', 'jquery.bar', 'jquery.ui.baz', 'jquery.ui.quux' ],
295 'jquery.foo,bar|jquery.ui.baz,quux',
298 'Regression fixed in r87497 (7fee86c38e) with dotless names',
299 [ 'foo', 'bar', 'baz' ],
303 'Prefixless modules after a prefixed module',
304 [ 'single.module', 'foobar', 'foobaz' ],
305 'single.module|foobar,foobaz',
309 [ 'foo', 'foo.baz', 'baz.quux', 'foo.bar' ],
310 'foo|foo.baz,bar|baz.quux',
311 [ 'foo', 'foo.baz', 'foo.bar', 'baz.quux' ],
317 * @dataProvider providePackedModules
318 * @covers ResourceLoader::makePackedModulesString
320 public function testMakePackedModulesString( $desc, $modules, $packed ) {
321 $this->assertEquals( $packed, ResourceLoader
::makePackedModulesString( $modules ), $desc );
325 * @dataProvider providePackedModules
326 * @covers ResourceLoader::expandModuleNames
328 public function testExpandModuleNames( $desc, $modules, $packed, $unpacked = null ) {
330 $unpacked ?
: $modules,
331 ResourceLoader
::expandModuleNames( $packed ),
336 public static function provideAddSource() {
338 [ 'foowiki', 'https://example.org/w/load.php', 'foowiki' ],
339 [ 'foowiki', [ 'loadScript' => 'https://example.org/w/load.php' ], 'foowiki' ],
342 'foowiki' => 'https://example.org/w/load.php',
343 'bazwiki' => 'https://example.com/w/load.php',
346 [ 'foowiki', 'bazwiki' ]
352 * @dataProvider provideAddSource
353 * @covers ResourceLoader::addSource
354 * @covers ResourceLoader::getSources
356 public function testAddSource( $name, $info, $expected ) {
357 $rl = new EmptyResourceLoader
;
358 $rl->addSource( $name, $info );
359 if ( is_array( $expected ) ) {
360 foreach ( $expected as $source ) {
361 $this->assertArrayHasKey( $source, $rl->getSources() );
364 $this->assertArrayHasKey( $expected, $rl->getSources() );
369 * @covers ResourceLoader::addSource
371 public function testAddSourceDupe() {
372 $rl = new EmptyResourceLoader
;
373 $this->setExpectedException(
374 MWException
::class, 'ResourceLoader duplicate source addition error'
376 $rl->addSource( 'foo', 'https://example.org/w/load.php' );
377 $rl->addSource( 'foo', 'https://example.com/w/load.php' );
381 * @covers ResourceLoader::addSource
383 public function testAddSourceInvalid() {
384 $rl = new EmptyResourceLoader
;
385 $this->setExpectedException( MWException
::class, 'with no "loadScript" key' );
386 $rl->addSource( 'foo', [ 'x' => 'https://example.org/w/load.php' ] );
389 public static function provideLoaderImplement() {
392 'title' => 'Implement scripts, styles and messages',
394 'name' => 'test.example',
395 'scripts' => 'mw.example();',
396 'styles' => [ 'css' => [ '.mw-example {}' ] ],
397 'messages' => [ 'example' => '' ],
400 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
411 'title' => 'Implement scripts',
413 'name' => 'test.example',
414 'scripts' => 'mw.example();',
417 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
422 'title' => 'Implement styles',
424 'name' => 'test.example',
426 'styles' => [ 'css' => [ '.mw-example {}' ] ],
428 'expected' => 'mw.loader.implement( "test.example", [], {
435 'title' => 'Implement scripts and messages',
437 'name' => 'test.example',
438 'scripts' => 'mw.example();',
439 'messages' => [ 'example' => '' ],
441 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
448 'title' => 'Implement scripts and templates',
450 'name' => 'test.example',
451 'scripts' => 'mw.example();',
452 'templates' => [ 'example.html' => '' ],
454 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
461 'title' => 'Implement unwrapped user script',
464 'scripts' => 'mw.example( 1 );',
467 'expected' => 'mw.loader.implement( "user", "mw.example( 1 );" );',
470 'title' => 'Implement multi-file script',
472 'name' => 'test.multifile',
477 'content' => 'mw.example( 1 );',
481 'content' => [ 'n' => 2 ],
485 'content' => 'mw.example( 3 );'
488 'main' => 'three.js',
492 mw.loader.implement( "test.multifile", {
495 "one.js": function ( require, module ) {
501 "three.js": function ( require, module ) {
512 * @dataProvider provideLoaderImplement
513 * @covers ResourceLoader::makeLoaderImplementScript
514 * @covers ResourceLoader::trimArray
516 public function testMakeLoaderImplementScript( $case ) {
519 'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' ), 'packageFiles' => [],
521 $rl = TestingAccessWrapper
::newFromClass( ResourceLoader
::class );
522 $context = new ResourceLoaderContext( new EmptyResourceLoader(), new FauxRequest( [
527 $rl->makeLoaderImplementScript(
530 ( $case['wrap'] && is_string( $case['scripts'] ) )
531 ?
new XmlJsCode( $case['scripts'] )
536 $case['packageFiles']
542 * @covers ResourceLoader::makeLoaderImplementScript
544 public function testMakeLoaderImplementScriptInvalid() {
545 $this->setExpectedException( MWException
::class, 'Invalid scripts error' );
546 $rl = TestingAccessWrapper
::newFromClass( ResourceLoader
::class );
547 $context = new ResourceLoaderContext( new EmptyResourceLoader(), new FauxRequest() );
548 $rl->makeLoaderImplementScript(
555 null // package files
560 * @covers ResourceLoader::makeLoaderRegisterScript
562 public function testMakeLoaderRegisterScript() {
563 $context = new ResourceLoaderContext( new EmptyResourceLoader(), new FauxRequest( [
567 'mw.loader.register([
573 ResourceLoader
::makeLoaderRegisterScript( $context, [
574 [ 'test.name', '1234567' ],
576 'Nested array parameter'
580 'mw.loader.register([
609 ResourceLoader
::makeLoaderRegisterScript( $context, [
610 [ 'test.foo', '100' , [], null, null ],
611 [ 'test.bar', '200', [ 'test.unknown' ], null ],
612 [ 'test.baz', '300', [ 'test.quux', 'test.foo' ], null ],
613 [ 'test.quux', '400', [], null, null, 'return true;' ],
615 'Compact dependency indexes'
620 * @covers ResourceLoader::makeLoaderSourcesScript
622 public function testMakeLoaderSourcesScript() {
623 $context = new ResourceLoaderContext( new EmptyResourceLoader(), new FauxRequest( [
627 'mw.loader.addSource({
628 "local": "/w/load.php"
630 ResourceLoader
::makeLoaderSourcesScript( $context, [ 'local' => '/w/load.php' ] )
633 'mw.loader.addSource({
634 "local": "/w/load.php",
635 "example": "https://example.org/w/load.php"
637 ResourceLoader
::makeLoaderSourcesScript( $context, [
638 'local' => '/w/load.php',
639 'example' => 'https://example.org/w/load.php'
643 'mw.loader.addSource([]);',
644 ResourceLoader
::makeLoaderSourcesScript( $context, [] )
648 private static function fakeSources() {
651 'loadScript' => '//example.org/w/load.php',
652 'apiScript' => '//example.org/w/api.php',
655 'loadScript' => '//example.com/w/load.php',
656 'apiScript' => '//example.com/w/api.php',
662 * @covers ResourceLoader::getLoadScript
664 public function testGetLoadScript() {
665 $rl = new EmptyResourceLoader();
666 $sources = self
::fakeSources();
667 $rl->addSource( $sources );
668 foreach ( [ 'examplewiki', 'example2wiki' ] as $name ) {
669 $this->assertEquals( $rl->getLoadScript( $name ), $sources[$name]['loadScript'] );
673 $rl->getLoadScript( 'thiswasneverreigstered' );
674 $this->assertTrue( false, 'ResourceLoader::getLoadScript should have thrown an exception' );
675 } catch ( MWException
$e ) {
676 $this->assertTrue( true );
680 protected function getFailFerryMock( $getter = 'getScript' ) {
681 $mock = $this->getMockBuilder( ResourceLoaderTestModule
::class )
682 ->setMethods( [ $getter ] )
684 $mock->method( $getter )->will( $this->throwException(
685 new Exception( 'Ferry not found' )
690 protected function getSimpleModuleMock( $script = '' ) {
691 $mock = $this->getMockBuilder( ResourceLoaderTestModule
::class )
692 ->setMethods( [ 'getScript' ] )
694 $mock->method( 'getScript' )->willReturn( $script );
698 protected function getSimpleStyleModuleMock( $styles = '' ) {
699 $mock = $this->getMockBuilder( ResourceLoaderTestModule
::class )
700 ->setMethods( [ 'getStyles' ] )
702 $mock->method( 'getStyles' )->willReturn( [ '' => $styles ] );
707 * @covers ResourceLoader::getCombinedVersion
709 public function testGetCombinedVersion() {
710 $rl = $this->getMockBuilder( EmptyResourceLoader
::class )
711 // Disable log from outputErrorAndLog
712 ->setMethods( [ 'outputErrorAndLog' ] )->getMock();
714 'foo' => [ 'class' => ResourceLoaderTestModule
::class ],
716 'factory' => function () {
717 return self
::getFailFerryMock();
720 'bar' => [ 'class' => ResourceLoaderTestModule
::class ],
722 $context = $this->getResourceLoaderContext( [], $rl );
726 $rl->getCombinedVersion( $context, [] ),
732 $rl->getCombinedVersion( $context, [ 'foo' ] ),
736 // Verify that getCombinedVersion() does not throw when ferry fails.
737 // Instead it gracefully continues to combine the remaining modules.
739 ResourceLoader
::makeHash( self
::BLANK_VERSION
. self
::BLANK_VERSION
),
740 $rl->getCombinedVersion( $context, [ 'foo', 'ferry', 'bar' ] ),
741 'compute foo+ferry+bar (T152266)'
745 public static function provideMakeModuleResponseConcat() {
751 'expected' => "foo()\n" . 'mw.loader.state({
754 'minified' => "foo()\n" . 'mw.loader.state({"foo":"ready"});',
755 'message' => 'Script without semi-colon',
762 'expected' => "foo()\nbar()\n" . 'mw.loader.state({
766 'minified' => "foo()\nbar()\n" . 'mw.loader.state({"foo":"ready","bar":"ready"});',
767 'message' => 'Two scripts without semi-colon',
771 'foo' => "foo()\n// bar();"
773 'expected' => "foo()\n// bar();\n" . 'mw.loader.state({
776 'minified' => "foo()\n" . 'mw.loader.state({"foo":"ready"});',
777 'message' => 'Script with semi-colon in comment (T162719)',
781 foreach ( $testcases as $i => $case ) {
788 $ret["#$i (minified)"] = [
799 * Verify how multiple scripts and mw.loader.state() calls are concatenated.
801 * @dataProvider provideMakeModuleResponseConcat
802 * @covers ResourceLoader::makeModuleResponse
804 public function testMakeModuleResponseConcat( $scripts, $expected, $debug, $message = null ) {
805 $rl = new EmptyResourceLoader();
806 $modules = array_map( function ( $script ) {
807 return self
::getSimpleModuleMock( $script );
810 $context = $this->getResourceLoaderContext(
812 'modules' => implode( '|', array_keys( $modules ) ),
814 'debug' => $debug ?
'true' : 'false',
819 $response = $rl->makeModuleResponse( $context, $modules );
820 $this->assertSame( [], $rl->getErrors(), 'Errors' );
821 $this->assertEquals( $expected, $response, $message ?
: 'Response' );
825 * @covers ResourceLoader::makeModuleResponse
827 public function testMakeModuleResponseEmpty() {
828 $rl = new EmptyResourceLoader();
829 $context = $this->getResourceLoaderContext(
830 [ 'modules' => '', 'only' => 'scripts' ],
834 $response = $rl->makeModuleResponse( $context, [] );
835 $this->assertSame( [], $rl->getErrors(), 'Errors' );
836 $this->assertRegExp( '/^\/\*.+no modules were requested.+\*\/$/ms', $response );
840 * Verify that when building module content in a load.php response,
841 * an exception from one module will not break script output from
844 * @covers ResourceLoader::makeModuleResponse
846 public function testMakeModuleResponseError() {
848 'foo' => self
::getSimpleModuleMock( 'foo();' ),
849 'ferry' => self
::getFailFerryMock(),
850 'bar' => self
::getSimpleModuleMock( 'bar();' ),
852 $rl = new EmptyResourceLoader();
853 $context = $this->getResourceLoaderContext(
855 'modules' => 'foo|ferry|bar',
861 // Disable log from makeModuleResponse via outputErrorAndLog
862 $this->setLogger( 'exception', new Psr\Log\
NullLogger() );
864 $response = $rl->makeModuleResponse( $context, $modules );
865 $errors = $rl->getErrors();
867 $this->assertCount( 1, $errors );
868 $this->assertRegExp( '/Ferry not found/', $errors[0] );
870 "foo();\nbar();\n" . 'mw.loader.state({
880 * Verify that exceptions in PHP for one module will not break others
881 * (stylesheet response).
883 * @covers ResourceLoader::makeModuleResponse
885 public function testMakeModuleResponseErrorCSS() {
887 'foo' => self
::getSimpleStyleModuleMock( '.foo{}' ),
888 'ferry' => self
::getFailFerryMock( 'getStyles' ),
889 'bar' => self
::getSimpleStyleModuleMock( '.bar{}' ),
891 $rl = new EmptyResourceLoader();
892 $context = $this->getResourceLoaderContext(
894 'modules' => 'foo|ferry|bar',
901 // Disable log from makeModuleResponse via outputErrorAndLog
902 $this->setLogger( 'exception', new Psr\Log\
NullLogger() );
904 $response = $rl->makeModuleResponse( $context, $modules );
905 $errors = $rl->getErrors();
907 $this->assertCount( 2, $errors );
908 $this->assertRegExp( '/Ferry not found/', $errors[0] );
909 $this->assertRegExp( '/Problem.+"ferry":\s*"error"/ms', $errors[1] );
917 * Verify that when building the startup module response,
918 * an exception from one module class will not break the entire
919 * startup module response. See T152266.
921 * @covers ResourceLoader::makeModuleResponse
923 public function testMakeModuleResponseStartupError() {
924 // This is an integration test that uses a lot of MediaWiki state,
925 // provide the full Config object here.
926 $rl = new EmptyResourceLoader( MediaWikiServices
::getInstance()->getMainConfig() );
928 'foo' => [ 'factory' => function () {
929 return self
::getSimpleModuleMock( 'foo();' );
931 'ferry' => [ 'factory' => function () {
932 return self
::getFailFerryMock();
934 'bar' => [ 'factory' => function () {
935 return self
::getSimpleModuleMock( 'bar();' );
938 $context = $this->getResourceLoaderContext(
940 'modules' => 'startup',
947 [ 'startup', 'foo', 'ferry', 'bar' ],
948 $rl->getModuleNames(),
952 // Disable log from makeModuleResponse via outputErrorAndLog
953 $this->setLogger( 'exception', new Psr\Log\
NullLogger() );
955 $modules = [ 'startup' => $rl->getModule( 'startup' ) ];
956 $response = $rl->makeModuleResponse( $context, $modules );
957 $errors = $rl->getErrors();
959 $this->assertRegExp( '/Ferry not found/', $errors[0] );
960 $this->assertCount( 1, $errors );
962 '/isCompatible.*window\.RLQ/s',
964 'startup response undisrupted (T152266)'
967 '/register\([^)]+"ferry",\s*""/s',
969 'startup response registers broken module'
972 '/state\([^)]+"ferry":\s*"error"/s',
974 'startup response sets state to error'
979 * Integration test for modules sending extra HTTP response headers.
981 * @covers ResourceLoaderModule::getHeaders
982 * @covers ResourceLoaderModule::buildContent
983 * @covers ResourceLoader::makeModuleResponse
985 public function testMakeModuleResponseExtraHeaders() {
986 $module = $this->getMockBuilder( ResourceLoaderTestModule
::class )
987 ->setMethods( [ 'getPreloadLinks' ] )->getMock();
988 $module->method( 'getPreloadLinks' )->willReturn( [
989 'https://example.org/script.js' => [ 'as' => 'script' ],
992 $rl = new EmptyResourceLoader();
993 $context = $this->getResourceLoaderContext(
994 [ 'modules' => 'foo', 'only' => 'scripts' ],
998 $modules = [ 'foo' => $module ];
999 $response = $rl->makeModuleResponse( $context, $modules );
1000 $extraHeaders = TestingAccessWrapper
::newFromObject( $rl )->extraHeaders
;
1002 $this->assertEquals(
1004 'Link: <https://example.org/script.js>;rel=preload;as=script'
1012 * @covers ResourceLoaderModule::getHeaders
1013 * @covers ResourceLoaderModule::buildContent
1014 * @covers ResourceLoader::makeModuleResponse
1016 public function testMakeModuleResponseExtraHeadersMulti() {
1017 $foo = $this->getMockBuilder( ResourceLoaderTestModule
::class )
1018 ->setMethods( [ 'getPreloadLinks' ] )->getMock();
1019 $foo->method( 'getPreloadLinks' )->willReturn( [
1020 'https://example.org/script.js' => [ 'as' => 'script' ],
1023 $bar = $this->getMockBuilder( ResourceLoaderTestModule
::class )
1024 ->setMethods( [ 'getPreloadLinks' ] )->getMock();
1025 $bar->method( 'getPreloadLinks' )->willReturn( [
1026 '/example.png' => [ 'as' => 'image' ],
1027 '/example.jpg' => [ 'as' => 'image' ],
1030 $rl = new EmptyResourceLoader();
1031 $context = $this->getResourceLoaderContext(
1032 [ 'modules' => 'foo|bar', 'only' => 'scripts' ],
1036 $modules = [ 'foo' => $foo, 'bar' => $bar ];
1037 $response = $rl->makeModuleResponse( $context, $modules );
1038 $extraHeaders = TestingAccessWrapper
::newFromObject( $rl )->extraHeaders
;
1039 $this->assertEquals(
1041 'Link: <https://example.org/script.js>;rel=preload;as=script',
1042 'Link: </example.png>;rel=preload;as=image,</example.jpg>;rel=preload;as=image'
1050 * @covers ResourceLoader::respond
1052 public function testRespondEmpty() {
1053 $rl = $this->getMockBuilder( EmptyResourceLoader
::class )
1055 'tryRespondNotModified',
1056 'sendResponseHeaders',
1057 'measureResponseTime',
1060 $context = $this->getResourceLoaderContext( [ 'modules' => '' ], $rl );
1062 $rl->expects( $this->once() )->method( 'measureResponseTime' );
1063 $this->expectOutputRegex( '/no modules were requested/' );
1065 $rl->respond( $context );
1069 * @covers ResourceLoader::respond
1071 public function testRespondSimple() {
1072 $module = new ResourceLoaderTestModule( [ 'script' => 'foo();' ] );
1073 $rl = $this->getMockBuilder( EmptyResourceLoader
::class )
1075 'measureResponseTime',
1076 'tryRespondNotModified',
1077 'sendResponseHeaders',
1078 'makeModuleResponse',
1081 $rl->register( 'test', [
1082 'factory' => function () use ( $module ) {
1086 $context = $this->getResourceLoaderContext(
1087 [ 'modules' => 'test', 'only' => null ],
1091 $rl->expects( $this->once() )->method( 'makeModuleResponse' )
1092 ->with( $context, [ 'test' => $module ] )
1093 ->willReturn( 'implement_foo;' );
1094 $this->expectOutputRegex( '/^implement_foo;/' );
1096 $rl->respond( $context );
1100 * Refuse requests for private modules.
1102 * @covers ResourceLoader::respond
1104 public function testRespondErrorPrivate() {
1105 $rl = $this->getMockBuilder( EmptyResourceLoader
::class )
1107 'measureResponseTime',
1108 'tryRespondNotModified',
1109 'sendResponseHeaders',
1113 'foo' => [ 'class' => ResourceLoaderTestModule
::class ],
1114 'bar' => [ 'class' => ResourceLoaderTestModule
::class, 'group' => 'private' ],
1116 $context = $this->getResourceLoaderContext(
1117 [ 'modules' => 'foo|bar', 'only' => null ],
1121 $this->expectOutputRegex( '/^\/\*.+Cannot build private module/s' );
1122 $rl->respond( $context );
1126 * @covers ResourceLoader::respond
1128 public function testRespondInternalFailures() {
1129 $module = new ResourceLoaderTestModule( [ 'script' => 'foo();' ] );
1130 $rl = $this->getMockBuilder( EmptyResourceLoader
::class )
1132 'measureResponseTime',
1133 'preloadModuleInfo',
1134 'getCombinedVersion',
1135 'tryRespondNotModified',
1136 'makeModuleResponse',
1137 'sendResponseHeaders',
1140 $rl->register( 'test', [
1141 'factory' => function () use ( $module ) {
1145 $context = $this->getResourceLoaderContext( [ 'modules' => 'test' ], $rl );
1146 // Disable logging from outputErrorAndLog
1147 $this->setLogger( 'exception', new Psr\Log\
NullLogger() );
1149 $rl->expects( $this->once() )->method( 'preloadModuleInfo' )
1150 ->willThrowException( new Exception( 'Preload error' ) );
1151 $rl->expects( $this->once() )->method( 'getCombinedVersion' )
1152 ->willThrowException( new Exception( 'Version error' ) );
1153 $rl->expects( $this->once() )->method( 'makeModuleResponse' )
1154 ->with( $context, [ 'test' => $module ] )
1155 ->willReturn( 'foo;' );
1156 // Internal errors should be caught and logged without affecting module output
1157 $this->expectOutputRegex( '/^\/\*.+Preload error.+Version error.+\*\/.*foo;/ms' );
1159 $rl->respond( $context );
1163 * @covers ResourceLoader::measureResponseTime
1165 public function testMeasureResponseTime() {
1166 $stats = $this->getMockBuilder( NullStatsdDataFactory
::class )
1167 ->setMethods( [ 'timing' ] )->getMock();
1168 $this->setService( 'StatsdDataFactory', $stats );
1170 $stats->expects( $this->once() )->method( 'timing' )
1171 ->with( 'resourceloader.responseTime', $this->anything() );
1173 $timing = new Timing();
1174 $timing->mark( 'requestShutdown' );
1175 $rl = TestingAccessWrapper
::newFromObject( new EmptyResourceLoader
);
1176 $rl->measureResponseTime( $timing );
1177 DeferredUpdates
::doUpdates();