3 use Wikimedia\TestingAccessWrapper
;
5 class ResourceLoaderTest
extends ResourceLoaderTestCase
{
7 protected function setUp() {
10 $this->setMwGlobals( [
11 'wgShowExceptionDetails' => true,
16 * Ensure the ResourceLoaderRegisterModules hook is called.
18 * @covers ResourceLoader::__construct
20 public function testConstructRegistrationHook() {
21 $resourceLoaderRegisterModulesHook = false;
23 $this->setMwGlobals( 'wgHooks', [
24 'ResourceLoaderRegisterModules' => [
25 function ( &$resourceLoader ) use ( &$resourceLoaderRegisterModulesHook ) {
26 $resourceLoaderRegisterModulesHook = true;
31 $unused = new ResourceLoader();
33 $resourceLoaderRegisterModulesHook,
34 'Hook ResourceLoaderRegisterModules called'
39 * @covers ResourceLoader::register
40 * @covers ResourceLoader::getModule
42 public function testRegisterValidObject() {
43 $module = new ResourceLoaderTestModule();
44 $resourceLoader = new EmptyResourceLoader();
45 $resourceLoader->register( 'test', $module );
46 $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
50 * @covers ResourceLoader::register
51 * @covers ResourceLoader::getModule
53 public function testRegisterValidArray() {
54 $module = new ResourceLoaderTestModule();
55 $resourceLoader = new EmptyResourceLoader();
56 // Covers case of register() setting $rl->moduleInfos,
57 // but $rl->modules lazy-populated by getModule()
58 $resourceLoader->register( 'test', [ 'object' => $module ] );
59 $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
63 * @covers ResourceLoader::register
65 public function testRegisterEmptyString() {
66 $module = new ResourceLoaderTestModule();
67 $resourceLoader = new EmptyResourceLoader();
68 $resourceLoader->register( '', $module );
69 $this->assertEquals( $module, $resourceLoader->getModule( '' ) );
73 * @covers ResourceLoader::register
75 public function testRegisterInvalidName() {
76 $resourceLoader = new EmptyResourceLoader();
77 $this->setExpectedException( MWException
::class, "name 'test!invalid' is invalid" );
78 $resourceLoader->register( 'test!invalid', new ResourceLoaderTestModule() );
82 * @covers ResourceLoader::register
84 public function testRegisterInvalidType() {
85 $resourceLoader = new EmptyResourceLoader();
86 $this->setExpectedException( MWException
::class, 'ResourceLoader module info type error' );
87 $resourceLoader->register( 'test', new stdClass() );
91 * @covers ResourceLoader::register
93 public function testRegisterDuplicate() {
94 $logger = $this->getMockBuilder( Psr\Log\LoggerInterface
::class )->getMock();
95 $logger->expects( $this->once() )
96 ->method( 'warning' );
97 $resourceLoader = new EmptyResourceLoader( null, $logger );
99 $module1 = new ResourceLoaderTestModule();
100 $module2 = new ResourceLoaderTestModule();
101 $resourceLoader->register( 'test', $module1 );
102 $resourceLoader->register( 'test', $module2 );
103 $this->assertSame( $module2, $resourceLoader->getModule( 'test' ) );
107 * @covers ResourceLoader::getModuleNames
109 public function testGetModuleNames() {
110 // Use an empty one so that core and extension modules don't get in.
111 $resourceLoader = new EmptyResourceLoader();
112 $resourceLoader->register( 'test.foo', new ResourceLoaderTestModule() );
113 $resourceLoader->register( 'test.bar', new ResourceLoaderTestModule() );
115 [ 'test.foo', 'test.bar' ],
116 $resourceLoader->getModuleNames()
120 public function provideTestIsFileModule() {
121 $fileModuleObj = $this->getMockBuilder( ResourceLoaderFileModule
::class )
122 ->disableOriginalConstructor()
126 new ResourceLoaderTestModule()
128 'FileModule object' => [ false,
131 'simple empty' => [ true,
134 'simple scripts' => [ true,
135 [ 'scripts' => 'example.js' ]
137 'simple scripts, raw and targets' => [ true, [
138 'scripts' => [ 'a.js', 'b.js' ],
140 'targets' => [ 'desktop', 'mobile' ],
142 'FileModule' => [ true,
143 [ 'class' => ResourceLoaderFileModule
::class, 'scripts' => 'example.js' ]
145 'TestModule' => [ false,
146 [ 'class' => ResourceLoaderTestModule
::class, 'scripts' => 'example.js' ]
148 'SkinModule (FileModule subclass)' => [ true,
149 [ 'class' => ResourceLoaderSkinModule
::class, 'scripts' => 'example.js' ]
151 'WikiModule' => [ false, [
152 'class' => ResourceLoaderWikiModule
::class,
153 'scripts' => [ 'MediaWiki:Example.js' ],
159 * @dataProvider provideTestIsFileModule
160 * @covers ResourceLoader::isFileModule
162 public function testIsFileModule( $expected, $module ) {
163 $rl = TestingAccessWrapper
::newFromObject( new EmptyResourceLoader() );
164 $rl->register( 'test', $module );
165 $this->assertSame( $expected, $rl->isFileModule( 'test' ) );
169 * @covers ResourceLoader::isFileModule
171 public function testIsFileModuleUnknown() {
172 $rl = TestingAccessWrapper
::newFromObject( new EmptyResourceLoader() );
173 $this->assertSame( false, $rl->isFileModule( 'unknown' ) );
177 * @covers ResourceLoader::isModuleRegistered
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' ) );
187 * @covers ResourceLoader::getModule
189 public function testGetModuleUnknown() {
190 $rl = new EmptyResourceLoader();
191 $this->assertSame( null, $rl->getModule( 'test' ) );
195 * @covers ResourceLoader::getModule
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' )
207 * @covers ResourceLoader::getModule
209 public function testGetModuleFactory() {
210 $factory = function ( array $info ) {
211 $this->assertArrayHasKey( 'kitten', $info );
212 return new ResourceLoaderTestModule( $info );
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' )
224 * @covers ResourceLoader::getModule
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'
237 * @covers ResourceLoader::getLessCompiler
239 public function testLessImportDirs() {
240 $rl = new EmptyResourceLoader();
241 $lc = $rl->getLessCompiler( [ 'foo' => '2px', 'Foo' => '#eeeeee' ] );
242 $basePath = dirname( dirname( __DIR__
) ) . '/data/less';
243 $lc->SetImportDirs( [
244 "$basePath/common" => '',
246 $css = $lc->parseFile( "$basePath/module/use-import-dir.less" )->getCss();
247 $this->assertStringEqualsFile( "$basePath/module/styles.css", $css );
250 public static function providePackedModules() {
253 'Example from makePackedModulesString doc comment',
254 [ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ],
255 'foo.bar,baz|bar.baz,quux',
258 'Example from expandModuleNames doc comment',
259 [ 'jquery.foo', 'jquery.bar', 'jquery.ui.baz', 'jquery.ui.quux' ],
260 'jquery.foo,bar|jquery.ui.baz,quux',
263 'Regression fixed in r87497 (7fee86c38e) with dotless names',
264 [ 'foo', 'bar', 'baz' ],
268 'Prefixless modules after a prefixed module',
269 [ 'single.module', 'foobar', 'foobaz' ],
270 'single.module|foobar,foobaz',
274 [ 'foo', 'foo.baz', 'baz.quux', 'foo.bar' ],
275 'foo|foo.baz,bar|baz.quux',
276 [ 'foo', 'foo.baz', 'foo.bar', 'baz.quux' ],
282 * @dataProvider providePackedModules
283 * @covers ResourceLoader::makePackedModulesString
285 public function testMakePackedModulesString( $desc, $modules, $packed ) {
286 $this->assertEquals( $packed, ResourceLoader
::makePackedModulesString( $modules ), $desc );
290 * @dataProvider providePackedModules
291 * @covers ResourceLoader::expandModuleNames
293 public function testExpandModuleNames( $desc, $modules, $packed, $unpacked = null ) {
295 $unpacked ?
: $modules,
296 ResourceLoader
::expandModuleNames( $packed ),
301 public static function provideAddSource() {
303 [ 'foowiki', 'https://example.org/w/load.php', 'foowiki' ],
304 [ 'foowiki', [ 'loadScript' => 'https://example.org/w/load.php' ], 'foowiki' ],
307 'foowiki' => 'https://example.org/w/load.php',
308 'bazwiki' => 'https://example.com/w/load.php',
311 [ 'foowiki', 'bazwiki' ]
317 * @dataProvider provideAddSource
318 * @covers ResourceLoader::addSource
319 * @covers ResourceLoader::getSources
321 public function testAddSource( $name, $info, $expected ) {
322 $rl = new ResourceLoader
;
323 $rl->addSource( $name, $info );
324 if ( is_array( $expected ) ) {
325 foreach ( $expected as $source ) {
326 $this->assertArrayHasKey( $source, $rl->getSources() );
329 $this->assertArrayHasKey( $expected, $rl->getSources() );
334 * @covers ResourceLoader::addSource
336 public function testAddSourceDupe() {
337 $rl = new ResourceLoader
;
338 $this->setExpectedException(
339 MWException
::class, 'ResourceLoader duplicate source addition error'
341 $rl->addSource( 'foo', 'https://example.org/w/load.php' );
342 $rl->addSource( 'foo', 'https://example.com/w/load.php' );
346 * @covers ResourceLoader::addSource
348 public function testAddSourceInvalid() {
349 $rl = new ResourceLoader
;
350 $this->setExpectedException( MWException
::class, 'with no "loadScript" key' );
351 $rl->addSource( 'foo', [ 'x' => 'https://example.org/w/load.php' ] );
354 public static function provideLoaderImplement() {
357 'title' => 'Implement scripts, styles and messages',
359 'name' => 'test.example',
360 'scripts' => 'mw.example();',
361 'styles' => [ 'css' => [ '.mw-example {}' ] ],
362 'messages' => [ 'example' => '' ],
365 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
376 'title' => 'Implement scripts',
378 'name' => 'test.example',
379 'scripts' => 'mw.example();',
382 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
387 'title' => 'Implement styles',
389 'name' => 'test.example',
391 'styles' => [ 'css' => [ '.mw-example {}' ] ],
393 'expected' => 'mw.loader.implement( "test.example", [], {
400 'title' => 'Implement scripts and messages',
402 'name' => 'test.example',
403 'scripts' => 'mw.example();',
404 'messages' => [ 'example' => '' ],
406 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
413 'title' => 'Implement scripts and templates',
415 'name' => 'test.example',
416 'scripts' => 'mw.example();',
417 'templates' => [ 'example.html' => '' ],
419 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
426 'title' => 'Implement unwrapped user script',
429 'scripts' => 'mw.example( 1 );',
432 'expected' => 'mw.loader.implement( "user", "mw.example( 1 );" );',
435 'title' => 'Implement multi-file script',
437 'name' => 'test.multifile',
442 'content' => 'mw.example( 1 );',
446 'content' => [ 'n' => 2 ],
450 'content' => 'mw.example( 3 );'
453 'main' => 'three.js',
457 mw.loader.implement( "test.multifile", {
460 "one.js": function ( require, module ) {
466 "three.js": function ( require, module ) {
477 * @dataProvider provideLoaderImplement
478 * @covers ResourceLoader::makeLoaderImplementScript
479 * @covers ResourceLoader::trimArray
481 public function testMakeLoaderImplementScript( $case ) {
484 'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' ), 'packageFiles' => [],
486 ResourceLoader
::clearCache();
487 $this->setMwGlobals( 'wgResourceLoaderDebug', true );
489 $rl = TestingAccessWrapper
::newFromClass( ResourceLoader
::class );
492 $rl->makeLoaderImplementScript(
494 ( $case['wrap'] && is_string( $case['scripts'] ) )
495 ?
new XmlJsCode( $case['scripts'] )
500 $case['packageFiles']
506 * @covers ResourceLoader::makeLoaderImplementScript
508 public function testMakeLoaderImplementScriptInvalid() {
509 $this->setExpectedException( MWException
::class, 'Invalid scripts error' );
510 $rl = TestingAccessWrapper
::newFromClass( ResourceLoader
::class );
511 $rl->makeLoaderImplementScript(
517 null // package files
522 * @covers ResourceLoader::makeLoaderRegisterScript
524 public function testMakeLoaderRegisterScript() {
526 'mw.loader.register( [
532 ResourceLoader
::makeLoaderRegisterScript( [
533 [ 'test.name', '1234567' ],
535 'Nested array parameter'
539 'mw.loader.register( [
568 ResourceLoader
::makeLoaderRegisterScript( [
569 [ 'test.foo', '100' , [], null, null ],
570 [ 'test.bar', '200', [ 'test.unknown' ], null ],
571 [ 'test.baz', '300', [ 'test.quux', 'test.foo' ], null ],
572 [ 'test.quux', '400', [], null, null, 'return true;' ],
574 'Compact dependency indexes'
579 * @covers ResourceLoader::makeLoaderSourcesScript
581 public function testMakeLoaderSourcesScript() {
583 'mw.loader.addSource( {
584 "local": "/w/load.php"
586 ResourceLoader
::makeLoaderSourcesScript( 'local', '/w/load.php' )
589 'mw.loader.addSource( {
590 "local": "/w/load.php"
592 ResourceLoader
::makeLoaderSourcesScript( [ 'local' => '/w/load.php' ] )
595 'mw.loader.addSource( {
596 "local": "/w/load.php",
597 "example": "https://example.org/w/load.php"
599 ResourceLoader
::makeLoaderSourcesScript( [
600 'local' => '/w/load.php',
601 'example' => 'https://example.org/w/load.php'
605 'mw.loader.addSource( [] );',
606 ResourceLoader
::makeLoaderSourcesScript( [] )
610 private static function fakeSources() {
613 'loadScript' => '//example.org/w/load.php',
614 'apiScript' => '//example.org/w/api.php',
617 'loadScript' => '//example.com/w/load.php',
618 'apiScript' => '//example.com/w/api.php',
624 * @covers ResourceLoader::getLoadScript
626 public function testGetLoadScript() {
627 $rl = new ResourceLoader();
628 $sources = self
::fakeSources();
629 $rl->addSource( $sources );
630 foreach ( [ 'examplewiki', 'example2wiki' ] as $name ) {
631 $this->assertEquals( $rl->getLoadScript( $name ), $sources[$name]['loadScript'] );
635 $rl->getLoadScript( 'thiswasneverreigstered' );
636 $this->assertTrue( false, 'ResourceLoader::getLoadScript should have thrown an exception' );
637 } catch ( MWException
$e ) {
638 $this->assertTrue( true );
642 protected function getFailFerryMock( $getter = 'getScript' ) {
643 $mock = $this->getMockBuilder( ResourceLoaderTestModule
::class )
644 ->setMethods( [ $getter ] )
646 $mock->method( $getter )->will( $this->throwException(
647 new Exception( 'Ferry not found' )
652 protected function getSimpleModuleMock( $script = '' ) {
653 $mock = $this->getMockBuilder( ResourceLoaderTestModule
::class )
654 ->setMethods( [ 'getScript' ] )
656 $mock->method( 'getScript' )->willReturn( $script );
660 protected function getSimpleStyleModuleMock( $styles = '' ) {
661 $mock = $this->getMockBuilder( ResourceLoaderTestModule
::class )
662 ->setMethods( [ 'getStyles' ] )
664 $mock->method( 'getStyles' )->willReturn( [ '' => $styles ] );
669 * @covers ResourceLoader::getCombinedVersion
671 public function testGetCombinedVersion() {
672 $rl = $this->getMockBuilder( EmptyResourceLoader
::class )
673 // Disable log from outputErrorAndLog
674 ->setMethods( [ 'outputErrorAndLog' ] )->getMock();
676 'foo' => self
::getSimpleModuleMock(),
677 'ferry' => self
::getFailFerryMock(),
678 'bar' => self
::getSimpleModuleMock(),
680 $context = $this->getResourceLoaderContext( [], $rl );
684 $rl->getCombinedVersion( $context, [] ),
689 ResourceLoader
::makeHash( self
::BLANK_VERSION
),
690 $rl->getCombinedVersion( $context, [ 'foo' ] ),
694 // Verify that getCombinedVersion() does not throw when ferry fails.
695 // Instead it gracefully continues to combine the remaining modules.
697 ResourceLoader
::makeHash( self
::BLANK_VERSION
. self
::BLANK_VERSION
),
698 $rl->getCombinedVersion( $context, [ 'foo', 'ferry', 'bar' ] ),
699 'compute foo+ferry+bar (T152266)'
703 public static function provideMakeModuleResponseConcat() {
709 'expected' => "foo()\n" . 'mw.loader.state( {
712 'minified' => "foo()\n" . 'mw.loader.state({"foo":"ready"});',
713 'message' => 'Script without semi-colon',
720 'expected' => "foo()\nbar()\n" . 'mw.loader.state( {
724 'minified' => "foo()\nbar()\n" . 'mw.loader.state({"foo":"ready","bar":"ready"});',
725 'message' => 'Two scripts without semi-colon',
729 'foo' => "foo()\n// bar();"
731 'expected' => "foo()\n// bar();\n" . 'mw.loader.state( {
734 'minified' => "foo()\n" . 'mw.loader.state({"foo":"ready"});',
735 'message' => 'Script with semi-colon in comment (T162719)',
739 foreach ( $testcases as $i => $case ) {
746 $ret["#$i (minified)"] = [
757 * Verify how multiple scripts and mw.loader.state() calls are concatenated.
759 * @dataProvider provideMakeModuleResponseConcat
760 * @covers ResourceLoader::makeModuleResponse
762 public function testMakeModuleResponseConcat( $scripts, $expected, $debug, $message = null ) {
763 $rl = new EmptyResourceLoader();
764 $modules = array_map( function ( $script ) {
765 return self
::getSimpleModuleMock( $script );
767 $rl->register( $modules );
769 $context = $this->getResourceLoaderContext(
771 'modules' => implode( '|', array_keys( $modules ) ),
773 'debug' => $debug ?
'true' : 'false',
778 $response = $rl->makeModuleResponse( $context, $modules );
779 $this->assertSame( [], $rl->getErrors(), 'Errors' );
780 $this->assertEquals( $expected, $response, $message ?
: 'Response' );
784 * @covers ResourceLoader::makeModuleResponse
786 public function testMakeModuleResponseEmpty() {
787 $rl = new EmptyResourceLoader();
788 $context = $this->getResourceLoaderContext(
789 [ 'modules' => '', 'only' => 'scripts' ],
793 $response = $rl->makeModuleResponse( $context, [] );
794 $this->assertSame( [], $rl->getErrors(), 'Errors' );
795 $this->assertRegExp( '/^\/\*.+no modules were requested.+\*\/$/ms', $response );
799 * Verify that when building module content in a load.php response,
800 * an exception from one module will not break script output from
803 * @covers ResourceLoader::makeModuleResponse
805 public function testMakeModuleResponseError() {
807 'foo' => self
::getSimpleModuleMock( 'foo();' ),
808 'ferry' => self
::getFailFerryMock(),
809 'bar' => self
::getSimpleModuleMock( 'bar();' ),
811 $rl = new EmptyResourceLoader();
812 $rl->register( $modules );
813 $context = $this->getResourceLoaderContext(
815 'modules' => 'foo|ferry|bar',
821 // Disable log from makeModuleResponse via outputErrorAndLog
822 $this->setLogger( 'exception', new Psr\Log\
NullLogger() );
824 $response = $rl->makeModuleResponse( $context, $modules );
825 $errors = $rl->getErrors();
827 $this->assertCount( 1, $errors );
828 $this->assertRegExp( '/Ferry not found/', $errors[0] );
830 "foo();\nbar();\n" . 'mw.loader.state( {
840 * Verify that exceptions in PHP for one module will not break others
841 * (stylesheet response).
843 * @covers ResourceLoader::makeModuleResponse
845 public function testMakeModuleResponseErrorCSS() {
847 'foo' => self
::getSimpleStyleModuleMock( '.foo{}' ),
848 'ferry' => self
::getFailFerryMock( 'getStyles' ),
849 'bar' => self
::getSimpleStyleModuleMock( '.bar{}' ),
851 $rl = new EmptyResourceLoader();
852 $rl->register( $modules );
853 $context = $this->getResourceLoaderContext(
855 'modules' => 'foo|ferry|bar',
862 // Disable log from makeModuleResponse via outputErrorAndLog
863 $this->setLogger( 'exception', new Psr\Log\
NullLogger() );
865 $response = $rl->makeModuleResponse( $context, $modules );
866 $errors = $rl->getErrors();
868 $this->assertCount( 2, $errors );
869 $this->assertRegExp( '/Ferry not found/', $errors[0] );
870 $this->assertRegExp( '/Problem.+"ferry":\s*"error"/ms', $errors[1] );
878 * Verify that when building the startup module response,
879 * an exception from one module class will not break the entire
880 * startup module response. See T152266.
882 * @covers ResourceLoader::makeModuleResponse
884 public function testMakeModuleResponseStartupError() {
885 $rl = new EmptyResourceLoader();
887 'foo' => self
::getSimpleModuleMock( 'foo();' ),
888 'ferry' => self
::getFailFerryMock(),
889 'bar' => self
::getSimpleModuleMock( 'bar();' ),
890 'startup' => [ 'class' => ResourceLoaderStartUpModule
::class ],
892 $context = $this->getResourceLoaderContext(
894 'modules' => 'startup',
901 [ 'foo', 'ferry', 'bar', 'startup' ],
902 $rl->getModuleNames(),
906 // Disable log from makeModuleResponse via outputErrorAndLog
907 $this->setLogger( 'exception', new Psr\Log\
NullLogger() );
909 $modules = [ 'startup' => $rl->getModule( 'startup' ) ];
910 $response = $rl->makeModuleResponse( $context, $modules );
911 $errors = $rl->getErrors();
913 $this->assertRegExp( '/Ferry not found/', $errors[0] );
914 $this->assertCount( 1, $errors );
916 '/isCompatible.*window\.RLQ/s',
918 'startup response undisrupted (T152266)'
921 '/register\([^)]+"ferry",\s*""/s',
923 'startup response registers broken module'
926 '/state\([^)]+"ferry":\s*"error"/s',
928 'startup response sets state to error'
933 * Integration test for modules sending extra HTTP response headers.
935 * @covers ResourceLoaderModule::getHeaders
936 * @covers ResourceLoaderModule::buildContent
937 * @covers ResourceLoader::makeModuleResponse
939 public function testMakeModuleResponseExtraHeaders() {
940 $module = $this->getMockBuilder( ResourceLoaderTestModule
::class )
941 ->setMethods( [ 'getPreloadLinks' ] )->getMock();
942 $module->method( 'getPreloadLinks' )->willReturn( [
943 'https://example.org/script.js' => [ 'as' => 'script' ],
946 $rl = new EmptyResourceLoader();
950 $context = $this->getResourceLoaderContext(
951 [ 'modules' => 'foo', 'only' => 'scripts' ],
955 $modules = [ 'foo' => $rl->getModule( 'foo' ) ];
956 $response = $rl->makeModuleResponse( $context, $modules );
957 $extraHeaders = TestingAccessWrapper
::newFromObject( $rl )->extraHeaders
;
961 'Link: <https://example.org/script.js>;rel=preload;as=script'
969 * @covers ResourceLoaderModule::getHeaders
970 * @covers ResourceLoaderModule::buildContent
971 * @covers ResourceLoader::makeModuleResponse
973 public function testMakeModuleResponseExtraHeadersMulti() {
974 $foo = $this->getMockBuilder( ResourceLoaderTestModule
::class )
975 ->setMethods( [ 'getPreloadLinks' ] )->getMock();
976 $foo->method( 'getPreloadLinks' )->willReturn( [
977 'https://example.org/script.js' => [ 'as' => 'script' ],
980 $bar = $this->getMockBuilder( ResourceLoaderTestModule
::class )
981 ->setMethods( [ 'getPreloadLinks' ] )->getMock();
982 $bar->method( 'getPreloadLinks' )->willReturn( [
983 '/example.png' => [ 'as' => 'image' ],
984 '/example.jpg' => [ 'as' => 'image' ],
987 $rl = new EmptyResourceLoader();
988 $rl->register( [ 'foo' => $foo, 'bar' => $bar ] );
989 $context = $this->getResourceLoaderContext(
990 [ 'modules' => 'foo|bar', 'only' => 'scripts' ],
994 $modules = [ 'foo' => $rl->getModule( 'foo' ), 'bar' => $rl->getModule( 'bar' ) ];
995 $response = $rl->makeModuleResponse( $context, $modules );
996 $extraHeaders = TestingAccessWrapper
::newFromObject( $rl )->extraHeaders
;
999 'Link: <https://example.org/script.js>;rel=preload;as=script',
1000 'Link: </example.png>;rel=preload;as=image,</example.jpg>;rel=preload;as=image'
1008 * @covers ResourceLoader::respond
1010 public function testRespondEmpty() {
1011 $rl = $this->getMockBuilder( EmptyResourceLoader
::class )
1013 'tryRespondNotModified',
1014 'sendResponseHeaders',
1015 'measureResponseTime',
1018 $context = $this->getResourceLoaderContext( [ 'modules' => '' ], $rl );
1020 $rl->expects( $this->once() )->method( 'measureResponseTime' );
1021 $this->expectOutputRegex( '/no modules were requested/' );
1023 $rl->respond( $context );
1027 * @covers ResourceLoader::respond
1029 public function testRespondSimple() {
1030 $module = new ResourceLoaderTestModule( [ 'script' => 'foo();' ] );
1031 $rl = $this->getMockBuilder( EmptyResourceLoader
::class )
1033 'measureResponseTime',
1034 'tryRespondNotModified',
1035 'sendResponseHeaders',
1036 'makeModuleResponse',
1039 $rl->register( 'test', $module );
1040 $context = $this->getResourceLoaderContext(
1041 [ 'modules' => 'test', 'only' => null ],
1045 $rl->expects( $this->once() )->method( 'makeModuleResponse' )
1046 ->with( $context, [ 'test' => $module ] )
1047 ->willReturn( 'implement_foo;' );
1048 $this->expectOutputRegex( '/^implement_foo;/' );
1050 $rl->respond( $context );
1054 * @covers ResourceLoader::respond
1056 public function testRespondInternalFailures() {
1057 $module = new ResourceLoaderTestModule( [ 'script' => 'foo();' ] );
1058 $rl = $this->getMockBuilder( EmptyResourceLoader
::class )
1060 'measureResponseTime',
1061 'preloadModuleInfo',
1062 'getCombinedVersion',
1063 'tryRespondNotModified',
1064 'makeModuleResponse',
1065 'sendResponseHeaders',
1068 $rl->register( 'test', $module );
1069 $context = $this->getResourceLoaderContext( [ 'modules' => 'test' ], $rl );
1070 // Disable logging from outputErrorAndLog
1071 $this->setLogger( 'exception', new Psr\Log\
NullLogger() );
1073 $rl->expects( $this->once() )->method( 'preloadModuleInfo' )
1074 ->willThrowException( new Exception( 'Preload error' ) );
1075 $rl->expects( $this->once() )->method( 'getCombinedVersion' )
1076 ->willThrowException( new Exception( 'Version error' ) );
1077 $rl->expects( $this->once() )->method( 'makeModuleResponse' )
1078 ->with( $context, [ 'test' => $module ] )
1079 ->willReturn( 'foo;' );
1080 // Internal errors should be caught and logged without affecting module output
1081 $this->expectOutputRegex( '/^\/\*.+Preload error.+Version error.+\*\/.*foo;/ms' );
1083 $rl->respond( $context );
1087 * @covers ResourceLoader::measureResponseTime
1089 public function testMeasureResponseTime() {
1090 $stats = $this->getMockBuilder( NullStatsdDataFactory
::class )
1091 ->setMethods( [ 'timing' ] )->getMock();
1092 $this->setService( 'StatsdDataFactory', $stats );
1094 $stats->expects( $this->once() )->method( 'timing' )
1095 ->with( 'resourceloader.responseTime', $this->anything() );
1097 $timing = new Timing();
1098 $timing->mark( 'requestShutdown' );
1099 $rl = TestingAccessWrapper
::newFromObject( new EmptyResourceLoader
);
1100 $rl->measureResponseTime( $timing );
1101 DeferredUpdates
::doUpdates();