1<?php
2
3namespace Drupal\KernelTests\Core\Theme;
4
5use Drupal\Core\Path\CurrentPathStack;
6use Drupal\Core\Path\PathMatcherInterface;
7use Drupal\Core\Routing\RouteMatchInterface;
8use Drupal\Core\Theme\Registry;
9use Drupal\Core\Utility\ThemeRegistry;
10use Drupal\KernelTests\KernelTestBase;
11
12/**
13 * Tests the behavior of the ThemeRegistry class.
14 *
15 * @group Theme
16 */
17class RegistryTest extends KernelTestBase {
18
19  /**
20   * Modules to enable.
21   *
22   * @var array
23   */
24  public static $modules = ['theme_test', 'system'];
25
26  protected $profile = 'testing';
27
28  /**
29   * Tests the behavior of the theme registry class.
30   */
31  public function testRaceCondition() {
32    // The theme registry is not marked as persistable in case we don't have a
33    // proper request.
34    \Drupal::request()->setMethod('GET');
35    $cid = 'test_theme_registry';
36
37    // Directly instantiate the theme registry, this will cause a base cache
38    // entry to be written in __construct().
39    $cache = \Drupal::cache();
40    $lock_backend = \Drupal::lock();
41    $registry = new ThemeRegistry($cid, $cache, $lock_backend, ['theme_registry'], $this->container->get('module_handler')->isLoaded());
42
43    $this->assertNotEmpty(\Drupal::cache()->get($cid), 'Cache entry was created.');
44
45    // Trigger a cache miss for an offset.
46    $this->assertNotEmpty($registry->get('theme_test_template_test'), 'Offset was returned correctly from the theme registry.');
47    // This will cause the ThemeRegistry class to write an updated version of
48    // the cache entry when it is destroyed, usually at the end of the request.
49    // Before that happens, manually delete the cache entry we created earlier
50    // so that the new entry is written from scratch.
51    \Drupal::cache()->delete($cid);
52
53    // Destroy the class so that it triggers a cache write for the offset.
54    $registry->destruct();
55
56    $this->assertNotEmpty(\Drupal::cache()->get($cid), 'Cache entry was created.');
57
58    // Create a new instance of the class. Confirm that both the offset
59    // requested previously, and one that has not yet been requested are both
60    // available.
61    $registry = new ThemeRegistry($cid, $cache, $lock_backend, ['theme_registry'], $this->container->get('module_handler')->isLoaded());
62    $this->assertNotEmpty($registry->get('theme_test_template_test'), 'Offset was returned correctly from the theme registry');
63    $this->assertNotEmpty($registry->get('theme_test_template_test_2'), 'Offset was returned correctly from the theme registry');
64  }
65
66  /**
67   * Tests the theme registry with multiple subthemes.
68   */
69  public function testMultipleSubThemes() {
70    $theme_handler = \Drupal::service('theme_handler');
71    \Drupal::service('theme_installer')->install(['test_basetheme', 'test_subtheme', 'test_subsubtheme']);
72
73    $registry_subsub_theme = new Registry($this->root, \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_subsubtheme');
74    $registry_subsub_theme->setThemeManager(\Drupal::theme());
75    $registry_sub_theme = new Registry($this->root, \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_subtheme');
76    $registry_sub_theme->setThemeManager(\Drupal::theme());
77    $registry_base_theme = new Registry($this->root, \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_basetheme');
78    $registry_base_theme->setThemeManager(\Drupal::theme());
79
80    $preprocess_functions = $registry_subsub_theme->get()['theme_test_template_test']['preprocess functions'];
81    $this->assertSame([
82      'template_preprocess',
83      'test_basetheme_preprocess_theme_test_template_test',
84      'test_subtheme_preprocess_theme_test_template_test',
85      'test_subsubtheme_preprocess_theme_test_template_test',
86    ], $preprocess_functions);
87
88    $preprocess_functions = $registry_sub_theme->get()['theme_test_template_test']['preprocess functions'];
89    $this->assertSame([
90      'template_preprocess',
91      'test_basetheme_preprocess_theme_test_template_test',
92      'test_subtheme_preprocess_theme_test_template_test',
93    ], $preprocess_functions);
94
95    $preprocess_functions = $registry_base_theme->get()['theme_test_template_test']['preprocess functions'];
96    $this->assertSame([
97      'template_preprocess',
98      'test_basetheme_preprocess_theme_test_template_test',
99    ], $preprocess_functions);
100  }
101
102  /**
103   * Tests the theme registry with suggestions.
104   */
105  public function testSuggestionPreprocessFunctions() {
106    $theme_handler = \Drupal::service('theme_handler');
107    \Drupal::service('theme_installer')->install(['test_theme']);
108
109    $registry_theme = new Registry($this->root, \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_theme');
110    $registry_theme->setThemeManager(\Drupal::theme());
111
112    $suggestions = ['__kitten', '__flamingo'];
113    $expected_preprocess_functions = [
114      'template_preprocess',
115      'theme_test_preprocess_theme_test_preprocess_suggestions',
116    ];
117    $suggestion = '';
118    $hook = 'theme_test_preprocess_suggestions';
119    do {
120      $hook .= "$suggestion";
121      $expected_preprocess_functions[] = "test_theme_preprocess_$hook";
122      $preprocess_functions = $registry_theme->get()[$hook]['preprocess functions'];
123      $this->assertSame($expected_preprocess_functions, $preprocess_functions, "$hook has correct preprocess functions.");
124    } while ($suggestion = array_shift($suggestions));
125
126    $expected_preprocess_functions = [
127      'template_preprocess',
128      'theme_test_preprocess_theme_test_preprocess_suggestions',
129      'test_theme_preprocess_theme_test_preprocess_suggestions',
130      'test_theme_preprocess_theme_test_preprocess_suggestions__kitten',
131    ];
132
133    $preprocess_functions = $registry_theme->get()['theme_test_preprocess_suggestions__kitten__bearcat']['preprocess functions'];
134    $this->assertSame($expected_preprocess_functions, $preprocess_functions, 'Suggestion implemented as a template correctly inherits preprocess functions.');
135
136    $this->assertTrue(isset($registry_theme->get()['theme_test_preprocess_suggestions__kitten__meerkat__tarsier__moose']), 'Preprocess function with an unimplemented lower-level suggestion is added to the registry.');
137  }
138
139  /**
140   * Tests that the theme registry can be altered by themes.
141   */
142  public function testThemeRegistryAlterByTheme() {
143
144    /** @var \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler */
145    $theme_handler = \Drupal::service('theme_handler');
146    \Drupal::service('theme_installer')->install(['test_theme']);
147    $this->config('system.theme')->set('default', 'test_theme')->save();
148
149    $registry = new Registry($this->root, \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_theme');
150    $registry->setThemeManager(\Drupal::theme());
151    $this->assertEqual('value', $registry->get()['theme_test_template_test']['variables']['additional']);
152  }
153
154  /**
155   * Tests front node theme suggestion generation.
156   */
157  public function testThemeSuggestions() {
158    // Mock the current page as the front page.
159    /** @var \Drupal\Core\Path\PathMatcherInterface $path_matcher */
160    $path_matcher = $this->prophesize(PathMatcherInterface::class);
161    $path_matcher->isFrontPage()->willReturn(TRUE);
162    $this->container->set('path.matcher', $path_matcher->reveal());
163    /** @var \Drupal\Core\Path\CurrentPathStack $path_matcher */
164    $path_current = $this->prophesize(CurrentPathStack::class);
165    $path_current->getPath()->willReturn('/node/1');
166    $this->container->set('path.current', $path_current->reveal());
167
168    // Check suggestions provided through hook_theme_suggestions_html().
169    $suggestions = \Drupal::moduleHandler()->invokeAll('theme_suggestions_html', [[]]);
170    $this->assertSame([
171      'html__node',
172      'html__node__%',
173      'html__node__1',
174      'html__front',
175    ], $suggestions, 'Found expected html node suggestions.');
176
177    // Check suggestions provided through hook_theme_suggestions_page().
178    $suggestions = \Drupal::moduleHandler()->invokeAll('theme_suggestions_page', [[]]);
179    $this->assertSame([
180      'page__node',
181      'page__node__%',
182      'page__node__1',
183      'page__front',
184    ], $suggestions, 'Found expected page node suggestions.');
185  }
186
187  /**
188   * Data provider for test40xThemeSuggestions().
189   *
190   * @return array
191   *   An associative array of 40x theme suggestions.
192   */
193  public function provider40xThemeSuggestions() {
194    return [
195      ['system.401', 'page__401'],
196      ['system.403', 'page__403'],
197      ['system.404', 'page__404'],
198    ];
199  }
200
201  /**
202   * Tests page theme suggestions for 40x responses.
203   *
204   * @dataProvider provider40xThemeSuggestions
205   */
206  public function test40xThemeSuggestions($route, $suggestion) {
207    /** @var \Drupal\Core\Path\PathMatcherInterface $path_matcher */
208    $path_matcher = $this->prophesize(PathMatcherInterface::class);
209    $path_matcher->isFrontPage()->willReturn(FALSE);
210    \Drupal::getContainer()->set('path.matcher', $path_matcher->reveal());
211    /** @var \Drupal\Core\Path\CurrentPathStack $path_current */
212    $path_current = $this->prophesize(CurrentPathStack::class);
213    $path_current->getPath()->willReturn('/node/123');
214    \Drupal::getContainer()->set('path.current', $path_current->reveal());
215    /** @var \Drupal\Core\Routing\RouteMatchInterface $route_matcher */
216    $route_matcher = $this->prophesize(RouteMatchInterface::class);
217    $route_matcher->getRouteName()->willReturn($route);
218    \Drupal::getContainer()->set('current_route_match', $route_matcher->reveal());
219
220    $suggestions = \Drupal::moduleHandler()->invokeAll('theme_suggestions_page', [[]]);
221    $this->assertSame([
222      'page__node',
223      'page__node__%',
224      'page__node__123',
225      'page__4xx',
226      $suggestion,
227    ], $suggestions);
228  }
229
230  /**
231   * Tests theme-provided templates that are registered by modules.
232   */
233  public function testThemeTemplatesRegisteredByModules() {
234    $theme_handler = \Drupal::service('theme_handler');
235    \Drupal::service('theme_installer')->install(['test_theme']);
236
237    $registry_theme = new Registry($this->root, \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_theme');
238    $registry_theme->setThemeManager(\Drupal::theme());
239
240    $expected = [
241      'template_preprocess',
242      'template_preprocess_container',
243      'template_preprocess_theme_test_registered_by_module',
244    ];
245    $registry = $registry_theme->get();
246    $this->assertEquals($expected, array_values($registry['theme_test_registered_by_module']['preprocess functions']));
247  }
248
249}
250