8a5641a3aa0bb55bd26caf6534a20ed640be4327
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / Unit / Resource / ResourceCompressorTest.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Core\Tests\Unit\Resource;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use TYPO3\CMS\Core\Core\Environment;
19 use TYPO3\CMS\Core\Resource\ResourceCompressor;
20 use TYPO3\CMS\Core\Utility\PathUtility;
21
22 /**
23 * Testcase for the ResourceCompressor class
24 */
25 class ResourceCompressorTest extends BaseTestCase
26 {
27 /**
28 * @var ResourceCompressor|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface
29 */
30 protected $subject;
31
32 /**
33 * Set up the test
34 */
35 protected function setUp(): void
36 {
37 parent::setUp();
38 $this->subject = $this->getAccessibleMock(ResourceCompressor::class, ['compressCssFile', 'compressJsFile', 'createMergedCssFile', 'createMergedJsFile', 'getFilenameFromMainDir', 'checkBaseDirectory']);
39 }
40
41 /**
42 * @return array
43 */
44 public function cssFixStatementsDataProvider(): array
45 {
46 return [
47 'nothing to do - no charset/import/namespace' => [
48 'body { background: #ffffff; }',
49 'body { background: #ffffff; }'
50 ],
51 'import in front' => [
52 '@import url(http://www.example.com/css); body { background: #ffffff; }',
53 'LF/* moved by compressor */LF@import url(http://www.example.com/css);LF/* moved by compressor */LFbody { background: #ffffff; }'
54 ],
55 'import in back, without quotes' => [
56 'body { background: #ffffff; } @import url(http://www.example.com/css);',
57 'LF/* moved by compressor */LF@import url(http://www.example.com/css);LF/* moved by compressor */LFbody { background: #ffffff; }'
58 ],
59 'import in back, with double-quotes' => [
60 'body { background: #ffffff; } @import url("http://www.example.com/css");',
61 'LF/* moved by compressor */LF@import url("http://www.example.com/css");LF/* moved by compressor */LFbody { background: #ffffff; }'
62 ],
63 'import in back, with single-quotes' => [
64 'body { background: #ffffff; } @import url(\'http://www.example.com/css\');',
65 'LF/* moved by compressor */LF@import url(\'http://www.example.com/css\');LF/* moved by compressor */LFbody { background: #ffffff; }'
66 ],
67 'import in middle and back, without quotes' => [
68 'body { background: #ffffff; } @import url(http://www.example.com/A); div { background: #000; } @import url(http://www.example.com/B);',
69 'LF/* moved by compressor */LF@import url(http://www.example.com/A);@import url(http://www.example.com/B);LF/* moved by compressor */LFbody { background: #ffffff; } div { background: #000; }'
70 ],
71 'charset declaration is unique' => [
72 'body { background: #ffffff; } @charset "UTF-8"; div { background: #000; }; @charset "UTF-8";',
73 '@charset "UTF-8";LF/* moved by compressor */LFbody { background: #ffffff; } div { background: #000; };'
74 ],
75 'order of charset, namespace and import is correct' => [
76 'body { background: #ffffff; } @charset "UTF-8"; div { background: #000; }; @import "file2.css"; @namespace url(http://www.w3.org/1999/xhtml);',
77 '@charset "UTF-8";LF/* moved by compressor */LF@namespace url(http://www.w3.org/1999/xhtml);LF/* moved by compressor */LF@import "file2.css";LF/* moved by compressor */LFbody { background: #ffffff; } div { background: #000; };'
78 ],
79 ];
80 }
81
82 /**
83 * @test
84 * @dataProvider cssFixStatementsDataProvider
85 * @param string $input
86 * @param string $expected
87 */
88 public function cssFixStatementsMovesStatementsToTopIfNeeded($input, $expected): void
89 {
90 $result = $this->subject->_call('cssFixStatements', $input);
91 $resultWithReadableLinefeed = str_replace(LF, 'LF', $result);
92 $this->assertEquals($expected, $resultWithReadableLinefeed);
93 }
94
95 /**
96 * @test
97 */
98 public function compressedCssFileIsFlaggedToNotCompressAgain(): void
99 {
100 $fileName = 'fooFile.css';
101 $compressedFileName = $fileName . '.gzip';
102 $testFileFixture = [
103 $fileName => [
104 'file' => $fileName,
105 'compress' => true,
106 ]
107 ];
108 $this->subject->expects($this->once())
109 ->method('compressCssFile')
110 ->with($fileName)
111 ->will($this->returnValue($compressedFileName));
112
113 $result = $this->subject->compressCssFiles($testFileFixture);
114
115 $this->assertArrayHasKey($compressedFileName, $result);
116 $this->assertArrayHasKey('compress', $result[$compressedFileName]);
117 $this->assertFalse($result[$compressedFileName]['compress']);
118 }
119
120 /**
121 * @test
122 */
123 public function compressedJsFileIsFlaggedToNotCompressAgain(): void
124 {
125 $fileName = 'fooFile.js';
126 $compressedFileName = $fileName . '.gzip';
127 $testFileFixture = [
128 $fileName => [
129 'file' => $fileName,
130 'compress' => true,
131 ]
132 ];
133 $this->subject->expects($this->once())
134 ->method('compressJsFile')
135 ->with($fileName)
136 ->will($this->returnValue($compressedFileName));
137
138 $result = $this->subject->compressJsFiles($testFileFixture);
139
140 $this->assertArrayHasKey($compressedFileName, $result);
141 $this->assertArrayHasKey('compress', $result[$compressedFileName]);
142 $this->assertFalse($result[$compressedFileName]['compress']);
143 }
144
145 /**
146 * @test
147 */
148 public function concatenatedCssFileIsFlaggedToNotConcatenateAgain(): void
149 {
150 $fileName = 'fooFile.css';
151 $concatenatedFileName = 'merged_' . $fileName;
152 $testFileFixture = [
153 $fileName => [
154 'file' => $fileName,
155 'excludeFromConcatenation' => false,
156 'media' => 'all',
157 ]
158 ];
159 $this->subject->expects($this->once())
160 ->method('createMergedCssFile')
161 ->will($this->returnValue($concatenatedFileName));
162
163 $result = $this->subject->concatenateCssFiles($testFileFixture);
164
165 $this->assertArrayHasKey($concatenatedFileName, $result);
166 $this->assertArrayHasKey('excludeFromConcatenation', $result[$concatenatedFileName]);
167 $this->assertTrue($result[$concatenatedFileName]['excludeFromConcatenation']);
168 }
169
170 /**
171 * @test
172 */
173 public function concatenatedCssFilesAreSeparatedByMediaType(): void
174 {
175 $allFileName = 'allFile.css';
176 $screenFileName1 = 'screenFile.css';
177 $screenFileName2 = 'screenFile2.css';
178 $testFileFixture = [
179 $allFileName => [
180 'file' => $allFileName,
181 'excludeFromConcatenation' => false,
182 'media' => 'all',
183 ],
184 // use two screen files to check if they are merged into one, even with a different media type
185 $screenFileName1 => [
186 'file' => $screenFileName1,
187 'excludeFromConcatenation' => false,
188 'media' => 'screen',
189 ],
190 $screenFileName2 => [
191 'file' => $screenFileName2,
192 'excludeFromConcatenation' => false,
193 'media' => 'screen',
194 ],
195 ];
196 $this->subject->expects($this->exactly(2))
197 ->method('createMergedCssFile')
198 ->will($this->onConsecutiveCalls(
199 $this->returnValue('merged_' . $allFileName),
200 $this->returnValue('merged_' . $screenFileName1)
201 ));
202
203 $result = $this->subject->concatenateCssFiles($testFileFixture);
204
205 $this->assertEquals([
206 'merged_' . $allFileName,
207 'merged_' . $screenFileName1
208 ], array_keys($result));
209 $this->assertEquals('all', $result['merged_' . $allFileName]['media']);
210 $this->assertEquals('screen', $result['merged_' . $screenFileName1]['media']);
211 }
212
213 /**
214 * @test
215 */
216 public function concatenatedCssFilesObeyForceOnTopOption(): void
217 {
218 $screen1FileName = 'screen1File.css';
219 $screen2FileName = 'screen2File.css';
220 $screen3FileName = 'screen3File.css';
221 $testFileFixture = [
222 $screen1FileName => [
223 'file' => $screen1FileName,
224 'excludeFromConcatenation' => false,
225 'media' => 'screen',
226 ],
227 $screen2FileName => [
228 'file' => $screen2FileName,
229 'excludeFromConcatenation' => false,
230 'media' => 'screen',
231 ],
232 $screen3FileName => [
233 'file' => $screen3FileName,
234 'excludeFromConcatenation' => false,
235 'forceOnTop' => true,
236 'media' => 'screen',
237 ],
238 ];
239 // Replace mocked method getFilenameFromMainDir by passthrough callback
240 $this->subject->expects($this->any())->method('getFilenameFromMainDir')->willReturnArgument(0);
241 $this->subject->expects($this->once())
242 ->method('createMergedCssFile')
243 ->with($this->equalTo([$screen3FileName, $screen1FileName, $screen2FileName]));
244
245 $this->subject->concatenateCssFiles($testFileFixture);
246 }
247
248 /**
249 * @test
250 */
251 public function concatenatedCssFilesObeyExcludeFromConcatenation(): void
252 {
253 $screen1FileName = 'screen1File.css';
254 $screen2FileName = 'screen2File.css';
255 $screen3FileName = 'screen3File.css';
256 $testFileFixture = [
257 $screen1FileName => [
258 'file' => $screen1FileName,
259 'excludeFromConcatenation' => false,
260 'media' => 'screen',
261 ],
262 $screen2FileName => [
263 'file' => $screen2FileName,
264 'excludeFromConcatenation' => true,
265 'media' => 'screen',
266 ],
267 $screen3FileName => [
268 'file' => $screen3FileName,
269 'excludeFromConcatenation' => false,
270 'media' => 'screen',
271 ],
272 ];
273 $this->subject->expects($this->any())->method('getFilenameFromMainDir')->willReturnArgument(0);
274 $this->subject->expects($this->once())
275 ->method('createMergedCssFile')
276 ->with($this->equalTo([$screen1FileName, $screen3FileName]))
277 ->will($this->returnValue('merged_screen'));
278
279 $result = $this->subject->concatenateCssFiles($testFileFixture);
280 $this->assertEquals([
281 $screen2FileName,
282 'merged_screen'
283 ], array_keys($result));
284 $this->assertEquals('screen', $result[$screen2FileName]['media']);
285 $this->assertEquals('screen', $result['merged_screen']['media']);
286 }
287
288 /**
289 * @test
290 */
291 public function concatenateJsFileIsFlaggedToNotConcatenateAgain(): void
292 {
293 $fileName = 'fooFile.js';
294 $concatenatedFileName = 'merged_' . $fileName;
295 $testFileFixture = [
296 $fileName => [
297 'file' => $fileName,
298 'excludeFromConcatenation' => false,
299 'section' => 'top',
300 ]
301 ];
302 $this->subject->expects($this->once())
303 ->method('createMergedJsFile')
304 ->will($this->returnValue($concatenatedFileName));
305
306 $result = $this->subject->concatenateJsFiles($testFileFixture);
307
308 $this->assertArrayHasKey($concatenatedFileName, $result);
309 $this->assertArrayHasKey('excludeFromConcatenation', $result[$concatenatedFileName]);
310 $this->assertTrue($result[$concatenatedFileName]['excludeFromConcatenation']);
311 }
312
313 /**
314 * @return array
315 */
316 public function concatenateJsFileAsyncDataProvider(): array
317 {
318 return [
319 'all files have no async' => [
320 [
321 [
322 'file' => 'file1.js',
323 'excludeFromConcatenation' => false,
324 'section' => 'top',
325 ],
326 [
327 'file' => 'file2.js',
328 'excludeFromConcatenation' => false,
329 'section' => 'top',
330 ],
331 ],
332 false
333 ],
334 'all files have async false' => [
335 [
336 [
337 'file' => 'file1.js',
338 'excludeFromConcatenation' => false,
339 'section' => 'top',
340 'async' => false,
341 ],
342 [
343 'file' => 'file2.js',
344 'excludeFromConcatenation' => false,
345 'section' => 'top',
346 'async' => false,
347 ],
348 ],
349 false
350 ],
351 'all files have async true' => [
352 [
353 [
354 'file' => 'file1.js',
355 'excludeFromConcatenation' => false,
356 'section' => 'top',
357 'async' => true,
358 ],
359 [
360 'file' => 'file2.js',
361 'excludeFromConcatenation' => false,
362 'section' => 'top',
363 'async' => true,
364 ],
365 ],
366 true
367 ],
368 'one file async true and one file async false' => [
369 [
370 [
371 'file' => 'file1.js',
372 'excludeFromConcatenation' => false,
373 'section' => 'top',
374 'async' => true,
375 ],
376 [
377 'file' => 'file2.js',
378 'excludeFromConcatenation' => false,
379 'section' => 'top',
380 'async' => false,
381 ],
382 ],
383 false
384 ],
385 'one file async true and one file async false but is excluded form concatenation' => [
386 [
387 [
388 'file' => 'file1.js',
389 'excludeFromConcatenation' => false,
390 'section' => 'top',
391 'async' => true,
392 ],
393 [
394 'file' => 'file2.js',
395 'excludeFromConcatenation' => true,
396 'section' => 'top',
397 'async' => false,
398 ],
399 ],
400 true
401 ],
402 'one file async false and one file async true but is excluded form concatenation' => [
403 [
404 [
405 'file' => 'file1.js',
406 'excludeFromConcatenation' => false,
407 'section' => 'top',
408 'async' => false,
409 ],
410 [
411 'file' => 'file2.js',
412 'excludeFromConcatenation' => true,
413 'section' => 'top',
414 'async' => true,
415 ],
416 ],
417 false
418 ],
419 ];
420 }
421
422 /**
423 * @test
424 * @dataProvider concatenateJsFileAsyncDataProvider
425 * @param string $input
426 * @param bool $expected
427 */
428 public function concatenateJsFileAddsAsyncPropertyIfAllFilesAreAsync(array $input, bool $expected): void
429 {
430 $concatenatedFileName = 'merged_foo.js';
431 $this->subject->expects($this->once())
432 ->method('createMergedJsFile')
433 ->will($this->returnValue($concatenatedFileName));
434
435 $result = $this->subject->concatenateJsFiles($input);
436
437 $this->assertSame($expected, $result[$concatenatedFileName]['async']);
438 }
439
440 /**
441 * @return array
442 */
443 public function calcStatementsDataProvider(): array
444 {
445 return [
446 'simple calc' => [
447 'calc(100% - 3px)',
448 'calc(100% - 3px)',
449 ],
450 'complex calc with parentheses at the beginning' => [
451 'calc((100%/20) - 2*3px)',
452 'calc((100%/20) - 2*3px)',
453 ],
454 'complex calc with parentheses at the end' => [
455 'calc(100%/20 - 2*3px - (200px + 3%))',
456 'calc(100%/20 - 2*3px - (200px + 3%))',
457 ],
458 'complex calc with many parentheses' => [
459 'calc((100%/20) - (2 * (3px - (200px + 3%))))',
460 'calc((100%/20) - (2 * (3px - (200px + 3%))))',
461 ],
462 ];
463 }
464
465 /**
466 * @test
467 * @dataProvider calcStatementsDataProvider
468 * @param string $input
469 * @param string $expected
470 */
471 public function calcFunctionMustRetainWhitespaces($input, $expected): void
472 {
473 $result = $this->subject->_call('compressCssString', $input);
474 $this->assertSame($expected, trim($result));
475 }
476
477 /**
478 * @return array
479 */
480 public function compressCssFileContentDataProvider(): array
481 {
482 $path = __DIR__ . '/ResourceCompressorTest/Fixtures/';
483 return [
484 // File. Tests:
485 // - Stripped comments and white-space.
486 // - Retain white-space in selectors. (http://drupal.org/node/472820)
487 // - Retain pseudo-selectors. (http://drupal.org/node/460448)
488 0 => [
489 $path . 'css_input_without_import.css',
490 $path . 'css_input_without_import.css.optimized.css'
491 ],
492 // File. Tests:
493 // - Retain comment hacks.
494 2 => [
495 $path . 'comment_hacks.css',
496 $path . 'comment_hacks.css.optimized.css'
497 ], /*
498 // File. Tests:
499 // - Any @charset declaration at the beginning of a file should be
500 // removed without breaking subsequent CSS.*/
501 6 => [
502 $path . 'charset_sameline.css',
503 $path . 'charset.css.optimized.css'
504 ],
505 7 => [
506 $path . 'charset_newline.css',
507 $path . 'charset.css.optimized.css'
508 ],
509 ];
510 }
511
512 /**
513 * Tests optimizing a CSS asset group.
514 *
515 * @test
516 * @dataProvider compressCssFileContentDataProvider
517 * @param string $cssFile
518 * @param string $expected
519 */
520 public function compressCssFileContent($cssFile, $expected): void
521 {
522 $cssContent = file_get_contents($cssFile);
523 $compressedCss = $this->subject->_call('compressCssString', $cssContent);
524 // we have to fix relative paths, if we aren't working on a file in our target directory
525 $relativeFilename = str_replace(Environment::getPublicPath() . '/', '', $cssFile);
526 if (strpos($relativeFilename, $this->subject->_get('targetDirectory')) === false) {
527 $compressedCss = $this->subject->_call('cssFixRelativeUrlPaths', $compressedCss, PathUtility::dirname($relativeFilename) . '/');
528 }
529 $this->assertEquals(file_get_contents($expected), $compressedCss, 'Group of file CSS assets optimized correctly.');
530 }
531 }