1<?php
2
3use Psr\Container\ContainerInterface;
4use Wikimedia\ObjectFactory;
5
6/**
7 * @group ResourceLoader
8 */
9class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
10
11	protected function setUp(): void {
12		parent::setUp();
13
14		$skinFactory = new SkinFactory(
15			new ObjectFactory( $this->createMock( ContainerInterface::class ) ), []
16		);
17		// The empty spec shouldn't matter since this test should never call it
18		$skinFactory->register(
19			'fakeskin',
20			'FakeSkin',
21			[]
22		);
23		$this->setService( 'SkinFactory', $skinFactory );
24
25		// This test is not expected to query any database
26		MediaWiki\MediaWikiServices::disableStorageBackend();
27	}
28
29	private static function getModules() {
30		$base = [
31			'localBasePath' => __DIR__,
32		];
33
34		return [
35			'noTemplateModule' => [],
36
37			'deprecatedModule' => $base + [
38				'deprecated' => true,
39			],
40			'deprecatedTomorrow' => $base + [
41				'deprecated' => 'Will be removed tomorrow.'
42			],
43
44			'htmlTemplateModule' => $base + [
45				'templates' => [
46					'templates/template.html',
47					'templates/template2.html',
48				]
49			],
50
51			'htmlTemplateUnknown' => $base + [
52				'templates' => [
53					'templates/notfound.html',
54				]
55			],
56
57			'aliasedHtmlTemplateModule' => $base + [
58				'templates' => [
59					'foo.html' => 'templates/template.html',
60					'bar.html' => 'templates/template2.html',
61				]
62			],
63
64			'templateModuleHandlebars' => $base + [
65				'templates' => [
66					'templates/template_awesome.handlebars',
67				],
68			],
69
70			'aliasFooFromBar' => $base + [
71				'templates' => [
72					'foo.foo' => 'templates/template.bar',
73				],
74			],
75		];
76	}
77
78	public static function providerTemplateDependencies() {
79		$modules = self::getModules();
80
81		return [
82			[
83				$modules['noTemplateModule'],
84				[],
85			],
86			[
87				$modules['htmlTemplateModule'],
88				[
89					'mediawiki.template',
90				],
91			],
92			[
93				$modules['templateModuleHandlebars'],
94				[
95					'mediawiki.template',
96					'mediawiki.template.handlebars',
97				],
98			],
99			[
100				$modules['aliasFooFromBar'],
101				[
102					'mediawiki.template',
103					'mediawiki.template.foo',
104				],
105			],
106		];
107	}
108
109	/**
110	 * @dataProvider providerTemplateDependencies
111	 * @covers ResourceLoaderFileModule::__construct
112	 * @covers ResourceLoaderFileModule::getDependencies
113	 */
114	public function testTemplateDependencies( $module, $expected ) {
115		$rl = new ResourceLoaderFileModule( $module );
116		$rl->setName( 'testing' );
117		$this->assertEquals( $rl->getDependencies(), $expected );
118	}
119
120	public static function providerDeprecatedModules() {
121		return [
122			[
123				'deprecatedModule',
124				'mw.log.warn("This page is using the deprecated ResourceLoader module \"deprecatedModule\".");',
125			],
126			[
127				'deprecatedTomorrow',
128				'mw.log.warn(' .
129					'"This page is using the deprecated ResourceLoader module \"deprecatedTomorrow\".\\n' .
130					"Will be removed tomorrow." .
131					'");'
132			]
133		];
134	}
135
136	/**
137	 * @dataProvider providerDeprecatedModules
138	 * @covers ResourceLoaderFileModule::getScript
139	 */
140	public function testDeprecatedModules( $name, $expected ) {
141		$modules = self::getModules();
142		$module = new ResourceLoaderFileModule( $modules[$name] );
143		$module->setName( $name );
144		$ctx = $this->getResourceLoaderContext();
145		$this->assertEquals( $module->getScript( $ctx ), $expected );
146	}
147
148	/**
149	 * @covers ResourceLoaderFileModule::getScript
150	 * @covers ResourceLoaderFileModule::getScriptFiles
151	 * @covers ResourceLoaderFileModule::readScriptFiles
152	 */
153	public function testGetScript() {
154		$module = new ResourceLoaderFileModule( [
155			'localBasePath' => __DIR__ . '/../../data/resourceloader',
156			'scripts' => [ 'script-nosemi.js', 'script-comment.js' ],
157		] );
158		$module->setName( 'testing' );
159		$ctx = $this->getResourceLoaderContext();
160		$this->assertEquals(
161			"/* eslint-disable */\nmw.foo()\n" .
162			"/* eslint-disable */\nmw.foo()\n// mw.bar();\n",
163			$module->getScript( $ctx ),
164			'scripts with newline at the end are concatenated without a newline'
165		);
166
167		$module = new ResourceLoaderFileModule( [
168			'localBasePath' => __DIR__ . '/../../data/resourceloader',
169			'scripts' => [ 'script-nosemi-nonl.js', 'script-comment-nonl.js' ],
170		] );
171		$module->setName( 'testing' );
172		$ctx = $this->getResourceLoaderContext();
173		$this->assertEquals(
174			"/* eslint-disable */\nmw.foo()" .
175			"\n" .
176			"/* eslint-disable */\nmw.foo()\n// mw.bar();" .
177			"\n",
178			$module->getScript( $ctx ),
179			'scripts without newline at the end are concatenated with a newline'
180		);
181	}
182
183	/**
184	 * @covers ResourceLoaderFileModule
185	 */
186	public function testGetAllSkinStyleFiles() {
187		$baseParams = [
188			'scripts' => [
189				'foo.js',
190				'bar.js',
191			],
192			'styles' => [
193				'foo.css',
194				'bar.css' => [ 'media' => 'print' ],
195				'screen.less' => [ 'media' => 'screen' ],
196				'screen-query.css' => [ 'media' => 'screen and (min-width: 400px)' ],
197			],
198			'skinStyles' => [
199				'default' => 'quux-fallback.less',
200				'fakeskin' => [
201					'baz-vector.css',
202					'quux-vector.less',
203				],
204			],
205			'messages' => [
206				'hello',
207				'world',
208			],
209		];
210
211		$module = new ResourceLoaderFileModule( $baseParams );
212		$module->setName( 'testing' );
213
214		$this->assertEquals(
215			[
216				'foo.css',
217				'baz-vector.css',
218				'quux-vector.less',
219				'quux-fallback.less',
220				'bar.css',
221				'screen.less',
222				'screen-query.css',
223			],
224			array_map( 'basename', $module->getAllStyleFiles() )
225		);
226	}
227
228	/**
229	 * Strip @noflip annotations from CSS code.
230	 * @param string $css
231	 * @return string
232	 */
233	private static function stripNoflip( $css ) {
234		return str_replace( '/*@noflip*/ ', '', $css );
235	}
236
237	/**
238	 * Confirm that 'ResourceModuleSkinStyles' skin attributes get injected
239	 * into the module, and have their file contents read correctly from their
240	 * own (out-of-module) directories.
241	 *
242	 * @covers ResourceLoader
243	 * @covers ResourceLoaderFileModule
244	 */
245	public function testInjectSkinStyles() {
246		$moduleDir = __DIR__ . '/../../data/resourceloader';
247		$skinDir = __DIR__ . '/../../data/resourceloader/myskin';
248		$rl = new ResourceLoader( new HashConfig( self::getSettings() ) );
249		$rl->setModuleSkinStyles( [
250			'fakeskin' => [
251				'localBasePath' => $skinDir,
252				'testing' => [
253					'override.css',
254				],
255			],
256		] );
257		$rl->register( 'testing', [
258			'localBasePath' => $moduleDir,
259			'styles' => [ 'simple.css' ],
260		] );
261		$ctx = $this->getResourceLoaderContext( [ 'skin' => 'fakeskin' ], $rl );
262
263		$module = $rl->getModule( 'testing' );
264		$this->assertInstanceOf( ResourceLoaderFileModule::class, $module );
265		$this->assertEquals(
266			[ 'all' => ".example { color: blue; }\n\n.override { line-height: 2; }\n" ],
267			$module->getStyles( $ctx )
268		);
269	}
270
271	/**
272	 * What happens when you mix @embed and @noflip?
273	 * This really is an integration test, but oh well.
274	 *
275	 * @covers ResourceLoaderFileModule::getStyles
276	 * @covers ResourceLoaderFileModule::getStyleFiles
277	 * @covers ResourceLoaderFileModule::readStyleFiles
278	 * @covers ResourceLoaderFileModule::readStyleFile
279	 */
280	public function testMixedCssAnnotations() {
281		$basePath = __DIR__ . '/../../data/css';
282		$testModule = new ResourceLoaderFileTestModule( [
283			'localBasePath' => $basePath,
284			'styles' => [ 'test.css' ],
285		] );
286		$testModule->setName( 'testing' );
287		$expectedModule = new ResourceLoaderFileTestModule( [
288			'localBasePath' => $basePath,
289			'styles' => [ 'expected.css' ],
290		] );
291		$expectedModule->setName( 'testing' );
292
293		$contextLtr = $this->getResourceLoaderContext( [
294			'lang' => 'en',
295			'dir' => 'ltr',
296		] );
297		$contextRtl = $this->getResourceLoaderContext( [
298			'lang' => 'he',
299			'dir' => 'rtl',
300		] );
301
302		// Since we want to compare the effect of @noflip+@embed against the effect of just @embed, and
303		// the @noflip annotations are always preserved, we need to strip them first.
304		$this->assertEquals(
305			$expectedModule->getStyles( $contextLtr ),
306			self::stripNoflip( $testModule->getStyles( $contextLtr ) ),
307			"/*@noflip*/ with /*@embed*/ gives correct results in LTR mode"
308		);
309		$this->assertEquals(
310			$expectedModule->getStyles( $contextLtr ),
311			self::stripNoflip( $testModule->getStyles( $contextRtl ) ),
312			"/*@noflip*/ with /*@embed*/ gives correct results in RTL mode"
313		);
314	}
315
316	/**
317	 * @covers ResourceLoaderFileModule
318	 */
319	public function testCssFlipping() {
320		$plain = new ResourceLoaderFileTestModule( [
321			'localBasePath' => __DIR__ . '/../../data/resourceloader',
322			'styles' => [ 'direction.css' ],
323		] );
324		$plain->setName( 'test' );
325
326		$context = $this->getResourceLoaderContext( [ 'lang' => 'en', 'dir' => 'ltr' ] );
327		$this->assertEquals(
328			$plain->getStyles( $context ),
329			[ 'all' => ".example { text-align: left; }\n" ],
330			'Unchanged styles in LTR mode'
331		);
332		$context = $this->getResourceLoaderContext( [ 'lang' => 'he', 'dir' => 'rtl' ] );
333		$this->assertEquals(
334			$plain->getStyles( $context ),
335			[ 'all' => ".example { text-align: right; }\n" ],
336			'Flipped styles in RTL mode'
337		);
338
339		$noflip = new ResourceLoaderFileTestModule( [
340			'localBasePath' => __DIR__ . '/../../data/resourceloader',
341			'styles' => [ 'direction.css' ],
342			'noflip' => true,
343		] );
344		$noflip->setName( 'test' );
345		$this->assertEquals(
346			$plain->getStyles( $context ),
347			[ 'all' => ".example { text-align: right; }\n" ],
348			'Unchanged styles in RTL mode with noflip at module level'
349		);
350	}
351
352	/**
353	 * Test reading files from elsewhere than localBasePath using ResourceLoaderFilePath.
354	 *
355	 * The use of ResourceLoaderFilePath objects resembles the way that ResourceLoader::getModule()
356	 * injects additional files when 'ResourceModuleSkinStyles' or 'OOUIThemePaths' skin attributes
357	 * apply to a given module.
358	 *
359	 * @covers ResourceLoaderFilePath::getLocalBasePath
360	 * @covers ResourceLoaderFilePath::getRemoteBasePath
361	 */
362	public function testResourceLoaderFilePath() {
363		$basePath = __DIR__ . '/../../data/blahblah';
364		$filePath = __DIR__ . '/../../data/rlfilepath';
365		$testModule = new ResourceLoaderFileModule( [
366			'localBasePath' => $basePath,
367			'remoteBasePath' => 'blahblah',
368			'styles' => new ResourceLoaderFilePath( 'style.css', $filePath, 'rlfilepath' ),
369			'skinStyles' => [
370				'vector' => new ResourceLoaderFilePath( 'skinStyle.css', $filePath, 'rlfilepath' ),
371			],
372			'scripts' => new ResourceLoaderFilePath( 'script.js', $filePath, 'rlfilepath' ),
373			'templates' => new ResourceLoaderFilePath( 'template.html', $filePath, 'rlfilepath' ),
374		] );
375		$expectedModule = new ResourceLoaderFileModule( [
376			'localBasePath' => $filePath,
377			'remoteBasePath' => 'rlfilepath',
378			'styles' => 'style.css',
379			'skinStyles' => [
380				'vector' => 'skinStyle.css',
381			],
382			'scripts' => 'script.js',
383			'templates' => 'template.html',
384		] );
385
386		$context = $this->getResourceLoaderContext();
387		$this->assertEquals(
388			$expectedModule->getModuleContent( $context ),
389			$testModule->getModuleContent( $context ),
390			"Using ResourceLoaderFilePath works correctly"
391		);
392	}
393
394	public static function providerGetTemplates() {
395		$modules = self::getModules();
396
397		return [
398			[
399				$modules['noTemplateModule'],
400				[],
401			],
402			[
403				$modules['templateModuleHandlebars'],
404				[
405					'templates/template_awesome.handlebars' => "wow\n",
406				],
407			],
408			[
409				$modules['htmlTemplateModule'],
410				[
411					'templates/template.html' => "<strong>hello</strong>\n",
412					'templates/template2.html' => "<div>goodbye</div>\n",
413				],
414			],
415			[
416				$modules['aliasedHtmlTemplateModule'],
417				[
418					'foo.html' => "<strong>hello</strong>\n",
419					'bar.html' => "<div>goodbye</div>\n",
420				],
421			],
422			[
423				$modules['htmlTemplateUnknown'],
424				false,
425			],
426		];
427	}
428
429	/**
430	 * @dataProvider providerGetTemplates
431	 * @covers ResourceLoaderFileModule::getTemplates
432	 */
433	public function testGetTemplates( $module, $expected ) {
434		$rl = new ResourceLoaderFileModule( $module );
435		$rl->setName( 'testing' );
436
437		if ( $expected === false ) {
438			$this->expectException( RuntimeException::class );
439			$rl->getTemplates();
440		} else {
441			$this->assertEquals( $rl->getTemplates(), $expected );
442		}
443	}
444
445	/**
446	 * @covers ResourceLoaderFileModule::stripBom
447	 */
448	public function testBomConcatenation() {
449		$basePath = __DIR__ . '/../../data/css';
450		$testModule = new ResourceLoaderFileTestModule( [
451			'localBasePath' => $basePath,
452			'styles' => [ 'bom.css' ],
453		] );
454		$testModule->setName( 'testing' );
455		$this->assertEquals(
456			substr( file_get_contents( "$basePath/bom.css" ), 0, 10 ),
457			"\xef\xbb\xbf.efbbbf",
458			'File has leading BOM'
459		);
460
461		$context = $this->getResourceLoaderContext();
462		$this->assertEquals(
463			$testModule->getStyles( $context ),
464			[ 'all' => ".efbbbf_bom_char_at_start_of_file {}\n" ],
465			'Leading BOM removed when concatenating files'
466		);
467	}
468
469	/**
470	 * @covers ResourceLoaderFileModule
471	 */
472	public function testLessFileCompilation() {
473		$context = $this->getResourceLoaderContext();
474		$basePath = __DIR__ . '/../../data/less/module';
475		$module = new ResourceLoaderFileTestModule( [
476			'localBasePath' => $basePath,
477			'styles' => [ 'styles.less' ],
478			'lessVars' => [ 'foo' => '2px', 'Foo' => '#eeeeee' ]
479		] );
480		$module->setName( 'test.less' );
481		$styles = $module->getStyles( $context );
482		$this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
483	}
484
485	public function provideGetVersionHash() {
486		$a = [];
487		$b = [
488			'lessVars' => [ 'key' => 'value' ],
489		];
490		yield 'with and without Less variables' => [ $a, $b, false ];
491
492		$a = [
493			'lessVars' => [ 'key' => 'value1' ],
494		];
495		$b = [
496			'lessVars' => [ 'key' => 'value2' ],
497		];
498		yield 'different Less variables' => [ $a, $b, false ];
499
500		$x = [
501			'lessVars' => [ 'key' => 'value' ],
502		];
503		yield 'identical Less variables' => [ $x, $x, true ];
504
505		$a = [
506			'packageFiles' => [ [ 'name' => 'data.json', 'callback' => static function () {
507				return [ 'aaa' ];
508			} ] ]
509		];
510		$b = [
511			'packageFiles' => [ [ 'name' => 'data.json', 'callback' => static function () {
512				return [ 'bbb' ];
513			} ] ]
514		];
515		yield 'packageFiles with different callback' => [ $a, $b, false ];
516
517		$a = [
518			'packageFiles' => [ [ 'name' => 'aaa.json', 'callback' => static function () {
519				return [ 'x' ];
520			} ] ]
521		];
522		$b = [
523			'packageFiles' => [ [ 'name' => 'bbb.json', 'callback' => static function () {
524				return [ 'x' ];
525			} ] ]
526		];
527		yield 'packageFiles with different file name and a callback' => [ $a, $b, false ];
528
529		$a = [
530			'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => static function () {
531				return [ 'A-version' ];
532			}, 'callback' => static function () {
533				throw new Exception( 'Unexpected computation' );
534			} ] ]
535		];
536		$b = [
537			'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => static function () {
538				return [ 'B-version' ];
539			}, 'callback' => static function () {
540				throw new Exception( 'Unexpected computation' );
541			} ] ]
542		];
543		yield 'packageFiles with different versionCallback' => [ $a, $b, false ];
544
545		$a = [
546			'packageFiles' => [ [ 'name' => 'aaa.json',
547				'versionCallback' => static function () {
548					return [ 'X-version' ];
549				},
550				'callback' => static function () {
551					throw new Exception( 'Unexpected computation' );
552				}
553			] ]
554		];
555		$b = [
556			'packageFiles' => [ [ 'name' => 'bbb.json',
557				'versionCallback' => static function () {
558					return [ 'X-version' ];
559				},
560				'callback' => static function () {
561					throw new Exception( 'Unexpected computation' );
562				}
563			] ]
564		];
565		yield 'packageFiles with different file name and a versionCallback' => [ $a, $b, false ];
566	}
567
568	/**
569	 * @dataProvider provideGetVersionHash
570	 * @covers ResourceLoaderFileModule::getDefinitionSummary
571	 * @covers ResourceLoaderFileModule::getFileHashes
572	 */
573	public function testGetVersionHash( $a, $b, $isEqual ) {
574		$context = $this->getResourceLoaderContext();
575
576		$moduleA = new ResourceLoaderFileTestModule( $a );
577		$moduleA->setConfig( $context->getResourceLoader()->getConfig() );
578		$versionA = $moduleA->getVersionHash( $context );
579		$moduleB = new ResourceLoaderFileTestModule( $b );
580		$moduleB->setConfig( $context->getResourceLoader()->getConfig() );
581		$versionB = $moduleB->getVersionHash( $context );
582
583		$this->assertSame(
584			$isEqual,
585			( $versionA === $versionB ),
586			'Whether versions hashes are equal'
587		);
588	}
589
590	public function provideGetScriptPackageFiles() {
591		$basePath = __DIR__ . '/../../data/resourceloader';
592		$base = [ 'localBasePath' => $basePath ];
593		$commentScript = file_get_contents( "$basePath/script-comment.js" );
594		$nosemiScript = file_get_contents( "$basePath/script-nosemi.js" );
595		$vueComponentDebug = trim( file_get_contents( "$basePath/vue-component-output-debug.js.txt" ) );
596		$vueComponentNonDebug = trim( file_get_contents( "$basePath/vue-component-output-nondebug.js.txt" ) );
597		$config = RequestContext::getMain()->getConfig();
598		return [
599			[
600				$base + [
601					'packageFiles' => [
602						'script-comment.js',
603						'script-nosemi.js'
604					]
605				],
606				[
607					'files' => [
608						'script-comment.js' => [
609							'type' => 'script',
610							'content' => $commentScript,
611						],
612						'script-nosemi.js' => [
613							'type' => 'script',
614							'content' => $nosemiScript
615						]
616					],
617					'main' => 'script-comment.js'
618				]
619			],
620			[
621				$base + [
622					'packageFiles' => [
623						'script-comment.js',
624						[ 'name' => 'script-nosemi.js', 'main' => true ]
625					],
626					'deprecated' => 'Deprecation test',
627					'name' => 'test-deprecated'
628				],
629				[
630					'files' => [
631						'script-comment.js' => [
632							'type' => 'script',
633							'content' => $commentScript,
634						],
635						'script-nosemi.js' => [
636							'type' => 'script',
637							'content' => 'mw.log.warn(' .
638								'"This page is using the deprecated ResourceLoader module \"test-deprecated\".\\n' .
639								"Deprecation test" .
640								'");' .
641								$nosemiScript
642						]
643					],
644					'main' => 'script-nosemi.js'
645				]
646			],
647			[
648				$base + [
649					'packageFiles' => [
650						[ 'name' => 'init.js', 'file' => 'script-comment.js', 'main' => true ],
651						[ 'name' => 'nosemi.js', 'file' => 'script-nosemi.js' ],
652					]
653				],
654				[
655					'files' => [
656						'init.js' => [
657							'type' => 'script',
658							'content' => $commentScript,
659						],
660						'nosemi.js' => [
661							'type' => 'script',
662							'content' => $nosemiScript
663						]
664					],
665					'main' => 'init.js'
666				]
667			],
668			'package file with callback' => [
669				$base + [
670					'packageFiles' => [
671						[ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ] ],
672						'sample.json',
673						[ 'name' => 'bar.js', 'content' => "console.log('Hello');" ],
674						[
675							'name' => 'data.json',
676							'callback' => static function ( $context, $config, $extra ) {
677								return [ 'langCode' => $context->getLanguage(), 'extra' => $extra ];
678							},
679							'callbackParam' => [ 'a' => 'b' ],
680						],
681						[ 'name' => 'config.json', 'config' => [
682							'Sitename',
683							'server' => 'ServerName',
684						] ],
685					]
686				],
687				[
688					'files' => [
689						'foo.json' => [
690							'type' => 'data',
691							'content' => [ 'Hello' => 'world' ],
692						],
693						'sample.json' => [
694							'type' => 'data',
695							'content' => (object)[ 'foo' => 'bar', 'answer' => 42 ],
696						],
697						'bar.js' => [
698							'type' => 'script',
699							'content' => "console.log('Hello');",
700						],
701						'data.json' => [
702							'type' => 'data',
703							'content' => [ 'langCode' => 'fy', 'extra' => [ 'a' => 'b' ] ],
704						],
705						'config.json' => [
706							'type' => 'data',
707							'content' => [
708								'Sitename' => $config->get( 'Sitename' ),
709								'server' => $config->get( 'ServerName' ),
710							]
711						]
712					],
713					'main' => 'bar.js'
714				],
715				[
716					'lang' => 'fy'
717				]
718			],
719			'package file with callback and versionCallback' => [
720				$base + [
721					'packageFiles' => [
722						[ 'name' => 'bar.js', 'content' => "console.log('Hello');" ],
723						[
724							'name' => 'data.json',
725							'versionCallback' => static function ( $context ) {
726								return 'x';
727							},
728							'callback' => static function ( $context, $config, $extra ) {
729								return [ 'langCode' => $context->getLanguage(), 'extra' => $extra ];
730							},
731							'callbackParam' => [ 'A', 'B' ]
732						],
733					]
734				],
735				[
736					'files' => [
737						'bar.js' => [
738							'type' => 'script',
739							'content' => "console.log('Hello');",
740						],
741						'data.json' => [
742							'type' => 'data',
743							'content' => [ 'langCode' => 'fy', 'extra' => [ 'A', 'B' ] ],
744						],
745					],
746					'main' => 'bar.js'
747				],
748				[
749					'lang' => 'fy'
750				]
751			],
752			'package file with callback that returns a file (1)' => [
753				$base + [
754					'packageFiles' => [
755						[ 'name' => 'dynamic.js', 'callback' => static function ( $context ) {
756							$file = $context->getLanguage() === 'fy' ? 'script-comment.js' : 'script-nosemi.js';
757							return new ResourceLoaderFilePath( $file );
758						} ]
759					]
760				],
761				[
762					'files' => [
763						'dynamic.js' => [
764							'type' => 'script',
765							'content' => $commentScript,
766						]
767					],
768					'main' => 'dynamic.js'
769				],
770				[
771					'lang' => 'fy'
772				]
773			],
774			'package file with callback that returns a file (2)' => [
775				$base + [
776					'packageFiles' => [
777						[ 'name' => 'dynamic.js', 'callback' => static function ( $context ) {
778							$file = $context->getLanguage() === 'fy' ? 'script-comment.js' : 'script-nosemi.js';
779							return new ResourceLoaderFilePath( $file );
780						} ]
781					]
782				],
783				[
784					'files' => [
785						'dynamic.js' => [
786							'type' => 'script',
787							'content' => $nosemiScript,
788						]
789					],
790					'main' => 'dynamic.js'
791				],
792				[
793					'lang' => 'nl'
794				]
795			],
796			'.vue file in debug mode' => [
797				$base + [
798					'packageFiles' => [
799						'vue-component.vue'
800					]
801				],
802				[
803					'files' => [
804						'vue-component.vue' => [
805							'type' => 'script',
806							'content' => $vueComponentDebug
807						]
808					],
809					'main' => 'vue-component.vue',
810				],
811				[
812					'debug' => 'true'
813				]
814			],
815			'.vue file in non-debug mode' => [
816				$base + [
817					'packageFiles' => [
818						'vue-component.vue'
819					],
820					'name' => 'nondebug',
821				],
822				[
823					'files' => [
824						'vue-component.vue' => [
825							'type' => 'script',
826							'content' => $vueComponentNonDebug
827						]
828					],
829					'main' => 'vue-component.vue'
830				],
831				[
832					'debug' => 'false'
833				]
834			],
835			[
836				$base + [
837					'packageFiles' => [
838						[ 'file' => 'script-comment.js' ]
839					]
840				],
841				LogicException::class
842			],
843			'package file with invalid callback' => [
844				$base + [
845					'packageFiles' => [
846						[ 'name' => 'foo.json', 'callback' => 'functionThatDoesNotExist142857' ]
847					]
848				],
849				LogicException::class
850			],
851			[
852				// 'config' not valid for 'script' type
853				$base + [
854					'packageFiles' => [
855						'foo.json' => [ 'type' => 'script', 'config' => [ 'Sitename' ] ]
856					]
857				],
858				LogicException::class
859			],
860			[
861				// 'config' not valid for '*.js' file
862				$base + [
863					'packageFiles' => [
864						[ 'name' => 'foo.js', 'config' => 'Sitename' ]
865					]
866				],
867				LogicException::class
868			],
869			[
870				// missing type/name/file.
871				$base + [
872					'packageFiles' => [
873						'foo.js' => [ 'garbage' => 'data' ]
874					]
875				],
876				LogicException::class
877			],
878			[
879				$base + [
880					'packageFiles' => [
881						'filethatdoesnotexist142857.js'
882					]
883				],
884				RuntimeException::class
885			],
886			[
887				// JSON can't be a main file
888				$base + [
889					'packageFiles' => [
890						'script-nosemi.js',
891						[ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ], 'main' => true ]
892					]
893				],
894				LogicException::class
895			]
896		];
897	}
898
899	/**
900	 * @dataProvider provideGetScriptPackageFiles
901	 * @covers ResourceLoaderFileModule::getScript
902	 * @covers ResourceLoaderFileModule::getPackageFiles
903	 * @covers ResourceLoaderFileModule::expandPackageFiles
904	 */
905	public function testGetScriptPackageFiles( $moduleDefinition, $expected, $contextOptions = [] ) {
906		$module = new ResourceLoaderFileModule( $moduleDefinition );
907		$context = $this->getResourceLoaderContext( $contextOptions );
908		$module->setConfig( $context->getResourceLoader()->getConfig() );
909		if ( isset( $moduleDefinition['name'] ) ) {
910			$module->setName( $moduleDefinition['name'] );
911		}
912		if ( is_string( $expected ) ) {
913			// Class name of expected exception
914			$this->expectException( $expected );
915			$module->getScript( $context );
916		} else {
917			// Array of expected return value
918			$this->assertEquals( $expected, $module->getScript( $context ) );
919		}
920	}
921
922	/**
923	 * @covers ResourceLoaderFileModule::requiresES6
924	 */
925	public function testRequiresES6() {
926		$module = new ResourceLoaderFileModule();
927		$this->assertFalse( $module->requiresES6(), 'requiresES6 defaults to false' );
928		$module = new ResourceLoaderFileModule( [ 'es6' => false ] );
929		$this->assertFalse( $module->requiresES6(), 'requiresES6 is false when set to false' );
930		$module = new ResourceLoaderFileModule( [ 'es6' => true ] );
931		$this->assertTrue( $module->requiresES6(), 'requiresES6 is true when set to true' );
932	}
933}
934