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