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() {
21 $this->overrideMwServices();
24 $this->setMwGlobals( 'wgHooks', [
25 'ResourceLoaderRegisterModules' => [
26 function ( &$resourceLoader ) use ( &$ranHook ) {
32 MediaWikiServices
::getInstance()->getResourceLoader();
34 $this->assertSame( 1, $ranHook, 'Hook was called' );
37 public static function provideInvalidModuleName() {
39 'name with 300 chars' => [ str_repeat( 'x', 300 ) ],
40 'name with bang' => [ 'this!that' ],
41 'name with comma' => [ 'this,that' ],
42 'name with pipe' => [ 'this|that' ],
46 public static function provideValidModuleName() {
48 'empty string' => [ '' ],
49 'simple name' => [ 'this.and-that2' ],
50 'name with 100 chars' => [ str_repeat( 'x', 100 ) ],
51 'name with hash' => [ 'this#that' ],
52 'name with slash' => [ 'this/that' ],
53 'name with at' => [ 'this@that' ],
58 * @dataProvider provideInvalidModuleName
59 * @covers ResourceLoader
61 public function testIsValidModuleName_invalid( $name ) {
62 $this->assertFalse( ResourceLoader
::isValidModuleName( $name ) );
66 * @dataProvider provideValidModuleName
67 * @covers ResourceLoader
69 public function testIsValidModuleName_valid( $name ) {
70 $this->assertTrue( ResourceLoader
::isValidModuleName( $name ) );
74 * @covers ResourceLoader::register
75 * @covers ResourceLoader::getModule
77 public function testRegisterValidObject() {
78 $module = new ResourceLoaderTestModule();
79 $resourceLoader = new EmptyResourceLoader();
80 $resourceLoader->register( 'test', $module );
81 $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
85 * @covers ResourceLoader::register
86 * @covers ResourceLoader::getModule
88 public function testRegisterValidArray() {
89 $module = new ResourceLoaderTestModule();
90 $resourceLoader = new EmptyResourceLoader();
91 // Covers case of register() setting $rl->moduleInfos,
92 // but $rl->modules lazy-populated by getModule()
93 $resourceLoader->register( 'test', [ 'object' => $module ] );
94 $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
98 * @covers ResourceLoader::register
101 public function testRegisterEmptyString() {
102 $module = new ResourceLoaderTestModule();
103 $resourceLoader = new EmptyResourceLoader();
104 $resourceLoader->register( '', $module );
105 $this->assertEquals( $module, $resourceLoader->getModule( '' ) );
109 * @covers ResourceLoader::register
112 public function testRegisterInvalidName() {
113 $resourceLoader = new EmptyResourceLoader();
114 $this->setExpectedException( MWException
::class, "name 'test!invalid' is invalid" );
115 $resourceLoader->register( 'test!invalid', new ResourceLoaderTestModule() );
119 * @covers ResourceLoader::register
121 public function testRegisterInvalidType() {
122 $resourceLoader = new EmptyResourceLoader();
123 $this->setExpectedException( MWException
::class, 'ResourceLoader module info type error' );
124 $resourceLoader->register( 'test', new stdClass() );
128 * @covers ResourceLoader::register
130 public function testRegisterDuplicate() {
131 $logger = $this->getMockBuilder( Psr\Log\LoggerInterface
::class )->getMock();
132 $logger->expects( $this->once() )
133 ->method( 'warning' );
134 $resourceLoader = new EmptyResourceLoader( null, $logger );
136 $module1 = new ResourceLoaderTestModule();
137 $module2 = new ResourceLoaderTestModule();
138 $resourceLoader->register( 'test', $module1 );
139 $resourceLoader->register( 'test', $module2 );
140 $this->assertSame( $module2, $resourceLoader->getModule( 'test' ) );
144 * @covers ResourceLoader::getModuleNames
146 public function testGetModuleNames() {
147 // Use an empty one so that core and extension modules don't get in.
148 $resourceLoader = new EmptyResourceLoader();
149 $resourceLoader->register( 'test.foo', new ResourceLoaderTestModule() );
150 $resourceLoader->register( 'test.bar', new ResourceLoaderTestModule() );
152 [ 'startup', 'test.foo', 'test.bar' ],
153 $resourceLoader->getModuleNames()
157 public function provideTestIsFileModule() {
158 $fileModuleObj = $this->getMockBuilder( ResourceLoaderFileModule
::class )
159 ->disableOriginalConstructor()
163 new ResourceLoaderTestModule()
165 'FileModule object' => [ false,
168 'simple empty' => [ true,
171 'simple scripts' => [ true,
172 [ 'scripts' => 'example.js' ]
174 'simple scripts, raw and targets' => [ true, [
175 'scripts' => [ 'a.js', 'b.js' ],
177 'targets' => [ 'desktop', 'mobile' ],
179 'FileModule' => [ true,
180 [ 'class' => ResourceLoaderFileModule
::class, 'scripts' => 'example.js' ]
182 'TestModule' => [ false,
183 [ 'class' => ResourceLoaderTestModule
::class, 'scripts' => 'example.js' ]
185 'SkinModule (FileModule subclass)' => [ true,
186 [ 'class' => ResourceLoaderSkinModule
::class, 'scripts' => 'example.js' ]
188 'WikiModule' => [ false, [
189 'class' => ResourceLoaderWikiModule
::class,
190 'scripts' => [ 'MediaWiki:Example.js' ],
196 * @dataProvider provideTestIsFileModule
197 * @covers ResourceLoader::isFileModule
199 public function testIsFileModule( $expected, $module ) {
200 $rl = TestingAccessWrapper
::newFromObject( new EmptyResourceLoader() );
201 $rl->register( 'test', $module );
202 $this->assertSame( $expected, $rl->isFileModule( 'test' ) );
206 * @covers ResourceLoader::isFileModule
208 public function testIsFileModuleUnknown() {
209 $rl = TestingAccessWrapper
::newFromObject( new EmptyResourceLoader() );
210 $this->assertSame( false, $rl->isFileModule( 'unknown' ) );
214 * @covers ResourceLoader::isModuleRegistered
216 public function testIsModuleRegistered() {
217 $rl = new EmptyResourceLoader();
218 $rl->register( 'test', new ResourceLoaderTestModule() );
219 $this->assertTrue( $rl->isModuleRegistered( 'test' ) );
220 $this->assertFalse( $rl->isModuleRegistered( 'test.unknown' ) );
224 * @covers ResourceLoader::getModule
226 public function testGetModuleUnknown() {
227 $rl = new EmptyResourceLoader();
228 $this->assertSame( null, $rl->getModule( 'test' ) );
232 * @covers ResourceLoader::getModule
234 public function testGetModuleClass() {
235 $rl = new EmptyResourceLoader();
236 $rl->register( 'test', [ 'class' => ResourceLoaderTestModule
::class ] );
237 $this->assertInstanceOf(
238 ResourceLoaderTestModule
::class,
239 $rl->getModule( 'test' )
244 * @covers ResourceLoader::getModule
246 public function testGetModuleFactory() {
247 $factory = function ( array $info ) {
248 $this->assertArrayHasKey( 'kitten', $info );
249 return new ResourceLoaderTestModule( $info );
252 $rl = new EmptyResourceLoader();
253 $rl->register( 'test', [ 'factory' => $factory, 'kitten' => 'little ball of fur' ] );
254 $this->assertInstanceOf(
255 ResourceLoaderTestModule
::class,
256 $rl->getModule( 'test' )
261 * @covers ResourceLoader::getModule
263 public function testGetModuleClassDefault() {
264 $rl = new EmptyResourceLoader();
265 $rl->register( 'test', [] );
266 $this->assertInstanceOf(
267 ResourceLoaderFileModule
::class,
268 $rl->getModule( 'test' ),
269 'Array-style module registrations default to FileModule'
274 * @covers ResourceLoader::getLessCompiler
276 public function testLessImportDirs() {
277 $rl = new EmptyResourceLoader();
278 $lc = $rl->getLessCompiler( [ 'foo' => '2px', 'Foo' => '#eeeeee' ] );
279 $basePath = dirname( dirname( __DIR__
) ) . '/data/less';
280 $lc->SetImportDirs( [
281 "$basePath/common" => '',
283 $css = $lc->parseFile( "$basePath/module/use-import-dir.less" )->getCss();
284 $this->assertStringEqualsFile( "$basePath/module/styles.css", $css );
287 public static function providePackedModules() {
290 'Example from makePackedModulesString doc comment',
291 [ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ],
292 'foo.bar,baz|bar.baz,quux',
295 'Example from expandModuleNames doc comment',
296 [ 'jquery.foo', 'jquery.bar', 'jquery.ui.baz', 'jquery.ui.quux' ],
297 'jquery.foo,bar|jquery.ui.baz,quux',
300 'Regression fixed in r87497 (7fee86c38e) with dotless names',
301 [ 'foo', 'bar', 'baz' ],
305 'Prefixless modules after a prefixed module',
306 [ 'single.module', 'foobar', 'foobaz' ],
307 'single.module|foobar,foobaz',
311 [ 'foo', 'foo.baz', 'baz.quux', 'foo.bar' ],
312 'foo|foo.baz,bar|baz.quux',
313 [ 'foo', 'foo.baz', 'foo.bar', 'baz.quux' ],
319 * @dataProvider providePackedModules
320 * @covers ResourceLoader::makePackedModulesString
322 public function testMakePackedModulesString( $desc, $modules, $packed ) {
323 $this->assertEquals( $packed, ResourceLoader
::makePackedModulesString( $modules ), $desc );
327 * @dataProvider providePackedModules
328 * @covers ResourceLoader::expandModuleNames
330 public function testExpandModuleNames( $desc, $modules, $packed, $unpacked = null ) {
332 $unpacked ?
: $modules,
333 ResourceLoader
::expandModuleNames( $packed ),
338 public static function provideAddSource() {
340 [ 'foowiki', 'https://example.org/w/load.php', 'foowiki' ],
341 [ 'foowiki', [ 'loadScript' => 'https://example.org/w/load.php' ], 'foowiki' ],
344 'foowiki' => 'https://example.org/w/load.php',
345 'bazwiki' => 'https://example.com/w/load.php',
348 [ 'foowiki', 'bazwiki' ]
354 * @dataProvider provideAddSource
355 * @covers ResourceLoader::addSource
356 * @covers ResourceLoader::getSources
358 public function testAddSource( $name, $info, $expected ) {
359 $rl = new EmptyResourceLoader
;
360 $rl->addSource( $name, $info );
361 if ( is_array( $expected ) ) {
362 foreach ( $expected as $source ) {
363 $this->assertArrayHasKey( $source, $rl->getSources() );
366 $this->assertArrayHasKey( $expected, $rl->getSources() );
371 * @covers ResourceLoader::addSource
373 public function testAddSourceDupe() {
374 $rl = new EmptyResourceLoader
;
375 $this->setExpectedException(
376 MWException
::class, 'ResourceLoader duplicate source addition error'
378 $rl->addSource( 'foo', 'https://example.org/w/load.php' );
379 $rl->addSource( 'foo', 'https://example.com/w/load.php' );
383 * @covers ResourceLoader::addSource
385 public function testAddSourceInvalid() {
386 $rl = new EmptyResourceLoader
;
387 $this->setExpectedException( MWException
::class, 'with no "loadScript" key' );
388 $rl->addSource( 'foo', [ 'x' => 'https://example.org/w/load.php' ] );
391 public static function provideLoaderImplement() {
394 'title' => 'Implement scripts, styles and messages',
396 'name' => 'test.example',
397 'scripts' => 'mw.example();',
398 'styles' => [ 'css' => [ '.mw-example {}' ] ],
399 'messages' => [ 'example' => '' ],
402 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
413 'title' => 'Implement scripts',
415 'name' => 'test.example',
416 'scripts' => 'mw.example();',
419 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
424 'title' => 'Implement styles',
426 'name' => 'test.example',
428 'styles' => [ 'css' => [ '.mw-example {}' ] ],
430 'expected' => 'mw.loader.implement( "test.example", [], {
437 'title' => 'Implement scripts and messages',
439 'name' => 'test.example',
440 'scripts' => 'mw.example();',
441 'messages' => [ 'example' => '' ],
443 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
450 'title' => 'Implement scripts and templates',
452 'name' => 'test.example',
453 'scripts' => 'mw.example();',
454 'templates' => [ 'example.html' => '' ],
456 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
463 'title' => 'Implement unwrapped user script',
466 'scripts' => 'mw.example( 1 );',
469 'expected' => 'mw.loader.implement( "user", "mw.example( 1 );" );',
472 'title' => 'Implement multi-file script',
474 'name' => 'test.multifile',
479 'content' => 'mw.example( 1 );',
483 'content' => [ 'n' => 2 ],
487 'content' => 'mw.example( 3 );'
490 'main' => 'three.js',
494 mw.loader.implement( "test.multifile", {
497 "one.js": function ( require, module ) {
503 "three.js": function ( require, module ) {
514 * @dataProvider provideLoaderImplement
515 * @covers ResourceLoader::makeLoaderImplementScript
516 * @covers ResourceLoader::trimArray
518 public function testMakeLoaderImplementScript( $case ) {
521 'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' ), 'packageFiles' => [],
523 ResourceLoader
::clearCache();
524 $this->setMwGlobals( 'wgResourceLoaderDebug', true );
526 $rl = TestingAccessWrapper
::newFromClass( ResourceLoader
::class );
529 $rl->makeLoaderImplementScript(
531 ( $case['wrap'] && is_string( $case['scripts'] ) )
532 ?
new XmlJsCode( $case['scripts'] )
537 $case['packageFiles']
543 * @covers ResourceLoader::makeLoaderImplementScript
545 public function testMakeLoaderImplementScriptInvalid() {
546 $this->setExpectedException( MWException
::class, 'Invalid scripts error' );
547 $rl = TestingAccessWrapper
::newFromClass( ResourceLoader
::class );
548 $rl->makeLoaderImplementScript(
554 null // package files
559 * @covers ResourceLoader::makeLoaderRegisterScript
561 public function testMakeLoaderRegisterScript() {
563 'mw.loader.register( [
569 ResourceLoader
::makeLoaderRegisterScript( [
570 [ 'test.name', '1234567' ],
572 'Nested array parameter'
576 'mw.loader.register( [
605 ResourceLoader
::makeLoaderRegisterScript( [
606 [ 'test.foo', '100' , [], null, null ],
607 [ 'test.bar', '200', [ 'test.unknown' ], null ],
608 [ 'test.baz', '300', [ 'test.quux', 'test.foo' ], null ],
609 [ 'test.quux', '400', [], null, null, 'return true;' ],
611 'Compact dependency indexes'
616 * @covers ResourceLoader::makeLoaderSourcesScript
618 public function testMakeLoaderSourcesScript() {
620 'mw.loader.addSource( {
621 "local": "/w/load.php"
623 ResourceLoader
::makeLoaderSourcesScript( 'local', '/w/load.php' )
626 'mw.loader.addSource( {
627 "local": "/w/load.php"
629 ResourceLoader
::makeLoaderSourcesScript( [ 'local' => '/w/load.php' ] )
632 'mw.loader.addSource( {
633 "local": "/w/load.php",
634 "example": "https://example.org/w/load.php"
636 ResourceLoader
::makeLoaderSourcesScript( [
637 'local' => '/w/load.php',
638 'example' => 'https://example.org/w/load.php'
642 'mw.loader.addSource( [] );',
643 ResourceLoader
::makeLoaderSourcesScript( [] )
647 private static function fakeSources() {
650 'loadScript' => '//example.org/w/load.php',
651 'apiScript' => '//example.org/w/api.php',
654 'loadScript' => '//example.com/w/load.php',
655 'apiScript' => '//example.com/w/api.php',
661 * @covers ResourceLoader::getLoadScript
663 public function testGetLoadScript() {
664 $rl = new EmptyResourceLoader();
665 $sources = self
::fakeSources();
666 $rl->addSource( $sources );
667 foreach ( [ 'examplewiki', 'example2wiki' ] as $name ) {
668 $this->assertEquals( $rl->getLoadScript( $name ), $sources[$name]['loadScript'] );
672 $rl->getLoadScript( 'thiswasneverreigstered' );
673 $this->assertTrue( false, 'ResourceLoader::getLoadScript should have thrown an exception' );
674 } catch ( MWException
$e ) {
675 $this->assertTrue( true );
679 protected function getFailFerryMock( $getter = 'getScript' ) {
680 $mock = $this->getMockBuilder( ResourceLoaderTestModule
::class )
681 ->setMethods( [ $getter ] )
683 $mock->method( $getter )->will( $this->throwException(
684 new Exception( 'Ferry not found' )
689 protected function getSimpleModuleMock( $script = '' ) {
690 $mock = $this->getMockBuilder( ResourceLoaderTestModule
::class )
691 ->setMethods( [ 'getScript' ] )
693 $mock->method( 'getScript' )->willReturn( $script );
697 protected function getSimpleStyleModuleMock( $styles = '' ) {
698 $mock = $this->getMockBuilder( ResourceLoaderTestModule
::class )
699 ->setMethods( [ 'getStyles' ] )
701 $mock->method( 'getStyles' )->willReturn( [ '' => $styles ] );
706 * @covers ResourceLoader::getCombinedVersion
708 public function testGetCombinedVersion() {
709 $rl = $this->getMockBuilder( EmptyResourceLoader
::class )
710 // Disable log from outputErrorAndLog
711 ->setMethods( [ 'outputErrorAndLog' ] )->getMock();
713 'foo' => self
::getSimpleModuleMock(),
714 'ferry' => self
::getFailFerryMock(),
715 'bar' => self
::getSimpleModuleMock(),
717 $context = $this->getResourceLoaderContext( [], $rl );
721 $rl->getCombinedVersion( $context, [] ),
726 ResourceLoader
::makeHash( self
::BLANK_VERSION
),
727 $rl->getCombinedVersion( $context, [ 'foo' ] ),
731 // Verify that getCombinedVersion() does not throw when ferry fails.
732 // Instead it gracefully continues to combine the remaining modules.
734 ResourceLoader
::makeHash( self
::BLANK_VERSION
. self
::BLANK_VERSION
),
735 $rl->getCombinedVersion( $context, [ 'foo', 'ferry', 'bar' ] ),
736 'compute foo+ferry+bar (T152266)'
740 public static function provideMakeModuleResponseConcat() {
746 'expected' => "foo()\n" . 'mw.loader.state( {
749 'minified' => "foo()\n" . 'mw.loader.state({"foo":"ready"});',
750 'message' => 'Script without semi-colon',
757 'expected' => "foo()\nbar()\n" . 'mw.loader.state( {
761 'minified' => "foo()\nbar()\n" . 'mw.loader.state({"foo":"ready","bar":"ready"});',
762 'message' => 'Two scripts without semi-colon',
766 'foo' => "foo()\n// bar();"
768 'expected' => "foo()\n// bar();\n" . 'mw.loader.state( {
771 'minified' => "foo()\n" . 'mw.loader.state({"foo":"ready"});',
772 'message' => 'Script with semi-colon in comment (T162719)',
776 foreach ( $testcases as $i => $case ) {
783 $ret["#$i (minified)"] = [
794 * Verify how multiple scripts and mw.loader.state() calls are concatenated.
796 * @dataProvider provideMakeModuleResponseConcat
797 * @covers ResourceLoader::makeModuleResponse
799 public function testMakeModuleResponseConcat( $scripts, $expected, $debug, $message = null ) {
800 $rl = new EmptyResourceLoader();
801 $modules = array_map( function ( $script ) {
802 return self
::getSimpleModuleMock( $script );
804 $rl->register( $modules );
806 $context = $this->getResourceLoaderContext(
808 'modules' => implode( '|', array_keys( $modules ) ),
810 'debug' => $debug ?
'true' : 'false',
815 $response = $rl->makeModuleResponse( $context, $modules );
816 $this->assertSame( [], $rl->getErrors(), 'Errors' );
817 $this->assertEquals( $expected, $response, $message ?
: 'Response' );
821 * @covers ResourceLoader::makeModuleResponse
823 public function testMakeModuleResponseEmpty() {
824 $rl = new EmptyResourceLoader();
825 $context = $this->getResourceLoaderContext(
826 [ 'modules' => '', 'only' => 'scripts' ],
830 $response = $rl->makeModuleResponse( $context, [] );
831 $this->assertSame( [], $rl->getErrors(), 'Errors' );
832 $this->assertRegExp( '/^\/\*.+no modules were requested.+\*\/$/ms', $response );
836 * Verify that when building module content in a load.php response,
837 * an exception from one module will not break script output from
840 * @covers ResourceLoader::makeModuleResponse
842 public function testMakeModuleResponseError() {
844 'foo' => self
::getSimpleModuleMock( 'foo();' ),
845 'ferry' => self
::getFailFerryMock(),
846 'bar' => self
::getSimpleModuleMock( 'bar();' ),
848 $rl = new EmptyResourceLoader();
849 $rl->register( $modules );
850 $context = $this->getResourceLoaderContext(
852 'modules' => 'foo|ferry|bar',
858 // Disable log from makeModuleResponse via outputErrorAndLog
859 $this->setLogger( 'exception', new Psr\Log\
NullLogger() );
861 $response = $rl->makeModuleResponse( $context, $modules );
862 $errors = $rl->getErrors();
864 $this->assertCount( 1, $errors );
865 $this->assertRegExp( '/Ferry not found/', $errors[0] );
867 "foo();\nbar();\n" . 'mw.loader.state( {
877 * Verify that exceptions in PHP for one module will not break others
878 * (stylesheet response).
880 * @covers ResourceLoader::makeModuleResponse
882 public function testMakeModuleResponseErrorCSS() {
884 'foo' => self
::getSimpleStyleModuleMock( '.foo{}' ),
885 'ferry' => self
::getFailFerryMock( 'getStyles' ),
886 'bar' => self
::getSimpleStyleModuleMock( '.bar{}' ),
888 $rl = new EmptyResourceLoader();
889 $rl->register( $modules );
890 $context = $this->getResourceLoaderContext(
892 'modules' => 'foo|ferry|bar',
899 // Disable log from makeModuleResponse via outputErrorAndLog
900 $this->setLogger( 'exception', new Psr\Log\
NullLogger() );
902 $response = $rl->makeModuleResponse( $context, $modules );
903 $errors = $rl->getErrors();
905 $this->assertCount( 2, $errors );
906 $this->assertRegExp( '/Ferry not found/', $errors[0] );
907 $this->assertRegExp( '/Problem.+"ferry":\s*"error"/ms', $errors[1] );
915 * Verify that when building the startup module response,
916 * an exception from one module class will not break the entire
917 * startup module response. See T152266.
919 * @covers ResourceLoader::makeModuleResponse
921 public function testMakeModuleResponseStartupError() {
922 // This is an integration test that uses a lot of MediaWiki state,
923 // provide the full Config object here.
924 $rl = new EmptyResourceLoader( MediaWikiServices
::getInstance()->getMainConfig() );
926 'foo' => self
::getSimpleModuleMock( 'foo();' ),
927 'ferry' => self
::getFailFerryMock(),
928 'bar' => self
::getSimpleModuleMock( 'bar();' ),
930 $context = $this->getResourceLoaderContext(
932 'modules' => 'startup',
939 [ 'startup', 'foo', 'ferry', 'bar' ],
940 $rl->getModuleNames(),
944 // Disable log from makeModuleResponse via outputErrorAndLog
945 $this->setLogger( 'exception', new Psr\Log\
NullLogger() );
947 $modules = [ 'startup' => $rl->getModule( 'startup' ) ];
948 $response = $rl->makeModuleResponse( $context, $modules );
949 $errors = $rl->getErrors();
951 $this->assertRegExp( '/Ferry not found/', $errors[0] );
952 $this->assertCount( 1, $errors );
954 '/isCompatible.*window\.RLQ/s',
956 'startup response undisrupted (T152266)'
959 '/register\([^)]+"ferry",\s*""/s',
961 'startup response registers broken module'
964 '/state\([^)]+"ferry":\s*"error"/s',
966 'startup response sets state to error'
971 * Integration test for modules sending extra HTTP response headers.
973 * @covers ResourceLoaderModule::getHeaders
974 * @covers ResourceLoaderModule::buildContent
975 * @covers ResourceLoader::makeModuleResponse
977 public function testMakeModuleResponseExtraHeaders() {
978 $module = $this->getMockBuilder( ResourceLoaderTestModule
::class )
979 ->setMethods( [ 'getPreloadLinks' ] )->getMock();
980 $module->method( 'getPreloadLinks' )->willReturn( [
981 'https://example.org/script.js' => [ 'as' => 'script' ],
984 $rl = new EmptyResourceLoader();
988 $context = $this->getResourceLoaderContext(
989 [ 'modules' => 'foo', 'only' => 'scripts' ],
993 $modules = [ 'foo' => $rl->getModule( 'foo' ) ];
994 $response = $rl->makeModuleResponse( $context, $modules );
995 $extraHeaders = TestingAccessWrapper
::newFromObject( $rl )->extraHeaders
;
999 'Link: <https://example.org/script.js>;rel=preload;as=script'
1007 * @covers ResourceLoaderModule::getHeaders
1008 * @covers ResourceLoaderModule::buildContent
1009 * @covers ResourceLoader::makeModuleResponse
1011 public function testMakeModuleResponseExtraHeadersMulti() {
1012 $foo = $this->getMockBuilder( ResourceLoaderTestModule
::class )
1013 ->setMethods( [ 'getPreloadLinks' ] )->getMock();
1014 $foo->method( 'getPreloadLinks' )->willReturn( [
1015 'https://example.org/script.js' => [ 'as' => 'script' ],
1018 $bar = $this->getMockBuilder( ResourceLoaderTestModule
::class )
1019 ->setMethods( [ 'getPreloadLinks' ] )->getMock();
1020 $bar->method( 'getPreloadLinks' )->willReturn( [
1021 '/example.png' => [ 'as' => 'image' ],
1022 '/example.jpg' => [ 'as' => 'image' ],
1025 $rl = new EmptyResourceLoader();
1026 $rl->register( [ 'foo' => $foo, 'bar' => $bar ] );
1027 $context = $this->getResourceLoaderContext(
1028 [ 'modules' => 'foo|bar', 'only' => 'scripts' ],
1032 $modules = [ 'foo' => $rl->getModule( 'foo' ), 'bar' => $rl->getModule( 'bar' ) ];
1033 $response = $rl->makeModuleResponse( $context, $modules );
1034 $extraHeaders = TestingAccessWrapper
::newFromObject( $rl )->extraHeaders
;
1035 $this->assertEquals(
1037 'Link: <https://example.org/script.js>;rel=preload;as=script',
1038 'Link: </example.png>;rel=preload;as=image,</example.jpg>;rel=preload;as=image'
1046 * @covers ResourceLoader::respond
1048 public function testRespondEmpty() {
1049 $rl = $this->getMockBuilder( EmptyResourceLoader
::class )
1051 'tryRespondNotModified',
1052 'sendResponseHeaders',
1053 'measureResponseTime',
1056 $context = $this->getResourceLoaderContext( [ 'modules' => '' ], $rl );
1058 $rl->expects( $this->once() )->method( 'measureResponseTime' );
1059 $this->expectOutputRegex( '/no modules were requested/' );
1061 $rl->respond( $context );
1065 * @covers ResourceLoader::respond
1067 public function testRespondSimple() {
1068 $module = new ResourceLoaderTestModule( [ 'script' => 'foo();' ] );
1069 $rl = $this->getMockBuilder( EmptyResourceLoader
::class )
1071 'measureResponseTime',
1072 'tryRespondNotModified',
1073 'sendResponseHeaders',
1074 'makeModuleResponse',
1077 $rl->register( 'test', $module );
1078 $context = $this->getResourceLoaderContext(
1079 [ 'modules' => 'test', 'only' => null ],
1083 $rl->expects( $this->once() )->method( 'makeModuleResponse' )
1084 ->with( $context, [ 'test' => $module ] )
1085 ->willReturn( 'implement_foo;' );
1086 $this->expectOutputRegex( '/^implement_foo;/' );
1088 $rl->respond( $context );
1092 * @covers ResourceLoader::respond
1094 public function testRespondInternalFailures() {
1095 $module = new ResourceLoaderTestModule( [ 'script' => 'foo();' ] );
1096 $rl = $this->getMockBuilder( EmptyResourceLoader
::class )
1098 'measureResponseTime',
1099 'preloadModuleInfo',
1100 'getCombinedVersion',
1101 'tryRespondNotModified',
1102 'makeModuleResponse',
1103 'sendResponseHeaders',
1106 $rl->register( 'test', $module );
1107 $context = $this->getResourceLoaderContext( [ 'modules' => 'test' ], $rl );
1108 // Disable logging from outputErrorAndLog
1109 $this->setLogger( 'exception', new Psr\Log\
NullLogger() );
1111 $rl->expects( $this->once() )->method( 'preloadModuleInfo' )
1112 ->willThrowException( new Exception( 'Preload error' ) );
1113 $rl->expects( $this->once() )->method( 'getCombinedVersion' )
1114 ->willThrowException( new Exception( 'Version error' ) );
1115 $rl->expects( $this->once() )->method( 'makeModuleResponse' )
1116 ->with( $context, [ 'test' => $module ] )
1117 ->willReturn( 'foo;' );
1118 // Internal errors should be caught and logged without affecting module output
1119 $this->expectOutputRegex( '/^\/\*.+Preload error.+Version error.+\*\/.*foo;/ms' );
1121 $rl->respond( $context );
1125 * @covers ResourceLoader::measureResponseTime
1127 public function testMeasureResponseTime() {
1128 $stats = $this->getMockBuilder( NullStatsdDataFactory
::class )
1129 ->setMethods( [ 'timing' ] )->getMock();
1130 $this->setService( 'StatsdDataFactory', $stats );
1132 $stats->expects( $this->once() )->method( 'timing' )
1133 ->with( 'resourceloader.responseTime', $this->anything() );
1135 $timing = new Timing();
1136 $timing->mark( 'requestShutdown' );
1137 $rl = TestingAccessWrapper
::newFromObject( new EmptyResourceLoader
);
1138 $rl->measureResponseTime( $timing );
1139 DeferredUpdates
::doUpdates();