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"
269 * Test reading files from elsewhere than localBasePath using ResourceLoaderFilePath.
271 * This mimics modules modified by skins using 'ResourceModuleSkinStyles' and 'OOUIThemePaths'
274 * @covers ResourceLoaderFilePath::getLocalBasePath
275 * @covers ResourceLoaderFilePath::getRemoteBasePath
277 public function testResourceLoaderFilePath() {
278 $basePath = __DIR__
. '/../../data/blahblah';
279 $filePath = __DIR__
. '/../../data/rlfilepath';
280 $testModule = new ResourceLoaderFileModule( [
281 'localBasePath' => $basePath,
282 'remoteBasePath' => 'blahblah',
283 'styles' => new ResourceLoaderFilePath( 'style.css', $filePath, 'rlfilepath' ),
285 'vector' => new ResourceLoaderFilePath( 'skinStyle.css', $filePath, 'rlfilepath' ),
287 'scripts' => new ResourceLoaderFilePath( 'script.js', $filePath, 'rlfilepath' ),
288 'templates' => new ResourceLoaderFilePath( 'template.html', $filePath, 'rlfilepath' ),
290 $expectedModule = new ResourceLoaderFileModule( [
291 'localBasePath' => $filePath,
292 'remoteBasePath' => 'rlfilepath',
293 'styles' => 'style.css',
295 'vector' => 'skinStyle.css',
297 'scripts' => 'script.js',
298 'templates' => 'template.html',
301 $context = $this->getResourceLoaderContext();
303 $expectedModule->getModuleContent( $context ),
304 $testModule->getModuleContent( $context ),
305 "Using ResourceLoaderFilePath works correctly"
309 public static function providerGetTemplates() {
310 $modules = self
::getModules();
314 $modules['noTemplateModule'],
318 $modules['templateModuleHandlebars'],
320 'templates/template_awesome.handlebars' => "wow\n",
324 $modules['htmlTemplateModule'],
326 'templates/template.html' => "<strong>hello</strong>\n",
327 'templates/template2.html' => "<div>goodbye</div>\n",
331 $modules['aliasedHtmlTemplateModule'],
333 'foo.html' => "<strong>hello</strong>\n",
334 'bar.html' => "<div>goodbye</div>\n",
338 $modules['htmlTemplateUnknown'],
345 * @dataProvider providerGetTemplates
346 * @covers ResourceLoaderFileModule::getTemplates
348 public function testGetTemplates( $module, $expected ) {
349 $rl = new ResourceLoaderFileModule( $module );
350 $rl->setName( 'testing' );
352 if ( $expected === false ) {
353 $this->setExpectedException( MWException
::class );
356 $this->assertEquals( $rl->getTemplates(), $expected );
361 * @covers ResourceLoaderFileModule::stripBom
363 public function testBomConcatenation() {
364 $basePath = __DIR__
. '/../../data/css';
365 $testModule = new ResourceLoaderFileTestModule( [
366 'localBasePath' => $basePath,
367 'styles' => [ 'bom.css' ],
369 $testModule->setName( 'testing' );
371 substr( file_get_contents( "$basePath/bom.css" ), 0, 10 ),
372 "\xef\xbb\xbf.efbbbf",
373 'File has leading BOM'
376 $context = $this->getResourceLoaderContext();
378 $testModule->getStyles( $context ),
379 [ 'all' => ".efbbbf_bom_char_at_start_of_file {}\n" ],
380 'Leading BOM removed when concatenating files'
385 * @covers ResourceLoaderFileModule::compileLessFile
387 public function testLessFileCompilation() {
388 $context = $this->getResourceLoaderContext();
389 $basePath = __DIR__
. '/../../data/less/module';
390 $module = new ResourceLoaderFileTestModule( [
391 'localBasePath' => $basePath,
392 'styles' => [ 'styles.less' ],
393 'lessVars' => [ 'foo' => '2px', 'Foo' => '#eeeeee' ]
395 $module->setName( 'test.less' );
396 $styles = $module->getStyles( $context );
397 $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
400 public function provideGetVersionHash() {
403 'lessVars' => [ 'key' => 'value' ],
405 yield
'with and without Less variables' => [ $a, $b, false ];
408 'lessVars' => [ 'key' => 'value1' ],
411 'lessVars' => [ 'key' => 'value2' ],
413 yield
'different Less variables' => [ $a, $b, false ];
416 'lessVars' => [ 'key' => 'value' ],
418 yield
'identical Less variables' => [ $x, $x, true ];
421 'packageFiles' => [ [ 'name' => 'data.json', 'callback' => function () {
426 'packageFiles' => [ [ 'name' => 'data.json', 'callback' => function () {
430 yield
'packageFiles with different callback' => [ $a, $b, false ];
433 'packageFiles' => [ [ 'name' => 'aaa.json', 'callback' => function () {
438 'packageFiles' => [ [ 'name' => 'bbb.json', 'callback' => function () {
442 yield
'packageFiles with different file name and a callback' => [ $a, $b, false ];
445 'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => function () {
446 return [ 'A-version' ];
447 }, 'callback' => function () {
448 throw new Exception( 'Unexpected computation' );
452 'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => function () {
453 return [ 'B-version' ];
454 }, 'callback' => function () {
455 throw new Exception( 'Unexpected computation' );
458 yield
'packageFiles with different versionCallback' => [ $a, $b, false ];
461 'packageFiles' => [ [ 'name' => 'aaa.json',
462 'versionCallback' => function () {
463 return [ 'X-version' ];
465 'callback' => function () {
466 throw new Exception( 'Unexpected computation' );
471 'packageFiles' => [ [ 'name' => 'bbb.json',
472 'versionCallback' => function () {
473 return [ 'X-version' ];
475 'callback' => function () {
476 throw new Exception( 'Unexpected computation' );
480 yield
'packageFiles with different file name and a versionCallback' => [ $a, $b, false ];
484 * @dataProvider provideGetVersionHash
485 * @covers ResourceLoaderFileModule::getDefinitionSummary
486 * @covers ResourceLoaderFileModule::getFileHashes
488 public function testGetVersionHash( $a, $b, $isEqual ) {
489 $context = $this->getResourceLoaderContext();
491 $moduleA = new ResourceLoaderFileTestModule( $a );
492 $versionA = $moduleA->getVersionHash( $context );
493 $moduleB = new ResourceLoaderFileTestModule( $b );
494 $versionB = $moduleB->getVersionHash( $context );
498 ( $versionA === $versionB ),
499 'Whether versions hashes are equal'
503 public function provideGetScriptPackageFiles() {
504 $basePath = __DIR__
. '/../../data/resourceloader';
505 $base = [ 'localBasePath' => $basePath ];
506 $commentScript = file_get_contents( "$basePath/script-comment.js" );
507 $nosemiScript = file_get_contents( "$basePath/script-nosemi.js" );
508 $config = RequestContext
::getMain()->getConfig();
519 'script-comment.js' => [
521 'content' => $commentScript,
523 'script-nosemi.js' => [
525 'content' => $nosemiScript
528 'main' => 'script-comment.js'
535 [ 'name' => 'script-nosemi.js', 'main' => true ]
537 'deprecated' => 'Deprecation test',
538 'name' => 'test-deprecated'
542 'script-comment.js' => [
544 'content' => $commentScript,
546 'script-nosemi.js' => [
548 'content' => 'mw.log.warn(' .
549 '"This page is using the deprecated ResourceLoader module \"test-deprecated\".\\n' .
555 'main' => 'script-nosemi.js'
561 [ 'name' => 'init.js', 'file' => 'script-comment.js', 'main' => true ],
562 [ 'name' => 'nosemi.js', 'file' => 'script-nosemi.js' ],
569 'content' => $commentScript,
573 'content' => $nosemiScript
579 'package file with callback' => [
582 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ] ],
584 [ 'name' => 'bar.js', 'content' => "console.log('Hello');" ],
585 [ 'name' => 'data.json', 'callback' => function ( $context ) {
586 return [ 'langCode' => $context->getLanguage() ];
588 [ 'name' => 'config.json', 'config' => [
590 'wgVersion' => 'Version',
598 'content' => [ 'Hello' => 'world' ],
602 'content' => (object)[ 'foo' => 'bar', 'answer' => 42 ],
606 'content' => "console.log('Hello');",
610 'content' => [ 'langCode' => 'fy' ]
615 'Sitename' => $config->get( 'Sitename' ),
616 'wgVersion' => $config->get( 'Version' ),
626 'package file with callback and versionCallback' => [
629 [ 'name' => 'bar.js', 'content' => "console.log('Hello');" ],
630 [ 'name' => 'data.json', 'versionCallback' => function ( $context ) {
631 return $context->getLanguage();
632 }, 'callback' => function ( $context ) {
633 return [ 'langCode' => $context->getLanguage() ];
641 'content' => "console.log('Hello');",
645 'content' => [ 'langCode' => 'fy' ]
657 [ 'file' => 'script-comment.js' ]
662 'package file with invalid callback' => [
665 [ 'name' => 'foo.json', 'callback' => 'functionThatDoesNotExist142857' ]
673 'foo.json' => [ 'type' => 'script', 'config' => [ 'Sitename' ] ]
681 [ 'name' => 'foo.js', 'config' => 'Sitename' ]
689 'foo.js' => [ 'garbage' => 'data' ]
697 'filethatdoesnotexist142857.js'
706 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ], 'main' => true ]
715 * @dataProvider provideGetScriptPackageFiles
716 * @covers ResourceLoaderFileModule::getScript
717 * @covers ResourceLoaderFileModule::getPackageFiles
718 * @covers ResourceLoaderFileModule::expandPackageFiles
720 public function testGetScriptPackageFiles( $moduleDefinition, $expected, $contextOptions = [] ) {
721 $module = new ResourceLoaderFileModule( $moduleDefinition );
722 $context = $this->getResourceLoaderContext( $contextOptions );
723 if ( isset( $moduleDefinition['name'] ) ) {
724 $module->setName( $moduleDefinition['name'] );
726 if ( $expected === false ) {
727 $this->setExpectedException( MWException
::class );
728 $module->getScript( $context );
730 $this->assertEquals( $expected, $module->getScript( $context ) );