resourceloader: Add unit tests for ResourceLoader::isFileModule
[lhc/web/wiklou.git] / tests / phpunit / includes / resourceloader / ResourceLoaderTest.php
1 <?php
2
3 use Wikimedia\TestingAccessWrapper;
4
5 class ResourceLoaderTest extends ResourceLoaderTestCase {
6
7 protected function setUp() {
8 parent::setUp();
9
10 $this->setMwGlobals( [
11 'wgResourceLoaderLESSImportPaths' => [
12 dirname( dirname( __DIR__ ) ) . '/data/less/common',
13 ],
14 'wgResourceLoaderLESSVars' => [
15 'foo' => '2px',
16 'Foo' => '#eeeeee',
17 'bar' => 5,
18 ],
19 // Clear ResourceLoaderGetConfigVars hooks (called by StartupModule)
20 // to avoid notices during testMakeModuleResponse for missing
21 // wgResourceLoaderLESSVars keys in extension hooks.
22 'wgHooks' => [],
23 'wgShowExceptionDetails' => true,
24 ] );
25 }
26
27 /**
28 * Ensure the ResourceLoaderRegisterModules hook is called.
29 *
30 * @covers ResourceLoader::__construct
31 */
32 public function testConstructRegistrationHook() {
33 $resourceLoaderRegisterModulesHook = false;
34
35 $this->setMwGlobals( 'wgHooks', [
36 'ResourceLoaderRegisterModules' => [
37 function ( &$resourceLoader ) use ( &$resourceLoaderRegisterModulesHook ) {
38 $resourceLoaderRegisterModulesHook = true;
39 }
40 ]
41 ] );
42
43 $unused = new ResourceLoader();
44 $this->assertTrue(
45 $resourceLoaderRegisterModulesHook,
46 'Hook ResourceLoaderRegisterModules called'
47 );
48 }
49
50 /**
51 * @covers ResourceLoader::register
52 * @covers ResourceLoader::getModule
53 */
54 public function testRegisterValidObject() {
55 $module = new ResourceLoaderTestModule();
56 $resourceLoader = new EmptyResourceLoader();
57 $resourceLoader->register( 'test', $module );
58 $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
59 }
60
61 /**
62 * @covers ResourceLoader::register
63 * @covers ResourceLoader::getModule
64 */
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' ) );
72 }
73
74 /**
75 * @covers ResourceLoader::register
76 */
77 public function testRegisterEmptyString() {
78 $module = new ResourceLoaderTestModule();
79 $resourceLoader = new EmptyResourceLoader();
80 $resourceLoader->register( '', $module );
81 $this->assertEquals( $module, $resourceLoader->getModule( '' ) );
82 }
83
84 /**
85 * @covers ResourceLoader::register
86 */
87 public function testRegisterInvalidName() {
88 $resourceLoader = new EmptyResourceLoader();
89 $this->setExpectedException( 'MWException', "name 'test!invalid' is invalid" );
90 $resourceLoader->register( 'test!invalid', new ResourceLoaderTestModule() );
91 }
92
93 /**
94 * @covers ResourceLoader::register
95 */
96 public function testRegisterInvalidType() {
97 $resourceLoader = new EmptyResourceLoader();
98 $this->setExpectedException( 'MWException', 'ResourceLoader module info type error' );
99 $resourceLoader->register( 'test', new stdClass() );
100 }
101
102 /**
103 * @covers ResourceLoader::getModuleNames
104 */
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() );
110 $this->assertEquals(
111 [ 'test.foo', 'test.bar' ],
112 $resourceLoader->getModuleNames()
113 );
114 }
115
116 public function provideTestIsFileModule() {
117 $fileModuleObj = $this->getMockBuilder( ResourceLoaderFileModule::class )
118 ->disableOriginalConstructor()
119 ->getMock();
120 return [
121 'object' => [ false,
122 new ResourceLoaderTestModule()
123 ],
124 'FileModule object' => [ false,
125 $fileModuleObj
126 ],
127 'simple empty' => [ true,
128 []
129 ],
130 'simple scripts' => [ true,
131 [ 'scripts' => 'example.js' ]
132 ],
133 'simple scripts, raw and targets' => [ true, [
134 'scripts' => [ 'a.js', 'b.js' ],
135 'raw' => true,
136 'targets' => [ 'desktop', 'mobile' ],
137 ] ],
138 'FileModule' => [ true,
139 [ 'class' => ResourceLoaderFileModule::class, 'scripts' => 'example.js' ]
140 ],
141 'TestModule' => [ false,
142 [ 'class' => ResourceLoaderTestModule::class, 'scripts' => 'example.js' ]
143 ],
144 'SkinModule (FileModule subclass)' => [ true,
145 [ 'class' => ResourceLoaderSkinModule::class, 'scripts' => 'example.js' ]
146 ],
147 'JqueryMsgModule (FileModule subclass)' => [ true, [
148 'class' => ResourceLoaderJqueryMsgModule::class,
149 'scripts' => 'example.js',
150 ] ],
151 'WikiModule' => [ false, [
152 'class' => ResourceLoaderWikiModule::class,
153 'scripts' => [ 'MediaWiki:Example.js' ],
154 ] ],
155 ];
156 }
157
158 /**
159 * @dataProvider provideTestIsFileModule
160 * @covers ResourceLoader::isFileModule
161 */
162 public function testIsFileModule( $expected, $module ) {
163 $rl = TestingAccessWrapper::newFromObject( new EmptyResourceLoader() );
164 $rl->register( 'test', $module );
165 $this->assertSame( $expected, $rl->isFileModule( 'test' ) );
166 }
167
168 /**
169 * @covers ResourceLoader::isModuleRegistered
170 */
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' ) );
176 }
177
178 /**
179 * @covers ResourceLoader::getModule
180 */
181 public function testGetModuleUnknown() {
182 $rl = new EmptyResourceLoader();
183 $this->assertSame( null, $rl->getModule( 'test' ) );
184 }
185
186 /**
187 * @covers ResourceLoader::getModule
188 */
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' )
195 );
196 }
197
198 /**
199 * @covers ResourceLoader::getModule
200 */
201 public function testGetModuleFactory() {
202 $factory = function( array $info ) {
203 $this->assertArrayHasKey( 'kitten', $info );
204 return new ResourceLoaderTestModule( $info );
205 };
206
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' )
212 );
213 }
214
215 /**
216 * @covers ResourceLoader::getModule
217 */
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'
225 );
226 }
227
228 /**
229 * @covers ResourceLoaderFileModule::compileLessFile
230 */
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' ],
237 ] );
238 $module->setName( 'test.less' );
239 $styles = $module->getStyles( $context );
240 $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
241 }
242
243 public static function providePackedModules() {
244 return [
245 [
246 'Example from makePackedModulesString doc comment',
247 [ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ],
248 'foo.bar,baz|bar.baz,quux',
249 ],
250 [
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',
254 ],
255 [
256 'Regression fixed in r88706 with dotless names',
257 [ 'foo', 'bar', 'baz' ],
258 'foo,bar,baz',
259 ],
260 [
261 'Prefixless modules after a prefixed module',
262 [ 'single.module', 'foobar', 'foobaz' ],
263 'single.module|foobar,foobaz',
264 ],
265 [
266 'Ordering',
267 [ 'foo', 'foo.baz', 'baz.quux', 'foo.bar' ],
268 'foo|foo.baz,bar|baz.quux',
269 [ 'foo', 'foo.baz', 'foo.bar', 'baz.quux' ],
270 ]
271 ];
272 }
273
274 /**
275 * @dataProvider providePackedModules
276 * @covers ResourceLoader::makePackedModulesString
277 */
278 public function testMakePackedModulesString( $desc, $modules, $packed ) {
279 $this->assertEquals( $packed, ResourceLoader::makePackedModulesString( $modules ), $desc );
280 }
281
282 /**
283 * @dataProvider providePackedModules
284 * @covers ResourceLoaderContext::expandModuleNames
285 */
286 public function testExpandModuleNames( $desc, $modules, $packed, $unpacked = null ) {
287 $this->assertEquals(
288 $unpacked ?: $modules,
289 ResourceLoaderContext::expandModuleNames( $packed ),
290 $desc
291 );
292 }
293
294 public static function provideAddSource() {
295 return [
296 [ 'foowiki', 'https://example.org/w/load.php', 'foowiki' ],
297 [ 'foowiki', [ 'loadScript' => 'https://example.org/w/load.php' ], 'foowiki' ],
298 [
299 [
300 'foowiki' => 'https://example.org/w/load.php',
301 'bazwiki' => 'https://example.com/w/load.php',
302 ],
303 null,
304 [ 'foowiki', 'bazwiki' ]
305 ]
306 ];
307 }
308
309 /**
310 * @dataProvider provideAddSource
311 * @covers ResourceLoader::addSource
312 * @covers ResourceLoader::getSources
313 */
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() );
320 }
321 } else {
322 $this->assertArrayHasKey( $expected, $rl->getSources() );
323 }
324 }
325
326 /**
327 * @covers ResourceLoader::addSource
328 */
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' );
334 }
335
336 /**
337 * @covers ResourceLoader::addSource
338 */
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' ] );
343 }
344
345 public static function provideLoaderImplement() {
346 return [
347 [ [
348 'title' => 'Implement scripts, styles and messages',
349
350 'name' => 'test.example',
351 'scripts' => 'mw.example();',
352 'styles' => [ 'css' => [ '.mw-example {}' ] ],
353 'messages' => [ 'example' => '' ],
354 'templates' => [],
355
356 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
357 mw.example();
358 }, {
359 "css": [
360 ".mw-example {}"
361 ]
362 }, {
363 "example": ""
364 } );',
365 ] ],
366 [ [
367 'title' => 'Implement scripts',
368
369 'name' => 'test.example',
370 'scripts' => 'mw.example();',
371 'styles' => [],
372
373 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
374 mw.example();
375 } );',
376 ] ],
377 [ [
378 'title' => 'Implement styles',
379
380 'name' => 'test.example',
381 'scripts' => [],
382 'styles' => [ 'css' => [ '.mw-example {}' ] ],
383
384 'expected' => 'mw.loader.implement( "test.example", [], {
385 "css": [
386 ".mw-example {}"
387 ]
388 } );',
389 ] ],
390 [ [
391 'title' => 'Implement scripts and messages',
392
393 'name' => 'test.example',
394 'scripts' => 'mw.example();',
395 'messages' => [ 'example' => '' ],
396
397 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
398 mw.example();
399 }, {}, {
400 "example": ""
401 } );',
402 ] ],
403 [ [
404 'title' => 'Implement scripts and templates',
405
406 'name' => 'test.example',
407 'scripts' => 'mw.example();',
408 'templates' => [ 'example.html' => '' ],
409
410 'expected' => 'mw.loader.implement( "test.example", function ( $, jQuery, require, module ) {
411 mw.example();
412 }, {}, {}, {
413 "example.html": ""
414 } );',
415 ] ],
416 [ [
417 'title' => 'Implement unwrapped user script',
418
419 'name' => 'user',
420 'scripts' => 'mw.example( 1 );',
421 'wrap' => false,
422
423 'expected' => 'mw.loader.implement( "user", "mw.example( 1 );" );',
424 ] ],
425 ];
426 }
427
428 /**
429 * @dataProvider provideLoaderImplement
430 * @covers ResourceLoader::makeLoaderImplementScript
431 * @covers ResourceLoader::trimArray
432 */
433 public function testMakeLoaderImplementScript( $case ) {
434 $case += [
435 'wrap' => true,
436 'styles' => [], 'templates' => [], 'messages' => new XmlJsCode( '{}' )
437 ];
438 ResourceLoader::clearCache();
439 $this->setMwGlobals( 'wgResourceLoaderDebug', true );
440
441 $rl = TestingAccessWrapper::newFromClass( 'ResourceLoader' );
442 $this->assertEquals(
443 $case['expected'],
444 $rl->makeLoaderImplementScript(
445 $case['name'],
446 ( $case['wrap'] && is_string( $case['scripts'] ) )
447 ? new XmlJsCode( $case['scripts'] )
448 : $case['scripts'],
449 $case['styles'],
450 $case['messages'],
451 $case['templates']
452 )
453 );
454 }
455
456 /**
457 * @covers ResourceLoader::makeLoaderImplementScript
458 */
459 public function testMakeLoaderImplementScriptInvalid() {
460 $this->setExpectedException( 'MWException', 'Invalid scripts error' );
461 $rl = TestingAccessWrapper::newFromClass( 'ResourceLoader' );
462 $rl->makeLoaderImplementScript(
463 'test', // name
464 123, // scripts
465 null, // styles
466 null, // messages
467 null // templates
468 );
469 }
470
471 /**
472 * @covers ResourceLoader::makeLoaderRegisterScript
473 */
474 public function testMakeLoaderRegisterScript() {
475 $this->assertEquals(
476 'mw.loader.register( [
477 [
478 "test.name",
479 "1234567"
480 ]
481 ] );',
482 ResourceLoader::makeLoaderRegisterScript( [
483 [ 'test.name', '1234567' ],
484 ] ),
485 'Nested array parameter'
486 );
487
488 $this->assertEquals(
489 'mw.loader.register( "test.name", "1234567" );',
490 ResourceLoader::makeLoaderRegisterScript(
491 'test.name',
492 '1234567'
493 ),
494 'Variadic parameters'
495 );
496 }
497
498 /**
499 * @covers ResourceLoader::makeLoaderSourcesScript
500 */
501 public function testMakeLoaderSourcesScript() {
502 $this->assertEquals(
503 'mw.loader.addSource( "local", "/w/load.php" );',
504 ResourceLoader::makeLoaderSourcesScript( 'local', '/w/load.php' )
505 );
506 $this->assertEquals(
507 'mw.loader.addSource( {
508 "local": "/w/load.php"
509 } );',
510 ResourceLoader::makeLoaderSourcesScript( [ 'local' => '/w/load.php' ] )
511 );
512 $this->assertEquals(
513 'mw.loader.addSource( {
514 "local": "/w/load.php",
515 "example": "https://example.org/w/load.php"
516 } );',
517 ResourceLoader::makeLoaderSourcesScript( [
518 'local' => '/w/load.php',
519 'example' => 'https://example.org/w/load.php'
520 ] )
521 );
522 $this->assertEquals(
523 'mw.loader.addSource( [] );',
524 ResourceLoader::makeLoaderSourcesScript( [] )
525 );
526 }
527
528 private static function fakeSources() {
529 return [
530 'examplewiki' => [
531 'loadScript' => '//example.org/w/load.php',
532 'apiScript' => '//example.org/w/api.php',
533 ],
534 'example2wiki' => [
535 'loadScript' => '//example.com/w/load.php',
536 'apiScript' => '//example.com/w/api.php',
537 ],
538 ];
539 }
540
541 /**
542 * @covers ResourceLoader::getLoadScript
543 */
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'] );
551 }
552
553 try {
554 $rl->getLoadScript( 'thiswasneverreigstered' );
555 $this->assertTrue( false, 'ResourceLoader::getLoadScript should have thrown an exception' );
556 } catch ( MWException $e ) {
557 $this->assertTrue( true );
558 }
559 }
560
561 protected function getFailFerryMock() {
562 $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
563 ->setMethods( [ 'getScript' ] )
564 ->getMock();
565 $mock->method( 'getScript' )->will( $this->throwException(
566 new Exception( 'Ferry not found' )
567 ) );
568 return $mock;
569 }
570
571 protected function getSimpleModuleMock( $script = '' ) {
572 $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
573 ->setMethods( [ 'getScript' ] )
574 ->getMock();
575 $mock->method( 'getScript' )->willReturn( $script );
576 return $mock;
577 }
578
579 /**
580 * @covers ResourceLoader::getCombinedVersion
581 */
582 public function testGetCombinedVersion() {
583 $rl = new EmptyResourceLoader();
584 $rl->register( [
585 'foo' => self::getSimpleModuleMock(),
586 'ferry' => self::getFailFerryMock(),
587 'bar' => self::getSimpleModuleMock(),
588 ] );
589 $context = $this->getResourceLoaderContext( [], $rl );
590
591 $this->assertEquals(
592 ResourceLoader::makeHash( self::BLANK_VERSION ),
593 $rl->getCombinedVersion( $context, [ 'foo' ] ),
594 'compute foo'
595 );
596
597 // Verify that getCombinedVersion() does not throw when ferry fails.
598 // Instead it gracefully continues to combine the remaining modules.
599 $this->assertEquals(
600 ResourceLoader::makeHash( self::BLANK_VERSION . self::BLANK_VERSION ),
601 $rl->getCombinedVersion( $context, [ 'foo', 'ferry', 'bar' ] ),
602 'compute foo+ferry+bar (T152266)'
603 );
604 }
605
606 /**
607 * Verify that when building module content in a load.php response,
608 * an exception from one module will not break script output from
609 * other modules.
610 */
611 public function testMakeModuleResponseError() {
612 $modules = [
613 'foo' => self::getSimpleModuleMock( 'foo();' ),
614 'ferry' => self::getFailFerryMock(),
615 'bar' => self::getSimpleModuleMock( 'bar();' ),
616 ];
617 $rl = new EmptyResourceLoader();
618 $rl->register( $modules );
619 $context = $this->getResourceLoaderContext(
620 [
621 'modules' => 'foo|ferry|bar',
622 'only' => 'scripts',
623 ],
624 $rl
625 );
626
627 $response = $rl->makeModuleResponse( $context, $modules );
628 $errors = $rl->getErrors();
629
630 $this->assertCount( 1, $errors );
631 $this->assertRegExp( '/Ferry not found/', $errors[0] );
632 $this->assertEquals(
633 'foo();bar();mw.loader.state( {
634 "ferry": "error",
635 "foo": "ready",
636 "bar": "ready"
637 } );',
638 $response
639 );
640 }
641
642 /**
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.
646 */
647 public function testMakeModuleResponseStartupError() {
648 $rl = new EmptyResourceLoader();
649 $rl->register( [
650 'foo' => self::getSimpleModuleMock( 'foo();' ),
651 'ferry' => self::getFailFerryMock(),
652 'bar' => self::getSimpleModuleMock( 'bar();' ),
653 'startup' => [ 'class' => 'ResourceLoaderStartUpModule' ],
654 ] );
655 $context = $this->getResourceLoaderContext(
656 [
657 'modules' => 'startup',
658 'only' => 'scripts',
659 ],
660 $rl
661 );
662
663 $this->assertEquals(
664 [ 'foo', 'ferry', 'bar', 'startup' ],
665 $rl->getModuleNames(),
666 'getModuleNames'
667 );
668
669 $modules = [ 'startup' => $rl->getModule( 'startup' ) ];
670 $response = $rl->makeModuleResponse( $context, $modules );
671 $errors = $rl->getErrors();
672
673 $this->assertRegExp( '/Ferry not found/', $errors[0] );
674 $this->assertCount( 1, $errors );
675 $this->assertRegExp(
676 '/isCompatible.*function startUp/s',
677 $response,
678 'startup response undisrupted (T152266)'
679 );
680 $this->assertRegExp(
681 '/register\([^)]+"ferry",\s*""/s',
682 $response,
683 'startup response registers broken module'
684 );
685 $this->assertRegExp(
686 '/state\([^)]+"ferry":\s*"error"/s',
687 $response,
688 'startup response sets state to error'
689 );
690 }
691 }