Merge "Exclude redirects from Special:Fewestrevisions"
[lhc/web/wiklou.git] / tests / phpunit / includes / resourceloader / ResourceLoaderFileModuleTest.php
1 <?php
2
3 /**
4 * @group ResourceLoader
5 */
6 class ResourceLoaderFileModuleTest extends ResourceLoaderTestCase {
7
8 protected function setUp() {
9 parent::setUp();
10
11 $skinFactory = new SkinFactory();
12 // The return value of the closure shouldn't matter since this test should
13 // never call it
14 $skinFactory->register(
15 'fakeskin',
16 'FakeSkin',
17 function () {
18 }
19 );
20 $this->setService( 'SkinFactory', $skinFactory );
21
22 // This test is not expected to query any database
23 MediaWiki\MediaWikiServices::disableStorageBackend();
24 }
25
26 private static function getModules() {
27 $base = [
28 'localBasePath' => __DIR__,
29 ];
30
31 return [
32 'noTemplateModule' => [],
33
34 'deprecatedModule' => $base + [
35 'deprecated' => true,
36 ],
37 'deprecatedTomorrow' => $base + [
38 'deprecated' => 'Will be removed tomorrow.'
39 ],
40
41 'htmlTemplateModule' => $base + [
42 'templates' => [
43 'templates/template.html',
44 'templates/template2.html',
45 ]
46 ],
47
48 'htmlTemplateUnknown' => $base + [
49 'templates' => [
50 'templates/notfound.html',
51 ]
52 ],
53
54 'aliasedHtmlTemplateModule' => $base + [
55 'templates' => [
56 'foo.html' => 'templates/template.html',
57 'bar.html' => 'templates/template2.html',
58 ]
59 ],
60
61 'templateModuleHandlebars' => $base + [
62 'templates' => [
63 'templates/template_awesome.handlebars',
64 ],
65 ],
66
67 'aliasFooFromBar' => $base + [
68 'templates' => [
69 'foo.foo' => 'templates/template.bar',
70 ],
71 ],
72 ];
73 }
74
75 public static function providerTemplateDependencies() {
76 $modules = self::getModules();
77
78 return [
79 [
80 $modules['noTemplateModule'],
81 [],
82 ],
83 [
84 $modules['htmlTemplateModule'],
85 [
86 'mediawiki.template',
87 ],
88 ],
89 [
90 $modules['templateModuleHandlebars'],
91 [
92 'mediawiki.template',
93 'mediawiki.template.handlebars',
94 ],
95 ],
96 [
97 $modules['aliasFooFromBar'],
98 [
99 'mediawiki.template',
100 'mediawiki.template.foo',
101 ],
102 ],
103 ];
104 }
105
106 /**
107 * @dataProvider providerTemplateDependencies
108 * @covers ResourceLoaderFileModule::__construct
109 * @covers ResourceLoaderFileModule::getDependencies
110 */
111 public function testTemplateDependencies( $module, $expected ) {
112 $rl = new ResourceLoaderFileModule( $module );
113 $rl->setName( 'testing' );
114 $this->assertEquals( $rl->getDependencies(), $expected );
115 }
116
117 public static function providerDeprecatedModules() {
118 return [
119 [
120 'deprecatedModule',
121 'mw.log.warn("This page is using the deprecated ResourceLoader module \"deprecatedModule\".");',
122 ],
123 [
124 'deprecatedTomorrow',
125 'mw.log.warn(' .
126 '"This page is using the deprecated ResourceLoader module \"deprecatedTomorrow\".\\n' .
127 "Will be removed tomorrow." .
128 '");'
129 ]
130 ];
131 }
132
133 /**
134 * @dataProvider providerDeprecatedModules
135 * @covers ResourceLoaderFileModule::getScript
136 */
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 );
143 }
144
145 /**
146 * @covers ResourceLoaderFileModule::getScript
147 * @covers ResourceLoaderFileModule::getScriptFiles
148 * @covers ResourceLoaderFileModule::readScriptFiles
149 */
150 public function testGetScript() {
151 $module = new ResourceLoaderFileModule( [
152 'localBasePath' => __DIR__ . '/../../data/resourceloader',
153 'scripts' => [ 'script-nosemi.js', 'script-comment.js' ],
154 ] );
155 $module->setName( 'testing' );
156 $ctx = $this->getResourceLoaderContext();
157 $this->assertEquals(
158 "/* eslint-disable */\nmw.foo()\n" .
159 "\n" .
160 "/* eslint-disable */\nmw.foo()\n// mw.bar();\n" .
161 "\n",
162 $module->getScript( $ctx ),
163 'scripts are concatenated with a new-line'
164 );
165 }
166
167 /**
168 * @covers ResourceLoaderFileModule::getAllStyleFiles
169 * @covers ResourceLoaderFileModule::getAllSkinStyleFiles
170 * @covers ResourceLoaderFileModule::getSkinStyleFiles
171 */
172 public function testGetAllSkinStyleFiles() {
173 $baseParams = [
174 'scripts' => [
175 'foo.js',
176 'bar.js',
177 ],
178 'styles' => [
179 'foo.css',
180 'bar.css' => [ 'media' => 'print' ],
181 'screen.less' => [ 'media' => 'screen' ],
182 'screen-query.css' => [ 'media' => 'screen and (min-width: 400px)' ],
183 ],
184 'skinStyles' => [
185 'default' => 'quux-fallback.less',
186 'fakeskin' => [
187 'baz-vector.css',
188 'quux-vector.less',
189 ],
190 ],
191 'messages' => [
192 'hello',
193 'world',
194 ],
195 ];
196
197 $module = new ResourceLoaderFileModule( $baseParams );
198 $module->setName( 'testing' );
199
200 $this->assertEquals(
201 [
202 'foo.css',
203 'baz-vector.css',
204 'quux-vector.less',
205 'quux-fallback.less',
206 'bar.css',
207 'screen.less',
208 'screen-query.css',
209 ],
210 array_map( 'basename', $module->getAllStyleFiles() )
211 );
212 }
213
214 /**
215 * Strip @noflip annotations from CSS code.
216 * @param string $css
217 * @return string
218 */
219 private static function stripNoflip( $css ) {
220 return str_replace( '/*@noflip*/ ', '', $css );
221 }
222
223 /**
224 * What happens when you mix @embed and @noflip?
225 * This really is an integration test, but oh well.
226 *
227 * @covers ResourceLoaderFileModule::getStyles
228 * @covers ResourceLoaderFileModule::getStyleFiles
229 * @covers ResourceLoaderFileModule::readStyleFiles
230 * @covers ResourceLoaderFileModule::readStyleFile
231 */
232 public function testMixedCssAnnotations() {
233 $basePath = __DIR__ . '/../../data/css';
234 $testModule = new ResourceLoaderFileTestModule( [
235 'localBasePath' => $basePath,
236 'styles' => [ 'test.css' ],
237 ] );
238 $testModule->setName( 'testing' );
239 $expectedModule = new ResourceLoaderFileTestModule( [
240 'localBasePath' => $basePath,
241 'styles' => [ 'expected.css' ],
242 ] );
243 $expectedModule->setName( 'testing' );
244
245 $contextLtr = $this->getResourceLoaderContext( [
246 'lang' => 'en',
247 'dir' => 'ltr',
248 ] );
249 $contextRtl = $this->getResourceLoaderContext( [
250 'lang' => 'he',
251 'dir' => 'rtl',
252 ] );
253
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.
256 $this->assertEquals(
257 $expectedModule->getStyles( $contextLtr ),
258 self::stripNoflip( $testModule->getStyles( $contextLtr ) ),
259 "/*@noflip*/ with /*@embed*/ gives correct results in LTR mode"
260 );
261 $this->assertEquals(
262 $expectedModule->getStyles( $contextLtr ),
263 self::stripNoflip( $testModule->getStyles( $contextRtl ) ),
264 "/*@noflip*/ with /*@embed*/ gives correct results in RTL mode"
265 );
266 }
267
268 /**
269 * Test reading files from elsewhere than localBasePath using ResourceLoaderFilePath.
270 *
271 * This mimics modules modified by skins using 'ResourceModuleSkinStyles' and 'OOUIThemePaths'
272 * skin attributes.
273 *
274 * @covers ResourceLoaderFilePath::getLocalBasePath
275 * @covers ResourceLoaderFilePath::getRemoteBasePath
276 */
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' ),
284 'skinStyles' => [
285 'vector' => new ResourceLoaderFilePath( 'skinStyle.css', $filePath, 'rlfilepath' ),
286 ],
287 'scripts' => new ResourceLoaderFilePath( 'script.js', $filePath, 'rlfilepath' ),
288 'templates' => new ResourceLoaderFilePath( 'template.html', $filePath, 'rlfilepath' ),
289 ] );
290 $expectedModule = new ResourceLoaderFileModule( [
291 'localBasePath' => $filePath,
292 'remoteBasePath' => 'rlfilepath',
293 'styles' => 'style.css',
294 'skinStyles' => [
295 'vector' => 'skinStyle.css',
296 ],
297 'scripts' => 'script.js',
298 'templates' => 'template.html',
299 ] );
300
301 $context = $this->getResourceLoaderContext();
302 $this->assertEquals(
303 $expectedModule->getModuleContent( $context ),
304 $testModule->getModuleContent( $context ),
305 "Using ResourceLoaderFilePath works correctly"
306 );
307 }
308
309 public static function providerGetTemplates() {
310 $modules = self::getModules();
311
312 return [
313 [
314 $modules['noTemplateModule'],
315 [],
316 ],
317 [
318 $modules['templateModuleHandlebars'],
319 [
320 'templates/template_awesome.handlebars' => "wow\n",
321 ],
322 ],
323 [
324 $modules['htmlTemplateModule'],
325 [
326 'templates/template.html' => "<strong>hello</strong>\n",
327 'templates/template2.html' => "<div>goodbye</div>\n",
328 ],
329 ],
330 [
331 $modules['aliasedHtmlTemplateModule'],
332 [
333 'foo.html' => "<strong>hello</strong>\n",
334 'bar.html' => "<div>goodbye</div>\n",
335 ],
336 ],
337 [
338 $modules['htmlTemplateUnknown'],
339 false,
340 ],
341 ];
342 }
343
344 /**
345 * @dataProvider providerGetTemplates
346 * @covers ResourceLoaderFileModule::getTemplates
347 */
348 public function testGetTemplates( $module, $expected ) {
349 $rl = new ResourceLoaderFileModule( $module );
350 $rl->setName( 'testing' );
351
352 if ( $expected === false ) {
353 $this->setExpectedException( MWException::class );
354 $rl->getTemplates();
355 } else {
356 $this->assertEquals( $rl->getTemplates(), $expected );
357 }
358 }
359
360 /**
361 * @covers ResourceLoaderFileModule::stripBom
362 */
363 public function testBomConcatenation() {
364 $basePath = __DIR__ . '/../../data/css';
365 $testModule = new ResourceLoaderFileTestModule( [
366 'localBasePath' => $basePath,
367 'styles' => [ 'bom.css' ],
368 ] );
369 $testModule->setName( 'testing' );
370 $this->assertEquals(
371 substr( file_get_contents( "$basePath/bom.css" ), 0, 10 ),
372 "\xef\xbb\xbf.efbbbf",
373 'File has leading BOM'
374 );
375
376 $context = $this->getResourceLoaderContext();
377 $this->assertEquals(
378 $testModule->getStyles( $context ),
379 [ 'all' => ".efbbbf_bom_char_at_start_of_file {}\n" ],
380 'Leading BOM removed when concatenating files'
381 );
382 }
383
384 /**
385 * @covers ResourceLoaderFileModule::compileLessFile
386 */
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' ]
394 ] );
395 $module->setName( 'test.less' );
396 $styles = $module->getStyles( $context );
397 $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
398 }
399
400 public function provideGetVersionHash() {
401 $a = [];
402 $b = [
403 'lessVars' => [ 'key' => 'value' ],
404 ];
405 yield 'with and without Less variables' => [ $a, $b, false ];
406
407 $a = [
408 'lessVars' => [ 'key' => 'value1' ],
409 ];
410 $b = [
411 'lessVars' => [ 'key' => 'value2' ],
412 ];
413 yield 'different Less variables' => [ $a, $b, false ];
414
415 $x = [
416 'lessVars' => [ 'key' => 'value' ],
417 ];
418 yield 'identical Less variables' => [ $x, $x, true ];
419
420 $a = [
421 'packageFiles' => [ [ 'name' => 'data.json', 'callback' => function () {
422 return [ 'aaa' ];
423 } ] ]
424 ];
425 $b = [
426 'packageFiles' => [ [ 'name' => 'data.json', 'callback' => function () {
427 return [ 'bbb' ];
428 } ] ]
429 ];
430 yield 'packageFiles with different callback' => [ $a, $b, false ];
431
432 $a = [
433 'packageFiles' => [ [ 'name' => 'aaa.json', 'callback' => function () {
434 return [ 'x' ];
435 } ] ]
436 ];
437 $b = [
438 'packageFiles' => [ [ 'name' => 'bbb.json', 'callback' => function () {
439 return [ 'x' ];
440 } ] ]
441 ];
442 yield 'packageFiles with different file name and a callback' => [ $a, $b, false ];
443
444 $a = [
445 'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => function () {
446 return [ 'A-version' ];
447 }, 'callback' => function () {
448 throw new Exception( 'Unexpected computation' );
449 } ] ]
450 ];
451 $b = [
452 'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => function () {
453 return [ 'B-version' ];
454 }, 'callback' => function () {
455 throw new Exception( 'Unexpected computation' );
456 } ] ]
457 ];
458 yield 'packageFiles with different versionCallback' => [ $a, $b, false ];
459
460 $a = [
461 'packageFiles' => [ [ 'name' => 'aaa.json',
462 'versionCallback' => function () {
463 return [ 'X-version' ];
464 },
465 'callback' => function () {
466 throw new Exception( 'Unexpected computation' );
467 }
468 ] ]
469 ];
470 $b = [
471 'packageFiles' => [ [ 'name' => 'bbb.json',
472 'versionCallback' => function () {
473 return [ 'X-version' ];
474 },
475 'callback' => function () {
476 throw new Exception( 'Unexpected computation' );
477 }
478 ] ]
479 ];
480 yield 'packageFiles with different file name and a versionCallback' => [ $a, $b, false ];
481 }
482
483 /**
484 * @dataProvider provideGetVersionHash
485 * @covers ResourceLoaderFileModule::getDefinitionSummary
486 * @covers ResourceLoaderFileModule::getFileHashes
487 */
488 public function testGetVersionHash( $a, $b, $isEqual ) {
489 $context = $this->getResourceLoaderContext();
490
491 $moduleA = new ResourceLoaderFileTestModule( $a );
492 $versionA = $moduleA->getVersionHash( $context );
493 $moduleB = new ResourceLoaderFileTestModule( $b );
494 $versionB = $moduleB->getVersionHash( $context );
495
496 $this->assertSame(
497 $isEqual,
498 ( $versionA === $versionB ),
499 'Whether versions hashes are equal'
500 );
501 }
502
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();
509 return [
510 [
511 $base + [
512 'packageFiles' => [
513 'script-comment.js',
514 'script-nosemi.js'
515 ]
516 ],
517 [
518 'files' => [
519 'script-comment.js' => [
520 'type' => 'script',
521 'content' => $commentScript,
522 ],
523 'script-nosemi.js' => [
524 'type' => 'script',
525 'content' => $nosemiScript
526 ]
527 ],
528 'main' => 'script-comment.js'
529 ]
530 ],
531 [
532 $base + [
533 'packageFiles' => [
534 'script-comment.js',
535 [ 'name' => 'script-nosemi.js', 'main' => true ]
536 ],
537 'deprecated' => 'Deprecation test',
538 'name' => 'test-deprecated'
539 ],
540 [
541 'files' => [
542 'script-comment.js' => [
543 'type' => 'script',
544 'content' => $commentScript,
545 ],
546 'script-nosemi.js' => [
547 'type' => 'script',
548 'content' => 'mw.log.warn(' .
549 '"This page is using the deprecated ResourceLoader module \"test-deprecated\".\\n' .
550 "Deprecation test" .
551 '");' .
552 $nosemiScript
553 ]
554 ],
555 'main' => 'script-nosemi.js'
556 ]
557 ],
558 [
559 $base + [
560 'packageFiles' => [
561 [ 'name' => 'init.js', 'file' => 'script-comment.js', 'main' => true ],
562 [ 'name' => 'nosemi.js', 'file' => 'script-nosemi.js' ],
563 ]
564 ],
565 [
566 'files' => [
567 'init.js' => [
568 'type' => 'script',
569 'content' => $commentScript,
570 ],
571 'nosemi.js' => [
572 'type' => 'script',
573 'content' => $nosemiScript
574 ]
575 ],
576 'main' => 'init.js'
577 ]
578 ],
579 'package file with callback' => [
580 $base + [
581 'packageFiles' => [
582 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ] ],
583 'sample.json',
584 [ 'name' => 'bar.js', 'content' => "console.log('Hello');" ],
585 [ 'name' => 'data.json', 'callback' => function ( $context ) {
586 return [ 'langCode' => $context->getLanguage() ];
587 } ],
588 [ 'name' => 'config.json', 'config' => [
589 'Sitename',
590 'wgVersion' => 'Version',
591 ] ],
592 ]
593 ],
594 [
595 'files' => [
596 'foo.json' => [
597 'type' => 'data',
598 'content' => [ 'Hello' => 'world' ],
599 ],
600 'sample.json' => [
601 'type' => 'data',
602 'content' => (object)[ 'foo' => 'bar', 'answer' => 42 ],
603 ],
604 'bar.js' => [
605 'type' => 'script',
606 'content' => "console.log('Hello');",
607 ],
608 'data.json' => [
609 'type' => 'data',
610 'content' => [ 'langCode' => 'fy' ]
611 ],
612 'config.json' => [
613 'type' => 'data',
614 'content' => [
615 'Sitename' => $config->get( 'Sitename' ),
616 'wgVersion' => $config->get( 'Version' ),
617 ]
618 ]
619 ],
620 'main' => 'bar.js'
621 ],
622 [
623 'lang' => 'fy'
624 ]
625 ],
626 'package file with callback and versionCallback' => [
627 $base + [
628 'packageFiles' => [
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() ];
634 } ],
635 ]
636 ],
637 [
638 'files' => [
639 'bar.js' => [
640 'type' => 'script',
641 'content' => "console.log('Hello');",
642 ],
643 'data.json' => [
644 'type' => 'data',
645 'content' => [ 'langCode' => 'fy' ]
646 ],
647 ],
648 'main' => 'bar.js'
649 ],
650 [
651 'lang' => 'fy'
652 ]
653 ],
654 [
655 $base + [
656 'packageFiles' => [
657 [ 'file' => 'script-comment.js' ]
658 ]
659 ],
660 false
661 ],
662 'package file with invalid callback' => [
663 $base + [
664 'packageFiles' => [
665 [ 'name' => 'foo.json', 'callback' => 'functionThatDoesNotExist142857' ]
666 ]
667 ],
668 false
669 ],
670 [
671 $base + [
672 'packageFiles' => [
673 'foo.json' => [ 'type' => 'script', 'config' => [ 'Sitename' ] ]
674 ]
675 ],
676 false
677 ],
678 [
679 $base + [
680 'packageFiles' => [
681 [ 'name' => 'foo.js', 'config' => 'Sitename' ]
682 ]
683 ],
684 false
685 ],
686 [
687 $base + [
688 'packageFiles' => [
689 'foo.js' => [ 'garbage' => 'data' ]
690 ]
691 ],
692 false
693 ],
694 [
695 $base + [
696 'packageFiles' => [
697 'filethatdoesnotexist142857.js'
698 ]
699 ],
700 false
701 ],
702 [
703 $base + [
704 'packageFiles' => [
705 'script-nosemi.js',
706 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ], 'main' => true ]
707 ]
708 ],
709 false
710 ]
711 ];
712 }
713
714 /**
715 * @dataProvider provideGetScriptPackageFiles
716 * @covers ResourceLoaderFileModule::getScript
717 * @covers ResourceLoaderFileModule::getPackageFiles
718 * @covers ResourceLoaderFileModule::expandPackageFiles
719 */
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'] );
725 }
726 if ( $expected === false ) {
727 $this->setExpectedException( MWException::class );
728 $module->getScript( $context );
729 } else {
730 $this->assertEquals( $expected, $module->getScript( $context ) );
731 }
732 }
733 }