RendererTest.php
Same filename in other branches
Namespace
Drupal\Tests\Core\RenderFile
-
core/
tests/ Drupal/ Tests/ Core/ Render/ RendererTest.php
View source
<?php
declare (strict_types=1);
namespace Drupal\Tests\Core\Render;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Markup;
use Drupal\Core\Template\Attribute;
// cspell:ignore fooalert
/**
* @coversDefaultClass \Drupal\Core\Render\Renderer
* @group Render
*/
class RendererTest extends RendererTestBase {
protected $defaultThemeVars = [
'#cache' => [
'contexts' => [
'languages:language_interface',
'theme',
],
'tags' => [],
'max-age' => Cache::PERMANENT,
],
'#attached' => [],
'#children' => '',
];
/**
* @covers ::render
* @covers ::doRender
*
* @dataProvider providerTestRenderBasic
*/
public function testRenderBasic($build, $expected, ?callable $setup_code = NULL) : void {
if (isset($setup_code)) {
$setup_code = $setup_code->bindTo($this);
$setup_code($this->themeManager);
}
if (isset($build['#markup'])) {
$this->assertNotInstanceOf(MarkupInterface::class, $build['#markup']);
}
$render_output = $this->renderer
->renderRoot($build);
$this->assertSame($expected, (string) $render_output);
if ($render_output !== '') {
$this->assertInstanceOf(MarkupInterface::class, $render_output);
$this->assertInstanceOf(MarkupInterface::class, $build['#markup']);
}
}
/**
* Provides a list of render arrays to test basic rendering.
*
* @return array
*/
public static function providerTestRenderBasic() {
$data = [];
// Part 1: the most simplistic render arrays possible, none using #theme.
// Pass a NULL.
$data[] = [
NULL,
'',
];
// Pass an empty string.
$data[] = [
'',
'',
];
// Previously printed, see ::renderTwice for a more integration-like test.
$data[] = [
[
'#markup' => 'foo',
'#printed' => TRUE,
],
'',
];
// Printed in pre_render.
$data[] = [
[
'#markup' => 'foo',
'#pre_render' => [
[
new TestCallables(),
'preRenderPrinted',
],
],
],
'',
];
// Basic #markup based renderable array.
$data[] = [
[
'#markup' => 'foo',
],
'foo',
];
// Basic #markup based renderable array with value '0'.
$data[] = [
[
'#markup' => '0',
],
'0',
];
// Basic #markup based renderable array with value 0.
$data[] = [
[
'#markup' => 0,
],
'0',
];
// Basic #markup based renderable array with value ''.
$data[] = [
[
'#markup' => '',
],
'',
];
// Basic #markup based renderable array with value NULL.
$data[] = [
[
'#markup' => NULL,
],
'',
];
// Basic #plain_text based renderable array.
$data[] = [
[
'#plain_text' => 'foo',
],
'foo',
];
// Mixing #plain_text and #markup based renderable array.
$data[] = [
[
'#plain_text' => '<em>foo</em>',
'#markup' => 'bar',
],
'<em>foo</em>',
];
// Safe strings in #plain_text are still escaped.
$data[] = [
[
'#plain_text' => Markup::create('<em>foo</em>'),
],
'<em>foo</em>',
];
// #plain_text based renderable array with value '0'.
$data[] = [
[
'#plain_text' => '0',
],
'0',
];
// #plain_text based renderable array with value 0.
$data[] = [
[
'#plain_text' => 0,
],
'0',
];
// #plain_text based renderable array with value ''.
$data[] = [
[
'#plain_text' => '',
],
'',
];
// #plain_text based renderable array with value NULL.
$data[] = [
[
'#plain_text' => NULL,
],
'',
];
// Renderable child element.
$data[] = [
[
'child' => [
'#markup' => 'bar',
],
],
'bar',
];
// XSS filtering test.
$data[] = [
[
'child' => [
'#markup' => "This is <script>alert('XSS')</script> test",
],
],
"This is alert('XSS') test",
];
// XSS filtering test.
$data[] = [
[
'child' => [
'#markup' => "This is <script>alert('XSS')</script> test",
'#allowed_tags' => [
'script',
],
],
],
"This is <script>alert('XSS')</script> test",
];
// XSS filtering test.
$data[] = [
[
'child' => [
'#markup' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>",
'#allowed_tags' => [
'em',
'strong',
],
],
],
"This is <em>alert('XSS')</em> <strong>test</strong>",
];
// Html escaping test.
$data[] = [
[
'child' => [
'#plain_text' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>",
],
],
"This is <script><em>alert('XSS')</em></script> <strong>test</strong>",
];
// XSS filtering by default test.
$data[] = [
[
'child' => [
'#markup' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>",
],
],
"This is <em>alert('XSS')</em> <strong>test</strong>",
];
// Ensure non-XSS tags are not filtered out.
$data[] = [
[
'child' => [
'#markup' => "This is <strong><script>alert('not a giraffe')</script></strong> test",
],
],
"This is <strong>alert('not a giraffe')</strong> test",
];
// #children set but empty, and renderable children.
$data[] = [
[
'#children' => '',
'child' => [
'#markup' => 'bar',
],
],
'bar',
];
// #children set, not empty, and renderable children. #children will be
// assumed oto be the rendered child elements, even though the #markup for
// 'child' differs.
$data[] = [
[
'#children' => 'foo',
'child' => [
'#markup' => 'bar',
],
],
'foo',
];
// Ensure that content added to #markup via a #pre_render callback is safe.
$data[] = [
[
'#markup' => 'foo',
'#pre_render' => [
function ($elements) {
$elements['#markup'] .= '<script>alert("bar");</script>';
return $elements;
},
],
],
'fooalert("bar");',
];
// Test #allowed_tags in combination with #markup and #pre_render.
$data[] = [
[
'#markup' => 'foo',
'#allowed_tags' => [
'script',
],
'#pre_render' => [
function ($elements) {
$elements['#markup'] .= '<script>alert("bar");</script>';
return $elements;
},
],
],
'foo<script>alert("bar");</script>',
];
// Ensure output is escaped when adding content to #check_plain through
// a #pre_render callback.
$data[] = [
[
'#plain_text' => 'foo',
'#pre_render' => [
function ($elements) {
$elements['#plain_text'] .= '<script>alert("bar");</script>';
return $elements;
},
],
],
'foo<script>alert("bar");</script>',
];
// Part 2: render arrays using #theme and #theme_wrappers.
// Tests that #theme and #theme_wrappers can co-exist on an element.
$build = [
'#theme' => 'common_test_foo',
'#foo' => 'foo',
'#bar' => 'bar',
'#theme_wrappers' => [
'container',
],
'#attributes' => [
'class' => [
'baz',
],
],
];
$setup_code_type_link = function ($themeManager) {
$themeManager->expects(static::exactly(2))
->method('render')
->with(static::logicalOr('common_test_foo', 'container'))
->willReturnCallback(function ($theme, $vars) {
if ($theme == 'container') {
return '<div' . (string) new Attribute($vars['#attributes']) . '>' . $vars['#children'] . "</div>\n";
}
return $vars['#foo'] . $vars['#bar'];
});
};
$data[] = [
$build,
'<div class="baz">foobar</div>' . "\n",
$setup_code_type_link,
];
// Tests that #theme_wrappers can disambiguate element attributes shared
// with rendering methods that build #children by using the alternate
// #theme_wrappers attribute override syntax.
$build = [
'#type' => 'link',
'#theme_wrappers' => [
'container' => [
'#attributes' => [
'class' => [
'baz',
],
],
],
],
'#attributes' => [
'id' => 'foo',
],
'#url' => 'https://www.drupal.org',
'#title' => 'bar',
];
$setup_code_type_link = function ($themeManager) {
$themeManager->expects(static::exactly(2))
->method('render')
->with(static::logicalOr('link', 'container'))
->willReturnCallback(function ($theme, $vars) {
if ($theme == 'container') {
return '<div' . (string) new Attribute($vars['#attributes']) . '>' . $vars['#children'] . "</div>\n";
}
$attributes = new Attribute([
'href' => $vars['#url'],
] + ($vars['#attributes'] ?? []));
return '<a' . (string) $attributes . '>' . $vars['#title'] . '</a>';
});
};
$data[] = [
$build,
'<div class="baz"><a href="https://www.drupal.org" id="foo">bar</a></div>' . "\n",
$setup_code_type_link,
];
// Tests that #theme_wrappers can disambiguate element attributes when the
// "base" attribute is not set for #theme.
$build = [
'#type' => 'link',
'#url' => 'https://www.drupal.org',
'#title' => 'foo',
'#theme_wrappers' => [
'container' => [
'#attributes' => [
'class' => [
'baz',
],
],
],
],
];
$data[] = [
$build,
'<div class="baz"><a href="https://www.drupal.org">foo</a></div>' . "\n",
$setup_code_type_link,
];
// Tests two 'container' #theme_wrappers, one using the "base" attributes
// and one using an override.
$build = [
'#attributes' => [
'class' => [
'foo',
],
],
'#theme_wrappers' => [
'container' => [
'#attributes' => [
'class' => [
'bar',
],
],
],
'container',
],
];
$setup_code = function ($themeManager) {
$themeManager->expects(static::exactly(2))
->method('render')
->with('container')
->willReturnCallback(function ($theme, $vars) {
return '<div' . (string) new Attribute($vars['#attributes']) . '>' . $vars['#children'] . "</div>\n";
});
};
$data[] = [
$build,
'<div class="foo"><div class="bar"></div>' . "\n" . '</div>' . "\n",
$setup_code,
];
// Tests array syntax theme hook suggestion in #theme_wrappers.
$build = [
'#theme_wrappers' => [
[
'container',
],
],
'#attributes' => [
'class' => [
'foo',
],
],
];
$setup_code = function ($themeManager) {
$themeManager->expects(static::once())
->method('render')
->with([
'container',
])
->willReturnCallback(function ($theme, $vars) {
return '<div' . (string) new Attribute($vars['#attributes']) . '>' . $vars['#children'] . "</div>\n";
});
};
$data[] = [
$build,
'<div class="foo"></div>' . "\n",
$setup_code,
];
// Part 3: render arrays using #markup as a fallback for #theme hooks.
// Theme suggestion is not implemented, #markup should be rendered.
$build = [
'#theme' => [
'suggestion_not_implemented',
],
'#markup' => 'foo',
];
$setup_code = function ($themeManager) {
$themeManager->expects(static::once())
->method('render')
->with([
'suggestion_not_implemented',
], static::anything())
->willReturn(FALSE);
};
$data[] = [
$build,
'foo',
$setup_code,
];
// Tests unimplemented theme suggestion, child #markup should be rendered.
$build = [
'#theme' => [
'suggestion_not_implemented',
],
'child' => [
'#markup' => 'foo',
],
];
$setup_code = function ($themeManager) {
$themeManager->expects(static::once())
->method('render')
->with([
'suggestion_not_implemented',
], static::anything())
->willReturn(FALSE);
};
$data[] = [
$build,
'foo',
$setup_code,
];
// Tests implemented theme suggestion: #markup should not be rendered.
$build = [
'#theme' => [
'common_test_empty',
],
'#markup' => 'foo',
];
$theme_function_output = static::randomContextValue();
$setup_code = function ($themeManager) use ($theme_function_output) {
$themeManager->expects(static::once())
->method('render')
->with([
'common_test_empty',
], static::anything())
->willReturn($theme_function_output);
};
$data[] = [
$build,
$theme_function_output,
$setup_code,
];
// Tests implemented theme suggestion: children should not be rendered.
$build = [
'#theme' => [
'common_test_empty',
],
'child' => [
'#markup' => 'foo',
],
];
$data[] = [
$build,
$theme_function_output,
$setup_code,
];
// Part 4: handling of #children and child renderable elements.
// #theme is implemented so the values of both #children and 'child' will
// be ignored - it is the responsibility of the theme hook to render these
// if appropriate.
$build = [
'#theme' => 'common_test_foo',
'#children' => 'baz',
'child' => [
'#markup' => 'boo',
],
];
$setup_code = function ($themeManager) {
$themeManager->expects(static::once())
->method('render')
->with('common_test_foo', static::anything())
->willReturn('foobar');
};
$data[] = [
$build,
'foobar',
$setup_code,
];
// #theme is implemented but #render_children is TRUE. As in the case where
// #theme is not set, empty #children means child elements are rendered
// recursively.
$build = [
'#theme' => 'common_test_foo',
'#children' => '',
'#render_children' => TRUE,
'child' => [
'#markup' => 'boo',
],
];
$setup_code = function ($themeManager) {
$themeManager->expects(static::never())
->method('render');
};
$data[] = [
$build,
'boo',
$setup_code,
];
// #theme is implemented but #render_children is TRUE. As in the case where
// #theme is not set, #children will take precedence over 'child'.
$build = [
'#theme' => 'common_test_foo',
'#children' => 'baz',
'#render_children' => TRUE,
'child' => [
'#markup' => 'boo',
],
];
$setup_code = function ($themeManager) {
$themeManager->expects(static::never())
->method('render');
};
$data[] = [
$build,
'baz',
$setup_code,
];
// #theme is implemented but #render_children is TRUE. In this case the
// calling code is expecting only the children to be rendered. #prefix and
// #suffix should not be inherited for the children.
$build = [
'#theme' => 'common_test_foo',
'#children' => '',
'#prefix' => 'kangaroo',
'#suffix' => 'unicorn',
'#render_children' => TRUE,
'child' => [
'#markup' => 'kitten',
],
];
$setup_code = function ($themeManager) {
$themeManager->expects(static::never())
->method('render');
};
$data[] = [
$build,
'kitten',
$setup_code,
];
return $data;
}
/**
* @covers ::render
* @covers ::doRender
*/
public function testRenderSorting() : void {
$first = $this->randomMachineName();
$second = $this->randomMachineName();
// Build an array with '#weight' set for each element.
$elements = [
'second' => [
'#weight' => 10,
'#markup' => $second,
],
'first' => [
'#weight' => 0,
'#markup' => $first,
],
];
$output = (string) $this->renderer
->renderRoot($elements);
// The lowest weight element should appear last in $output.
$this->assertGreaterThan(strpos($output, $first), strpos($output, $second));
// Confirm that the $elements array has '#sorted' set to TRUE.
$this->assertTrue($elements['#sorted'], "'#sorted' => TRUE was added to the array");
// Pass $elements through \Drupal\Core\Render\Element::children() and
// ensure it remains sorted in the correct order.
// \Drupal::service('renderer')->render() will return an empty string if
// used on the same array in the same request.
$children = Element::children($elements);
$this->assertSame('first', array_shift($children), 'Child found in the correct order.');
$this->assertSame('second', array_shift($children), 'Child found in the correct order.');
}
/**
* @covers ::render
* @covers ::doRender
*/
public function testRenderSortingWithSetHashSorted() : void {
$first = $this->randomMachineName();
$second = $this->randomMachineName();
// The same array structure again, but with #sorted set to TRUE.
$elements = [
'second' => [
'#weight' => 10,
'#markup' => $second,
],
'first' => [
'#weight' => 0,
'#markup' => $first,
],
'#sorted' => TRUE,
];
$output = (string) $this->renderer
->renderRoot($elements);
// The elements should appear in output in the same order as the array.
$this->assertLessThan(strpos($output, $first), strpos($output, $second));
}
/**
* @covers ::render
* @covers ::doRender
*
* @dataProvider providerAccessValues
*/
public function testRenderWithPresetAccess($access) : void {
$build = [
'#access' => $access,
];
$this->assertAccess($build, $access);
}
/**
* @covers ::render
* @covers ::doRender
*
* @dataProvider providerAccessValues
*/
public function testRenderWithAccessCallbackCallable($access) : void {
$build = [
'#access_callback' => function () use ($access) {
return $access;
},
];
$this->assertAccess($build, $access);
}
/**
* Ensures that the #access property wins over the callable.
*
* @covers ::render
* @covers ::doRender
*
* @dataProvider providerAccessValues
*/
public function testRenderWithAccessPropertyAndCallback($access) : void {
$build = [
'#access' => $access,
'#access_callback' => function () {
return TRUE;
},
];
$this->assertAccess($build, $access);
}
/**
* @covers ::render
* @covers ::doRender
*
* @dataProvider providerAccessValues
*/
public function testRenderWithAccessControllerResolved($access) : void {
switch ($access) {
case AccessResult::allowed():
$method = 'accessResultAllowed';
break;
case AccessResult::forbidden():
$method = 'accessResultForbidden';
break;
case FALSE:
$method = 'accessFalse';
break;
case TRUE:
$method = 'accessTrue';
break;
}
$build = [
'#access_callback' => 'Drupal\\Tests\\Core\\Render\\TestAccessClass::' . $method,
];
$this->assertAccess($build, $access);
}
/**
* @covers ::render
* @covers ::doRender
*/
public function testRenderAccessCacheabilityDependencyInheritance() : void {
$build = [
'#access' => AccessResult::allowed()->addCacheContexts([
'user',
]),
];
$this->renderer
->renderInIsolation($build);
$this->assertEqualsCanonicalizing([
'languages:language_interface',
'theme',
'user',
], $build['#cache']['contexts']);
}
/**
* Tests rendering same render array twice.
*
* Tests that a first render returns the rendered output and a second doesn't
* because of the #printed property. Also tests that correct metadata has been
* set for re-rendering.
*
* @covers ::render
* @covers ::doRender
*
* @dataProvider providerRenderTwice
*/
public function testRenderTwice($build) : void {
$this->assertEquals('kittens', $this->renderer
->renderRoot($build));
$this->assertEquals('kittens', $build['#markup']);
$this->assertEquals([
'kittens-147',
], $build['#cache']['tags']);
$this->assertTrue($build['#printed']);
// We don't want to reprint already printed render arrays.
$this->assertEquals('', $this->renderer
->renderRoot($build));
}
/**
* Provides a list of render array iterations.
*
* @return array
*/
public static function providerRenderTwice() {
return [
[
[
'#markup' => 'kittens',
'#cache' => [
'tags' => [
'kittens-147',
],
],
],
],
[
[
'child' => [
'#markup' => 'kittens',
'#cache' => [
'tags' => [
'kittens-147',
],
],
],
],
],
[
[
'#render_children' => TRUE,
'child' => [
'#markup' => 'kittens',
'#cache' => [
'tags' => [
'kittens-147',
],
],
],
],
],
];
}
/**
* Ensures that #access is taken in account when rendering #render_children.
*/
public function testRenderChildrenAccess() : void {
$build = [
'#access' => FALSE,
'#render_children' => TRUE,
'child' => [
'#markup' => 'kittens',
],
];
$this->assertEquals('', $this->renderer
->renderRoot($build));
}
/**
* Provides a list of both booleans.
*
* @return array
*/
public static function providerAccessValues() {
return [
[
FALSE,
],
[
TRUE,
],
[
AccessResult::forbidden(),
],
[
AccessResult::allowed(),
],
];
}
/**
* Asserts that a render array with access checking renders correctly.
*
* @param array $build
* A render array with either #access or #access_callback.
* @param \Drupal\Core\Access\AccessResultInterface|bool $access
* Whether the render array is accessible or not.
*
* @internal
*/
protected function assertAccess(array $build, $access) : void {
$sensitive_content = $this->randomContextValue();
$build['#markup'] = $sensitive_content;
if ($access instanceof AccessResultInterface && $access->isAllowed() || $access === TRUE) {
$this->assertSame($sensitive_content, (string) $this->renderer
->renderRoot($build));
}
else {
$this->assertSame('', (string) $this->renderer
->renderRoot($build));
}
}
/**
* @covers ::render
* @covers ::doRender
*/
public function testRenderWithoutThemeArguments() : void {
$element = [
'#theme' => 'common_test_foo',
];
$this->themeManager
->expects($this->once())
->method('render')
->with('common_test_foo', $this->defaultThemeVars + $element)
->willReturn('foobar');
// Test that defaults work.
$this->assertEquals('foobar', $this->renderer
->renderRoot($element), 'Defaults work');
}
/**
* @covers ::render
* @covers ::doRender
*/
public function testRenderWithThemeArguments() : void {
$element = [
'#theme' => 'common_test_foo',
'#foo' => $this->randomMachineName(),
'#bar' => $this->randomMachineName(),
];
$this->themeManager
->expects($this->once())
->method('render')
->with('common_test_foo', $this->defaultThemeVars + $element)
->willReturnCallback(function ($hook, $vars) {
return $vars['#foo'] . $vars['#bar'];
});
// Tests that passing arguments to the theme function works.
$this->assertEquals($this->renderer
->renderRoot($element), $element['#foo'] . $element['#bar'], 'Passing arguments to theme functions works');
}
/**
* Provides a list of access conditions and expected cache metadata.
*
* @return array
*/
public static function providerRenderCache() {
return [
'full access' => [
NULL,
[
'render_cache_tag',
'render_cache_tag_child:1',
'render_cache_tag_child:2',
],
],
'no child access' => [
AccessResult::forbidden()->addCacheTags([
'render_cache_tag_child_access:1',
'render_cache_tag_child_access:2',
]),
[
'render_cache_tag',
'render_cache_tag_child:1',
'render_cache_tag_child:2',
'render_cache_tag_child_access:1',
'render_cache_tag_child_access:2',
],
],
];
}
/**
* @covers ::render
* @covers ::doRender
* @covers \Drupal\Core\Render\RenderCache::get
* @covers \Drupal\Core\Render\RenderCache::set
*
* @dataProvider providerRenderCache
*/
public function testRenderCache($child_access, $expected_tags) : void {
$this->setUpRequest();
$this->setUpMemoryCache();
// Create an empty element.
$test_element = [
'#cache' => [
'keys' => [
'render_cache_test',
],
'tags' => [
'render_cache_tag',
],
],
'#markup' => '',
'child' => [
'#access' => $child_access,
'#cache' => [
'keys' => [
'render_cache_test_child',
],
'tags' => [
'render_cache_tag_child:1',
'render_cache_tag_child:2',
],
],
'#markup' => '',
],
];
// Render the element and confirm that it goes through the rendering
// process (which will set $element['#printed']).
$element = $test_element;
$this->renderer
->renderRoot($element);
$this->assertTrue(isset($element['#printed']), 'No cache hit');
// Render the element again and confirm that it is retrieved from the cache
// instead (so $element['#printed'] will not be set).
$element = $test_element;
$this->renderer
->renderRoot($element);
$this->assertFalse(isset($element['#printed']), 'Cache hit');
// Test that cache tags are correctly collected from the render element,
// including the ones from its subchild.
$this->assertEquals($expected_tags, $element['#cache']['tags'], 'Cache tags were collected from the element and its subchild.');
// The cache item also has a 'rendered' cache tag.
$cache_item = $this->cacheFactory
->get('render')
->get([
'render_cache_test',
], CacheableMetadata::createFromRenderArray($element));
$this->assertSame(Cache::mergeTags($expected_tags, [
'rendered',
]), $cache_item->tags);
}
/**
* @covers ::render
* @covers ::doRender
* @covers \Drupal\Core\Render\RenderCache::get
* @covers \Drupal\Core\Render\RenderCache::set
*
* @dataProvider providerTestRenderCacheMaxAge
*/
public function testRenderCacheMaxAge($max_age, $is_render_cached, $render_cache_item_expire) : void {
$this->setUpRequest();
$this->setUpMemoryCache();
$element = [
'#cache' => [
'keys' => [
'render_cache_test',
],
'max-age' => $max_age,
],
'#markup' => '',
];
$this->renderer
->renderRoot($element);
$cache_item = $this->cacheFactory
->get('render')
->get([
'render_cache_test',
], CacheableMetadata::createFromRenderArray($element));
if (!$is_render_cached) {
$this->assertFalse($cache_item);
}
else {
$this->assertNotFalse($cache_item);
$this->assertSame($render_cache_item_expire, $cache_item->expire);
}
}
public static function providerTestRenderCacheMaxAge() {
return [
[
0,
FALSE,
NULL,
],
[
60,
TRUE,
(int) $_SERVER['REQUEST_TIME'] + 60,
],
[
Cache::PERMANENT,
TRUE,
-1,
],
];
}
/**
* Tests that #cache_properties are properly handled.
*
* @param array $expected_results
* An associative array of expected results keyed by property name.
*
* @covers ::render
* @covers ::doRender
* @covers \Drupal\Core\Render\RenderCache::get
* @covers \Drupal\Core\Render\RenderCache::set
* @covers \Drupal\Core\Render\RenderCache::getCacheableRenderArray
*
* @dataProvider providerTestRenderCacheProperties
*/
public function testRenderCacheProperties(array $expected_results) : void {
$this->setUpRequest();
$this->setUpMemoryCache();
$element = $original = [
'#cache' => [
'keys' => [
'render_cache_test',
],
],
// Collect expected property names.
'#cache_properties' => array_keys(array_filter($expected_results)),
'child1' => [
'#markup' => Markup::create('1'),
],
'child2' => [
'#markup' => Markup::create('2'),
],
// Mark the value as safe.
'#custom_property' => Markup::create('custom_value'),
'#custom_property_array' => [
'custom value',
],
];
$this->renderer
->renderRoot($element);
$cache = $this->cacheFactory
->get('render');
$data = $cache->get([
'render_cache_test',
], CacheableMetadata::createFromRenderArray($element))->data;
// Check that parent markup is ignored when caching children's markup.
$this->assertEquals($data['#markup'] === '', (bool) Element::children($data));
// Check that the element properties are cached as specified.
foreach ($expected_results as $property => $expected) {
$cached = !empty($data[$property]);
$this->assertEquals($cached, (bool) $expected);
// Check that only the #markup key is preserved for children.
if ($cached) {
$this->assertEquals($data[$property], $original[$property]);
}
}
// #custom_property_array can not be a safe_cache_property.
$safe_cache_properties = array_diff(Element::properties(array_filter($expected_results)), [
'#custom_property_array',
]);
foreach ($safe_cache_properties as $cache_property) {
$this->assertInstanceOf(MarkupInterface::class, $data[$cache_property]);
}
}
/**
* Data provider for ::testRenderCacheProperties().
*
* @return array
* An array of associative arrays of expected results keyed by property
* name.
*/
public static function providerTestRenderCacheProperties() {
return [
[
[],
],
[
[
'child1' => 0,
'child2' => 0,
'#custom_property' => 0,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 0,
'child2' => 0,
'#custom_property' => 1,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 0,
'child2' => 1,
'#custom_property' => 0,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 0,
'child2' => 1,
'#custom_property' => 1,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 1,
'child2' => 0,
'#custom_property' => 0,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 1,
'child2' => 0,
'#custom_property' => 1,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 1,
'child2' => 1,
'#custom_property' => 0,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 1,
'child2' => 1,
'#custom_property' => 1,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 1,
'child2' => 1,
'#custom_property' => 1,
'#custom_property_array' => 1,
],
],
];
}
/**
* @covers ::addCacheableDependency
*
* @dataProvider providerTestAddCacheableDependency
*/
public function testAddCacheableDependency(array $build, $object, array $expected) : void {
$this->renderer
->addCacheableDependency($build, $object);
$this->assertEquals($build, $expected);
}
public static function providerTestAddCacheableDependency() {
return [
// Empty render array, typical default cacheability.
[
[],
new TestCacheableDependency([], [], Cache::PERMANENT),
[
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => Cache::PERMANENT,
],
],
],
// Empty render array, some cacheability.
[
[],
new TestCacheableDependency([
'user.roles',
], [
'foo',
], Cache::PERMANENT),
[
'#cache' => [
'contexts' => [
'user.roles',
],
'tags' => [
'foo',
],
'max-age' => Cache::PERMANENT,
],
],
],
// Cacheable render array, some cacheability.
[
[
'#cache' => [
'contexts' => [
'theme',
],
'tags' => [
'bar',
],
'max-age' => 600,
],
],
new TestCacheableDependency([
'user.roles',
], [
'foo',
], Cache::PERMANENT),
[
'#cache' => [
'contexts' => [
'theme',
'user.roles',
],
'tags' => [
'bar',
'foo',
],
'max-age' => 600,
],
],
],
// Cacheable render array, no cacheability.
[
[
'#cache' => [
'contexts' => [
'theme',
],
'tags' => [
'bar',
],
'max-age' => 600,
],
],
new \stdClass(),
[
'#cache' => [
'contexts' => [
'theme',
],
'tags' => [
'bar',
],
'max-age' => 0,
],
],
],
];
}
}
class TestAccessClass implements TrustedCallbackInterface {
public static function accessTrue() {
return TRUE;
}
public static function accessFalse() {
return FALSE;
}
public static function accessResultAllowed() {
return AccessResult::allowed();
}
public static function accessResultForbidden() {
return AccessResult::forbidden();
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return [
'accessTrue',
'accessFalse',
'accessResultAllowed',
'accessResultForbidden',
];
}
}
class TestCallables implements TrustedCallbackInterface {
public function preRenderPrinted($elements) {
$elements['#printed'] = TRUE;
return $elements;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return [
'preRenderPrinted',
];
}
}
Classes
Title | Deprecated | Summary |
---|---|---|
RendererTest | @coversDefaultClass \Drupal\Core\Render\Renderer @group Render | |
TestAccessClass | ||
TestCallables |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.