3 use Wikimedia\TestingAccessWrapper;
7 protected function setUp() {
11 'wgShowExceptionDetails' =>
true,
21 $resourceLoaderRegisterModulesHook =
false;
24 'ResourceLoaderRegisterModules' => [
26 $resourceLoaderRegisterModulesHook =
true;
31 $unused =
new ResourceLoader();
33 $resourceLoaderRegisterModulesHook,
34 'Hook ResourceLoaderRegisterModules called'
86 $this->setExpectedException(
MWException::class,
'ResourceLoader module info type error' );
95 $logger->expects( $this->once() )
96 ->method(
'warning' );
115 [
'test.foo',
'test.bar' ],
122 ->disableOriginalConstructor()
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,
145 'TestModule' => [
false,
148 'SkinModule (FileModule subclass)' => [
true,
151 'WikiModule' => [
false, [
153 'scripts' => [
'MediaWiki:Example.js' ],
164 $rl->register(
'test', $module );
165 $this->assertSame( $expected, $rl->isFileModule(
'test' ) );
173 $this->assertSame(
false, $rl->isFileModule(
'unknown' ) );
182 $this->assertTrue( $rl->isModuleRegistered(
'test' ) );
183 $this->assertFalse( $rl->isModuleRegistered(
'test.unknown' ) );
191 $this->assertSame(
null, $rl->getModule(
'test' ) );
200 $this->assertInstanceOf(
202 $rl->getModule(
'test' )
210 $factory =
function (
array $info ) {
211 $this->assertArrayHasKey(
'kitten', $info );
216 $rl->register(
'test', [
'factory' => $factory,
'kitten' =>
'little ball of fur' ] );
217 $this->assertInstanceOf(
219 $rl->getModule(
'test' )
228 $rl->register(
'test', [] );
229 $this->assertInstanceOf(
231 $rl->getModule(
'test' ),
232 'Array-style module registrations default to FileModule'
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 );
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' ],
286 $this->assertEquals( $packed, ResourceLoader::makePackedModulesString(
$modules ), $desc );
296 ResourceLoader::expandModuleNames( $packed ),
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' ]
322 $rl =
new ResourceLoader;
323 $rl->addSource(
$name, $info );
324 if ( is_array( $expected ) ) {
326 $this->assertArrayHasKey(
$source, $rl->getSources() );
329 $this->assertArrayHasKey( $expected, $rl->getSources() );
337 $rl =
new ResourceLoader;
338 $this->setExpectedException(
341 $rl->addSource(
'foo',
'https://example.org/w/load.php' );
342 $rl->addSource(
'foo',
'https://example.com/w/load.php' );
349 $rl =
new ResourceLoader;
351 $rl->addSource(
'foo', [
'x' =>
'https://example.org/w/load.php' ] );
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 ) {
484 'styles' => [],
'templates' => [],
'messages' =>
new XmlJsCode(
'{}' ),
'packageFiles' => [],
486 ResourceLoader::clearCache();
492 $rl->makeLoaderImplementScript(
494 ( $case[
'wrap'] && is_string( $case[
'scripts'] ) )
500 $case[
'packageFiles']
511 $rl->makeLoaderImplementScript(
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'
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( [] )
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',
627 $rl =
new ResourceLoader();
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' );
638 $this->assertTrue(
true );
644 ->setMethods( [ $getter ] )
646 $mock->method( $getter )->will( $this->throwException(
647 new Exception(
'Ferry not found' )
654 ->setMethods( [
'getScript' ] )
656 $mock->method(
'getScript' )->willReturn( $script );
662 ->setMethods( [
'getStyles' ] )
664 $mock->method(
'getStyles' )->willReturn( [
'' => $styles ] );
674 ->setMethods( [
'outputErrorAndLog' ] )->getMock();
676 'foo' => self::getSimpleModuleMock(),
677 'ferry' => self::getFailFerryMock(),
678 'bar' => self::getSimpleModuleMock(),
684 $rl->getCombinedVersion(
$context, [] ),
689 ResourceLoader::makeHash( self::BLANK_VERSION ),
690 $rl->getCombinedVersion(
$context, [
'foo' ] ),
697 ResourceLoader::makeHash( self::BLANK_VERSION . self::BLANK_VERSION ),
698 $rl->getCombinedVersion(
$context, [
'foo',
'ferry',
'bar' ] ),
699 'compute foo+ferry+bar (T152266)'
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)"] = [
764 $modules = array_map(
function ( $script ) {
771 'modules' => implode(
'|', array_keys(
$modules ) ),
773 'debug' =>
$debug ?
'true' :
'false',
779 $this->assertSame( [], $rl->getErrors(),
'Errors' );
780 $this->assertEquals( $expected,
$response, $message ?:
'Response' );
789 [
'modules' =>
'',
'only' =>
'scripts' ],
794 $this->assertSame( [], $rl->getErrors(),
'Errors' );
795 $this->assertRegExp(
'/^\/\*.+no modules were requested.+\*\/$/ms',
$response );
815 'modules' =>
'foo|ferry|bar',
822 $this->
setLogger(
'exception',
new Psr\Log\NullLogger() );
825 $errors = $rl->getErrors();
827 $this->assertCount( 1, $errors );
828 $this->assertRegExp(
'/Ferry not found/', $errors[0] );
830 "foo();\nbar();\n" .
'mw.loader.state( {
855 'modules' =>
'foo|ferry|bar',
863 $this->
setLogger(
'exception',
new Psr\Log\NullLogger() );
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] );
887 'foo' => self::getSimpleModuleMock(
'foo();' ),
888 'ferry' => self::getFailFerryMock(),
889 'bar' => self::getSimpleModuleMock(
'bar();' ),
894 'modules' =>
'startup',
901 [
'foo',
'ferry',
'bar',
'startup' ],
902 $rl->getModuleNames(),
907 $this->
setLogger(
'exception',
new Psr\Log\NullLogger() );
909 $modules = [
'startup' => $rl->getModule(
'startup' ) ];
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'
941 ->setMethods( [
'getPreloadLinks' ] )->getMock();
942 $module->method(
'getPreloadLinks' )->willReturn( [
943 'https://example.org/script.js' => [
'as' =>
'script' ],
951 [
'modules' =>
'foo',
'only' =>
'scripts' ],
955 $modules = [
'foo' => $rl->getModule(
'foo' ) ];
957 $extraHeaders = TestingAccessWrapper::newFromObject( $rl )->extraHeaders;
961 'Link: <https://example.org/script.js>;rel=preload;as=script'
975 ->setMethods( [
'getPreloadLinks' ] )->getMock();
976 $foo->method(
'getPreloadLinks' )->willReturn( [
977 'https://example.org/script.js' => [
'as' =>
'script' ],
981 ->setMethods( [
'getPreloadLinks' ] )->getMock();
982 $bar->method(
'getPreloadLinks' )->willReturn( [
983 '/example.png' => [
'as' =>
'image' ],
984 '/example.jpg' => [
'as' =>
'image' ],
988 $rl->register( [
'foo' => $foo,
'bar' => $bar ] );
990 [
'modules' =>
'foo|bar',
'only' =>
'scripts' ],
994 $modules = [
'foo' => $rl->getModule(
'foo' ),
'bar' => $rl->getModule(
'bar' ) ];
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'
1013 'tryRespondNotModified',
1014 'sendResponseHeaders',
1015 'measureResponseTime',
1020 $rl->expects( $this->once() )->method(
'measureResponseTime' );
1021 $this->expectOutputRegex(
'/no modules were requested/' );
1033 'measureResponseTime',
1034 'tryRespondNotModified',
1035 'sendResponseHeaders',
1036 'makeModuleResponse',
1039 $rl->register(
'test', $module );
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;/' );
1060 'measureResponseTime',
1061 'preloadModuleInfo',
1062 'getCombinedVersion',
1063 'tryRespondNotModified',
1064 'makeModuleResponse',
1065 'sendResponseHeaders',
1068 $rl->register(
'test', $module );
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;' );
1081 $this->expectOutputRegex(
'/^\/\*.+Preload error.+Version error.+\*\/.*foo;/ms' );
1091 ->setMethods( [
'timing' ] )->getMock();
1092 $this->
setService(
'StatsdDataFactory', $stats );
1094 $stats->expects( $this->once() )->method(
'timing' )
1095 ->with(
'resourceloader.responseTime', $this->
anything() );
1098 $timing->mark(
'requestShutdown' );
1100 $rl->measureResponseTime( $timing );