annotate core/modules/system/src/Tests/Theme/ThemeTest.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\system\Tests\Theme;
Chris@0 4
Chris@0 5 use Drupal\Component\Serialization\Json;
Chris@0 6 use Drupal\simpletest\WebTestBase;
Chris@0 7 use Drupal\test_theme\ThemeClass;
Chris@0 8 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
Chris@0 9 use Symfony\Component\HttpFoundation\Request;
Chris@0 10 use Symfony\Component\Routing\Route;
Chris@0 11 use Drupal\Component\Render\MarkupInterface;
Chris@0 12
Chris@0 13 /**
Chris@0 14 * Tests low-level theme functions.
Chris@0 15 *
Chris@0 16 * @group Theme
Chris@0 17 */
Chris@0 18 class ThemeTest extends WebTestBase {
Chris@0 19
Chris@0 20 /**
Chris@0 21 * Modules to enable.
Chris@0 22 *
Chris@0 23 * @var array
Chris@0 24 */
Chris@0 25 public static $modules = ['theme_test', 'node'];
Chris@0 26
Chris@0 27 protected function setUp() {
Chris@0 28 parent::setUp();
Chris@0 29 \Drupal::service('theme_handler')->install(['test_theme']);
Chris@0 30 }
Chris@0 31
Chris@0 32 /**
Chris@0 33 * Test attribute merging.
Chris@0 34 *
Chris@0 35 * Render arrays that use a render element and templates (and hence call
Chris@0 36 * template_preprocess()) must ensure the attributes at different occasions
Chris@0 37 * are all merged correctly:
Chris@0 38 * - $variables['attributes'] as passed in to the theme hook implementation.
Chris@0 39 * - the render element's #attributes
Chris@0 40 * - any attributes set in the template's preprocessing function
Chris@0 41 */
Chris@0 42 public function testAttributeMerging() {
Chris@0 43 $theme_test_render_element = [
Chris@0 44 'elements' => [
Chris@0 45 '#attributes' => ['data-foo' => 'bar'],
Chris@0 46 ],
Chris@0 47 'attributes' => [
Chris@0 48 'id' => 'bazinga',
Chris@0 49 ],
Chris@0 50 ];
Chris@0 51 $this->assertThemeOutput('theme_test_render_element', $theme_test_render_element, '<div id="bazinga" data-foo="bar" data-variables-are-preprocessed></div>' . "\n");
Chris@0 52 }
Chris@0 53
Chris@0 54 /**
Chris@0 55 * Test that ThemeManager renders the expected data types.
Chris@0 56 */
Chris@0 57 public function testThemeDataTypes() {
Chris@0 58 // theme_test_false is an implemented theme hook so \Drupal::theme() service
Chris@0 59 // should return a string or an object that implements MarkupInterface,
Chris@0 60 // even though the theme function itself can return anything.
Chris@0 61 $foos = ['null' => NULL, 'false' => FALSE, 'integer' => 1, 'string' => 'foo', 'empty_string' => ''];
Chris@0 62 foreach ($foos as $type => $example) {
Chris@0 63 $output = \Drupal::theme()->render('theme_test_foo', ['foo' => $example]);
Chris@0 64 $this->assertTrue($output instanceof MarkupInterface || is_string($output), format_string('\Drupal::theme() returns an object that implements MarkupInterface or a string for data type @type.', ['@type' => $type]));
Chris@0 65 if ($output instanceof MarkupInterface) {
Chris@0 66 $this->assertIdentical((string) $example, $output->__toString());
Chris@0 67 }
Chris@0 68 elseif (is_string($output)) {
Chris@0 69 $this->assertIdentical($output, '', 'A string will be return when the theme returns an empty string.');
Chris@0 70 }
Chris@0 71 }
Chris@0 72
Chris@0 73 // suggestionnotimplemented is not an implemented theme hook so \Drupal::theme() service
Chris@0 74 // should return FALSE instead of a string.
Chris@0 75 $output = \Drupal::theme()->render(['suggestionnotimplemented'], []);
Chris@0 76 $this->assertIdentical($output, FALSE, '\Drupal::theme() returns FALSE when a hook suggestion is not implemented.');
Chris@0 77 }
Chris@0 78
Chris@0 79 /**
Chris@0 80 * Test function theme_get_suggestions() for SA-CORE-2009-003.
Chris@0 81 */
Chris@0 82 public function testThemeSuggestions() {
Chris@0 83 // Set the front page as something random otherwise the CLI
Chris@0 84 // test runner fails.
Chris@0 85 $this->config('system.site')->set('page.front', '/nobody-home')->save();
Chris@0 86 $args = ['node', '1', 'edit'];
Chris@0 87 $suggestions = theme_get_suggestions($args, 'page');
Chris@0 88 $this->assertEqual($suggestions, ['page__node', 'page__node__%', 'page__node__1', 'page__node__edit'], 'Found expected node edit page suggestions');
Chris@0 89 // Check attack vectors.
Chris@0 90 $args = ['node', '\\1'];
Chris@0 91 $suggestions = theme_get_suggestions($args, 'page');
Chris@0 92 $this->assertEqual($suggestions, ['page__node', 'page__node__%', 'page__node__1'], 'Removed invalid \\ from suggestions');
Chris@0 93 $args = ['node', '1/'];
Chris@0 94 $suggestions = theme_get_suggestions($args, 'page');
Chris@0 95 $this->assertEqual($suggestions, ['page__node', 'page__node__%', 'page__node__1'], 'Removed invalid / from suggestions');
Chris@0 96 $args = ['node', "1\0"];
Chris@0 97 $suggestions = theme_get_suggestions($args, 'page');
Chris@0 98 $this->assertEqual($suggestions, ['page__node', 'page__node__%', 'page__node__1'], 'Removed invalid \\0 from suggestions');
Chris@0 99 // Define path with hyphens to be used to generate suggestions.
Chris@0 100 $args = ['node', '1', 'hyphen-path'];
Chris@0 101 $result = ['page__node', 'page__node__%', 'page__node__1', 'page__node__hyphen_path'];
Chris@0 102 $suggestions = theme_get_suggestions($args, 'page');
Chris@0 103 $this->assertEqual($suggestions, $result, 'Found expected page suggestions for paths containing hyphens.');
Chris@0 104 }
Chris@0 105
Chris@0 106 /**
Chris@0 107 * Ensures preprocess functions run even for suggestion implementations.
Chris@0 108 *
Chris@0 109 * The theme hook used by this test has its base preprocess function in a
Chris@0 110 * separate file, so this test also ensures that that file is correctly loaded
Chris@0 111 * when needed.
Chris@0 112 */
Chris@0 113 public function testPreprocessForSuggestions() {
Chris@0 114 // Test with both an unprimed and primed theme registry.
Chris@0 115 drupal_theme_rebuild();
Chris@0 116 for ($i = 0; $i < 2; $i++) {
Chris@0 117 $this->drupalGet('theme-test/suggestion');
Chris@0 118 $this->assertText('Theme hook implementor=test_theme_theme_test__suggestion(). Foo=template_preprocess_theme_test', 'Theme hook suggestion ran with data available from a preprocess function for the base hook.');
Chris@0 119 }
Chris@0 120 }
Chris@0 121
Chris@0 122 /**
Chris@0 123 * Tests the priority of some theme negotiators.
Chris@0 124 */
Chris@0 125 public function testNegotiatorPriorities() {
Chris@0 126 $this->drupalGet('theme-test/priority');
Chris@0 127
Chris@0 128 // Ensure that the custom theme negotiator was not able to set the theme.
Chris@0 129
Chris@0 130 $this->assertNoText('Theme hook implementor=test_theme_theme_test__suggestion(). Foo=template_preprocess_theme_test', 'Theme hook suggestion ran with data available from a preprocess function for the base hook.');
Chris@0 131 }
Chris@0 132
Chris@0 133 /**
Chris@0 134 * Ensures that non-HTML requests never initialize themes.
Chris@0 135 */
Chris@0 136 public function testThemeOnNonHtmlRequest() {
Chris@0 137 $this->drupalGet('theme-test/non-html');
Chris@0 138 $json = Json::decode($this->getRawContent());
Chris@0 139 $this->assertFalse($json['theme_initialized']);
Chris@0 140 }
Chris@0 141
Chris@0 142 /**
Chris@0 143 * Ensure page-front template suggestion is added when on front page.
Chris@0 144 */
Chris@0 145 public function testFrontPageThemeSuggestion() {
Chris@0 146 // Set the current route to user.login because theme_get_suggestions() will
Chris@0 147 // query it to see if we are on the front page.
Chris@0 148 $request = Request::create('/user/login');
Chris@0 149 $request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'user.login');
Chris@0 150 $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, new Route('/user/login'));
Chris@0 151 \Drupal::requestStack()->push($request);
Chris@0 152 $this->config('system.site')->set('page.front', '/user/login')->save();
Chris@0 153 $suggestions = theme_get_suggestions(['user', 'login'], 'page');
Chris@0 154 // Set it back to not annoy the batch runner.
Chris@0 155 \Drupal::requestStack()->pop();
Chris@0 156 $this->assertTrue(in_array('page__front', $suggestions), 'Front page template was suggested.');
Chris@0 157 }
Chris@0 158
Chris@0 159 /**
Chris@0 160 * Ensures a theme's .info.yml file is able to override a module CSS file from being added to the page.
Chris@0 161 *
Chris@0 162 * @see test_theme.info.yml
Chris@0 163 */
Chris@0 164 public function testCSSOverride() {
Chris@0 165 // Reuse the same page as in testPreprocessForSuggestions(). We're testing
Chris@0 166 // what is output to the HTML HEAD based on what is in a theme's .info.yml
Chris@0 167 // file, so it doesn't matter what page we get, as long as it is themed with
Chris@0 168 // the test theme. First we test with CSS aggregation disabled.
Chris@0 169 $config = $this->config('system.performance');
Chris@0 170 $config->set('css.preprocess', 0);
Chris@0 171 $config->save();
Chris@0 172 $this->drupalGet('theme-test/suggestion');
Chris@0 173 $this->assertNoText('js.module.css', 'The theme\'s .info.yml file is able to override a module CSS file from being added to the page.');
Chris@0 174
Chris@0 175 // Also test with aggregation enabled, simply ensuring no PHP errors are
Chris@0 176 // triggered during drupal_build_css_cache() when a source file doesn't
Chris@0 177 // exist. Then allow remaining tests to continue with aggregation disabled
Chris@0 178 // by default.
Chris@0 179 $config->set('css.preprocess', 1);
Chris@0 180 $config->save();
Chris@0 181 $this->drupalGet('theme-test/suggestion');
Chris@0 182 $config->set('css.preprocess', 0);
Chris@0 183 $config->save();
Chris@0 184 }
Chris@0 185
Chris@0 186 /**
Chris@0 187 * Ensures a themes template is overridable based on the 'template' filename.
Chris@0 188 */
Chris@0 189 public function testTemplateOverride() {
Chris@0 190 $this->config('system.theme')
Chris@0 191 ->set('default', 'test_theme')
Chris@0 192 ->save();
Chris@0 193 $this->drupalGet('theme-test/template-test');
Chris@0 194 $this->assertText('Success: Template overridden.', 'Template overridden by defined \'template\' filename.');
Chris@0 195 }
Chris@0 196
Chris@0 197 /**
Chris@0 198 * Ensures a theme template can override a theme function.
Chris@0 199 */
Chris@0 200 public function testFunctionOverride() {
Chris@0 201 $this->drupalGet('theme-test/function-template-overridden');
Chris@0 202 $this->assertText('Success: Template overrides theme function.', 'Theme function overridden by test_theme template.');
Chris@0 203 }
Chris@0 204
Chris@0 205 /**
Chris@0 206 * Test the listInfo() function.
Chris@0 207 */
Chris@0 208 public function testListThemes() {
Chris@0 209 $theme_handler = $this->container->get('theme_handler');
Chris@0 210 $theme_handler->install(['test_subtheme']);
Chris@0 211 $themes = $theme_handler->listInfo();
Chris@0 212
Chris@0 213 // Check if ThemeHandlerInterface::listInfo() retrieves enabled themes.
Chris@0 214 $this->assertIdentical(1, $themes['test_theme']->status, 'Installed theme detected');
Chris@0 215
Chris@0 216 // Check if ThemeHandlerInterface::listInfo() returns disabled themes.
Chris@0 217 // Check for base theme and subtheme lists.
Chris@0 218 $base_theme_list = ['test_basetheme' => 'Theme test base theme'];
Chris@0 219 $sub_theme_list = ['test_subsubtheme' => 'Theme test subsubtheme', 'test_subtheme' => 'Theme test subtheme'];
Chris@0 220
Chris@0 221 $this->assertIdentical($themes['test_basetheme']->sub_themes, $sub_theme_list, 'Base theme\'s object includes list of subthemes.');
Chris@0 222 $this->assertIdentical($themes['test_subtheme']->base_themes, $base_theme_list, 'Subtheme\'s object includes list of base themes.');
Chris@0 223 // Check for theme engine in subtheme.
Chris@0 224 $this->assertIdentical($themes['test_subtheme']->engine, 'twig', 'Subtheme\'s object includes the theme engine.');
Chris@0 225 // Check for theme engine prefix.
Chris@0 226 $this->assertIdentical($themes['test_basetheme']->prefix, 'twig', 'Base theme\'s object includes the theme engine prefix.');
Chris@0 227 $this->assertIdentical($themes['test_subtheme']->prefix, 'twig', 'Subtheme\'s object includes the theme engine prefix.');
Chris@0 228 }
Chris@0 229
Chris@0 230 /**
Chris@0 231 * Tests child element rendering for 'render element' theme hooks.
Chris@0 232 */
Chris@0 233 public function testDrupalRenderChildren() {
Chris@0 234 $element = [
Chris@0 235 '#theme' => 'theme_test_render_element_children',
Chris@0 236 'child' => [
Chris@0 237 '#markup' => 'Foo',
Chris@0 238 ],
Chris@0 239 ];
Chris@0 240 $this->assertThemeOutput('theme_test_render_element_children', $element, 'Foo', 'drupal_render() avoids #theme recursion loop when rendering a render element.');
Chris@0 241
Chris@0 242 $element = [
Chris@0 243 '#theme_wrappers' => ['theme_test_render_element_children'],
Chris@0 244 'child' => [
Chris@0 245 '#markup' => 'Foo',
Chris@0 246 ],
Chris@0 247 ];
Chris@0 248 $this->assertThemeOutput('theme_test_render_element_children', $element, 'Foo', 'drupal_render() avoids #theme_wrappers recursion loop when rendering a render element.');
Chris@0 249 }
Chris@0 250
Chris@0 251 /**
Chris@0 252 * Tests theme can provide classes.
Chris@0 253 */
Chris@0 254 public function testClassLoading() {
Chris@0 255 new ThemeClass();
Chris@0 256 }
Chris@0 257
Chris@0 258 /**
Chris@0 259 * Tests drupal_find_theme_templates().
Chris@0 260 */
Chris@0 261 public function testFindThemeTemplates() {
Chris@0 262 $registry = $this->container->get('theme.registry')->get();
Chris@0 263 $templates = drupal_find_theme_templates($registry, '.html.twig', drupal_get_path('theme', 'test_theme'));
Chris@0 264 $this->assertEqual($templates['node__1']['template'], 'node--1', 'Template node--1.tpl.twig was found in test_theme.');
Chris@0 265 }
Chris@0 266
Chris@0 267 /**
Chris@0 268 * Tests that the page variable is not prematurely flattened.
Chris@0 269 *
Chris@0 270 * Some modules check the page array in template_preprocess_html(), so we
Chris@0 271 * ensure that it has not been rendered prematurely.
Chris@0 272 */
Chris@0 273 public function testPreprocessHtml() {
Chris@0 274 $this->drupalGet('');
Chris@0 275 $attributes = $this->xpath('/html/body[@theme_test_page_variable="Page variable is an array."]');
Chris@0 276 $this->assertTrue(count($attributes) == 1, 'In template_preprocess_html(), the page variable is still an array (not rendered yet).');
Chris@0 277 $this->assertText('theme test page bottom markup', 'Modules are able to set the page bottom region.');
Chris@0 278 }
Chris@0 279
Chris@0 280 /**
Chris@0 281 * Tests that region attributes can be manipulated via preprocess functions.
Chris@0 282 */
Chris@0 283 public function testRegionClass() {
Chris@0 284 \Drupal::service('module_installer')->install(['block', 'theme_region_test']);
Chris@0 285
Chris@0 286 // Place a block.
Chris@0 287 $this->drupalPlaceBlock('system_main_block');
Chris@0 288 $this->drupalGet('');
Chris@0 289 $elements = $this->cssSelect(".region-sidebar-first.new_class");
Chris@0 290 $this->assertEqual(count($elements), 1, 'New class found.');
Chris@0 291 }
Chris@0 292
Chris@0 293 /**
Chris@0 294 * Ensures suggestion preprocess functions run for default implementations.
Chris@0 295 *
Chris@0 296 * The theme hook used by this test has its base preprocess function in a
Chris@0 297 * separate file, so this test also ensures that that file is correctly loaded
Chris@0 298 * when needed.
Chris@0 299 */
Chris@0 300 public function testSuggestionPreprocessForDefaults() {
Chris@0 301 $this->config('system.theme')->set('default', 'test_theme')->save();
Chris@0 302 // Test with both an unprimed and primed theme registry.
Chris@0 303 drupal_theme_rebuild();
Chris@0 304 for ($i = 0; $i < 2; $i++) {
Chris@0 305 $this->drupalGet('theme-test/preprocess-suggestions');
Chris@0 306 $items = $this->cssSelect('.suggestion');
Chris@0 307 $expected_values = [
Chris@0 308 'Suggestion',
Chris@0 309 'Kitten',
Chris@0 310 'Monkey',
Chris@0 311 'Kitten',
Chris@0 312 'Flamingo',
Chris@0 313 ];
Chris@0 314 foreach ($expected_values as $key => $value) {
Chris@0 315 $this->assertEqual((string) $value, $items[$key]);
Chris@0 316 }
Chris@0 317 }
Chris@0 318 }
Chris@0 319
Chris@0 320 }