4 * @group ResourceLoader
6 class ResourceLoaderFileModuleTest
extends ResourceLoaderTestCase
{
8 protected function setUp() {
11 $skinFactory = new SkinFactory();
12 // The return value of the closure shouldn't matter since this test should
14 $skinFactory->register(
20 $this->setService( 'SkinFactory', $skinFactory );
22 // This test is not expected to query any database
23 MediaWiki\MediaWikiServices
::disableStorageBackend();
26 private static function getModules() {
28 'localBasePath' => __DIR__
,
32 'noTemplateModule' => [],
34 'deprecatedModule' => $base +
[
37 'deprecatedTomorrow' => $base +
[
38 'deprecated' => 'Will be removed tomorrow.'
41 'htmlTemplateModule' => $base +
[
43 'templates/template.html',
44 'templates/template2.html',
48 'htmlTemplateUnknown' => $base +
[
50 'templates/notfound.html',
54 'aliasedHtmlTemplateModule' => $base +
[
56 'foo.html' => 'templates/template.html',
57 'bar.html' => 'templates/template2.html',
61 'templateModuleHandlebars' => $base +
[
63 'templates/template_awesome.handlebars',
67 'aliasFooFromBar' => $base +
[
69 'foo.foo' => 'templates/template.bar',
75 public static function providerTemplateDependencies() {
76 $modules = self
::getModules();
80 $modules['noTemplateModule'],
84 $modules['htmlTemplateModule'],
90 $modules['templateModuleHandlebars'],
93 'mediawiki.template.handlebars',
97 $modules['aliasFooFromBar'],
100 'mediawiki.template.foo',
107 * @dataProvider providerTemplateDependencies
108 * @covers ResourceLoaderFileModule::__construct
109 * @covers ResourceLoaderFileModule::getDependencies
111 public function testTemplateDependencies( $module, $expected ) {
112 $rl = new ResourceLoaderFileModule( $module );
113 $rl->setName( 'testing' );
114 $this->assertEquals( $rl->getDependencies(), $expected );
117 public static function providerDeprecatedModules() {
121 'mw.log.warn("This page is using the deprecated ResourceLoader module \"deprecatedModule\".");',
124 'deprecatedTomorrow',
126 '"This page is using the deprecated ResourceLoader module \"deprecatedTomorrow\".\\n' .
127 "Will be removed tomorrow." .
134 * @dataProvider providerDeprecatedModules
135 * @covers ResourceLoaderFileModule::getScript
137 public function testDeprecatedModules( $name, $expected ) {
138 $modules = self
::getModules();
139 $module = new ResourceLoaderFileModule( $modules[$name] );
140 $module->setName( $name );
141 $ctx = $this->getResourceLoaderContext();
142 $this->assertEquals( $module->getScript( $ctx ), $expected );
146 * @covers ResourceLoaderFileModule::getScript
147 * @covers ResourceLoaderFileModule::getScriptFiles
148 * @covers ResourceLoaderFileModule::readScriptFiles
150 public function testGetScript() {
151 $module = new ResourceLoaderFileModule( [
152 'localBasePath' => __DIR__
. '/../../data/resourceloader',
153 'scripts' => [ 'script-nosemi.js', 'script-comment.js' ],
155 $module->setName( 'testing' );
156 $ctx = $this->getResourceLoaderContext();
158 "/* eslint-disable */\nmw.foo()\n" .
160 "/* eslint-disable */\nmw.foo()\n// mw.bar();\n" .
162 $module->getScript( $ctx ),
163 'scripts are concatenated with a new-line'
168 * @covers ResourceLoaderFileModule::getAllStyleFiles
169 * @covers ResourceLoaderFileModule::getAllSkinStyleFiles
170 * @covers ResourceLoaderFileModule::getSkinStyleFiles
172 public function testGetAllSkinStyleFiles() {
180 'bar.css' => [ 'media' => 'print' ],
181 'screen.less' => [ 'media' => 'screen' ],
182 'screen-query.css' => [ 'media' => 'screen and (min-width: 400px)' ],
185 'default' => 'quux-fallback.less',
197 $module = new ResourceLoaderFileModule( $baseParams );
198 $module->setName( 'testing' );
205 'quux-fallback.less',
210 array_map( 'basename', $module->getAllStyleFiles() )
215 * Strip @noflip annotations from CSS code.
219 private static function stripNoflip( $css ) {
220 return str_replace( '/*@noflip*/ ', '', $css );
224 * What happens when you mix @embed and @noflip?
225 * This really is an integration test, but oh well.
227 * @covers ResourceLoaderFileModule::getStyles
228 * @covers ResourceLoaderFileModule::getStyleFiles
229 * @covers ResourceLoaderFileModule::readStyleFiles
230 * @covers ResourceLoaderFileModule::readStyleFile
232 public function testMixedCssAnnotations() {
233 $basePath = __DIR__
. '/../../data/css';
234 $testModule = new ResourceLoaderFileTestModule( [
235 'localBasePath' => $basePath,
236 'styles' => [ 'test.css' ],
238 $testModule->setName( 'testing' );
239 $expectedModule = new ResourceLoaderFileTestModule( [
240 'localBasePath' => $basePath,
241 'styles' => [ 'expected.css' ],
243 $expectedModule->setName( 'testing' );
245 $contextLtr = $this->getResourceLoaderContext( [
249 $contextRtl = $this->getResourceLoaderContext( [
254 // Since we want to compare the effect of @noflip+@embed against the effect of just @embed, and
255 // the @noflip annotations are always preserved, we need to strip them first.
257 $expectedModule->getStyles( $contextLtr ),
258 self
::stripNoflip( $testModule->getStyles( $contextLtr ) ),
259 "/*@noflip*/ with /*@embed*/ gives correct results in LTR mode"
262 $expectedModule->getStyles( $contextLtr ),
263 self
::stripNoflip( $testModule->getStyles( $contextRtl ) ),
264 "/*@noflip*/ with /*@embed*/ gives correct results in RTL mode"
268 public static function providerGetTemplates() {
269 $modules = self
::getModules();
273 $modules['noTemplateModule'],
277 $modules['templateModuleHandlebars'],
279 'templates/template_awesome.handlebars' => "wow\n",
283 $modules['htmlTemplateModule'],
285 'templates/template.html' => "<strong>hello</strong>\n",
286 'templates/template2.html' => "<div>goodbye</div>\n",
290 $modules['aliasedHtmlTemplateModule'],
292 'foo.html' => "<strong>hello</strong>\n",
293 'bar.html' => "<div>goodbye</div>\n",
297 $modules['htmlTemplateUnknown'],
304 * @dataProvider providerGetTemplates
305 * @covers ResourceLoaderFileModule::getTemplates
307 public function testGetTemplates( $module, $expected ) {
308 $rl = new ResourceLoaderFileModule( $module );
309 $rl->setName( 'testing' );
311 if ( $expected === false ) {
312 $this->setExpectedException( MWException
::class );
315 $this->assertEquals( $rl->getTemplates(), $expected );
320 * @covers ResourceLoaderFileModule::stripBom
322 public function testBomConcatenation() {
323 $basePath = __DIR__
. '/../../data/css';
324 $testModule = new ResourceLoaderFileTestModule( [
325 'localBasePath' => $basePath,
326 'styles' => [ 'bom.css' ],
328 $testModule->setName( 'testing' );
330 substr( file_get_contents( "$basePath/bom.css" ), 0, 10 ),
331 "\xef\xbb\xbf.efbbbf",
332 'File has leading BOM'
335 $context = $this->getResourceLoaderContext();
337 $testModule->getStyles( $context ),
338 [ 'all' => ".efbbbf_bom_char_at_start_of_file {}\n" ],
339 'Leading BOM removed when concatenating files'
344 * @covers ResourceLoaderFileModule::compileLessFile
346 public function testLessFileCompilation() {
347 $context = $this->getResourceLoaderContext();
348 $basePath = __DIR__
. '/../../data/less/module';
349 $module = new ResourceLoaderFileTestModule( [
350 'localBasePath' => $basePath,
351 'styles' => [ 'styles.less' ],
352 'lessVars' => [ 'foo' => '2px', 'Foo' => '#eeeeee' ]
354 $module->setName( 'test.less' );
355 $styles = $module->getStyles( $context );
356 $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
359 public function provideGetVersionHash() {
362 'lessVars' => [ 'key' => 'value' ],
364 yield
'with and without Less variables' => [ $a, $b, false ];
367 'lessVars' => [ 'key' => 'value1' ],
370 'lessVars' => [ 'key' => 'value2' ],
372 yield
'different Less variables' => [ $a, $b, false ];
375 'lessVars' => [ 'key' => 'value' ],
377 yield
'identical Less variables' => [ $x, $x, true ];
380 'packageFiles' => [ [ 'name' => 'data.json', 'callback' => function () {
385 'packageFiles' => [ [ 'name' => 'data.json', 'callback' => function () {
389 yield
'packageFiles with different callback' => [ $a, $b, false ];
392 'packageFiles' => [ [ 'name' => 'aaa.json', 'callback' => function () {
397 'packageFiles' => [ [ 'name' => 'bbb.json', 'callback' => function () {
401 yield
'packageFiles with different file name and a callback' => [ $a, $b, false ];
404 'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => function () {
405 return [ 'A-version' ];
406 }, 'callback' => function () {
407 throw new Exception( 'Unexpected computation' );
411 'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => function () {
412 return [ 'B-version' ];
413 }, 'callback' => function () {
414 throw new Exception( 'Unexpected computation' );
417 yield
'packageFiles with different versionCallback' => [ $a, $b, false ];
420 'packageFiles' => [ [ 'name' => 'aaa.json',
421 'versionCallback' => function () {
422 return [ 'X-version' ];
424 'callback' => function () {
425 throw new Exception( 'Unexpected computation' );
430 'packageFiles' => [ [ 'name' => 'bbb.json',
431 'versionCallback' => function () {
432 return [ 'X-version' ];
434 'callback' => function () {
435 throw new Exception( 'Unexpected computation' );
439 yield
'packageFiles with different file name and a versionCallback' => [ $a, $b, false ];
443 * @dataProvider provideGetVersionHash
444 * @covers ResourceLoaderFileModule::getDefinitionSummary
445 * @covers ResourceLoaderFileModule::getFileHashes
447 public function testGetVersionHash( $a, $b, $isEqual ) {
448 $context = $this->getResourceLoaderContext();
450 $moduleA = new ResourceLoaderFileTestModule( $a );
451 $versionA = $moduleA->getVersionHash( $context );
452 $moduleB = new ResourceLoaderFileTestModule( $b );
453 $versionB = $moduleB->getVersionHash( $context );
457 ( $versionA === $versionB ),
458 'Whether versions hashes are equal'
462 public function provideGetScriptPackageFiles() {
463 $basePath = __DIR__
. '/../../data/resourceloader';
464 $base = [ 'localBasePath' => $basePath ];
465 $commentScript = file_get_contents( "$basePath/script-comment.js" );
466 $nosemiScript = file_get_contents( "$basePath/script-nosemi.js" );
467 $config = RequestContext
::getMain()->getConfig();
478 'script-comment.js' => [
480 'content' => $commentScript,
482 'script-nosemi.js' => [
484 'content' => $nosemiScript
487 'main' => 'script-comment.js'
494 [ 'name' => 'script-nosemi.js', 'main' => true ]
496 'deprecated' => 'Deprecation test',
497 'name' => 'test-deprecated'
501 'script-comment.js' => [
503 'content' => $commentScript,
505 'script-nosemi.js' => [
507 'content' => 'mw.log.warn(' .
508 '"This page is using the deprecated ResourceLoader module \"test-deprecated\".\\n' .
514 'main' => 'script-nosemi.js'
520 [ 'name' => 'init.js', 'file' => 'script-comment.js', 'main' => true ],
521 [ 'name' => 'nosemi.js', 'file' => 'script-nosemi.js' ],
528 'content' => $commentScript,
532 'content' => $nosemiScript
538 'package file with callback' => [
541 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ] ],
543 [ 'name' => 'bar.js', 'content' => "console.log('Hello');" ],
544 [ 'name' => 'data.json', 'callback' => function ( $context ) {
545 return [ 'langCode' => $context->getLanguage() ];
547 [ 'name' => 'config.json', 'config' => [
549 'wgVersion' => 'Version',
557 'content' => [ 'Hello' => 'world' ],
561 'content' => (object)[ 'foo' => 'bar', 'answer' => 42 ],
565 'content' => "console.log('Hello');",
569 'content' => [ 'langCode' => 'fy' ]
574 'Sitename' => $config->get( 'Sitename' ),
575 'wgVersion' => $config->get( 'Version' ),
585 'package file with callback and versionCallback' => [
588 [ 'name' => 'bar.js', 'content' => "console.log('Hello');" ],
589 [ 'name' => 'data.json', 'versionCallback' => function ( $context ) {
590 return $context->getLanguage();
591 }, 'callback' => function ( $context ) {
592 return [ 'langCode' => $context->getLanguage() ];
600 'content' => "console.log('Hello');",
604 'content' => [ 'langCode' => 'fy' ]
616 [ 'file' => 'script-comment.js' ]
621 'package file with invalid callback' => [
624 [ 'name' => 'foo.json', 'callback' => 'functionThatDoesNotExist142857' ]
632 'foo.json' => [ 'type' => 'script', 'config' => [ 'Sitename' ] ]
640 [ 'name' => 'foo.js', 'config' => 'Sitename' ]
648 'foo.js' => [ 'garbage' => 'data' ]
656 'filethatdoesnotexist142857.js'
665 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ], 'main' => true ]
674 * @dataProvider provideGetScriptPackageFiles
675 * @covers ResourceLoaderFileModule::getScript
676 * @covers ResourceLoaderFileModule::getPackageFiles
677 * @covers ResourceLoaderFileModule::expandPackageFiles
679 public function testGetScriptPackageFiles( $moduleDefinition, $expected, $contextOptions = [] ) {
680 $module = new ResourceLoaderFileModule( $moduleDefinition );
681 $context = $this->getResourceLoaderContext( $contextOptions );
682 if ( isset( $moduleDefinition['name'] ) ) {
683 $module->setName( $moduleDefinition['name'] );
685 if ( $expected === false ) {
686 $this->setExpectedException( MWException
::class );
687 $module->getScript( $context );
689 $this->assertEquals( $expected, $module->getScript( $context ) );