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