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()
117 * @covers ResourceLoader::isModuleRegistered
119 public function testIsModuleRegistered() {
120 $rl = new EmptyResourceLoader();
121 $rl->register( 'test', new ResourceLoaderTestModule() );
122 $this->assertTrue( $rl->isModuleRegistered( 'test' ) );
123 $this->assertFalse( $rl->isModuleRegistered( 'test.unknown' ) );
127 * @covers ResourceLoader::getModule
129 public function testGetModuleUnknown() {
130 $rl = new EmptyResourceLoader();
131 $this->assertSame( null, $rl->getModule( 'test' ) );
135 * @covers ResourceLoader::getModule
137 public function testGetModuleClass() {
138 $rl = new EmptyResourceLoader();
139 $rl->register( 'test', [ 'class' => ResourceLoaderTestModule
::class ] );
140 $this->assertInstanceOf(
141 ResourceLoaderTestModule
::class,
142 $rl->getModule( 'test' )
147 * @covers ResourceLoader::getModule
149 public function testGetModuleClassDefault() {
150 $rl = new EmptyResourceLoader();
151 $rl->register( 'test', [] );
152 $this->assertInstanceOf(
153 ResourceLoaderFileModule
::class,
154 $rl->getModule( 'test' ),
155 'Array-style module registrations default to FileModule'
160 * @covers ResourceLoaderFileModule::compileLessFile
162 public function testLessFileCompilation() {
163 $context = $this->getResourceLoaderContext();
164 $basePath = __DIR__
. '/../../data/less/module';
165 $module = new ResourceLoaderFileModule( [
166 'localBasePath' => $basePath,
167 'styles' => [ 'styles.less' ],
169 $module->setName( 'test.less' );
170 $styles = $module->getStyles( $context );
171 $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
174 public static function providePackedModules() {
177 'Example from makePackedModulesString doc comment',
178 [ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ],
179 'foo.bar,baz|bar.baz,quux',
182 'Example from expandModuleNames doc comment',
183 [ 'jquery.foo', 'jquery.bar', 'jquery.ui.baz', 'jquery.ui.quux' ],
184 'jquery.foo,bar|jquery.ui.baz,quux',
187 'Regression fixed in r88706 with dotless names',
188 [ 'foo', 'bar', 'baz' ],
192 'Prefixless modules after a prefixed module',
193 [ 'single.module', 'foobar', 'foobaz' ],
194 'single.module|foobar,foobaz',
198 [ 'foo', 'foo.baz', 'baz.quux', 'foo.bar' ],
199 'foo|foo.baz,bar|baz.quux',
200 [ 'foo', 'foo.baz', 'foo.bar', 'baz.quux' ],
206 * @dataProvider providePackedModules
207 * @covers ResourceLoader::makePackedModulesString
209 public function testMakePackedModulesString( $desc, $modules, $packed ) {
210 $this->assertEquals( $packed, ResourceLoader
::makePackedModulesString( $modules ), $desc );
214 * @dataProvider providePackedModules
215 * @covers ResourceLoaderContext::expandModuleNames
217 public function testExpandModuleNames( $desc, $modules, $packed, $unpacked = null ) {
219 $unpacked ?
: $modules,
220 ResourceLoaderContext
::expandModuleNames( $packed ),
225 public static function provideAddSource() {
227 [ 'foowiki', 'https://example.org/w/load.php', 'foowiki' ],
228 [ 'foowiki', [ 'loadScript' => 'https://example.org/w/load.php' ], 'foowiki' ],
231 'foowiki' => 'https://example.org/w/load.php',
232 'bazwiki' => 'https://example.com/w/load.php',
235 [ 'foowiki', 'bazwiki' ]
241 * @dataProvider provideAddSource
242 * @covers ResourceLoader::addSource
243 * @covers ResourceLoader::getSources
245 public function testAddSource( $name, $info, $expected ) {
246 $rl = new ResourceLoader
;
247 $rl->addSource( $name, $info );
248 if ( is_array( $expected ) ) {
249 foreach ( $expected as $source ) {
250 $this->assertArrayHasKey( $source, $rl->getSources() );
253 $this->assertArrayHasKey( $expected, $rl->getSources() );
258 * @covers ResourceLoader::addSource
260 public function testAddSourceDupe() {
261 $rl = new ResourceLoader
;
262 $this->setExpectedException( 'MWException', 'ResourceLoader duplicate source addition error' );
263 $rl->addSource( 'foo', 'https://example.org/w/load.php' );
264 $rl->addSource( 'foo', 'https://example.com/w/load.php' );
268 * @covers ResourceLoader::addSource
270 public function testAddSourceInvalid() {
271 $rl = new ResourceLoader
;
272 $this->setExpectedException( 'MWException', 'with no "loadScript" key' );
273 $rl->addSource( 'foo', [ 'x' => 'https://example.org/w/load.php' ] );
276 public static function provideLoaderImplement() {
279 'title' => 'Implement scripts, styles and messages',
281 'name' => 'test.example',
282 'scripts' => 'mw.example();',
283 'styles' => [ 'css' => [ '.mw-example {}' ] ],
284 'messages' => [ 'example' => '' ],
287 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
298 'title' => 'Implement scripts',
300 'name' => 'test.example',
301 'scripts' => 'mw.example();',
304 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
309 'title' => 'Implement styles',
311 'name' => 'test.example',
313 'styles' => [ 'css' => [ '.mw-example {}' ] ],
315 'expected' => 'mw.loader.implement( "test.example", [], {
322 'title' => 'Implement scripts and messages',
324 'name' => 'test.example',
325 'scripts' => 'mw.example();',
326 'messages' => [ 'example' => '' ],
328 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
335 'title' => 'Implement scripts and templates',
337 'name' => 'test.example',
338 'scripts' => 'mw.example();',
339 'templates' => [ 'example.html' => '' ],
341 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
348 'title' => 'Implement unwrapped user script',
351 'scripts' => 'mw.example( 1 );',
354 'expected' => 'mw.loader.implement( "user", "mw.example( 1 );" );',
360 * @dataProvider provideLoaderImplement
361 * @covers ResourceLoader::makeLoaderImplementScript
362 * @covers ResourceLoader::trimArray
364 public function testMakeLoaderImplementScript( $case ) {
367 'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' )
369 ResourceLoader
::clearCache();
370 $this->setMwGlobals( 'wgResourceLoaderDebug', true );
372 $rl = TestingAccessWrapper
::newFromClass( 'ResourceLoader' );
375 $rl->makeLoaderImplementScript(
377 ( $case['wrap'] && is_string( $case['scripts'] ) )
378 ?
new XmlJsCode( $case['scripts'] )
388 * @covers ResourceLoader::makeLoaderImplementScript
390 public function testMakeLoaderImplementScriptInvalid() {
391 $this->setExpectedException( 'MWException', 'Invalid scripts error' );
392 $rl = TestingAccessWrapper
::newFromClass( 'ResourceLoader' );
393 $rl->makeLoaderImplementScript(
403 * @covers ResourceLoader::makeLoaderRegisterScript
405 public function testMakeLoaderRegisterScript() {
407 'mw.loader.register( [
413 ResourceLoader
::makeLoaderRegisterScript( [
414 [ 'test.name', '1234567' ],
416 'Nested array parameter'
420 'mw.loader.register( "test.name", "1234567" );',
421 ResourceLoader
::makeLoaderRegisterScript(
425 'Variadic parameters'
430 * @covers ResourceLoader::makeLoaderSourcesScript
432 public function testMakeLoaderSourcesScript() {
434 'mw.loader.addSource( "local", "/w/load.php" );',
435 ResourceLoader
::makeLoaderSourcesScript( 'local', '/w/load.php' )
438 'mw.loader.addSource( {
439 "local": "/w/load.php"
441 ResourceLoader
::makeLoaderSourcesScript( [ 'local' => '/w/load.php' ] )
444 'mw.loader.addSource( {
445 "local": "/w/load.php",
446 "example": "https://example.org/w/load.php"
448 ResourceLoader
::makeLoaderSourcesScript( [
449 'local' => '/w/load.php',
450 'example' => 'https://example.org/w/load.php'
454 'mw.loader.addSource( [] );',
455 ResourceLoader
::makeLoaderSourcesScript( [] )
459 private static function fakeSources() {
462 'loadScript' => '//example.org/w/load.php',
463 'apiScript' => '//example.org/w/api.php',
466 'loadScript' => '//example.com/w/load.php',
467 'apiScript' => '//example.com/w/api.php',
473 * @covers ResourceLoader::getLoadScript
475 public function testGetLoadScript() {
476 $this->setMwGlobals( 'wgResourceLoaderSources', [] );
477 $rl = new ResourceLoader();
478 $sources = self
::fakeSources();
479 $rl->addSource( $sources );
480 foreach ( [ 'examplewiki', 'example2wiki' ] as $name ) {
481 $this->assertEquals( $rl->getLoadScript( $name ), $sources[$name]['loadScript'] );
485 $rl->getLoadScript( 'thiswasneverreigstered' );
486 $this->assertTrue( false, 'ResourceLoader::getLoadScript should have thrown an exception' );
487 } catch ( MWException
$e ) {
488 $this->assertTrue( true );
492 protected function getFailFerryMock() {
493 $mock = $this->getMockBuilder( ResourceLoaderTestModule
::class )
494 ->setMethods( [ 'getScript' ] )
496 $mock->method( 'getScript' )->will( $this->throwException(
497 new Exception( 'Ferry not found' )
502 protected function getSimpleModuleMock( $script = '' ) {
503 $mock = $this->getMockBuilder( ResourceLoaderTestModule
::class )
504 ->setMethods( [ 'getScript' ] )
506 $mock->method( 'getScript' )->willReturn( $script );
511 * @covers ResourceLoader::getCombinedVersion
513 public function testGetCombinedVersion() {
514 $rl = new EmptyResourceLoader();
516 'foo' => self
::getSimpleModuleMock(),
517 'ferry' => self
::getFailFerryMock(),
518 'bar' => self
::getSimpleModuleMock(),
520 $context = $this->getResourceLoaderContext( [], $rl );
523 ResourceLoader
::makeHash( self
::BLANK_VERSION
),
524 $rl->getCombinedVersion( $context, [ 'foo' ] ),
528 // Verify that getCombinedVersion() does not throw when ferry fails.
529 // Instead it gracefully continues to combine the remaining modules.
531 ResourceLoader
::makeHash( self
::BLANK_VERSION
. self
::BLANK_VERSION
),
532 $rl->getCombinedVersion( $context, [ 'foo', 'ferry', 'bar' ] ),
533 'compute foo+ferry+bar (T152266)'
538 * Verify that when building module content in a load.php response,
539 * an exception from one module will not break script output from
542 public function testMakeModuleResponseError() {
544 'foo' => self
::getSimpleModuleMock( 'foo();' ),
545 'ferry' => self
::getFailFerryMock(),
546 'bar' => self
::getSimpleModuleMock( 'bar();' ),
548 $rl = new EmptyResourceLoader();
549 $rl->register( $modules );
550 $context = $this->getResourceLoaderContext(
552 'modules' => 'foo|ferry|bar',
558 $response = $rl->makeModuleResponse( $context, $modules );
559 $errors = $rl->getErrors();
561 $this->assertCount( 1, $errors );
562 $this->assertRegExp( '/Ferry not found/', $errors[0] );
564 'foo();bar();mw.loader.state( {
574 * Verify that when building the startup module response,
575 * an exception from one module class will not break the entire
576 * startup module response. See T152266.
578 public function testMakeModuleResponseStartupError() {
579 $rl = new EmptyResourceLoader();
581 'foo' => self
::getSimpleModuleMock( 'foo();' ),
582 'ferry' => self
::getFailFerryMock(),
583 'bar' => self
::getSimpleModuleMock( 'bar();' ),
584 'startup' => [ 'class' => 'ResourceLoaderStartUpModule' ],
586 $context = $this->getResourceLoaderContext(
588 'modules' => 'startup',
595 [ 'foo', 'ferry', 'bar', 'startup' ],
596 $rl->getModuleNames(),
600 $modules = [ 'startup' => $rl->getModule( 'startup' ) ];
601 $response = $rl->makeModuleResponse( $context, $modules );
602 $errors = $rl->getErrors();
604 $this->assertRegExp( '/Ferry not found/', $errors[0] );
605 $this->assertCount( 1, $errors );
607 '/isCompatible.*function startUp/s',
609 'startup response undisrupted (T152266)'
612 '/register\([^)]+"ferry",\s*""/s',
614 'startup response registers broken module'
617 '/state\([^)]+"ferry":\s*"error"/s',
619 'startup response sets state to error'