2618e781a149c0a9c83bb450d7e71266274638b9
3 class ResourceLoaderTest
extends ResourceLoaderTestCase
{
5 protected function setUp() {
9 'wgResourceLoaderLESSImportPaths' => [
10 dirname( dirname( __DIR__
) ) . '/data/less/common',
12 'wgResourceLoaderLESSVars' => [
17 // Clear ResourceLoaderGetConfigVars hooks (called by StartupModule)
18 // to avoid notices during testMakeModuleResponse for missing
19 // wgResourceLoaderLESSVars keys in extension hooks.
21 'wgShowExceptionDetails' => true,
26 * Ensure the ResourceLoaderRegisterModules hook is called.
28 * @covers ResourceLoader::__construct
30 public function testConstructRegistrationHook() {
31 $resourceLoaderRegisterModulesHook = false;
33 $this->setMwGlobals( 'wgHooks', [
34 'ResourceLoaderRegisterModules' => [
35 function ( &$resourceLoader ) use ( &$resourceLoaderRegisterModulesHook ) {
36 $resourceLoaderRegisterModulesHook = true;
41 $unused = new ResourceLoader();
43 $resourceLoaderRegisterModulesHook,
44 'Hook ResourceLoaderRegisterModules called'
49 * @covers ResourceLoader::register
50 * @covers ResourceLoader::getModule
52 public function testRegisterValidObject() {
53 $module = new ResourceLoaderTestModule();
54 $resourceLoader = new EmptyResourceLoader();
55 $resourceLoader->register( 'test', $module );
56 $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
60 * @covers ResourceLoader::register
61 * @covers ResourceLoader::getModule
63 public function testRegisterValidArray() {
64 $module = new ResourceLoaderTestModule();
65 $resourceLoader = new EmptyResourceLoader();
66 // Covers case of register() setting $rl->moduleInfos,
67 // but $rl->modules lazy-populated by getModule()
68 $resourceLoader->register( 'test', [ 'object' => $module ] );
69 $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
73 * @covers ResourceLoader::register
75 public function testRegisterEmptyString() {
76 $module = new ResourceLoaderTestModule();
77 $resourceLoader = new EmptyResourceLoader();
78 $resourceLoader->register( '', $module );
79 $this->assertEquals( $module, $resourceLoader->getModule( '' ) );
83 * @covers ResourceLoader::register
85 public function testRegisterInvalidName() {
86 $resourceLoader = new EmptyResourceLoader();
87 $this->setExpectedException( 'MWException', "name 'test!invalid' is invalid" );
88 $resourceLoader->register( 'test!invalid', new ResourceLoaderTestModule() );
92 * @covers ResourceLoader::register
94 public function testRegisterInvalidType() {
95 $resourceLoader = new EmptyResourceLoader();
96 $this->setExpectedException( 'MWException', 'ResourceLoader module info type error' );
97 $resourceLoader->register( 'test', new stdClass() );
101 * @covers ResourceLoader::getModuleNames
103 public function testGetModuleNames() {
104 // Use an empty one so that core and extension modules don't get in.
105 $resourceLoader = new EmptyResourceLoader();
106 $resourceLoader->register( 'test.foo', new ResourceLoaderTestModule() );
107 $resourceLoader->register( 'test.bar', new ResourceLoaderTestModule() );
109 [ 'test.foo', 'test.bar' ],
110 $resourceLoader->getModuleNames()
115 * @covers ResourceLoader::isModuleRegistered
117 public function testIsModuleRegistered() {
118 $rl = new EmptyResourceLoader();
119 $rl->register( 'test', new ResourceLoaderTestModule() );
120 $this->assertTrue( $rl->isModuleRegistered( 'test' ) );
121 $this->assertFalse( $rl->isModuleRegistered( 'test.unknown' ) );
125 * @covers ResourceLoader::getModule
127 public function testGetModuleUnknown() {
128 $rl = new EmptyResourceLoader();
129 $this->assertSame( null, $rl->getModule( 'test' ) );
133 * @covers ResourceLoader::getModule
135 public function testGetModuleClass() {
136 $rl = new EmptyResourceLoader();
137 $rl->register( 'test', [ 'class' => ResourceLoaderTestModule
::class ] );
138 $this->assertInstanceOf(
139 ResourceLoaderTestModule
::class,
140 $rl->getModule( 'test' )
145 * @covers ResourceLoader::getModule
147 public function testGetModuleClassDefault() {
148 $rl = new EmptyResourceLoader();
149 $rl->register( 'test', [] );
150 $this->assertInstanceOf(
151 ResourceLoaderFileModule
::class,
152 $rl->getModule( 'test' ),
153 'Array-style module registrations default to FileModule'
158 * @covers ResourceLoaderFileModule::compileLessFile
160 public function testLessFileCompilation() {
161 $context = $this->getResourceLoaderContext();
162 $basePath = __DIR__
. '/../../data/less/module';
163 $module = new ResourceLoaderFileModule( [
164 'localBasePath' => $basePath,
165 'styles' => [ 'styles.less' ],
167 $module->setName( 'test.less' );
168 $styles = $module->getStyles( $context );
169 $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
172 public static function providePackedModules() {
175 'Example from makePackedModulesString doc comment',
176 [ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ],
177 'foo.bar,baz|bar.baz,quux',
180 'Example from expandModuleNames doc comment',
181 [ 'jquery.foo', 'jquery.bar', 'jquery.ui.baz', 'jquery.ui.quux' ],
182 'jquery.foo,bar|jquery.ui.baz,quux',
185 'Regression fixed in r88706 with dotless names',
186 [ 'foo', 'bar', 'baz' ],
190 'Prefixless modules after a prefixed module',
191 [ 'single.module', 'foobar', 'foobaz' ],
192 'single.module|foobar,foobaz',
196 [ 'foo', 'foo.baz', 'baz.quux', 'foo.bar' ],
197 'foo|foo.baz,bar|baz.quux',
198 [ 'foo', 'foo.baz', 'foo.bar', 'baz.quux' ],
204 * @dataProvider providePackedModules
205 * @covers ResourceLoader::makePackedModulesString
207 public function testMakePackedModulesString( $desc, $modules, $packed ) {
208 $this->assertEquals( $packed, ResourceLoader
::makePackedModulesString( $modules ), $desc );
212 * @dataProvider providePackedModules
213 * @covers ResourceLoaderContext::expandModuleNames
215 public function testExpandModuleNames( $desc, $modules, $packed, $unpacked = null ) {
217 $unpacked ?
: $modules,
218 ResourceLoaderContext
::expandModuleNames( $packed ),
223 public static function provideAddSource() {
225 [ 'foowiki', 'https://example.org/w/load.php', 'foowiki' ],
226 [ 'foowiki', [ 'loadScript' => 'https://example.org/w/load.php' ], 'foowiki' ],
229 'foowiki' => 'https://example.org/w/load.php',
230 'bazwiki' => 'https://example.com/w/load.php',
233 [ 'foowiki', 'bazwiki' ]
239 * @dataProvider provideAddSource
240 * @covers ResourceLoader::addSource
241 * @covers ResourceLoader::getSources
243 public function testAddSource( $name, $info, $expected ) {
244 $rl = new ResourceLoader
;
245 $rl->addSource( $name, $info );
246 if ( is_array( $expected ) ) {
247 foreach ( $expected as $source ) {
248 $this->assertArrayHasKey( $source, $rl->getSources() );
251 $this->assertArrayHasKey( $expected, $rl->getSources() );
256 * @covers ResourceLoader::addSource
258 public function testAddSourceDupe() {
259 $rl = new ResourceLoader
;
260 $this->setExpectedException( 'MWException', 'ResourceLoader duplicate source addition error' );
261 $rl->addSource( 'foo', 'https://example.org/w/load.php' );
262 $rl->addSource( 'foo', 'https://example.com/w/load.php' );
266 * @covers ResourceLoader::addSource
268 public function testAddSourceInvalid() {
269 $rl = new ResourceLoader
;
270 $this->setExpectedException( 'MWException', 'with no "loadScript" key' );
271 $rl->addSource( 'foo', [ 'x' => 'https://example.org/w/load.php' ] );
274 public static function provideLoaderImplement() {
277 'title' => 'Implement scripts, styles and messages',
279 'name' => 'test.example',
280 'scripts' => 'mw.example();',
281 'styles' => [ 'css' => [ '.mw-example {}' ] ],
282 'messages' => [ 'example' => '' ],
285 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
296 'title' => 'Implement scripts',
298 'name' => 'test.example',
299 'scripts' => 'mw.example();',
302 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
307 'title' => 'Implement styles',
309 'name' => 'test.example',
311 'styles' => [ 'css' => [ '.mw-example {}' ] ],
313 'expected' => 'mw.loader.implement( "test.example", [], {
320 'title' => 'Implement scripts and messages',
322 'name' => 'test.example',
323 'scripts' => 'mw.example();',
324 'messages' => [ 'example' => '' ],
326 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
333 'title' => 'Implement scripts and templates',
335 'name' => 'test.example',
336 'scripts' => 'mw.example();',
337 'templates' => [ 'example.html' => '' ],
339 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
346 'title' => 'Implement unwrapped user script',
349 'scripts' => 'mw.example( 1 );',
352 'expected' => 'mw.loader.implement( "user", "mw.example( 1 );" );',
358 * @dataProvider provideLoaderImplement
359 * @covers ResourceLoader::makeLoaderImplementScript
360 * @covers ResourceLoader::trimArray
362 public function testMakeLoaderImplementScript( $case ) {
365 'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' )
367 ResourceLoader
::clearCache();
368 $this->setMwGlobals( 'wgResourceLoaderDebug', true );
370 $rl = TestingAccessWrapper
::newFromClass( 'ResourceLoader' );
373 $rl->makeLoaderImplementScript(
375 ( $case['wrap'] && is_string( $case['scripts'] ) )
376 ?
new XmlJsCode( $case['scripts'] )
386 * @covers ResourceLoader::makeLoaderImplementScript
388 public function testMakeLoaderImplementScriptInvalid() {
389 $this->setExpectedException( 'MWException', 'Invalid scripts error' );
390 $rl = TestingAccessWrapper
::newFromClass( 'ResourceLoader' );
391 $rl->makeLoaderImplementScript(
401 * @covers ResourceLoader::makeLoaderRegisterScript
403 public function testMakeLoaderRegisterScript() {
405 'mw.loader.register( [
411 ResourceLoader
::makeLoaderRegisterScript( [
412 [ 'test.name', '1234567' ],
414 'Nested array parameter'
418 'mw.loader.register( "test.name", "1234567" );',
419 ResourceLoader
::makeLoaderRegisterScript(
423 'Variadic parameters'
428 * @covers ResourceLoader::makeLoaderSourcesScript
430 public function testMakeLoaderSourcesScript() {
432 'mw.loader.addSource( "local", "/w/load.php" );',
433 ResourceLoader
::makeLoaderSourcesScript( 'local', '/w/load.php' )
436 'mw.loader.addSource( {
437 "local": "/w/load.php"
439 ResourceLoader
::makeLoaderSourcesScript( [ 'local' => '/w/load.php' ] )
442 'mw.loader.addSource( {
443 "local": "/w/load.php",
444 "example": "https://example.org/w/load.php"
446 ResourceLoader
::makeLoaderSourcesScript( [
447 'local' => '/w/load.php',
448 'example' => 'https://example.org/w/load.php'
452 'mw.loader.addSource( [] );',
453 ResourceLoader
::makeLoaderSourcesScript( [] )
457 private static function fakeSources() {
460 'loadScript' => '//example.org/w/load.php',
461 'apiScript' => '//example.org/w/api.php',
464 'loadScript' => '//example.com/w/load.php',
465 'apiScript' => '//example.com/w/api.php',
471 * @covers ResourceLoader::getLoadScript
473 public function testGetLoadScript() {
474 $this->setMwGlobals( 'wgResourceLoaderSources', [] );
475 $rl = new ResourceLoader();
476 $sources = self
::fakeSources();
477 $rl->addSource( $sources );
478 foreach ( [ 'examplewiki', 'example2wiki' ] as $name ) {
479 $this->assertEquals( $rl->getLoadScript( $name ), $sources[$name]['loadScript'] );
483 $rl->getLoadScript( 'thiswasneverreigstered' );
484 $this->assertTrue( false, 'ResourceLoader::getLoadScript should have thrown an exception' );
485 } catch ( MWException
$e ) {
486 $this->assertTrue( true );
490 protected function getFailFerryMock() {
491 $mock = $this->getMockBuilder( ResourceLoaderTestModule
::class )
492 ->setMethods( [ 'getScript' ] )
494 $mock->method( 'getScript' )->will( $this->throwException(
495 new Exception( 'Ferry not found' )
500 protected function getSimpleModuleMock( $script = '' ) {
501 $mock = $this->getMockBuilder( ResourceLoaderTestModule
::class )
502 ->setMethods( [ 'getScript' ] )
504 $mock->method( 'getScript' )->willReturn( $script );
509 * @covers ResourceLoader::getCombinedVersion
511 public function testGetCombinedVersion() {
512 $rl = new EmptyResourceLoader();
514 'foo' => self
::getSimpleModuleMock(),
515 'ferry' => self
::getFailFerryMock(),
516 'bar' => self
::getSimpleModuleMock(),
518 $context = $this->getResourceLoaderContext( [], $rl );
521 ResourceLoader
::makeHash( self
::BLANK_VERSION
),
522 $rl->getCombinedVersion( $context, [ 'foo' ] ),
526 // Verify that getCombinedVersion() does not throw when ferry fails.
527 // Instead it gracefully continues to combine the remaining modules.
529 ResourceLoader
::makeHash( self
::BLANK_VERSION
. self
::BLANK_VERSION
),
530 $rl->getCombinedVersion( $context, [ 'foo', 'ferry', 'bar' ] ),
531 'compute foo+ferry+bar (T152266)'
536 * Verify that when building module content in a load.php response,
537 * an exception from one module will not break script output from
540 public function testMakeModuleResponseError() {
542 'foo' => self
::getSimpleModuleMock( 'foo();' ),
543 'ferry' => self
::getFailFerryMock(),
544 'bar' => self
::getSimpleModuleMock( 'bar();' ),
546 $rl = new EmptyResourceLoader();
547 $rl->register( $modules );
548 $context = $this->getResourceLoaderContext(
550 'modules' => 'foo|ferry|bar',
556 $response = $rl->makeModuleResponse( $context, $modules );
557 $errors = $rl->getErrors();
559 $this->assertCount( 1, $errors );
560 $this->assertRegExp( '/Ferry not found/', $errors[0] );
562 'foo();bar();mw.loader.state( {
572 * Verify that when building the startup module response,
573 * an exception from one module class will not break the entire
574 * startup module response. See T152266.
576 public function testMakeModuleResponseStartupError() {
577 $rl = new EmptyResourceLoader();
579 'foo' => self
::getSimpleModuleMock( 'foo();' ),
580 'ferry' => self
::getFailFerryMock(),
581 'bar' => self
::getSimpleModuleMock( 'bar();' ),
582 'startup' => [ 'class' => 'ResourceLoaderStartUpModule' ],
584 $context = $this->getResourceLoaderContext(
586 'modules' => 'startup',
593 [ 'foo', 'ferry', 'bar', 'startup' ],
594 $rl->getModuleNames(),
598 $modules = [ 'startup' => $rl->getModule( 'startup' ) ];
599 $response = $rl->makeModuleResponse( $context, $modules );
600 $errors = $rl->getErrors();
602 $this->assertRegExp( '/Ferry not found/', $errors[0] );
603 $this->assertCount( 1, $errors );
605 '/isCompatible.*function startUp/s',
607 'startup response undisrupted (T152266)'
610 '/register\([^)]+"ferry",\s*""/s',
612 'startup response registers broken module'
615 '/state\([^)]+"ferry":\s*"error"/s',
617 'startup response sets state to error'