3 use Wikimedia\TestingAccessWrapper
;
5 class ResourceLoaderTest
extends ResourceLoaderTestCase
{
7 protected function setUp() {
10 $this->setMwGlobals( [
11 'wgResourceLoaderLESSImportPaths' => [
12 dirname( dirname( __DIR__
) ) . '/data/less/common',
14 'wgResourceLoaderLESSVars' => [
19 // Clear ResourceLoaderGetConfigVars hooks (called by StartupModule)
20 // to avoid notices during testMakeModuleResponse for missing
21 // wgResourceLoaderLESSVars keys in extension hooks.
23 'wgShowExceptionDetails' => true,
28 * Ensure the ResourceLoaderRegisterModules hook is called.
30 * @covers ResourceLoader::__construct
32 public function testConstructRegistrationHook() {
33 $resourceLoaderRegisterModulesHook = false;
35 $this->setMwGlobals( 'wgHooks', [
36 'ResourceLoaderRegisterModules' => [
37 function ( &$resourceLoader ) use ( &$resourceLoaderRegisterModulesHook ) {
38 $resourceLoaderRegisterModulesHook = true;
43 $unused = new ResourceLoader();
45 $resourceLoaderRegisterModulesHook,
46 'Hook ResourceLoaderRegisterModules called'
51 * @covers ResourceLoader::register
52 * @covers ResourceLoader::getModule
54 public function testRegisterValidObject() {
55 $module = new ResourceLoaderTestModule();
56 $resourceLoader = new EmptyResourceLoader();
57 $resourceLoader->register( 'test', $module );
58 $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
62 * @covers ResourceLoader::register
63 * @covers ResourceLoader::getModule
65 public function testRegisterValidArray() {
66 $module = new ResourceLoaderTestModule();
67 $resourceLoader = new EmptyResourceLoader();
68 // Covers case of register() setting $rl->moduleInfos,
69 // but $rl->modules lazy-populated by getModule()
70 $resourceLoader->register( 'test', [ 'object' => $module ] );
71 $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
75 * @covers ResourceLoader::register
77 public function testRegisterEmptyString() {
78 $module = new ResourceLoaderTestModule();
79 $resourceLoader = new EmptyResourceLoader();
80 $resourceLoader->register( '', $module );
81 $this->assertEquals( $module, $resourceLoader->getModule( '' ) );
85 * @covers ResourceLoader::register
87 public function testRegisterInvalidName() {
88 $resourceLoader = new EmptyResourceLoader();
89 $this->setExpectedException( 'MWException', "name 'test!invalid' is invalid" );
90 $resourceLoader->register( 'test!invalid', new ResourceLoaderTestModule() );
94 * @covers ResourceLoader::register
96 public function testRegisterInvalidType() {
97 $resourceLoader = new EmptyResourceLoader();
98 $this->setExpectedException( 'MWException', 'ResourceLoader module info type error' );
99 $resourceLoader->register( 'test', new stdClass() );
103 * @covers ResourceLoader::getModuleNames
105 public function testGetModuleNames() {
106 // Use an empty one so that core and extension modules don't get in.
107 $resourceLoader = new EmptyResourceLoader();
108 $resourceLoader->register( 'test.foo', new ResourceLoaderTestModule() );
109 $resourceLoader->register( 'test.bar', new ResourceLoaderTestModule() );
111 [ 'test.foo', 'test.bar' ],
112 $resourceLoader->getModuleNames()
116 public function provideTestIsFileModule() {
117 $fileModuleObj = $this->getMockBuilder( ResourceLoaderFileModule
::class )
118 ->disableOriginalConstructor()
122 new ResourceLoaderTestModule()
124 'FileModule object' => [ false,
127 'simple empty' => [ true,
130 'simple scripts' => [ true,
131 [ 'scripts' => 'example.js' ]
133 'simple scripts, raw and targets' => [ true, [
134 'scripts' => [ 'a.js', 'b.js' ],
136 'targets' => [ 'desktop', 'mobile' ],
138 'FileModule' => [ true,
139 [ 'class' => ResourceLoaderFileModule
::class, 'scripts' => 'example.js' ]
141 'TestModule' => [ false,
142 [ 'class' => ResourceLoaderTestModule
::class, 'scripts' => 'example.js' ]
144 'SkinModule (FileModule subclass)' => [ true,
145 [ 'class' => ResourceLoaderSkinModule
::class, 'scripts' => 'example.js' ]
147 'JqueryMsgModule (FileModule subclass)' => [ true, [
148 'class' => ResourceLoaderJqueryMsgModule
::class,
149 '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::isModuleRegistered
171 public function testIsModuleRegistered() {
172 $rl = new EmptyResourceLoader();
173 $rl->register( 'test', new ResourceLoaderTestModule() );
174 $this->assertTrue( $rl->isModuleRegistered( 'test' ) );
175 $this->assertFalse( $rl->isModuleRegistered( 'test.unknown' ) );
179 * @covers ResourceLoader::getModule
181 public function testGetModuleUnknown() {
182 $rl = new EmptyResourceLoader();
183 $this->assertSame( null, $rl->getModule( 'test' ) );
187 * @covers ResourceLoader::getModule
189 public function testGetModuleClass() {
190 $rl = new EmptyResourceLoader();
191 $rl->register( 'test', [ 'class' => ResourceLoaderTestModule
::class ] );
192 $this->assertInstanceOf(
193 ResourceLoaderTestModule
::class,
194 $rl->getModule( 'test' )
199 * @covers ResourceLoader::getModule
201 public function testGetModuleFactory() {
202 $factory = function( array $info ) {
203 $this->assertArrayHasKey( 'kitten', $info );
204 return new ResourceLoaderTestModule( $info );
207 $rl = new EmptyResourceLoader();
208 $rl->register( 'test', [ 'factory' => $factory, 'kitten' => 'little ball of fur' ] );
209 $this->assertInstanceOf(
210 ResourceLoaderTestModule
::class,
211 $rl->getModule( 'test' )
216 * @covers ResourceLoader::getModule
218 public function testGetModuleClassDefault() {
219 $rl = new EmptyResourceLoader();
220 $rl->register( 'test', [] );
221 $this->assertInstanceOf(
222 ResourceLoaderFileModule
::class,
223 $rl->getModule( 'test' ),
224 'Array-style module registrations default to FileModule'
229 * @covers ResourceLoaderFileModule::compileLessFile
231 public function testLessFileCompilation() {
232 $context = $this->getResourceLoaderContext();
233 $basePath = __DIR__
. '/../../data/less/module';
234 $module = new ResourceLoaderFileModule( [
235 'localBasePath' => $basePath,
236 'styles' => [ 'styles.less' ],
238 $module->setName( 'test.less' );
239 $styles = $module->getStyles( $context );
240 $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
243 public static function providePackedModules() {
246 'Example from makePackedModulesString doc comment',
247 [ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ],
248 'foo.bar,baz|bar.baz,quux',
251 'Example from expandModuleNames doc comment',
252 [ 'jquery.foo', 'jquery.bar', 'jquery.ui.baz', 'jquery.ui.quux' ],
253 'jquery.foo,bar|jquery.ui.baz,quux',
256 'Regression fixed in r88706 with dotless names',
257 [ 'foo', 'bar', 'baz' ],
261 'Prefixless modules after a prefixed module',
262 [ 'single.module', 'foobar', 'foobaz' ],
263 'single.module|foobar,foobaz',
267 [ 'foo', 'foo.baz', 'baz.quux', 'foo.bar' ],
268 'foo|foo.baz,bar|baz.quux',
269 [ 'foo', 'foo.baz', 'foo.bar', 'baz.quux' ],
275 * @dataProvider providePackedModules
276 * @covers ResourceLoader::makePackedModulesString
278 public function testMakePackedModulesString( $desc, $modules, $packed ) {
279 $this->assertEquals( $packed, ResourceLoader
::makePackedModulesString( $modules ), $desc );
283 * @dataProvider providePackedModules
284 * @covers ResourceLoaderContext::expandModuleNames
286 public function testExpandModuleNames( $desc, $modules, $packed, $unpacked = null ) {
288 $unpacked ?
: $modules,
289 ResourceLoaderContext
::expandModuleNames( $packed ),
294 public static function provideAddSource() {
296 [ 'foowiki', 'https://example.org/w/load.php', 'foowiki' ],
297 [ 'foowiki', [ 'loadScript' => 'https://example.org/w/load.php' ], 'foowiki' ],
300 'foowiki' => 'https://example.org/w/load.php',
301 'bazwiki' => 'https://example.com/w/load.php',
304 [ 'foowiki', 'bazwiki' ]
310 * @dataProvider provideAddSource
311 * @covers ResourceLoader::addSource
312 * @covers ResourceLoader::getSources
314 public function testAddSource( $name, $info, $expected ) {
315 $rl = new ResourceLoader
;
316 $rl->addSource( $name, $info );
317 if ( is_array( $expected ) ) {
318 foreach ( $expected as $source ) {
319 $this->assertArrayHasKey( $source, $rl->getSources() );
322 $this->assertArrayHasKey( $expected, $rl->getSources() );
327 * @covers ResourceLoader::addSource
329 public function testAddSourceDupe() {
330 $rl = new ResourceLoader
;
331 $this->setExpectedException( 'MWException', 'ResourceLoader duplicate source addition error' );
332 $rl->addSource( 'foo', 'https://example.org/w/load.php' );
333 $rl->addSource( 'foo', 'https://example.com/w/load.php' );
337 * @covers ResourceLoader::addSource
339 public function testAddSourceInvalid() {
340 $rl = new ResourceLoader
;
341 $this->setExpectedException( 'MWException', 'with no "loadScript" key' );
342 $rl->addSource( 'foo', [ 'x' => 'https://example.org/w/load.php' ] );
345 public static function provideLoaderImplement() {
348 'title' => 'Implement scripts, styles and messages',
350 'name' => 'test.example',
351 'scripts' => 'mw.example();',
352 'styles' => [ 'css' => [ '.mw-example {}' ] ],
353 'messages' => [ 'example' => '' ],
356 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
367 'title' => 'Implement scripts',
369 'name' => 'test.example',
370 'scripts' => 'mw.example();',
373 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
378 'title' => 'Implement styles',
380 'name' => 'test.example',
382 'styles' => [ 'css' => [ '.mw-example {}' ] ],
384 'expected' => 'mw.loader.implement( "test.example", [], {
391 'title' => 'Implement scripts and messages',
393 'name' => 'test.example',
394 'scripts' => 'mw.example();',
395 'messages' => [ 'example' => '' ],
397 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
404 'title' => 'Implement scripts and templates',
406 'name' => 'test.example',
407 'scripts' => 'mw.example();',
408 'templates' => [ 'example.html' => '' ],
410 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
417 'title' => 'Implement unwrapped user script',
420 'scripts' => 'mw.example( 1 );',
423 'expected' => 'mw.loader.implement( "user", "mw.example( 1 );" );',
429 * @dataProvider provideLoaderImplement
430 * @covers ResourceLoader::makeLoaderImplementScript
431 * @covers ResourceLoader::trimArray
433 public function testMakeLoaderImplementScript( $case ) {
436 'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' )
438 ResourceLoader
::clearCache();
439 $this->setMwGlobals( 'wgResourceLoaderDebug', true );
441 $rl = TestingAccessWrapper
::newFromClass( 'ResourceLoader' );
444 $rl->makeLoaderImplementScript(
446 ( $case['wrap'] && is_string( $case['scripts'] ) )
447 ?
new XmlJsCode( $case['scripts'] )
457 * @covers ResourceLoader::makeLoaderImplementScript
459 public function testMakeLoaderImplementScriptInvalid() {
460 $this->setExpectedException( 'MWException', 'Invalid scripts error' );
461 $rl = TestingAccessWrapper
::newFromClass( 'ResourceLoader' );
462 $rl->makeLoaderImplementScript(
472 * @covers ResourceLoader::makeLoaderRegisterScript
474 public function testMakeLoaderRegisterScript() {
476 'mw.loader.register( [
482 ResourceLoader
::makeLoaderRegisterScript( [
483 [ 'test.name', '1234567' ],
485 'Nested array parameter'
489 'mw.loader.register( "test.name", "1234567" );',
490 ResourceLoader
::makeLoaderRegisterScript(
494 'Variadic parameters'
499 * @covers ResourceLoader::makeLoaderSourcesScript
501 public function testMakeLoaderSourcesScript() {
503 'mw.loader.addSource( "local", "/w/load.php" );',
504 ResourceLoader
::makeLoaderSourcesScript( 'local', '/w/load.php' )
507 'mw.loader.addSource( {
508 "local": "/w/load.php"
510 ResourceLoader
::makeLoaderSourcesScript( [ 'local' => '/w/load.php' ] )
513 'mw.loader.addSource( {
514 "local": "/w/load.php",
515 "example": "https://example.org/w/load.php"
517 ResourceLoader
::makeLoaderSourcesScript( [
518 'local' => '/w/load.php',
519 'example' => 'https://example.org/w/load.php'
523 'mw.loader.addSource( [] );',
524 ResourceLoader
::makeLoaderSourcesScript( [] )
528 private static function fakeSources() {
531 'loadScript' => '//example.org/w/load.php',
532 'apiScript' => '//example.org/w/api.php',
535 'loadScript' => '//example.com/w/load.php',
536 'apiScript' => '//example.com/w/api.php',
542 * @covers ResourceLoader::getLoadScript
544 public function testGetLoadScript() {
545 $this->setMwGlobals( 'wgResourceLoaderSources', [] );
546 $rl = new ResourceLoader();
547 $sources = self
::fakeSources();
548 $rl->addSource( $sources );
549 foreach ( [ 'examplewiki', 'example2wiki' ] as $name ) {
550 $this->assertEquals( $rl->getLoadScript( $name ), $sources[$name]['loadScript'] );
554 $rl->getLoadScript( 'thiswasneverreigstered' );
555 $this->assertTrue( false, 'ResourceLoader::getLoadScript should have thrown an exception' );
556 } catch ( MWException
$e ) {
557 $this->assertTrue( true );
561 protected function getFailFerryMock() {
562 $mock = $this->getMockBuilder( ResourceLoaderTestModule
::class )
563 ->setMethods( [ 'getScript' ] )
565 $mock->method( 'getScript' )->will( $this->throwException(
566 new Exception( 'Ferry not found' )
571 protected function getSimpleModuleMock( $script = '' ) {
572 $mock = $this->getMockBuilder( ResourceLoaderTestModule
::class )
573 ->setMethods( [ 'getScript' ] )
575 $mock->method( 'getScript' )->willReturn( $script );
580 * @covers ResourceLoader::getCombinedVersion
582 public function testGetCombinedVersion() {
583 $rl = new EmptyResourceLoader();
585 'foo' => self
::getSimpleModuleMock(),
586 'ferry' => self
::getFailFerryMock(),
587 'bar' => self
::getSimpleModuleMock(),
589 $context = $this->getResourceLoaderContext( [], $rl );
592 ResourceLoader
::makeHash( self
::BLANK_VERSION
),
593 $rl->getCombinedVersion( $context, [ 'foo' ] ),
597 // Verify that getCombinedVersion() does not throw when ferry fails.
598 // Instead it gracefully continues to combine the remaining modules.
600 ResourceLoader
::makeHash( self
::BLANK_VERSION
. self
::BLANK_VERSION
),
601 $rl->getCombinedVersion( $context, [ 'foo', 'ferry', 'bar' ] ),
602 'compute foo+ferry+bar (T152266)'
607 * Verify that when building module content in a load.php response,
608 * an exception from one module will not break script output from
611 public function testMakeModuleResponseError() {
613 'foo' => self
::getSimpleModuleMock( 'foo();' ),
614 'ferry' => self
::getFailFerryMock(),
615 'bar' => self
::getSimpleModuleMock( 'bar();' ),
617 $rl = new EmptyResourceLoader();
618 $rl->register( $modules );
619 $context = $this->getResourceLoaderContext(
621 'modules' => 'foo|ferry|bar',
627 $response = $rl->makeModuleResponse( $context, $modules );
628 $errors = $rl->getErrors();
630 $this->assertCount( 1, $errors );
631 $this->assertRegExp( '/Ferry not found/', $errors[0] );
633 'foo();bar();mw.loader.state( {
643 * Verify that when building the startup module response,
644 * an exception from one module class will not break the entire
645 * startup module response. See T152266.
647 public function testMakeModuleResponseStartupError() {
648 $rl = new EmptyResourceLoader();
650 'foo' => self
::getSimpleModuleMock( 'foo();' ),
651 'ferry' => self
::getFailFerryMock(),
652 'bar' => self
::getSimpleModuleMock( 'bar();' ),
653 'startup' => [ 'class' => 'ResourceLoaderStartUpModule' ],
655 $context = $this->getResourceLoaderContext(
657 'modules' => 'startup',
664 [ 'foo', 'ferry', 'bar', 'startup' ],
665 $rl->getModuleNames(),
669 $modules = [ 'startup' => $rl->getModule( 'startup' ) ];
670 $response = $rl->makeModuleResponse( $context, $modules );
671 $errors = $rl->getErrors();
673 $this->assertRegExp( '/Ferry not found/', $errors[0] );
674 $this->assertCount( 1, $errors );
676 '/isCompatible.*function startUp/s',
678 'startup response undisrupted (T152266)'
681 '/register\([^)]+"ferry",\s*""/s',
683 'startup response registers broken module'
686 '/state\([^)]+"ferry":\s*"error"/s',
688 'startup response sets state to error'