5 * Contains \Drupal\Tests\Core\Asset\LibraryDiscoveryParserTest.
8 namespace Drupal\Tests\Core\Asset;
10 use Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException;
11 use Drupal\Core\Asset\Exception\InvalidLibraryFileException;
12 use Drupal\Core\Asset\Exception\LibraryDefinitionMissingLicenseException;
13 use Drupal\Core\Asset\LibraryDiscoveryParser;
14 use Drupal\Tests\UnitTestCase;
17 * @coversDefaultClass \Drupal\Core\Asset\LibraryDiscoveryParser
20 class LibraryDiscoveryParserTest extends UnitTestCase {
23 * The tested library discovery parser service.
25 * @var \Drupal\Core\Asset\LibraryDiscoveryParser|\Drupal\Tests\Core\Asset\TestLibraryDiscoveryParser
27 protected $libraryDiscoveryParser;
30 * The mocked cache backend.
32 * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
37 * The mocked module handler.
39 * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
41 protected $moduleHandler;
44 * The mocked theme manager.
46 * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
48 protected $themeManager;
51 * The mocked lock backend.
53 * @var \Drupal\Core\Lock\LockBackendInterface|\PHPUnit_Framework_MockObject_MockObject
60 protected function setUp() {
63 $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
64 $this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');
65 $mock_active_theme = $this->getMockBuilder('Drupal\Core\Theme\ActiveTheme')
66 ->disableOriginalConstructor()
68 $mock_active_theme->expects($this->any())
69 ->method('getLibrariesOverride')
71 $this->themeManager->expects($this->any())
72 ->method('getActiveTheme')
73 ->willReturn($mock_active_theme);
74 $this->libraryDiscoveryParser = new TestLibraryDiscoveryParser($this->root, $this->moduleHandler, $this->themeManager);
78 * Tests that basic functionality works for getLibraryByName.
80 * @covers ::buildByExtension
82 public function testBuildByExtensionSimple() {
83 $this->moduleHandler->expects($this->atLeastOnce())
84 ->method('moduleExists')
85 ->with('example_module')
86 ->will($this->returnValue(TRUE));
88 $path = __DIR__ . '/library_test_files';
89 $path = substr($path, strlen($this->root) + 1);
90 $this->libraryDiscoveryParser->setPaths('module', 'example_module', $path);
92 $libraries = $this->libraryDiscoveryParser->buildByExtension('example_module');
93 $library = $libraries['example'];
95 $this->assertCount(0, $library['js']);
96 $this->assertCount(1, $library['css']);
97 $this->assertCount(0, $library['dependencies']);
98 $this->assertEquals($path . '/css/example.css', $library['css'][0]['data']);
100 // Ensures that VERSION is replaced by the current core version.
101 $this->assertEquals(\Drupal::VERSION, $library['version']);
105 * Tests that a theme can be used instead of a module.
107 * @covers ::buildByExtension
109 public function testBuildByExtensionWithTheme() {
110 $this->moduleHandler->expects($this->atLeastOnce())
111 ->method('moduleExists')
112 ->with('example_theme')
113 ->will($this->returnValue(FALSE));
115 $path = __DIR__ . '/library_test_files';
116 $path = substr($path, strlen($this->root) + 1);
117 $this->libraryDiscoveryParser->setPaths('theme', 'example_theme', $path);
119 $libraries = $this->libraryDiscoveryParser->buildByExtension('example_theme');
120 $library = $libraries['example'];
122 $this->assertCount(0, $library['js']);
123 $this->assertCount(1, $library['css']);
124 $this->assertCount(0, $library['dependencies']);
125 $this->assertEquals($path . '/css/example.css', $library['css'][0]['data']);
129 * Tests that a module with a missing library file results in FALSE.
131 * @covers ::buildByExtension
133 public function testBuildByExtensionWithMissingLibraryFile() {
134 $this->moduleHandler->expects($this->atLeastOnce())
135 ->method('moduleExists')
136 ->with('example_module')
137 ->will($this->returnValue(TRUE));
139 $path = __DIR__ . '/library_test_files_not_existing';
140 $path = substr($path, strlen($this->root) + 1);
141 $this->libraryDiscoveryParser->setPaths('module', 'example_module', $path);
143 $this->assertSame($this->libraryDiscoveryParser->buildByExtension('example_module'), []);
147 * Tests that an exception is thrown when a libraries file couldn't be parsed.
149 * @covers ::buildByExtension
151 public function testInvalidLibrariesFile() {
152 $this->moduleHandler->expects($this->atLeastOnce())
153 ->method('moduleExists')
154 ->with('invalid_file')
155 ->will($this->returnValue(TRUE));
157 $path = __DIR__ . '/library_test_files';
158 $path = substr($path, strlen($this->root) + 1);
159 $this->libraryDiscoveryParser->setPaths('module', 'invalid_file', $path);
161 $this->setExpectedException(InvalidLibraryFileException::class);
162 $this->libraryDiscoveryParser->buildByExtension('invalid_file');
166 * Tests that an exception is thrown when no CSS/JS/setting is specified.
168 * @covers ::buildByExtension
170 public function testBuildByExtensionWithMissingInformation() {
171 $this->moduleHandler->expects($this->atLeastOnce())
172 ->method('moduleExists')
173 ->with('example_module_missing_information')
174 ->will($this->returnValue(TRUE));
176 $path = __DIR__ . '/library_test_files';
177 $path = substr($path, strlen($this->root) + 1);
178 $this->libraryDiscoveryParser->setPaths('module', 'example_module_missing_information', $path);
180 $this->setExpectedException(IncompleteLibraryDefinitionException::class, "Incomplete library definition for definition 'example' in extension 'example_module_missing_information'");
181 $this->libraryDiscoveryParser->buildByExtension('example_module_missing_information');
185 * Tests the version property, and how it propagates to the contained assets.
187 * @covers ::buildByExtension
189 public function testVersion() {
190 $this->moduleHandler->expects($this->atLeastOnce())
191 ->method('moduleExists')
193 ->will($this->returnValue(TRUE));
195 $path = __DIR__ . '/library_test_files';
196 $path = substr($path, strlen($this->root) + 1);
197 $this->libraryDiscoveryParser->setPaths('module', 'versions', $path);
199 $libraries = $this->libraryDiscoveryParser->buildByExtension('versions');
201 $this->assertFalse(array_key_exists('version', $libraries['versionless']));
202 $this->assertEquals(-1, $libraries['versionless']['css'][0]['version']);
203 $this->assertEquals(-1, $libraries['versionless']['js'][0]['version']);
205 $this->assertEquals('9.8.7.6', $libraries['versioned']['version']);
206 $this->assertEquals('9.8.7.6', $libraries['versioned']['css'][0]['version']);
207 $this->assertEquals('9.8.7.6', $libraries['versioned']['js'][0]['version']);
209 $this->assertEquals(\Drupal::VERSION, $libraries['core-versioned']['version']);
210 $this->assertEquals(\Drupal::VERSION, $libraries['core-versioned']['css'][0]['version']);
211 $this->assertEquals(\Drupal::VERSION, $libraries['core-versioned']['js'][0]['version']);
216 * Tests that the version property of external libraries is handled.
218 * @covers ::buildByExtension
220 public function testExternalLibraries() {
221 $this->moduleHandler->expects($this->atLeastOnce())
222 ->method('moduleExists')
224 ->will($this->returnValue(TRUE));
226 $path = __DIR__ . '/library_test_files';
227 $path = substr($path, strlen($this->root) + 1);
228 $this->libraryDiscoveryParser->setPaths('module', 'external', $path);
230 $libraries = $this->libraryDiscoveryParser->buildByExtension('external');
231 $library = $libraries['example_external'];
233 $this->assertEquals('http://example.com/css/example_external.css', $library['css'][0]['data']);
234 $this->assertEquals('http://example.com/example_external.js', $library['js'][0]['data']);
235 $this->assertEquals('3.14', $library['version']);
239 * Ensures that CSS weights are taken into account properly.
241 * @covers ::buildByExtension
243 public function testDefaultCssWeights() {
244 $this->moduleHandler->expects($this->atLeastOnce())
245 ->method('moduleExists')
246 ->with('css_weights')
247 ->will($this->returnValue(TRUE));
249 $path = __DIR__ . '/library_test_files';
250 $path = substr($path, strlen($this->root) + 1);
251 $this->libraryDiscoveryParser->setPaths('module', 'css_weights', $path);
253 $libraries = $this->libraryDiscoveryParser->buildByExtension('css_weights');
254 $library = $libraries['example'];
255 $css = $library['css'];
256 $this->assertCount(10, $css);
258 // The following default weights are tested:
260 // - CSS_LAYOUT: -100
261 // - CSS_COMPONENT: 0
264 $this->assertEquals(200, $css[0]['weight']);
265 $this->assertEquals(200 + 29, $css[1]['weight']);
266 $this->assertEquals(-200, $css[2]['weight']);
267 $this->assertEquals(-200 + 97, $css[3]['weight']);
268 $this->assertEquals(-100, $css[4]['weight']);
269 $this->assertEquals(-100 + 92, $css[5]['weight']);
270 $this->assertEquals(0, $css[6]['weight']);
271 $this->assertEquals(45, $css[7]['weight']);
272 $this->assertEquals(100, $css[8]['weight']);
273 $this->assertEquals(100 + 8, $css[9]['weight']);
277 * Ensures that you cannot provide positive weights for JavaScript libraries.
279 * @covers ::buildByExtension
281 public function testJsWithPositiveWeight() {
282 $this->moduleHandler->expects($this->atLeastOnce())
283 ->method('moduleExists')
284 ->with('js_positive_weight')
285 ->will($this->returnValue(TRUE));
287 $path = __DIR__ . '/library_test_files';
288 $path = substr($path, strlen($this->root) + 1);
289 $this->libraryDiscoveryParser->setPaths('module', 'js_positive_weight', $path);
291 $this->setExpectedException(\UnexpectedValueException::class);
292 $this->libraryDiscoveryParser->buildByExtension('js_positive_weight');
296 * Tests a library with CSS/JavaScript and a setting.
298 * @covers ::buildByExtension
300 public function testLibraryWithCssJsSetting() {
301 $this->moduleHandler->expects($this->atLeastOnce())
302 ->method('moduleExists')
303 ->with('css_js_settings')
304 ->will($this->returnValue(TRUE));
306 $path = __DIR__ . '/library_test_files';
307 $path = substr($path, strlen($this->root) + 1);
308 $this->libraryDiscoveryParser->setPaths('module', 'css_js_settings', $path);
310 $libraries = $this->libraryDiscoveryParser->buildByExtension('css_js_settings');
311 $library = $libraries['example'];
313 // Ensures that the group and type are set automatically.
314 $this->assertEquals(-100, $library['js'][0]['group']);
315 $this->assertEquals('file', $library['js'][0]['type']);
316 $this->assertEquals($path . '/js/example.js', $library['js'][0]['data']);
318 $this->assertEquals(0, $library['css'][0]['group']);
319 $this->assertEquals('file', $library['css'][0]['type']);
320 $this->assertEquals($path . '/css/base.css', $library['css'][0]['data']);
322 $this->assertEquals(['key' => 'value'], $library['drupalSettings']);
326 * Tests a library with dependencies.
328 * @covers ::buildByExtension
330 public function testLibraryWithDependencies() {
331 $this->moduleHandler->expects($this->atLeastOnce())
332 ->method('moduleExists')
333 ->with('dependencies')
334 ->will($this->returnValue(TRUE));
336 $path = __DIR__ . '/library_test_files';
337 $path = substr($path, strlen($this->root) + 1);
338 $this->libraryDiscoveryParser->setPaths('module', 'dependencies', $path);
340 $libraries = $this->libraryDiscoveryParser->buildByExtension('dependencies');
341 $library = $libraries['example'];
343 $this->assertCount(2, $library['dependencies']);
344 $this->assertEquals('external/example_external', $library['dependencies'][0]);
345 $this->assertEquals('example_module/example', $library['dependencies'][1]);
349 * Tests a library with a couple of data formats like full URL.
351 * @covers ::buildByExtension
353 public function testLibraryWithDataTypes() {
354 $this->moduleHandler->expects($this->atLeastOnce())
355 ->method('moduleExists')
357 ->will($this->returnValue(TRUE));
359 $path = __DIR__ . '/library_test_files';
360 $path = substr($path, strlen($this->root) + 1);
361 $this->libraryDiscoveryParser->setPaths('module', 'data_types', $path);
363 $this->libraryDiscoveryParser->setFileValidUri('public://test.css', TRUE);
364 $this->libraryDiscoveryParser->setFileValidUri('public://test2.css', FALSE);
366 $libraries = $this->libraryDiscoveryParser->buildByExtension('data_types');
367 $library = $libraries['example'];
369 $this->assertCount(5, $library['css']);
370 $this->assertEquals('external', $library['css'][0]['type']);
371 $this->assertEquals('http://example.com/test.css', $library['css'][0]['data']);
372 $this->assertEquals('file', $library['css'][1]['type']);
373 $this->assertEquals('tmp/test.css', $library['css'][1]['data']);
374 $this->assertEquals('external', $library['css'][2]['type']);
375 $this->assertEquals('//cdn.com/test.css', $library['css'][2]['data']);
376 $this->assertEquals('file', $library['css'][3]['type']);
377 $this->assertEquals('public://test.css', $library['css'][3]['data']);
381 * Tests a library with JavaScript-specific flags.
383 * @covers ::buildByExtension
385 public function testLibraryWithJavaScript() {
386 $this->moduleHandler->expects($this->atLeastOnce())
387 ->method('moduleExists')
389 ->will($this->returnValue(TRUE));
391 $path = __DIR__ . '/library_test_files';
392 $path = substr($path, strlen($this->root) + 1);
393 $this->libraryDiscoveryParser->setPaths('module', 'js', $path);
395 $libraries = $this->libraryDiscoveryParser->buildByExtension('js');
396 $library = $libraries['example'];
398 $this->assertCount(2, $library['js']);
399 $this->assertEquals(FALSE, $library['js'][0]['minified']);
400 $this->assertEquals(TRUE, $library['js'][1]['minified']);
403 * Tests that an exception is thrown when license is missing when 3rd party.
405 * @covers ::buildByExtension
407 public function testLibraryThirdPartyWithMissingLicense() {
408 $this->moduleHandler->expects($this->atLeastOnce())
409 ->method('moduleExists')
410 ->with('licenses_missing_information')
411 ->will($this->returnValue(TRUE));
413 $path = __DIR__ . '/library_test_files';
414 $path = substr($path, strlen($this->root) + 1);
415 $this->libraryDiscoveryParser->setPaths('module', 'licenses_missing_information', $path);
417 $this->setExpectedException(LibraryDefinitionMissingLicenseException::class, "Missing license information in library definition for definition 'no-license-info-but-remote' extension 'licenses_missing_information': it has a remote, but no license.");
418 $this->libraryDiscoveryParser->buildByExtension('licenses_missing_information');
422 * Tests a library with various licenses, some GPL-compatible, some not.
424 * @covers ::buildByExtension
426 public function testLibraryWithLicenses() {
427 $this->moduleHandler->expects($this->atLeastOnce())
428 ->method('moduleExists')
430 ->will($this->returnValue(TRUE));
432 $path = __DIR__ . '/library_test_files';
433 $path = substr($path, strlen($this->root) + 1);
434 $this->libraryDiscoveryParser->setPaths('module', 'licenses', $path);
436 $libraries = $this->libraryDiscoveryParser->buildByExtension('licenses');
438 // For libraries without license info, the default license is applied.
439 $library = $libraries['no-license-info'];
440 $this->assertCount(1, $library['css']);
441 $this->assertCount(1, $library['js']);
442 $this->assertTrue(isset($library['license']));
444 'name' => 'GNU-GPL-2.0-or-later',
445 'url' => 'https://www.drupal.org/licensing/faq',
446 'gpl-compatible' => TRUE,
448 $this->assertEquals($library['license'], $default_license);
450 // GPL2-licensed libraries.
451 $library = $libraries['gpl2'];
452 $this->assertCount(1, $library['css']);
453 $this->assertCount(1, $library['js']);
454 $expected_license = [
456 'url' => 'https://url-to-gpl2-license',
457 'gpl-compatible' => TRUE,
459 $this->assertEquals($library['license'], $expected_license);
461 // MIT-licensed libraries.
462 $library = $libraries['mit'];
463 $this->assertCount(1, $library['css']);
464 $this->assertCount(1, $library['js']);
465 $expected_license = [
467 'url' => 'https://url-to-mit-license',
468 'gpl-compatible' => TRUE,
470 $this->assertEquals($library['license'], $expected_license);
472 // Libraries in the Public Domain.
473 $library = $libraries['public-domain'];
474 $this->assertCount(1, $library['css']);
475 $this->assertCount(1, $library['js']);
476 $expected_license = [
477 'name' => 'Public Domain',
478 'url' => 'https://url-to-public-domain-license',
479 'gpl-compatible' => TRUE,
481 $this->assertEquals($library['license'], $expected_license);
483 // Apache-licensed libraries.
484 $library = $libraries['apache'];
485 $this->assertCount(1, $library['css']);
486 $this->assertCount(1, $library['js']);
487 $expected_license = [
489 'url' => 'https://url-to-apache-license',
490 'gpl-compatible' => FALSE,
492 $this->assertEquals($library['license'], $expected_license);
494 // Copyrighted libraries.
495 $library = $libraries['copyright'];
496 $this->assertCount(1, $library['css']);
497 $this->assertCount(1, $library['js']);
498 $expected_license = [
499 'name' => '© Some company',
500 'gpl-compatible' => FALSE,
502 $this->assertEquals($library['license'], $expected_license);
506 * Verifies assertions catch invalid CSS declarations.
508 * @dataProvider providerTestCssAssert
512 * Verify an assertion fails if CSS declarations have non-existent categories.
514 * @param string $extension
515 * The css extension to build.
516 * @param string $exception_message
517 * The expected exception message.
519 * @dataProvider providerTestCssAssert
521 public function testCssAssert($extension, $exception_message) {
522 $this->moduleHandler->expects($this->atLeastOnce())
523 ->method('moduleExists')
525 ->will($this->returnValue(TRUE));
527 $path = __DIR__ . '/library_test_files';
528 $path = substr($path, strlen($this->root) + 1);
529 $this->libraryDiscoveryParser->setPaths('module', $extension, $path);
531 $this->setExpectedException(\AssertionError::class, $exception_message);
532 $this->libraryDiscoveryParser->buildByExtension($extension);
536 * Data provider for testing bad CSS declarations.
538 public function providerTestCssAssert() {
540 'css_bad_category' => ['css_bad_category', 'See https://www.drupal.org/node/2274843.'],
541 'Improper CSS nesting' => ['css_bad_nesting', 'CSS must be nested under a category. See https://www.drupal.org/node/2274843.'],
542 'Improper CSS nesting array' => ['css_bad_nesting_array', 'CSS files should be specified as key/value pairs, where the values are configuration options. See https://www.drupal.org/node/2274843.'],
549 * Wraps the tested class to mock the external dependencies.
551 class TestLibraryDiscoveryParser extends LibraryDiscoveryParser {
555 protected $validUris;
557 protected function drupalGetPath($type, $name) {
558 return isset($this->paths[$type][$name]) ? $this->paths[$type][$name] : NULL;
561 public function setPaths($type, $name, $path) {
562 $this->paths[$type][$name] = $path;
565 protected function fileValidUri($source) {
566 return isset($this->validUris[$source]) ? $this->validUris[$source] : FALSE;
569 public function setFileValidUri($source, $valid) {
570 $this->validUris[$source] = $valid;
575 if (!defined('CSS_AGGREGATE_DEFAULT')) {
576 define('CSS_AGGREGATE_DEFAULT', 0);
578 if (!defined('CSS_AGGREGATE_THEME')) {
579 define('CSS_AGGREGATE_THEME', 100);
581 if (!defined('CSS_BASE')) {
582 define('CSS_BASE', -200);
584 if (!defined('CSS_LAYOUT')) {
585 define('CSS_LAYOUT', -100);
587 if (!defined('CSS_COMPONENT')) {
588 define('CSS_COMPONENT', 0);
590 if (!defined('CSS_STATE')) {
591 define('CSS_STATE', 100);
593 if (!defined('CSS_THEME')) {
594 define('CSS_THEME', 200);
596 if (!defined('JS_SETTING')) {
597 define('JS_SETTING', -200);
599 if (!defined('JS_LIBRARY')) {
600 define('JS_LIBRARY', -100);
602 if (!defined('JS_DEFAULT')) {
603 define('JS_DEFAULT', 0);
605 if (!defined('JS_THEME')) {
606 define('JS_THEME', 100);