Merge "Improve return types in class MagicWordArray"
[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 ], [
351 'lessVars' => [ 'foo' => '2px', 'Foo' => '#eeeeee' ]
352 ] );
353 $module->setName( 'test.less' );
354 $styles = $module->getStyles( $context );
355 $this->assertStringEqualsFile( $basePath . '/styles.css', $styles['all'] );
356 }
357
358 /**
359 * @covers ResourceLoaderFileModule::getDefinitionSummary
360 * @covers ResourceLoaderFileModule::getFileHashes
361 */
362 public function testGetVersionHash() {
363 $context = $this->getResourceLoaderContext();
364
365 // Less variables
366 $module = new ResourceLoaderFileTestModule();
367 $version = $module->getVersionHash( $context );
368 $module = new ResourceLoaderFileTestModule( [], [
369 'lessVars' => [ 'key' => 'value' ],
370 ] );
371 $this->assertNotEquals(
372 $version,
373 $module->getVersionHash( $context ),
374 'Using less variables is significant'
375 );
376 }
377
378 public function providerGetScriptPackageFiles() {
379 $basePath = __DIR__ . '/../../data/resourceloader';
380 $base = [ 'localBasePath' => $basePath ];
381 $commentScript = file_get_contents( "$basePath/script-comment.js" );
382 $nosemiScript = file_get_contents( "$basePath/script-nosemi.js" );
383 $config = RequestContext::getMain()->getConfig();
384 return [
385 [
386 $base + [
387 'packageFiles' => [
388 'script-comment.js',
389 'script-nosemi.js'
390 ]
391 ],
392 [
393 'files' => [
394 'script-comment.js' => [
395 'type' => 'script',
396 'content' => $commentScript,
397 ],
398 'script-nosemi.js' => [
399 'type' => 'script',
400 'content' => $nosemiScript
401 ]
402 ],
403 'main' => 'script-comment.js'
404 ]
405 ],
406 [
407 $base + [
408 'packageFiles' => [
409 'script-comment.js',
410 [ 'name' => 'script-nosemi.js', 'main' => true ]
411 ],
412 'deprecated' => 'Deprecation test',
413 'name' => 'test-deprecated'
414 ],
415 [
416 'files' => [
417 'script-comment.js' => [
418 'type' => 'script',
419 'content' => $commentScript,
420 ],
421 'script-nosemi.js' => [
422 'type' => 'script',
423 'content' => 'mw.log.warn(' .
424 '"This page is using the deprecated ResourceLoader module \"test-deprecated\".\\n' .
425 "Deprecation test" .
426 '");' .
427 $nosemiScript
428 ]
429 ],
430 'main' => 'script-nosemi.js'
431 ]
432 ],
433 [
434 $base + [
435 'packageFiles' => [
436 [ 'name' => 'init.js', 'file' => 'script-comment.js', 'main' => true ],
437 [ 'name' => 'nosemi.js', 'file' => 'script-nosemi.js' ],
438 ]
439 ],
440 [
441 'files' => [
442 'init.js' => [
443 'type' => 'script',
444 'content' => $commentScript,
445 ],
446 'nosemi.js' => [
447 'type' => 'script',
448 'content' => $nosemiScript
449 ]
450 ],
451 'main' => 'init.js'
452 ]
453 ],
454 [
455 $base + [
456 'packageFiles' => [
457 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ] ],
458 'sample.json',
459 [ 'name' => 'bar.js', 'content' => "console.log('Hello');" ],
460 [ 'name' => 'data.json', 'callback' => function ( $context ) {
461 return [ 'langCode' => $context->getLanguage() ];
462 } ],
463 [ 'name' => 'config.json', 'config' => [
464 'Sitename',
465 'wgVersion' => 'Version',
466 ] ],
467 ]
468 ],
469 [
470 'files' => [
471 'foo.json' => [
472 'type' => 'data',
473 'content' => [ 'Hello' => 'world' ],
474 ],
475 'sample.json' => [
476 'type' => 'data',
477 'content' => (object)[ 'foo' => 'bar', 'answer' => 42 ],
478 ],
479 'bar.js' => [
480 'type' => 'script',
481 'content' => "console.log('Hello');",
482 ],
483 'data.json' => [
484 'type' => 'data',
485 'content' => [ 'langCode' => 'fy' ]
486 ],
487 'config.json' => [
488 'type' => 'data',
489 'content' => [
490 'Sitename' => $config->get( 'Sitename' ),
491 'wgVersion' => $config->get( 'Version' ),
492 ]
493 ]
494 ],
495 'main' => 'bar.js'
496 ],
497 [
498 'lang' => 'fy'
499 ]
500 ],
501 [
502 $base + [
503 'packageFiles' => [
504 [ 'file' => 'script-comment.js' ]
505 ]
506 ],
507 false
508 ],
509 [
510 $base + [
511 'packageFiles' => [
512 [ 'name' => 'foo.json', 'callback' => 'functionThatDoesNotExist142857' ]
513 ]
514 ],
515 false
516 ],
517 [
518 $base + [
519 'packageFiles' => [
520 'foo.json' => [ 'type' => 'script', 'config' => [ 'Sitename' ] ]
521 ]
522 ],
523 false
524 ],
525 [
526 $base + [
527 'packageFiles' => [
528 [ 'name' => 'foo.js', 'config' => 'Sitename' ]
529 ]
530 ],
531 false
532 ],
533 [
534 $base + [
535 'packageFiles' => [
536 'foo.js' => [ 'garbage' => 'data' ]
537 ]
538 ],
539 false
540 ],
541 [
542 $base + [
543 'packageFiles' => [
544 'filethatdoesnotexist142857.js'
545 ]
546 ],
547 false
548 ],
549 [
550 $base + [
551 'packageFiles' => [
552 'script-nosemi.js',
553 [ 'name' => 'foo.json', 'content' => [ 'Hello' => 'world' ], 'main' => true ]
554 ]
555 ],
556 false
557 ]
558 ];
559 }
560
561 /**
562 * @dataProvider providerGetScriptPackageFiles
563 * @covers ResourceLoaderFileModule::getScript
564 * @covers ResourceLoaderFileModule::getPackageFiles
565 * @covers ResourceLoaderFileModule::expandPackageFiles
566 */
567 public function testGetScriptPackageFiles( $moduleDefinition, $expected, $contextOptions = [] ) {
568 $module = new ResourceLoaderFileModule( $moduleDefinition );
569 $context = $this->getResourceLoaderContext( $contextOptions );
570 if ( isset( $moduleDefinition['name'] ) ) {
571 $module->setName( $moduleDefinition['name'] );
572 }
573 if ( $expected === false ) {
574 $this->setExpectedException( MWException::class );
575 $module->getScript( $context );
576 } else {
577 $this->assertEquals( $expected, $module->getScript( $context ) );
578 }
579 }
580 }