Merge ".mailmap: Correct two contributor names"
[mediawiki.git] / tests / phpunit / includes / ResourceLoader / FileModuleTest.php
blobe77ee37cf648aef3ee9e702d949137e756852e6e
1 <?php
3 namespace MediaWiki\Tests\ResourceLoader;
5 use LogicException;
6 use MediaWiki\Config\HashConfig;
7 use MediaWiki\MainConfigNames;
8 use MediaWiki\MediaWikiServices;
9 use MediaWiki\ResourceLoader\FileModule;
10 use MediaWiki\ResourceLoader\FilePath;
11 use MediaWiki\ResourceLoader\ResourceLoader;
12 use MediaWiki\Tests\Unit\DummyServicesTrait;
13 use RuntimeException;
14 use SkinFactory;
15 use Wikimedia\TestingAccessWrapper;
17 /**
18 * @group ResourceLoader
19 * @covers \MediaWiki\ResourceLoader\FileModule
21 class FileModuleTest extends ResourceLoaderTestCase {
22 use DummyServicesTrait;
24 protected function setUp(): void {
25 parent::setUp();
27 $skinFactory = new SkinFactory( $this->getDummyObjectFactory(), [] );
28 // The empty spec shouldn't matter since this test should never call it
29 $skinFactory->register(
30 'fakeskin',
31 'FakeSkin',
34 $this->setService( 'SkinFactory', $skinFactory );
36 // This test is not expected to query any database
37 $this->getServiceContainer()->disableStorage();
40 private static function getModules() {
41 $base = [
42 'localBasePath' => __DIR__,
45 return [
46 'noTemplateModule' => [],
48 'htmlTemplateModule' => $base + [
49 'templates' => [
50 'templates/template.html',
51 'templates/template2.html',
55 'htmlTemplateUnknown' => $base + [
56 'templates' => [
57 'templates/notfound.html',
61 'aliasedHtmlTemplateModule' => $base + [
62 'templates' => [
63 'foo.html' => 'templates/template.html',
64 'bar.html' => 'templates/template2.html',
68 'templateModuleHandlebars' => $base + [
69 'templates' => [
70 'templates/template_awesome.handlebars',
74 'aliasFooFromBar' => $base + [
75 'templates' => [
76 'foo.foo' => 'templates/template.bar',
82 public static function providerTemplateDependencies() {
83 $modules = self::getModules();
85 return [
87 $modules['noTemplateModule'],
88 [],
91 $modules['htmlTemplateModule'],
93 'mediawiki.template',
97 $modules['templateModuleHandlebars'],
99 'mediawiki.template',
100 'mediawiki.template.handlebars',
104 $modules['aliasFooFromBar'],
106 'mediawiki.template',
107 'mediawiki.template.foo',
114 * @dataProvider providerTemplateDependencies
116 public function testTemplateDependencies( $module, $expected ) {
117 $rl = new FileModule( $module );
118 $rl->setName( 'testing' );
119 $this->assertEquals( $expected, $rl->getDependencies() );
122 public function testGetScript() {
123 $localBasePath = __DIR__ . '/../../data/resourceloader';
124 $remoteBasePath = '/w';
125 $module = new FileModule( [
126 'localBasePath' => $localBasePath,
127 'remoteBasePath' => $remoteBasePath,
128 'scripts' => [ 'script-nosemi.js', 'script-comment.js' ],
129 ] );
130 $module->setName( 'testing' );
131 $ctx = $this->getResourceLoaderContext();
132 $this->assertEquals(
134 'plainScripts' => [
135 'script-nosemi.js' => [
136 'name' => 'script-nosemi.js',
137 'content' => "/* eslint-disable */\nmw.foo()\n",
138 'type' => 'script',
139 'filePath' => new FilePath(
140 'script-nosemi.js',
141 $localBasePath,
142 $remoteBasePath
145 'script-comment.js' => [
146 'name' => 'script-comment.js',
147 'content' => "/* eslint-disable */\nmw.foo()\n// mw.bar();\n",
148 'type' => 'script',
149 'filePath' => new FilePath(
150 'script-comment.js',
151 $localBasePath,
152 $remoteBasePath
157 $module->getScript( $ctx )
162 * @covers \MediaWiki\ResourceLoader\FileModule
163 * @covers \MediaWiki\ResourceLoader\Module
164 * @covers \MediaWiki\ResourceLoader\ResourceLoader
166 public function testGetURLsForDebug() {
167 $ctx = $this->getResourceLoaderContext();
168 $module = new FileModule( [
169 'localBasePath' => __DIR__ . '/../../data/resourceloader',
170 'remoteBasePath' => '/w/something',
171 'styles' => [ 'simple.css' ],
172 'scripts' => [ 'script-comment.js' ],
173 ] );
174 $module->setName( 'testing' );
175 $module->setConfig( $ctx->getResourceLoader()->getConfig() );
177 $this->assertEquals(
179 'https://example.org/w/something/script-comment.js'
181 $module->getScriptURLsForDebug( $ctx ),
182 'script urls'
184 $this->assertEquals(
185 [ 'all' => [
186 '/w/something/simple.css'
187 ] ],
188 $module->getStyleURLsForDebug( $ctx ),
189 'style urls'
193 public function testGetAllSkinStyleFiles() {
194 $baseParams = [
195 'scripts' => [
196 'foo.js',
197 'bar.js',
199 'styles' => [
200 'foo.css',
201 'bar.css' => [ 'media' => 'print' ],
202 'screen.less' => [ 'media' => 'screen' ],
203 'screen-query.css' => [ 'media' => 'screen and (min-width: 400px)' ],
205 'skinStyles' => [
206 'default' => 'quux-fallback.less',
207 'fakeskin' => [
208 'baz-vector.css',
209 'quux-vector.less',
212 'messages' => [
213 'hello',
214 'world',
218 $module = new FileModule( $baseParams );
219 $module->setName( 'testing' );
221 $this->assertEquals(
223 'foo.css',
224 'baz-vector.css',
225 'quux-vector.less',
226 'quux-fallback.less',
227 'bar.css',
228 'screen.less',
229 'screen-query.css',
231 array_map( 'basename', $module->getAllStyleFiles() )
236 * Strip @noflip annotations from CSS code.
237 * @param string $css
238 * @return string
240 private static function stripNoflip( $css ) {
241 return str_replace( '/*@noflip*/ ', '', $css );
245 * Confirm that 'ResourceModuleSkinStyles' skin attributes get injected
246 * into the module, and have their file contents read correctly from their
247 * own (out-of-module) directories.
249 * @covers \MediaWiki\ResourceLoader\FileModule
250 * @covers \MediaWiki\ResourceLoader\ResourceLoader
252 public function testInjectSkinStyles() {
253 $moduleDir = __DIR__ . '/../../data/resourceloader';
254 $skinDir = __DIR__ . '/../../data/resourceloader/myskin';
255 $rl = new ResourceLoader( new HashConfig( self::getSettings() ) );
256 $rl->setModuleSkinStyles( [
257 'fakeskin' => [
258 'localBasePath' => $skinDir,
259 'testing' => [
260 'override.css',
263 ] );
264 $rl->register( 'testing', [
265 'localBasePath' => $moduleDir,
266 'styles' => [ 'simple.css' ],
267 ] );
268 $ctx = $this->getResourceLoaderContext( [ 'skin' => 'fakeskin' ], $rl );
270 $module = $rl->getModule( 'testing' );
271 $this->assertInstanceOf( FileModule::class, $module );
272 $this->assertEquals(
273 [ 'all' => ".example { color: blue; }\n\n.override { line-height: 2; }\n" ],
274 $module->getStyles( $ctx )
279 * Verify what happens when you mix @embed and @noflip.
281 public function testMixedCssAnnotations() {
282 $basePath = __DIR__ . '/../../data/css';
283 $testModule = new ResourceLoaderFileTestModule( [
284 'localBasePath' => $basePath,
285 'styles' => [ 'test.css' ],
286 ] );
287 $testModule->setName( 'testing' );
288 $expectedModule = new ResourceLoaderFileTestModule( [
289 'localBasePath' => $basePath,
290 'styles' => [ 'expected.css' ],
291 ] );
292 $expectedModule->setName( 'testing' );
294 $contextLtr = $this->getResourceLoaderContext( [
295 'lang' => 'en',
296 'dir' => 'ltr',
297 ] );
298 $contextRtl = $this->getResourceLoaderContext( [
299 'lang' => 'he',
300 'dir' => 'rtl',
301 ] );
303 // Since we want to compare the effect of @noflip+@embed against the effect of just @embed, and
304 // the @noflip annotations are always preserved, we need to strip them first.
305 $this->assertEquals(
306 $expectedModule->getStyles( $contextLtr ),
307 self::stripNoflip( $testModule->getStyles( $contextLtr ) ),
308 "/*@noflip*/ with /*@embed*/ gives correct results in LTR mode"
310 $this->assertEquals(
311 $expectedModule->getStyles( $contextLtr ),
312 self::stripNoflip( $testModule->getStyles( $contextRtl ) ),
313 "/*@noflip*/ with /*@embed*/ gives correct results in RTL mode"
317 public function testCssFlipping() {
318 $plain = new ResourceLoaderFileTestModule( [
319 'localBasePath' => __DIR__ . '/../../data/resourceloader',
320 'styles' => [ 'direction.css' ],
321 ] );
322 $plain->setName( 'test' );
324 $context = $this->getResourceLoaderContext( [ 'lang' => 'en', 'dir' => 'ltr' ] );
325 $this->assertEquals(
326 [ 'all' => ".example { text-align: left; }\n" ],
327 $plain->getStyles( $context ),
328 'Unchanged styles in LTR mode'
330 $context = $this->getResourceLoaderContext( [ 'lang' => 'he', 'dir' => 'rtl' ] );
331 $this->assertEquals(
332 [ 'all' => ".example { text-align: right; }\n" ],
333 $plain->getStyles( $context ),
334 'Flipped styles in RTL mode'
337 $noflip = new ResourceLoaderFileTestModule( [
338 'localBasePath' => __DIR__ . '/../../data/resourceloader',
339 'styles' => [ 'direction.css' ],
340 'noflip' => true,
341 ] );
342 $noflip->setName( 'test' );
343 $this->assertEquals(
344 [ 'all' => ".example { text-align: right; }\n" ],
345 $plain->getStyles( $context ),
346 'Unchanged styles in RTL mode with noflip at module level'
351 * Test reading files from elsewhere than localBasePath using FilePath.
353 * The use of FilePath objects resembles the way that ResourceLoader::getModule()
354 * injects additional files when 'ResourceModuleSkinStyles' or 'OOUIThemePaths'
355 * skin attributes apply to a given module.
357 public function testResourceLoaderFilePath() {
358 $basePath = __DIR__ . '/../../data/blahblah';
359 $filePath = __DIR__ . '/../../data/rlfilepath';
360 $testModule = new FileModule( [
361 'localBasePath' => $basePath,
362 'remoteBasePath' => 'blahblah',
363 'styles' => new FilePath( 'style.css', $filePath, 'rlfilepath' ),
364 'skinStyles' => [
365 'vector' => new FilePath( 'skinStyle.css', $filePath, 'rlfilepath' ),
367 'scripts' => new FilePath( 'script.js', $filePath, 'rlfilepath' ),
368 'templates' => new FilePath( 'template.html', $filePath, 'rlfilepath' ),
369 ] );
370 $testModule->setName( 'testModule' );
371 $expectedModule = new FileModule( [
372 'localBasePath' => $filePath,
373 'remoteBasePath' => 'rlfilepath',
374 'styles' => 'style.css',
375 'skinStyles' => [
376 'vector' => 'skinStyle.css',
378 'scripts' => 'script.js',
379 'templates' => 'template.html',
380 ] );
381 $expectedModule->setName( 'expectedModule' );
383 $context = $this->getResourceLoaderContext();
384 $this->assertEquals(
385 $expectedModule->getModuleContent( $context ),
386 $testModule->getModuleContent( $context ),
387 "Using ResourceLoaderFilePath works correctly"
391 public static function providerGetTemplates() {
392 $modules = self::getModules();
394 return [
396 $modules['noTemplateModule'],
400 $modules['templateModuleHandlebars'],
402 'templates/template_awesome.handlebars' => "wow\n",
406 $modules['htmlTemplateModule'],
408 'templates/template.html' => "<strong>hello</strong>\n",
409 'templates/template2.html' => "<div>goodbye</div>\n",
413 $modules['aliasedHtmlTemplateModule'],
415 'foo.html' => "<strong>hello</strong>\n",
416 'bar.html' => "<div>goodbye</div>\n",
420 $modules['htmlTemplateUnknown'],
421 false,
427 * @dataProvider providerGetTemplates
429 public function testGetTemplates( $module, $expected ) {
430 $rl = new FileModule( $module );
431 $rl->setName( 'testing' );
433 if ( $expected === false ) {
434 $this->expectException( RuntimeException::class );
435 $rl->getTemplates();
436 } else {
437 $this->assertEquals( $expected, $rl->getTemplates() );
441 public function testBomConcatenation() {
442 $basePath = __DIR__ . '/../../data/css';
443 $testModule = new ResourceLoaderFileTestModule( [
444 'localBasePath' => $basePath,
445 'styles' => [ 'bom.css' ],
446 ] );
447 $testModule->setName( 'testing' );
448 $this->assertEquals(
449 "\xef\xbb\xbf.efbbbf",
450 substr( file_get_contents( "$basePath/bom.css" ), 0, 10 ),
451 'File has leading BOM'
454 $context = $this->getResourceLoaderContext();
455 $this->assertEquals(
456 [ 'all' => ".efbbbf_bom_char_at_start_of_file {}\n" ],
457 $testModule->getStyles( $context ),
458 'Leading BOM removed when concatenating files'
462 public function testLessFileCompilation() {
463 $context = $this->getResourceLoaderContext();
464 $basePath = __DIR__ . '/../../data/less/module';
465 $module = new ResourceLoaderFileTestModule( [
466 'localBasePath' => $basePath,
467 'styles' => [ 'styles.less' ],
468 'lessVars' => [ 'foo' => '2px', 'Foo' => '#eeeeee' ]
469 ] );
470 $module->setName( 'test.less' );
471 $module->setConfig( $context->getResourceLoader()->getConfig() );
472 $styles = $module->getStyles( $context );
473 $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
476 public static function provideGetVersionHash() {
477 $a = [];
478 $b = [
479 'lessVars' => [ 'key' => 'value' ],
481 yield 'with and without Less variables' => [ $a, $b, false ];
483 $a = [
484 'lessVars' => [ 'key' => 'value1' ],
486 $b = [
487 'lessVars' => [ 'key' => 'value2' ],
489 yield 'different Less variables' => [ $a, $b, false ];
491 $x = [
492 'lessVars' => [ 'key' => 'value' ],
494 yield 'identical Less variables' => [ $x, $x, true ];
496 $a = [
497 'packageFiles' => [ [ 'name' => 'data.json', 'callback' => static function () {
498 return [ 'aaa' ];
499 } ] ]
501 $b = [
502 'packageFiles' => [ [ 'name' => 'data.json', 'callback' => static function () {
503 return [ 'bbb' ];
504 } ] ]
506 yield 'packageFiles with different callback' => [ $a, $b, false ];
508 $a = [
509 'packageFiles' => [ [ 'name' => 'aaa.json', 'callback' => static function () {
510 return [ 'x' ];
511 } ] ]
513 $b = [
514 'packageFiles' => [ [ 'name' => 'bbb.json', 'callback' => static function () {
515 return [ 'x' ];
516 } ] ]
518 yield 'packageFiles with different file name and a callback' => [ $a, $b, false ];
520 $a = [
521 'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => static function () {
522 return [ 'A-version' ];
523 }, 'callback' => static function () {
524 throw new LogicException( 'Unexpected computation' );
525 } ] ]
527 $b = [
528 'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => static function () {
529 return [ 'B-version' ];
530 }, 'callback' => static function () {
531 throw new LogicException( 'Unexpected computation' );
532 } ] ]
534 yield 'packageFiles with different versionCallback' => [ $a, $b, false ];
536 $a = [
537 'packageFiles' => [ [ 'name' => 'aaa.json',
538 'versionCallback' => static function () {
539 return [ 'X-version' ];
541 'callback' => static function () {
542 throw new LogicException( 'Unexpected computation' );
546 $b = [
547 'packageFiles' => [ [ 'name' => 'bbb.json',
548 'versionCallback' => static function () {
549 return [ 'X-version' ];
551 'callback' => static function () {
552 throw new LogicException( 'Unexpected computation' );
556 yield 'packageFiles with different file name and a versionCallback' => [ $a, $b, false ];
560 * @dataProvider provideGetVersionHash
562 public function testGetVersionHash( $a, $b, $isEqual ) {
563 $context = $this->getResourceLoaderContext( [ 'debug' => 'false' ] );
565 $moduleA = new ResourceLoaderFileTestModule( $a );
566 $moduleA->setConfig( $context->getResourceLoader()->getConfig() );
567 $versionA = $moduleA->getVersionHash( $context );
568 $moduleB = new ResourceLoaderFileTestModule( $b );
569 $moduleB->setConfig( $context->getResourceLoader()->getConfig() );
570 $versionB = $moduleB->getVersionHash( $context );
572 $this->assertSame(
573 $isEqual,
574 ( $versionA === $versionB ),
575 'Whether versions hashes are equal'
579 public static function provideGetScriptPackageFiles() {
580 $basePath = __DIR__ . '/../../data/resourceloader';
581 $basePathB = __DIR__ . '/../../data/resourceloader-b';
582 $base = [ 'localBasePath' => $basePath ];
583 $commentScript = file_get_contents( "$basePath/script-comment.js" );
584 $nosemiScript = file_get_contents( "$basePath/script-nosemi.js" );
585 $nosemiBScript = file_get_contents( "$basePathB/script-nosemi.js" );
586 $vueComponentDebug = trim( file_get_contents( "$basePath/vue-component-output-debug.js.txt" ) );
587 $vueComponentNonDebug = trim( file_get_contents( "$basePath/vue-component-output-nondebug.js.txt" ) );
588 $config = MediaWikiServices::getInstance()->getMainConfig();
589 return [
590 'plain package' => [
591 $base + [
592 'packageFiles' => [
593 'script-comment.js',
594 'script-nosemi.js'
598 'files' => [
599 'script-comment.js' => [
600 'type' => 'script',
601 'content' => $commentScript,
602 'filePath' => 'script-comment.js'
604 'script-nosemi.js' => [
605 'type' => 'script',
606 'content' => $nosemiScript,
607 'filePath' => 'script-nosemi.js'
610 'main' => 'script-comment.js'
613 'explicit main file' => [
614 $base + [
615 'packageFiles' => [
616 [ 'name' => 'init.js', 'file' => 'script-comment.js', 'main' => true ],
617 [ 'name' => 'nosemi.js', 'file' => 'script-nosemi.js' ],
621 'files' => [
622 'init.js' => [
623 'type' => 'script',
624 'content' => $commentScript,
625 'filePath' => 'script-comment.js',
627 'nosemi.js' => [
628 'type' => 'script',
629 'content' => $nosemiScript,
630 'filePath' => 'script-nosemi.js',
633 'main' => 'init.js'
636 'package file with callback' => [
637 $base + [
638 'packageFiles' => [
639 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ] ],
640 'sample.json',
641 [ 'name' => 'bar.js', 'content' => "console.log('Hello');" ],
643 'name' => 'data.json',
644 'callback' => static function ( $context, $config, $extra ) {
645 return [ 'langCode' => $context->getLanguage(), 'extra' => $extra ];
647 'callbackParam' => [ 'a' => 'b' ],
649 [ 'name' => 'config.json', 'config' => [
650 'Sitename',
651 'server' => 'ServerName',
652 ] ],
656 'files' => [
657 'foo.json' => [
658 'type' => 'data',
659 'content' => [ 'Hello' => 'world' ],
660 'virtualFilePath' => 'foo.json',
662 'sample.json' => [
663 'type' => 'data',
664 'content' => (object)[ 'foo' => 'bar', 'answer' => 42 ],
665 'filePath' => 'sample.json',
667 'bar.js' => [
668 'type' => 'script',
669 'content' => "console.log('Hello');",
670 'virtualFilePath' => 'bar.js',
672 'data.json' => [
673 'type' => 'data',
674 'content' => [ 'langCode' => 'fy', 'extra' => [ 'a' => 'b' ] ],
675 'virtualFilePath' => 'data.json',
677 'config.json' => [
678 'type' => 'data',
679 'content' => [
680 'Sitename' => $config->get( MainConfigNames::Sitename ),
681 'server' => $config->get( MainConfigNames::ServerName ),
683 'virtualFilePath' => 'config.json',
686 'main' => 'bar.js'
689 'lang' => 'fy'
692 'package file with callback and versionCallback' => [
693 $base + [
694 'packageFiles' => [
695 [ 'name' => 'bar.js', 'content' => "console.log('Hello');" ],
697 'name' => 'data.json',
698 'versionCallback' => static function ( $context ) {
699 return 'x';
701 'callback' => static function ( $context, $config, $extra ) {
702 return [ 'langCode' => $context->getLanguage(), 'extra' => $extra ];
704 'callbackParam' => [ 'A', 'B' ]
709 'files' => [
710 'bar.js' => [
711 'type' => 'script',
712 'content' => "console.log('Hello');",
713 'virtualFilePath' => 'bar.js',
715 'data.json' => [
716 'type' => 'data',
717 'content' => [ 'langCode' => 'fy', 'extra' => [ 'A', 'B' ] ],
718 'virtualFilePath' => 'data.json',
721 'main' => 'bar.js'
724 'lang' => 'fy'
727 'package file with callback that returns a file (1)' => [
728 $base + [
729 'packageFiles' => [
730 [ 'name' => 'dynamic.js', 'callback' => static function ( $context ) {
731 $file = $context->getLanguage() === 'fy' ? 'script-comment.js' : 'script-nosemi.js';
732 return new FilePath( $file );
737 'files' => [
738 'dynamic.js' => [
739 'type' => 'script',
740 'content' => $commentScript,
741 'filePath' => 'script-comment.js',
744 'main' => 'dynamic.js'
747 'lang' => 'fy'
750 'package file with callback that returns a file (2)' => [
751 $base + [
752 'packageFiles' => [
753 [ 'name' => 'dynamic.js', 'callback' => static function ( $context ) {
754 $file = $context->getLanguage() === 'fy' ? 'script-comment.js' : 'script-nosemi.js';
755 return new FilePath( $file );
760 'files' => [
761 'dynamic.js' => [
762 'type' => 'script',
763 'content' => $nosemiScript,
764 'filePath' => 'script-nosemi.js'
767 'main' => 'dynamic.js'
770 'lang' => 'nl'
773 'package file with callback that returns a file with base path' => [
774 $base + [
775 'packageFiles' => [
776 [ 'name' => 'dynamic.js', 'callback' => static function () use ( $basePathB ) {
777 return new FilePath( 'script-nosemi.js', $basePathB );
782 'files' => [
783 'dynamic.js' => [
784 'type' => 'script',
785 'content' => $nosemiBScript,
786 'filePath' => 'script-nosemi.js',
789 'main' => 'dynamic.js'
792 '.vue file in debug mode' => [
793 $base + [
794 'packageFiles' => [
795 'vue-component.vue'
799 'files' => [
800 'vue-component.vue' => [
801 'type' => 'script',
802 'content' => $vueComponentDebug,
803 'filePath' => 'vue-component.vue',
806 'main' => 'vue-component.vue',
809 'debug' => 'true'
812 '.vue file in non-debug mode' => [
813 $base + [
814 'packageFiles' => [
815 'vue-component.vue'
817 'name' => 'nondebug',
820 'files' => [
821 'vue-component.vue' => [
822 'type' => 'script',
823 'content' => $vueComponentNonDebug,
824 'filePath' => 'vue-component.vue',
827 'main' => 'vue-component.vue'
830 'debug' => 'false'
833 'missing name' => [
834 $base + [
835 'packageFiles' => [
836 [ 'file' => 'script-comment.js' ]
839 LogicException::class
841 'package file with invalid callback' => [
842 $base + [
843 'packageFiles' => [
844 [ 'name' => 'foo.json', 'callback' => 'functionThatDoesNotExist142857' ]
847 LogicException::class
849 'config not valid for script type' => [
850 $base + [
851 'packageFiles' => [
852 'foo.json' => [ 'type' => 'script', 'config' => [ 'Sitename' ] ]
855 LogicException::class
857 'config not valid for *.js file' => [
858 $base + [
859 'packageFiles' => [
860 [ 'name' => 'foo.js', 'config' => 'Sitename' ]
863 LogicException::class
865 'missing type/name/file' => [
866 $base + [
867 'packageFiles' => [
868 'foo.js' => [ 'garbage' => 'data' ]
871 LogicException::class
873 'nonexistent file' => [
874 $base + [
875 'packageFiles' => [
876 'filethatdoesnotexist142857.js'
879 RuntimeException::class
881 'JSON can\'t be a main file' => [
882 $base + [
883 'packageFiles' => [
884 'script-nosemi.js',
885 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ], 'main' => true ]
888 LogicException::class
894 * @dataProvider provideGetScriptPackageFiles
896 public function testGetScriptPackageFiles( $moduleDefinition, $expected, $contextOptions = [] ) {
897 $module = new FileModule( $moduleDefinition );
898 $context = $this->getResourceLoaderContext( $contextOptions );
899 $module->setConfig( $context->getResourceLoader()->getConfig() );
900 if ( isset( $moduleDefinition['name'] ) ) {
901 $module->setName( $moduleDefinition['name'] );
903 if ( is_string( $expected ) ) {
904 // $expected is the class name of the expected exception
905 $this->expectException( $expected );
906 $module->getScript( $context );
907 $this->fail( "$expected exception expected" );
910 // Check name property and convert filePath to plain data
911 $result = $module->getScript( $context );
912 foreach ( $result['files'] as $name => &$file ) {
913 $this->assertSame( $name, $file['name'] );
914 unset( $file['name'] );
915 if ( isset( $file['filePath'] ) ) {
916 $this->assertInstanceOf( FilePath::class, $file['filePath'] );
917 $file['filePath'] = $file['filePath']->getPath();
919 if ( isset( $file['virtualFilePath'] ) ) {
920 $this->assertInstanceOf( FilePath::class, $file['virtualFilePath'] );
921 $file['virtualFilePath'] = $file['virtualFilePath']->getPath();
924 // Check the rest of the result
925 $this->assertEquals( $expected, $result );
928 public function testRequiresES6() {
929 $module = new FileModule();
930 $this->assertTrue( $module->requiresES6(), 'requiresES6 defaults to true' );
931 $module = new FileModule( [ 'es6' => false ] );
932 $this->assertTrue( $module->requiresES6(), 'requiresES6 is true even when set to false' );
933 $module = new FileModule( [ 'es6' => true ] );
934 $this->assertTrue( $module->requiresES6(), 'requiresES6 is true when set to true' );
938 * @covers \Wikimedia\DependencyStore\DependencyStore
940 public function testIndirectDependencies() {
941 $context = $this->getResourceLoaderContext();
942 $moduleInfo = [ 'dir' => __DIR__ . '/../../data/less/module',
943 'lessVars' => [ 'foo' => '2px', 'Foo' => '#eeeeee' ], 'name' => 'styles-dependencies' ];
945 $module = $this->newModuleRequest( $moduleInfo, $context );
946 $module->getStyles( $context );
948 $module = $this->newModuleRequest( $moduleInfo, $context );
949 $dependencies = $module->getFileDependencies( $context );
951 $expectedDependencies = [ realpath( __DIR__ . '/../../data/less/common/test.common.mixins.less' ),
952 realpath( __DIR__ . '/../../data/less/module/dependency.less' ) ];
954 $this->assertEquals( $expectedDependencies, $dependencies );
958 * @covers \Wikimedia\DependencyStore\DependencyStore
960 public function testIndirectDependenciesUpdate() {
961 $context = $this->getResourceLoaderContext();
962 $tempDir = $this->getNewTempDirectory();
963 $moduleInfo = [ 'dir' => $tempDir, 'name' => 'new-dependencies' ];
965 file_put_contents( "$tempDir/styles.less", "@import './test.less';" );
966 file_put_contents( "$tempDir/test.less", "div { color: red; } " );
968 $module = $this->newModuleRequest( $moduleInfo, $context );
969 $module->getStyles( $context );
971 $module = $this->newModuleRequest( $moduleInfo, $context );
972 $dependencies = $module->getFileDependencies( $context );
974 $expectedDependencies = [ realpath( $tempDir . '/test.less' ) ];
976 $this->assertEquals( $expectedDependencies, $dependencies );
978 file_put_contents( "$tempDir/styles.less", "@import './pink.less';" );
979 file_put_contents( "$tempDir/pink.less", "div { color: pink; } " );
981 $module = $this->newModuleRequest( $moduleInfo, $context );
982 $module->getStyles( $context );
984 $module = $this->newModuleRequest( $moduleInfo, $context );
985 $dependencies = $module->getFileDependencies( $context );
987 $expectedDependencies = [ realpath( $tempDir . '/pink.less' ) ];
989 $this->assertEquals( $expectedDependencies, $dependencies );
992 public function newModuleRequest( $moduleInfo, $context ) {
993 $module = new ResourceLoaderFileTestModule( [
994 'localBasePath' => $moduleInfo['dir'],
995 'styles' => [ 'styles.less' ],
996 'lessVars' => $moduleInfo['lessVars'] ?? null
997 ] );
999 $module->setName( $moduleInfo['name'] );
1000 $module->setConfig( $context->getResourceLoader()->getConfig() );
1001 $wrapper = TestingAccessWrapper::newFromObject( $module );
1002 return $wrapper;