Remember checkbox state on Special:Block if checkbox disabled
[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 public static function providerGetTemplates() {
269 $modules = self::getModules();
270
271 return [
272 [
273 $modules['noTemplateModule'],
274 [],
275 ],
276 [
277 $modules['templateModuleHandlebars'],
278 [
279 'templates/template_awesome.handlebars' => "wow\n",
280 ],
281 ],
282 [
283 $modules['htmlTemplateModule'],
284 [
285 'templates/template.html' => "<strong>hello</strong>\n",
286 'templates/template2.html' => "<div>goodbye</div>\n",
287 ],
288 ],
289 [
290 $modules['aliasedHtmlTemplateModule'],
291 [
292 'foo.html' => "<strong>hello</strong>\n",
293 'bar.html' => "<div>goodbye</div>\n",
294 ],
295 ],
296 [
297 $modules['htmlTemplateUnknown'],
298 false,
299 ],
300 ];
301 }
302
303 /**
304 * @dataProvider providerGetTemplates
305 * @covers ResourceLoaderFileModule::getTemplates
306 */
307 public function testGetTemplates( $module, $expected ) {
308 $rl = new ResourceLoaderFileModule( $module );
309 $rl->setName( 'testing' );
310
311 if ( $expected === false ) {
312 $this->setExpectedException( MWException::class );
313 $rl->getTemplates();
314 } else {
315 $this->assertEquals( $rl->getTemplates(), $expected );
316 }
317 }
318
319 /**
320 * @covers ResourceLoaderFileModule::stripBom
321 */
322 public function testBomConcatenation() {
323 $basePath = __DIR__ . '/../../data/css';
324 $testModule = new ResourceLoaderFileTestModule( [
325 'localBasePath' => $basePath,
326 'styles' => [ 'bom.css' ],
327 ] );
328 $testModule->setName( 'testing' );
329 $this->assertEquals(
330 substr( file_get_contents( "$basePath/bom.css" ), 0, 10 ),
331 "\xef\xbb\xbf.efbbbf",
332 'File has leading BOM'
333 );
334
335 $context = $this->getResourceLoaderContext();
336 $this->assertEquals(
337 $testModule->getStyles( $context ),
338 [ 'all' => ".efbbbf_bom_char_at_start_of_file {}\n" ],
339 'Leading BOM removed when concatenating files'
340 );
341 }
342
343 /**
344 * @covers ResourceLoaderFileModule::compileLessFile
345 */
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' ]
353 ] );
354 $module->setName( 'test.less' );
355 $styles = $module->getStyles( $context );
356 $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
357 }
358
359 public function provideGetVersionHash() {
360 $a = [];
361 $b = [
362 'lessVars' => [ 'key' => 'value' ],
363 ];
364 yield 'with and without Less variables' => [ $a, $b, false ];
365
366 $a = [
367 'lessVars' => [ 'key' => 'value1' ],
368 ];
369 $b = [
370 'lessVars' => [ 'key' => 'value2' ],
371 ];
372 yield 'different Less variables' => [ $a, $b, false ];
373
374 $x = [
375 'lessVars' => [ 'key' => 'value' ],
376 ];
377 yield 'identical Less variables' => [ $x, $x, true ];
378
379 $a = [
380 'packageFiles' => [ [ 'name' => 'data.json', 'callback' => function () {
381 return [ 'aaa' ];
382 } ] ]
383 ];
384 $b = [
385 'packageFiles' => [ [ 'name' => 'data.json', 'callback' => function () {
386 return [ 'bbb' ];
387 } ] ]
388 ];
389 yield 'packageFiles with different callback' => [ $a, $b, false ];
390
391 $a = [
392 'packageFiles' => [ [ 'name' => 'aaa.json', 'callback' => function () {
393 return [ 'x' ];
394 } ] ]
395 ];
396 $b = [
397 'packageFiles' => [ [ 'name' => 'bbb.json', 'callback' => function () {
398 return [ 'x' ];
399 } ] ]
400 ];
401 yield 'packageFiles with different file name and a callback' => [ $a, $b, false ];
402
403 $a = [
404 'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => function () {
405 return [ 'A-version' ];
406 }, 'callback' => function () {
407 throw new Exception( 'Unexpected computation' );
408 } ] ]
409 ];
410 $b = [
411 'packageFiles' => [ [ 'name' => 'data.json', 'versionCallback' => function () {
412 return [ 'B-version' ];
413 }, 'callback' => function () {
414 throw new Exception( 'Unexpected computation' );
415 } ] ]
416 ];
417 yield 'packageFiles with different versionCallback' => [ $a, $b, false ];
418
419 $a = [
420 'packageFiles' => [ [ 'name' => 'aaa.json',
421 'versionCallback' => function () {
422 return [ 'X-version' ];
423 },
424 'callback' => function () {
425 throw new Exception( 'Unexpected computation' );
426 }
427 ] ]
428 ];
429 $b = [
430 'packageFiles' => [ [ 'name' => 'bbb.json',
431 'versionCallback' => function () {
432 return [ 'X-version' ];
433 },
434 'callback' => function () {
435 throw new Exception( 'Unexpected computation' );
436 }
437 ] ]
438 ];
439 yield 'packageFiles with different file name and a versionCallback' => [ $a, $b, false ];
440 }
441
442 /**
443 * @dataProvider provideGetVersionHash
444 * @covers ResourceLoaderFileModule::getDefinitionSummary
445 * @covers ResourceLoaderFileModule::getFileHashes
446 */
447 public function testGetVersionHash( $a, $b, $isEqual ) {
448 $context = $this->getResourceLoaderContext();
449
450 $moduleA = new ResourceLoaderFileTestModule( $a );
451 $versionA = $moduleA->getVersionHash( $context );
452 $moduleB = new ResourceLoaderFileTestModule( $b );
453 $versionB = $moduleB->getVersionHash( $context );
454
455 $this->assertSame(
456 $isEqual,
457 ( $versionA === $versionB ),
458 'Whether versions hashes are equal'
459 );
460 }
461
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();
468 return [
469 [
470 $base + [
471 'packageFiles' => [
472 'script-comment.js',
473 'script-nosemi.js'
474 ]
475 ],
476 [
477 'files' => [
478 'script-comment.js' => [
479 'type' => 'script',
480 'content' => $commentScript,
481 ],
482 'script-nosemi.js' => [
483 'type' => 'script',
484 'content' => $nosemiScript
485 ]
486 ],
487 'main' => 'script-comment.js'
488 ]
489 ],
490 [
491 $base + [
492 'packageFiles' => [
493 'script-comment.js',
494 [ 'name' => 'script-nosemi.js', 'main' => true ]
495 ],
496 'deprecated' => 'Deprecation test',
497 'name' => 'test-deprecated'
498 ],
499 [
500 'files' => [
501 'script-comment.js' => [
502 'type' => 'script',
503 'content' => $commentScript,
504 ],
505 'script-nosemi.js' => [
506 'type' => 'script',
507 'content' => 'mw.log.warn(' .
508 '"This page is using the deprecated ResourceLoader module \"test-deprecated\".\\n' .
509 "Deprecation test" .
510 '");' .
511 $nosemiScript
512 ]
513 ],
514 'main' => 'script-nosemi.js'
515 ]
516 ],
517 [
518 $base + [
519 'packageFiles' => [
520 [ 'name' => 'init.js', 'file' => 'script-comment.js', 'main' => true ],
521 [ 'name' => 'nosemi.js', 'file' => 'script-nosemi.js' ],
522 ]
523 ],
524 [
525 'files' => [
526 'init.js' => [
527 'type' => 'script',
528 'content' => $commentScript,
529 ],
530 'nosemi.js' => [
531 'type' => 'script',
532 'content' => $nosemiScript
533 ]
534 ],
535 'main' => 'init.js'
536 ]
537 ],
538 'package file with callback' => [
539 $base + [
540 'packageFiles' => [
541 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ] ],
542 'sample.json',
543 [ 'name' => 'bar.js', 'content' => "console.log('Hello');" ],
544 [ 'name' => 'data.json', 'callback' => function ( $context ) {
545 return [ 'langCode' => $context->getLanguage() ];
546 } ],
547 [ 'name' => 'config.json', 'config' => [
548 'Sitename',
549 'wgVersion' => 'Version',
550 ] ],
551 ]
552 ],
553 [
554 'files' => [
555 'foo.json' => [
556 'type' => 'data',
557 'content' => [ 'Hello' => 'world' ],
558 ],
559 'sample.json' => [
560 'type' => 'data',
561 'content' => (object)[ 'foo' => 'bar', 'answer' => 42 ],
562 ],
563 'bar.js' => [
564 'type' => 'script',
565 'content' => "console.log('Hello');",
566 ],
567 'data.json' => [
568 'type' => 'data',
569 'content' => [ 'langCode' => 'fy' ]
570 ],
571 'config.json' => [
572 'type' => 'data',
573 'content' => [
574 'Sitename' => $config->get( 'Sitename' ),
575 'wgVersion' => $config->get( 'Version' ),
576 ]
577 ]
578 ],
579 'main' => 'bar.js'
580 ],
581 [
582 'lang' => 'fy'
583 ]
584 ],
585 'package file with callback and versionCallback' => [
586 $base + [
587 'packageFiles' => [
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() ];
593 } ],
594 ]
595 ],
596 [
597 'files' => [
598 'bar.js' => [
599 'type' => 'script',
600 'content' => "console.log('Hello');",
601 ],
602 'data.json' => [
603 'type' => 'data',
604 'content' => [ 'langCode' => 'fy' ]
605 ],
606 ],
607 'main' => 'bar.js'
608 ],
609 [
610 'lang' => 'fy'
611 ]
612 ],
613 [
614 $base + [
615 'packageFiles' => [
616 [ 'file' => 'script-comment.js' ]
617 ]
618 ],
619 false
620 ],
621 'package file with invalid callback' => [
622 $base + [
623 'packageFiles' => [
624 [ 'name' => 'foo.json', 'callback' => 'functionThatDoesNotExist142857' ]
625 ]
626 ],
627 false
628 ],
629 [
630 $base + [
631 'packageFiles' => [
632 'foo.json' => [ 'type' => 'script', 'config' => [ 'Sitename' ] ]
633 ]
634 ],
635 false
636 ],
637 [
638 $base + [
639 'packageFiles' => [
640 [ 'name' => 'foo.js', 'config' => 'Sitename' ]
641 ]
642 ],
643 false
644 ],
645 [
646 $base + [
647 'packageFiles' => [
648 'foo.js' => [ 'garbage' => 'data' ]
649 ]
650 ],
651 false
652 ],
653 [
654 $base + [
655 'packageFiles' => [
656 'filethatdoesnotexist142857.js'
657 ]
658 ],
659 false
660 ],
661 [
662 $base + [
663 'packageFiles' => [
664 'script-nosemi.js',
665 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ], 'main' => true ]
666 ]
667 ],
668 false
669 ]
670 ];
671 }
672
673 /**
674 * @dataProvider provideGetScriptPackageFiles
675 * @covers ResourceLoaderFileModule::getScript
676 * @covers ResourceLoaderFileModule::getPackageFiles
677 * @covers ResourceLoaderFileModule::expandPackageFiles
678 */
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'] );
684 }
685 if ( $expected === false ) {
686 $this->setExpectedException( MWException::class );
687 $module->getScript( $context );
688 } else {
689 $this->assertEquals( $expected, $module->getScript( $context ) );
690 }
691 }
692 }