5 * Contains \Drupal\Tests\Core\Theme\RegistryTest.
8 namespace Drupal\Tests\Core\Theme;
10 use Drupal\Core\Theme\ActiveTheme;
11 use Drupal\Core\Theme\Registry;
12 use Drupal\Tests\UnitTestCase;
15 * @coversDefaultClass \Drupal\Core\Theme\Registry
18 class RegistryTest extends UnitTestCase {
21 * The tested theme registry.
23 * @var \Drupal\Tests\Core\Theme\TestRegistry
28 * The mocked cache backend.
30 * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
35 * The mocked lock backend.
37 * @var \Drupal\Core\Lock\LockBackendInterface|\PHPUnit_Framework_MockObject_MockObject
42 * The mocked module handler.
44 * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
46 protected $moduleHandler;
49 * The mocked theme handler.
51 * @var \Drupal\Core\Extension\ThemeHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
53 protected $themeHandler;
56 * The mocked theme initialization.
58 * @var \Drupal\Core\Theme\ThemeInitializationInterface|\PHPUnit_Framework_MockObject_MockObject
60 protected $themeInitialization;
65 * @var \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_MockObject
67 protected $themeManager;
70 * The list of functions that get_defined_functions() should provide.
74 public static $functions = [];
79 protected function setUp() {
82 $this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
83 $this->lock = $this->getMock('Drupal\Core\Lock\LockBackendInterface');
84 $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
85 $this->themeHandler = $this->getMock('Drupal\Core\Extension\ThemeHandlerInterface');
86 $this->themeInitialization = $this->getMock('Drupal\Core\Theme\ThemeInitializationInterface');
87 $this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');
95 protected function tearDown() {
97 static::$functions = [];
101 * Tests getting the theme registry defined by a module.
103 public function testGetRegistryForModule() {
104 $test_theme = new ActiveTheme([
105 'name' => 'test_theme',
106 'path' => 'core/modules/system/tests/themes/test_theme/test_theme.info.yml',
109 'stylesheets_remove' => [],
110 'libraries_override' => [],
111 'libraries_extend' => [],
113 'extension' => '.twig',
117 $test_stable = new ActiveTheme([
118 'name' => 'test_stable',
119 'path' => 'core/modules/system/tests/themes/test_stable/test_stable.info.yml',
122 'stylesheets_remove' => [],
123 'libraries_override' => [],
124 'libraries_extend' => [],
126 'extension' => '.twig',
130 $this->themeManager->expects($this->exactly(2))
131 ->method('getActiveTheme')
132 ->willReturnOnConsecutiveCalls($test_theme, $test_stable);
134 // Include the module and theme files so that hook_theme can be called.
135 include_once $this->root . '/core/modules/system/tests/modules/theme_test/theme_test.module';
136 include_once $this->root . '/core/modules/system/tests/themes/test_stable/test_stable.theme';
137 $this->moduleHandler->expects($this->exactly(2))
138 ->method('getImplementations')
140 ->will($this->returnValue(['theme_test']));
141 $this->moduleHandler->expects($this->atLeastOnce())
142 ->method('getModuleList')
145 $registry = $this->registry->get();
147 // Ensure that the registry entries from the module are found.
148 $this->assertArrayHasKey('theme_test', $registry);
149 $this->assertArrayHasKey('theme_test_template_test', $registry);
150 $this->assertArrayHasKey('theme_test_template_test_2', $registry);
151 $this->assertArrayHasKey('theme_test_suggestion_provided', $registry);
152 $this->assertArrayHasKey('theme_test_specific_suggestions', $registry);
153 $this->assertArrayHasKey('theme_test_suggestions', $registry);
154 $this->assertArrayHasKey('theme_test_function_suggestions', $registry);
155 $this->assertArrayHasKey('theme_test_foo', $registry);
156 $this->assertArrayHasKey('theme_test_render_element', $registry);
157 $this->assertArrayHasKey('theme_test_render_element_children', $registry);
158 $this->assertArrayHasKey('theme_test_function_template_override', $registry);
160 $this->assertArrayNotHasKey('test_theme_not_existing_function', $registry);
161 $this->assertFalse(in_array('test_stable_preprocess_theme_test_render_element', $registry['theme_test_render_element']['preprocess functions']));
163 $info = $registry['theme_test_function_suggestions'];
164 $this->assertEquals('module', $info['type']);
165 $this->assertEquals('core/modules/system/tests/modules/theme_test', $info['theme path']);
166 $this->assertEquals('theme_theme_test_function_suggestions', $info['function']);
167 $this->assertEquals([], $info['variables']);
169 // The second call will initialize with the second theme. Ensure that this
170 // returns a different object and the discovery for the second theme's
171 // preprocess function worked.
172 $other_registry = $this->registry->get();
173 $this->assertNotSame($registry, $other_registry);
174 $this->assertTrue(in_array('test_stable_preprocess_theme_test_render_element', $other_registry['theme_test_render_element']['preprocess functions']));
178 * @covers ::postProcessExtension
179 * @covers ::completeSuggestion
180 * @covers ::mergePreprocessFunctions
182 * @dataProvider providerTestPostProcessExtension
184 * @param array $defined_functions
185 * An array of functions to be used in place of get_defined_functions().
186 * @param array $hooks
187 * An array of theme hooks to process.
188 * @param array $expected
189 * The expected results.
191 public function testPostProcessExtension($defined_functions, $hooks, $expected) {
192 static::$functions['user'] = $defined_functions;
194 $theme = $this->prophesize(ActiveTheme::class);
195 $theme->getBaseThemes()->willReturn([]);
196 $theme->getName()->willReturn('test');
197 $theme->getEngine()->willReturn('twig');
199 $this->moduleHandler->expects($this->atLeastOnce())
200 ->method('getModuleList')
203 $class = new \ReflectionClass(TestRegistry::class);
204 $reflection_method = $class->getMethod('postProcessExtension');
205 $reflection_method->setAccessible(TRUE);
206 $reflection_method->invokeArgs($this->registry, [&$hooks, $theme->reveal()]);
208 $this->assertArrayEquals($expected, $hooks);
212 * Provides test data to ::testPostProcessExtension().
214 public function providerTestPostProcessExtension() {
215 // This is test data for unit testing
216 // \Drupal\Core\Theme\Registry::postProcessExtension(), not what happens
217 // before it. Therefore, for all test data:
218 // - Explicitly defined hooks also come with explicitly defined preprocess
219 // functions, because those are collected in
220 // \Drupal\Core\Theme\Registry::processExtension().
221 // - Explicitly defined hooks that set a 'base hook' also have
222 // 'incomplete preprocess functions' set to TRUE, since that is done in
223 // \Drupal\Core\Theme\Registry::processExtension().
226 // Test the discovery of suggestions via the presence of preprocess
227 // functions that follow the "__" naming pattern.
228 $data['base_hook_with_autodiscovered_suggestions'] = [
229 'defined_functions' => [
230 'test_preprocess_test_hook__suggestion',
231 'test_preprocess_test_hook__suggestion__another',
235 'preprocess functions' => ['explicit_preprocess_test_hook'],
240 'preprocess functions' => [
241 'explicit_preprocess_test_hook',
244 'test_hook__suggestion' => [
245 'preprocess functions' => [
246 'explicit_preprocess_test_hook',
247 'test_preprocess_test_hook__suggestion',
249 'base hook' => 'test_hook',
251 'test_hook__suggestion__another' => [
252 'preprocess functions' => [
253 'explicit_preprocess_test_hook',
254 'test_preprocess_test_hook__suggestion',
255 'test_preprocess_test_hook__suggestion__another',
257 'base hook' => 'test_hook',
262 // Test that suggestions following the "__" naming pattern can also be
263 // explicitly defined in hook_theme(), such as 'field__node__title' defined
265 $data['base_hook_with_explicit_suggestions'] = [
266 'defined_functions' => [],
269 'preprocess functions' => ['explicit_preprocess_test_hook'],
271 'test_hook__suggestion__another' => [
272 'base hook' => 'test_hook',
273 'preprocess functions' => ['explicit_preprocess_test_hook__suggestion__another'],
274 'incomplete preprocess functions' => TRUE,
279 'preprocess functions' => [
280 'explicit_preprocess_test_hook',
283 'test_hook__suggestion__another' => [
284 'preprocess functions' => [
285 'explicit_preprocess_test_hook',
286 'explicit_preprocess_test_hook__suggestion__another',
288 'base hook' => 'test_hook',
293 // Same as above, but also test that a preprocess function for an
294 // intermediary suggestion level gets discovered.
295 $data['base_hook_with_explicit_suggestions_and_intermediary_preprocess_function'] = [
296 'defined_functions' => [
297 'test_preprocess_test_hook__suggestion',
301 'preprocess functions' => ['explicit_preprocess_test_hook'],
303 'test_hook__suggestion__another' => [
304 'base hook' => 'test_hook',
305 'preprocess functions' => ['explicit_preprocess_test_hook__suggestion__another'],
306 'incomplete preprocess functions' => TRUE,
311 'preprocess functions' => [
312 'explicit_preprocess_test_hook',
315 'test_hook__suggestion' => [
316 'preprocess functions' => [
317 'explicit_preprocess_test_hook',
318 'test_preprocess_test_hook__suggestion',
320 'base hook' => 'test_hook',
322 'test_hook__suggestion__another' => [
323 'preprocess functions' => [
324 'explicit_preprocess_test_hook',
325 'test_preprocess_test_hook__suggestion',
326 'explicit_preprocess_test_hook__suggestion__another',
328 'base hook' => 'test_hook',
333 // Test that hooks not following the "__" naming pattern can explicitly
334 // specify a base hook, such as is done in
335 // \Drupal\Core\Layout\LayoutPluginManager::getThemeImplementations().
336 $data['child_hook_without_magic_naming'] = [
337 'defined_functions' => [],
340 'preprocess functions' => ['explicit_preprocess_test_hook'],
343 'base hook' => 'test_hook',
344 'preprocess functions' => ['explicit_preprocess_child_hook'],
345 'incomplete preprocess functions' => TRUE,
350 'preprocess functions' => [
351 'explicit_preprocess_test_hook',
355 'preprocess functions' => [
356 'explicit_preprocess_test_hook',
357 'explicit_preprocess_child_hook',
359 'base hook' => 'test_hook',
364 // Same as above, but also test that such child hooks can also be extended
365 // with magically named suggestions.
366 $data['child_hook_with_suggestions'] = [
367 'defined_functions' => [
368 'test_preprocess_child_hook__suggestion',
369 'test_preprocess_child_hook__suggestion__another',
373 'preprocess functions' => ['explicit_preprocess_test_hook'],
376 'base hook' => 'test_hook',
377 'preprocess functions' => ['explicit_preprocess_child_hook'],
378 'incomplete preprocess functions' => TRUE,
383 'preprocess functions' => [
384 'explicit_preprocess_test_hook',
388 'preprocess functions' => [
389 'explicit_preprocess_test_hook',
390 'explicit_preprocess_child_hook',
392 'base hook' => 'test_hook',
394 'child_hook__suggestion' => [
395 'preprocess functions' => [
396 'explicit_preprocess_test_hook',
397 'explicit_preprocess_child_hook',
398 'test_preprocess_child_hook__suggestion',
400 'base hook' => 'test_hook',
402 'child_hook__suggestion__another' => [
403 'preprocess functions' => [
404 'explicit_preprocess_test_hook',
405 'explicit_preprocess_child_hook',
406 'test_preprocess_child_hook__suggestion',
407 'test_preprocess_child_hook__suggestion__another',
409 'base hook' => 'test_hook',
414 // Test that a suggestion following the "__" naming pattern can specify a
415 // different base hook than what is implied by that pattern. Ensure that
416 // preprocess functions from both the naming pattern and from 'base hook'
418 $data['suggestion_with_alternate_base_hook'] = [
419 'defined_functions' => [
420 'test_preprocess_test_hook__suggestion',
424 'preprocess functions' => ['explicit_preprocess_test_hook'],
426 'alternate_base_hook' => [
427 'preprocess functions' => ['explicit_preprocess_alternate_base_hook'],
429 'test_hook__suggestion__another' => [
430 'base hook' => 'alternate_base_hook',
431 'preprocess functions' => ['explicit_preprocess_test_hook__suggestion__another'],
432 'incomplete preprocess functions' => TRUE,
437 'preprocess functions' => [
438 'explicit_preprocess_test_hook',
441 'alternate_base_hook' => [
442 'preprocess functions' => [
443 'explicit_preprocess_alternate_base_hook',
446 'test_hook__suggestion' => [
447 'preprocess functions' => [
448 'explicit_preprocess_test_hook',
449 'test_preprocess_test_hook__suggestion',
451 'base hook' => 'test_hook',
453 'test_hook__suggestion__another' => [
454 'preprocess functions' => [
455 'explicit_preprocess_alternate_base_hook',
456 'explicit_preprocess_test_hook',
457 'test_preprocess_test_hook__suggestion',
458 'explicit_preprocess_test_hook__suggestion__another',
460 'base hook' => 'alternate_base_hook',
465 // Test when a base hook is missing.
466 $data['missing_base_hook'] = [
467 'defined_functions' => [],
470 'base hook' => 'test_hook',
471 'preprocess functions' => ['explicit_preprocess_child_hook'],
472 'incomplete preprocess functions' => TRUE,
477 'preprocess functions' => [
478 'explicit_preprocess_child_hook',
480 'base hook' => 'test_hook',
488 protected function setupTheme() {
489 $this->registry = new TestRegistry($this->root, $this->cache, $this->lock, $this->moduleHandler, $this->themeHandler, $this->themeInitialization);
490 $this->registry->setThemeManager($this->themeManager);
495 class TestRegistry extends Registry {
497 protected function getPath($module) {
498 if ($module == 'theme_test') {
499 return 'core/modules/system/tests/modules/theme_test';
505 namespace Drupal\Core\Theme;
507 use Drupal\Tests\Core\Theme\RegistryTest;
510 * Overrides get_defined_functions() with a configurable mock.
512 function get_defined_functions() {
513 return RegistryTest::$functions ?: \get_defined_functions();