class RendererPlaceholdersTest in Drupal 8
Same name and namespace in other branches
- 9 core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php \Drupal\Tests\Core\Render\RendererPlaceholdersTest
- 10 core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php \Drupal\Tests\Core\Render\RendererPlaceholdersTest
@coversDefaultClass \Drupal\Core\Render\Renderer @covers \Drupal\Core\Render\RenderCache @covers \Drupal\Core\Render\PlaceholderingRenderCache @group Render
Hierarchy
- class \Drupal\Tests\UnitTestCase extends \PHPUnit\Framework\TestCase uses PhpunitCompatibilityTrait
- class \Drupal\Tests\Core\Render\RendererTestBase
- class \Drupal\Tests\Core\Render\RendererPlaceholdersTest
- class \Drupal\Tests\Core\Render\RendererTestBase
Expanded class hierarchy of RendererPlaceholdersTest
File
- core/
tests/ Drupal/ Tests/ Core/ Render/ RendererPlaceholdersTest.php, line 23 - Contains \Drupal\Tests\Core\Render\RendererPlaceholdersTest.
Namespace
Drupal\Tests\Core\RenderView source
class RendererPlaceholdersTest extends RendererTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() {
// Disable the required cache contexts, so that this test can test just the
// placeholder replacement behavior.
$this->rendererConfig['required_cache_contexts'] = [];
parent::setUp();
}
/**
* Provides the two classes of placeholders: cacheable and uncacheable.
*
* I.e. with or without #cache[keys].
*
* Also, different types:
* - A) automatically generated placeholder
* - 1) manually triggered (#create_placeholder = TRUE)
* - 2) automatically triggered (based on max-age = 0 at the top level)
* - 3) automatically triggered (based on high cardinality cache contexts at
* the top level)
* - 4) automatically triggered (based on high-invalidation frequency cache
* tags at the top level)
* - 5) automatically triggered (based on max-age = 0 in its subtree, i.e.
* via bubbling)
* - 6) automatically triggered (based on high cardinality cache contexts in
* its subtree, i.e. via bubbling)
* - 7) automatically triggered (based on high-invalidation frequency cache
* tags in its subtree, i.e. via bubbling)
* - B) manually generated placeholder
*
* So, in total 2*8 = 16 permutations. (On one axis: uncacheable vs.
* uncacheable = 2; on the other axis: A1–7 and B = 8.)
*
* @todo Case A5 is not yet supported by core. So that makes for only 14
* permutations currently, instead of 16. That will be done in
* https://www.drupal.org/node/2559847
*
* @return array
*/
public function providerPlaceholders() {
$args = [
$this
->randomContextValue(),
];
$generate_placeholder_markup = function ($cache_keys = NULL) use ($args) {
$token_render_array = [
'#lazy_builder' => [
'Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback',
$args,
],
];
if (is_array($cache_keys)) {
$token_render_array['#cache']['keys'] = $cache_keys;
}
$token = Crypt::hashBase64(serialize($token_render_array));
// \Drupal\Core\Render\Markup::create() is necessary as the render
// system would mangle this markup. As this is exactly what happens at
// runtime this is a valid use-case.
return Markup::create('<drupal-render-placeholder callback="Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback" arguments="' . '0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>');
};
$extract_placeholder_render_array = function ($placeholder_render_array) {
return array_intersect_key($placeholder_render_array, [
'#lazy_builder' => TRUE,
'#cache' => TRUE,
]);
};
// Note the presence of '#create_placeholder'.
$base_element_a1 = [
'#attached' => [
'drupalSettings' => [
'foo' => 'bar',
],
],
'placeholder' => [
'#cache' => [
'contexts' => [],
],
'#create_placeholder' => TRUE,
'#lazy_builder' => [
'Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback',
$args,
],
],
];
// Note the absence of '#create_placeholder', presence of max-age=0 at the
// top level.
$base_element_a2 = [
'#attached' => [
'drupalSettings' => [
'foo' => 'bar',
],
],
'placeholder' => [
'#cache' => [
'contexts' => [],
'max-age' => 0,
],
'#lazy_builder' => [
'Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback',
$args,
],
],
];
// Note the absence of '#create_placeholder', presence of high cardinality
// cache context at the top level.
$base_element_a3 = [
'#attached' => [
'drupalSettings' => [
'foo' => 'bar',
],
],
'placeholder' => [
'#cache' => [
'contexts' => [
'user',
],
],
'#lazy_builder' => [
'Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback',
$args,
],
],
];
// Note the absence of '#create_placeholder', presence of high-invalidation
// frequency cache tag at the top level.
$base_element_a4 = [
'#attached' => [
'drupalSettings' => [
'foo' => 'bar',
],
],
'placeholder' => [
'#cache' => [
'contexts' => [],
'tags' => [
'current-temperature',
],
],
'#lazy_builder' => [
'Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback',
$args,
],
],
];
// Note the absence of '#create_placeholder', presence of max-age=0 created
// by the #lazy_builder callback.
// @todo in https://www.drupal.org/node/2559847
$base_element_a5 = [];
// Note the absence of '#create_placeholder', presence of high cardinality
// cache context created by the #lazy_builder callback.
// @see \Drupal\Tests\Core\Render\PlaceholdersTest::callbackPerUser()
$base_element_a6 = [
'#attached' => [
'drupalSettings' => [
'foo' => 'bar',
],
],
'placeholder' => [
'#cache' => [
'contexts' => [],
],
'#lazy_builder' => [
'Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callbackPerUser',
$args,
],
],
];
// Note the absence of '#create_placeholder', presence of high-invalidation
// frequency cache tag created by the #lazy_builder callback.
// @see \Drupal\Tests\Core\Render\PlaceholdersTest::callbackTagCurrentTemperature()
$base_element_a7 = [
'#attached' => [
'drupalSettings' => [
'foo' => 'bar',
],
],
'placeholder' => [
'#cache' => [
'contexts' => [],
],
'#lazy_builder' => [
'Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callbackTagCurrentTemperature',
$args,
],
],
];
// Note the absence of '#create_placeholder', but the presence of
// '#attached[placeholders]'.
$base_element_b = [
'#markup' => $generate_placeholder_markup(),
'#attached' => [
'drupalSettings' => [
'foo' => 'bar',
],
'placeholders' => [
(string) $generate_placeholder_markup() => [
'#lazy_builder' => [
'Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback',
$args,
],
],
],
],
];
$keys = [
'placeholder',
'output',
'can',
'be',
'render',
'cached',
'too',
];
$cases = [];
// Case one: render array that has a placeholder that is:
// - automatically created, but manually triggered (#create_placeholder = TRUE)
// - uncacheable
$element_without_cache_keys = $base_element_a1;
$expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a1['placeholder']);
$cases[] = [
$element_without_cache_keys,
$args,
$expected_placeholder_render_array,
FALSE,
[],
[],
[],
];
// Case two: render array that has a placeholder that is:
// - automatically created, but manually triggered (#create_placeholder = TRUE)
// - cacheable
$element_with_cache_keys = $base_element_a1;
$element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
$expected_placeholder_render_array['#cache']['keys'] = $keys;
$cases[] = [
$element_with_cache_keys,
$args,
$expected_placeholder_render_array,
$keys,
[],
[],
[
'#markup' => '<p>This is a rendered placeholder!</p>',
'#attached' => [
'drupalSettings' => [
'dynamic_animal' => $args[0],
],
],
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => Cache::PERMANENT,
],
],
];
// Case three: render array that has a placeholder that is:
// - automatically created, and automatically triggered due to max-age=0
// - uncacheable
$element_without_cache_keys = $base_element_a2;
$expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a2['placeholder']);
$cases[] = [
$element_without_cache_keys,
$args,
$expected_placeholder_render_array,
FALSE,
[],
[],
[],
];
// Case four: render array that has a placeholder that is:
// - automatically created, but automatically triggered due to max-age=0
// - cacheable
$element_with_cache_keys = $base_element_a2;
$element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
$expected_placeholder_render_array['#cache']['keys'] = $keys;
$cases[] = [
$element_with_cache_keys,
$args,
$expected_placeholder_render_array,
FALSE,
[],
[],
[],
];
// Case five: render array that has a placeholder that is:
// - automatically created, and automatically triggered due to high
// cardinality cache contexts
// - uncacheable
$element_without_cache_keys = $base_element_a3;
$expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a3['placeholder']);
$cases[] = [
$element_without_cache_keys,
$args,
$expected_placeholder_render_array,
FALSE,
[],
[],
[],
];
// Case six: render array that has a placeholder that is:
// - automatically created, and automatically triggered due to high
// cardinality cache contexts
// - cacheable
$element_with_cache_keys = $base_element_a3;
$element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
$expected_placeholder_render_array['#cache']['keys'] = $keys;
// The CID parts here consist of the cache keys plus the 'user' cache
// context, which in this unit test is simply the given cache context token,
// see \Drupal\Tests\Core\Render\RendererTestBase::setUp().
$cid_parts = array_merge($keys, [
'user',
]);
$cases[] = [
$element_with_cache_keys,
$args,
$expected_placeholder_render_array,
$cid_parts,
[],
[],
[
'#markup' => '<p>This is a rendered placeholder!</p>',
'#attached' => [
'drupalSettings' => [
'dynamic_animal' => $args[0],
],
],
'#cache' => [
'contexts' => [
'user',
],
'tags' => [],
'max-age' => Cache::PERMANENT,
],
],
];
// Case seven: render array that has a placeholder that is:
// - automatically created, and automatically triggered due to high
// invalidation frequency cache tags
// - uncacheable
$element_without_cache_keys = $base_element_a4;
$expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a4['placeholder']);
$cases[] = [
$element_without_cache_keys,
$args,
$expected_placeholder_render_array,
FALSE,
[],
[],
[],
];
// Case eight: render array that has a placeholder that is:
// - automatically created, and automatically triggered due to high
// invalidation frequency cache tags
// - cacheable
$element_with_cache_keys = $base_element_a4;
$element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
$expected_placeholder_render_array['#cache']['keys'] = $keys;
$cases[] = [
$element_with_cache_keys,
$args,
$expected_placeholder_render_array,
$keys,
[],
[],
[
'#markup' => '<p>This is a rendered placeholder!</p>',
'#attached' => [
'drupalSettings' => [
'dynamic_animal' => $args[0],
],
],
'#cache' => [
'contexts' => [],
'tags' => [
'current-temperature',
],
'max-age' => Cache::PERMANENT,
],
],
];
// Case nine: render array that DOES NOT have a placeholder that is:
// - NOT created, despite max-age=0 that is bubbled
// - uncacheable
// (because the render element with #lazy_builder does not have #cache[keys]
// and hence the max-age=0 bubbles up further)
// @todo in https://www.drupal.org/node/2559847
// Case ten: render array that has a placeholder that is:
// - automatically created, and automatically triggered due to max-age=0
// that is bubbled
// - cacheable
// @todo in https://www.drupal.org/node/2559847
// Case eleven: render array that DOES NOT have a placeholder that is:
// - NOT created, despite high cardinality cache contexts that are bubbled
// - uncacheable
$element_without_cache_keys = $base_element_a6;
$expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a6['placeholder']);
$cases[] = [
$element_without_cache_keys,
$args,
$expected_placeholder_render_array,
FALSE,
[
'user',
],
[],
[],
];
// Case twelve: render array that has a placeholder that is:
// - automatically created, and automatically triggered due to high
// cardinality cache contexts that are bubbled
// - cacheable
$element_with_cache_keys = $base_element_a6;
$element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
$expected_placeholder_render_array['#cache']['keys'] = $keys;
$cases[] = [
$element_with_cache_keys,
$args,
$expected_placeholder_render_array,
$keys,
[
'user',
],
[],
[
'#markup' => '<p>This is a rendered placeholder!</p>',
'#attached' => [
'drupalSettings' => [
'dynamic_animal' => $args[0],
],
],
'#cache' => [
'contexts' => [
'user',
],
'tags' => [],
'max-age' => Cache::PERMANENT,
],
],
];
// Case thirteen: render array that has a placeholder that is:
// - automatically created, and automatically triggered due to high
// invalidation frequency cache tags that are bubbled
// - uncacheable
$element_without_cache_keys = $base_element_a7;
$expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a7['placeholder']);
$cases[] = [
$element_without_cache_keys,
$args,
$expected_placeholder_render_array,
FALSE,
[],
[
'current-temperature',
],
[],
];
// Case fourteen: render array that has a placeholder that is:
// - automatically created, and automatically triggered due to high
// invalidation frequency cache tags that are bubbled
// - cacheable
$element_with_cache_keys = $base_element_a7;
$element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
$expected_placeholder_render_array['#cache']['keys'] = $keys;
$cases[] = [
$element_with_cache_keys,
$args,
$expected_placeholder_render_array,
$keys,
[],
[],
[
'#markup' => '<p>This is a rendered placeholder!</p>',
'#attached' => [
'drupalSettings' => [
'dynamic_animal' => $args[0],
],
],
'#cache' => [
'contexts' => [],
'tags' => [
'current-temperature',
],
'max-age' => Cache::PERMANENT,
],
],
];
// Case fifteen: render array that has a placeholder that is:
// - manually created
// - uncacheable
$x = $base_element_b;
$expected_placeholder_render_array = $x['#attached']['placeholders'][(string) $generate_placeholder_markup()];
unset($x['#attached']['placeholders'][(string) $generate_placeholder_markup()]['#cache']);
$cases[] = [
$x,
$args,
$expected_placeholder_render_array,
FALSE,
[],
[],
[],
];
// Case sixteen: render array that has a placeholder that is:
// - manually created
// - cacheable
$x = $base_element_b;
$x['#markup'] = $placeholder_markup = $generate_placeholder_markup($keys);
$placeholder_markup = (string) $placeholder_markup;
$x['#attached']['placeholders'] = [
$placeholder_markup => [
'#lazy_builder' => [
'Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback',
$args,
],
'#cache' => [
'keys' => $keys,
],
],
];
$expected_placeholder_render_array = $x['#attached']['placeholders'][$placeholder_markup];
$cases[] = [
$x,
$args,
$expected_placeholder_render_array,
$keys,
[],
[],
[
'#markup' => '<p>This is a rendered placeholder!</p>',
'#attached' => [
'drupalSettings' => [
'dynamic_animal' => $args[0],
],
],
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => Cache::PERMANENT,
],
],
];
return $cases;
}
/**
* Generates an element with a placeholder.
*
* @return array
* An array containing:
* - A render array containing a placeholder.
* - The context used for that #lazy_builder callback.
*/
protected function generatePlaceholderElement() {
$args = [
$this
->randomContextValue(),
];
$test_element = [];
$test_element['#attached']['drupalSettings']['foo'] = 'bar';
$test_element['placeholder']['#cache']['keys'] = [
'placeholder',
'output',
'can',
'be',
'render',
'cached',
'too',
];
$test_element['placeholder']['#cache']['contexts'] = [];
$test_element['placeholder']['#create_placeholder'] = TRUE;
$test_element['placeholder']['#lazy_builder'] = [
'Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback',
$args,
];
return [
$test_element,
$args,
];
}
/**
* @param false|array $cid_parts
* @param string[] $bubbled_cache_contexts
* Additional cache contexts that were bubbled when the placeholder was
* rendered.
* @param array $expected_data
* A render array with the expected values.
*/
protected function assertPlaceholderRenderCache($cid_parts, array $bubbled_cache_contexts, array $expected_data) {
if ($cid_parts !== FALSE) {
if ($bubbled_cache_contexts) {
// Verify render cached placeholder.
$cached_element = $this->memoryCache
->get(implode(':', $cid_parts))->data;
$expected_redirect_element = [
'#cache_redirect' => TRUE,
'#cache' => $expected_data['#cache'] + [
'keys' => $cid_parts,
'bin' => 'render',
],
];
$this
->assertEquals($expected_redirect_element, $cached_element, 'The correct cache redirect exists.');
}
// Verify render cached placeholder.
$cached = $this->memoryCache
->get(implode(':', array_merge($cid_parts, $bubbled_cache_contexts)));
$cached_element = $cached->data;
$this
->assertEquals($expected_data, $cached_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by the placeholder being replaced.');
}
}
/**
* @covers ::render
* @covers ::doRender
*
* @dataProvider providerPlaceholders
*/
public function testUncacheableParent($element, $args, array $expected_placeholder_render_array, $placeholder_cid_parts, array $bubbled_cache_contexts, array $bubbled_cache_tags, array $placeholder_expected_render_cache_array) {
if ($placeholder_cid_parts) {
$this
->setupMemoryCache();
}
else {
$this
->setUpUnusedCache();
}
$this
->setUpRequest('GET');
// No #cache on parent element.
$element['#prefix'] = '<p>#cache disabled</p>';
$output = $this->renderer
->renderRoot($element);
$this
->assertSame('<p>#cache disabled</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
$this
->assertSame('<p>#cache disabled</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
$expected_js_settings = [
'foo' => 'bar',
'dynamic_animal' => $args[0],
];
$this
->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.');
$this
->assertPlaceholderRenderCache($placeholder_cid_parts, $bubbled_cache_contexts, $placeholder_expected_render_cache_array);
}
/**
* @covers ::render
* @covers ::doRender
* @covers \Drupal\Core\Render\RenderCache::get
* @covers \Drupal\Core\Render\RenderCache::set
* @covers \Drupal\Core\Render\RenderCache::createCacheID
*
* @dataProvider providerPlaceholders
*/
public function testCacheableParent($test_element, $args, array $expected_placeholder_render_array, $placeholder_cid_parts, array $bubbled_cache_contexts, array $bubbled_cache_tags, array $placeholder_expected_render_cache_array) {
$element = $test_element;
$this
->setupMemoryCache();
$this
->setUpRequest('GET');
$token = Crypt::hashBase64(serialize($expected_placeholder_render_array));
$placeholder_callback = $expected_placeholder_render_array['#lazy_builder'][0];
$expected_placeholder_markup = '<drupal-render-placeholder callback="' . $placeholder_callback . '" arguments="0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>';
$this
->assertSame($expected_placeholder_markup, Html::normalize($expected_placeholder_markup), 'Placeholder unaltered by Html::normalize() which is used by FilterHtmlCorrector.');
// GET request: #cache enabled, cache miss.
$element['#cache'] = [
'keys' => [
'placeholder_test_GET',
],
];
$element['#prefix'] = '<p>#cache enabled, GET</p>';
$output = $this->renderer
->renderRoot($element);
$this
->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
$this
->assertTrue(isset($element['#printed']), 'No cache hit');
$this
->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
$expected_js_settings = [
'foo' => 'bar',
'dynamic_animal' => $args[0],
];
$this
->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.');
$this
->assertPlaceholderRenderCache($placeholder_cid_parts, $bubbled_cache_contexts, $placeholder_expected_render_cache_array);
// GET request: validate cached data.
$cached = $this->memoryCache
->get('placeholder_test_GET');
// There are three edge cases, where the shape of the render cache item for
// the parent (with CID 'placeholder_test_GET') is vastly different. These
// are the cases where:
// - the placeholder is uncacheable (because it has no #cache[keys]), and;
// - cacheability metadata that meets auto_placeholder_conditions is bubbled
$has_uncacheable_lazy_builder = !isset($test_element['placeholder']['#cache']['keys']) && isset($test_element['placeholder']['#lazy_builder']);
// Edge cases: always where both bubbling of an auto-placeholdering
// condition happens from within a #lazy_builder that is uncacheable.
// - uncacheable + A5 (cache max-age)
// @todo in https://www.drupal.org/node/2559847
// - uncacheable + A6 (cache context)
$edge_case_a6_uncacheable = $has_uncacheable_lazy_builder && $test_element['placeholder']['#lazy_builder'][0] === 'Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callbackPerUser';
// - uncacheable + A7 (cache tag)
$edge_case_a7_uncacheable = $has_uncacheable_lazy_builder && $test_element['placeholder']['#lazy_builder'][0] === 'Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callbackTagCurrentTemperature';
// The redirect-cacheable edge case: a high-cardinality cache context is
// bubbled from a #lazy_builder callback for an uncacheable placeholder. The
// element containing the uncacheable placeholder has cache keys set, and
// due to the bubbled cache contexts it creates a cache redirect.
if ($edge_case_a6_uncacheable) {
$cached_element = $cached->data;
$expected_redirect = [
'#cache_redirect' => TRUE,
'#cache' => [
'keys' => [
'placeholder_test_GET',
],
'contexts' => [
'user',
],
'tags' => [],
'max-age' => Cache::PERMANENT,
'bin' => 'render',
],
];
$this
->assertEquals($expected_redirect, $cached_element);
// Follow the redirect.
$cached_element = $this->memoryCache
->get('placeholder_test_GET:' . implode(':', $bubbled_cache_contexts))->data;
$expected_element = [
'#markup' => '<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>',
'#attached' => [
'drupalSettings' => [
'foo' => 'bar',
'dynamic_animal' => $args[0],
],
],
'#cache' => [
'contexts' => $bubbled_cache_contexts,
'tags' => [],
'max-age' => Cache::PERMANENT,
],
];
$this
->assertEquals($expected_element, $cached_element, 'The parent is render cached with a redirect in ase a cache context is bubbled from an uncacheable child (no #cache[keys]) with a #lazy_builder.');
}
elseif ($edge_case_a7_uncacheable) {
$cached_element = $cached->data;
$expected_element = [
'#markup' => '<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>',
'#attached' => [
'drupalSettings' => [
'foo' => 'bar',
'dynamic_animal' => $args[0],
],
],
'#cache' => [
'contexts' => [],
'tags' => $bubbled_cache_tags,
'max-age' => Cache::PERMANENT,
],
];
$this
->assertEquals($expected_element, $cached_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by placeholder #lazy_builder callbacks.');
}
else {
$cached_element = $cached->data;
$expected_element = [
'#markup' => '<p>#cache enabled, GET</p>' . $expected_placeholder_markup,
'#attached' => [
'drupalSettings' => [
'foo' => 'bar',
],
'placeholders' => [
$expected_placeholder_markup => [
'#lazy_builder' => [
'Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback',
$args,
],
],
],
],
'#cache' => [
'contexts' => [],
'tags' => $bubbled_cache_tags,
'max-age' => Cache::PERMANENT,
],
];
$expected_element['#attached']['placeholders'][$expected_placeholder_markup] = $expected_placeholder_render_array;
$this
->assertEquals($expected_element, $cached_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by placeholder #lazy_builder callbacks.');
}
// GET request: #cache enabled, cache hit.
$element = $test_element;
$element['#cache'] = [
'keys' => [
'placeholder_test_GET',
],
];
$element['#prefix'] = '<p>#cache enabled, GET</p>';
$output = $this->renderer
->renderRoot($element);
$this
->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
$this
->assertFalse(isset($element['#printed']), 'Cache hit');
$this
->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
$expected_js_settings = [
'foo' => 'bar',
'dynamic_animal' => $args[0],
];
$this
->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.');
}
/**
* @covers ::render
* @covers ::doRender
* @covers \Drupal\Core\Render\RenderCache::get
* @covers ::replacePlaceholders
*
* @dataProvider providerPlaceholders
*/
public function testCacheableParentWithPostRequest($test_element, $args) {
$this
->setUpUnusedCache();
// Verify behavior when handling a non-GET request, e.g. a POST request:
// also in that case, placeholders must be replaced.
$this
->setUpRequest('POST');
// POST request: #cache enabled, cache miss.
$element = $test_element;
$element['#cache'] = [
'keys' => [
'placeholder_test_POST',
],
];
$element['#prefix'] = '<p>#cache enabled, POST</p>';
$output = $this->renderer
->renderRoot($element);
$this
->assertSame('<p>#cache enabled, POST</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
$this
->assertTrue(isset($element['#printed']), 'No cache hit');
$this
->assertSame('<p>#cache enabled, POST</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
$expected_js_settings = [
'foo' => 'bar',
'dynamic_animal' => $args[0],
];
$this
->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.');
// Even when the child element's placeholder is cacheable, it should not
// generate a render cache item.
$this
->assertPlaceholderRenderCache(FALSE, [], []);
}
/**
* @covers ::render
* @covers ::doRender
* @covers \Drupal\Core\Render\RenderCache::get
* @covers \Drupal\Core\Render\PlaceholderingRenderCache::get
* @covers \Drupal\Core\Render\PlaceholderingRenderCache::set
* @covers ::replacePlaceholders
*
* @dataProvider providerPlaceholders
*/
public function testPlaceholderingDisabledForPostRequests($test_element, $args) {
$this
->setUpUnusedCache();
$this
->setUpRequest('POST');
$element = $test_element;
// Render without replacing placeholders, to allow this test to see which
// #attached[placeholders] there are, if any.
$this->renderer
->executeInRenderContext(new RenderContext(), function () use (&$element) {
return $this->renderer
->render($element);
});
// Only test cases where the placeholders have been specified manually are
// allowed to have placeholders. This means that of the different situations
// listed in providerPlaceholders(), only type B can have attached
// placeholders. Everything else, whether:
// 1. manual placeholdering
// 2. automatic placeholdering via already-present cacheability metadata
// 3. automatic placeholdering via bubbled cacheability metadata
// All three of those should NOT result in placeholders.
if (!isset($test_element['#attached']['placeholders'])) {
$this
->assertFalse(isset($element['#attached']['placeholders']), 'No placeholders created.');
}
}
/**
* Tests a placeholder that adds another placeholder.
*
* E.g. when rendering a node in a placeholder the rendering of that node
* needs a placeholder of its own to be executed (to render the node links).
*
* @covers ::render
* @covers ::doRender
* @covers ::replacePlaceholders
*/
public function testRecursivePlaceholder() {
$args = [
$this
->randomContextValue(),
];
$element = [];
$element['#create_placeholder'] = TRUE;
$element['#lazy_builder'] = [
'Drupal\\Tests\\Core\\Render\\RecursivePlaceholdersTest::callback',
$args,
];
$output = $this->renderer
->renderRoot($element);
$this
->assertEquals('<p>This is a rendered placeholder!</p>', $output, 'The output has been modified by the indirect, recursive placeholder #lazy_builder callback.');
$this
->assertSame((string) $element['#markup'], '<p>This is a rendered placeholder!</p>', '#markup is overridden by the indirect, recursive placeholder #lazy_builder callback.');
$expected_js_settings = [
'dynamic_animal' => $args[0],
];
$this
->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified by the indirect, recursive placeholder #lazy_builder callback.');
}
/**
* @covers ::render
* @covers ::doRender
*/
public function testInvalidLazyBuilder() {
$element = [];
$element['#lazy_builder'] = '\\Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback';
$this
->expectException(\DomainException::class);
$this
->expectExceptionMessage('The #lazy_builder property must have an array as a value.');
$this->renderer
->renderRoot($element);
}
/**
* @covers ::render
* @covers ::doRender
*/
public function testInvalidLazyBuilderArguments() {
$element = [];
$element['#lazy_builder'] = [
'\\Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback',
'arg1',
'arg2',
];
$this
->expectException(\DomainException::class);
$this
->expectExceptionMessage('The #lazy_builder property must have an array as a value, containing two values: the callback, and the arguments for the callback.');
$this->renderer
->renderRoot($element);
}
/**
* @covers ::render
* @covers ::doRender
*
* @see testNonScalarLazybuilderCallbackContext
*/
public function testScalarLazybuilderCallbackContext() {
$element = [];
$element['#lazy_builder'] = [
'\\Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback',
[
'string' => 'foo',
'bool' => TRUE,
'int' => 1337,
'float' => 3.14,
'null' => NULL,
],
];
$result = $this->renderer
->renderRoot($element);
$this
->assertInstanceOf('\\Drupal\\Core\\Render\\Markup', $result);
$this
->assertEquals('<p>This is a rendered placeholder!</p>', (string) $result);
}
/**
* @covers ::render
* @covers ::doRender
*/
public function testNonScalarLazybuilderCallbackContext() {
$element = [];
$element['#lazy_builder'] = [
'\\Drupal\\Tests\\Core\\Render\\PlaceholdersTest::callback',
[
'string' => 'foo',
'bool' => TRUE,
'int' => 1337,
'float' => 3.14,
'null' => NULL,
// array is not one of the scalar types.
'array' => [
'hi!',
],
],
];
$this
->expectException(\DomainException::class);
$this
->expectExceptionMessage("A #lazy_builder callback's context may only contain scalar values or NULL.");
$this->renderer
->renderRoot($element);
}
/**
* @covers ::render
* @covers ::doRender
*/
public function testChildrenPlusBuilder() {
$element = [];
$element['#lazy_builder'] = [
'Drupal\\Tests\\Core\\Render\\RecursivePlaceholdersTest::callback',
[],
];
$element['child_a']['#markup'] = 'Oh hai!';
$element['child_b']['#markup'] = 'kthxbai';
$this
->expectException(\DomainException::class);
$this
->expectExceptionMessage('When a #lazy_builder callback is specified, no children can exist; all children must be generated by the #lazy_builder callback. You specified the following children: child_a, child_b.');
$this->renderer
->renderRoot($element);
}
/**
* @covers ::render
* @covers ::doRender
*/
public function testPropertiesPlusBuilder() {
$element = [];
$element['#lazy_builder'] = [
'Drupal\\Tests\\Core\\Render\\RecursivePlaceholdersTest::callback',
[],
];
$element['#llama'] = '#awesome';
$element['#piglet'] = '#cute';
$this
->expectException(\DomainException::class);
$this
->expectExceptionMessage('When a #lazy_builder callback is specified, no properties can exist; all properties must be generated by the #lazy_builder callback. You specified the following properties: #llama, #piglet.');
$this->renderer
->renderRoot($element);
}
/**
* @covers ::render
* @covers ::doRender
*/
public function testCreatePlaceholderPropertyWithoutLazyBuilder() {
$element = [];
$element['#create_placeholder'] = TRUE;
$this
->expectException(\LogicException::class);
$this
->expectExceptionMessage('When #create_placeholder is set, a #lazy_builder callback must be present as well.');
$this->renderer
->renderRoot($element);
}
/**
* Create an element with a child and subchild. Each element has the same
* #lazy_builder callback, but with different contexts. They don't modify
* markup, only attach additional drupalSettings.
*
* @covers ::render
* @covers ::doRender
* @covers \Drupal\Core\Render\RenderCache::get
* @covers ::replacePlaceholders
*/
public function testRenderChildrenPlaceholdersDifferentArguments() {
$this
->setUpRequest();
$this
->setupMemoryCache();
$this->cacheContextsManager
->expects($this
->any())
->method('convertTokensToKeys')
->willReturnArgument(0);
$this->controllerResolver
->expects($this
->any())
->method('getControllerFromDefinition')
->willReturnArgument(0);
$this
->setupThemeManagerForDetails();
$args_1 = [
'foo',
TRUE,
];
$args_2 = [
'bar',
TRUE,
];
$args_3 = [
'baz',
TRUE,
];
$test_element = $this
->generatePlaceholdersWithChildrenTestElement($args_1, $args_2, $args_3);
$element = $test_element;
$output = $this->renderer
->renderRoot($element);
$expected_output = <<<HTML
<details>
<summary>Parent</summary>
<div class="details-wrapper"><details>
<summary>Child</summary>
<div class="details-wrapper">Subchild</div>
</details></div>
</details>
HTML;
$this
->assertSame($expected_output, (string) $output, 'Output is not overridden.');
$this
->assertTrue(isset($element['#printed']), 'No cache hit');
$this
->assertSame($expected_output, (string) $element['#markup'], '#markup is not overridden.');
$expected_js_settings = [
'foo' => 'bar',
'dynamic_animal' => [
$args_1[0] => TRUE,
$args_2[0] => TRUE,
$args_3[0] => TRUE,
],
];
$this
->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each placeholder #lazy_builder callback exist.');
// GET request: validate cached data.
$cached_element = $this->memoryCache
->get('simpletest:renderer:children_placeholders')->data;
$expected_element = [
'#attached' => [
'drupalSettings' => [
'foo' => 'bar',
],
'placeholders' => [
'parent-x-parent' => [
'#lazy_builder' => [
__NAMESPACE__ . '\\PlaceholdersTest::callback',
$args_1,
],
],
'child-x-child' => [
'#lazy_builder' => [
__NAMESPACE__ . '\\PlaceholdersTest::callback',
$args_2,
],
],
'subchild-x-subchild' => [
'#lazy_builder' => [
__NAMESPACE__ . '\\PlaceholdersTest::callback',
$args_3,
],
],
],
],
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => Cache::PERMANENT,
],
];
$dom = Html::load($cached_element['#markup']);
$xpath = new \DOMXPath($dom);
$parent = $xpath
->query('//details/summary[text()="Parent"]')->length;
$child = $xpath
->query('//details/div[@class="details-wrapper"]/details/summary[text()="Child"]')->length;
$subchild = $xpath
->query('//details/div[@class="details-wrapper"]/details/div[@class="details-wrapper" and text()="Subchild"]')->length;
$this
->assertTrue($parent && $child && $subchild, 'The correct data is cached: the stored #markup is not affected by placeholder #lazy_builder callbacks.');
// Remove markup because it's compared above in the xpath.
unset($cached_element['#markup']);
$this
->assertEquals($cached_element, $expected_element, 'The correct data is cached: the stored #attached properties are not affected by placeholder #lazy_builder callbacks.');
// GET request: #cache enabled, cache hit.
$element = $test_element;
$output = $this->renderer
->renderRoot($element);
$this
->assertSame($expected_output, (string) $output, 'Output is not overridden.');
$this
->assertFalse(isset($element['#printed']), 'Cache hit');
$this
->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each placeholder #lazy_builder callback exist.');
// Use the exact same element, but now unset #cache; ensure we get the same
// result.
unset($test_element['#cache']);
$element = $test_element;
$output = $this->renderer
->renderRoot($element);
$this
->assertSame($expected_output, (string) $output, 'Output is not overridden.');
$this
->assertSame($expected_output, (string) $element['#markup'], '#markup is not overridden.');
$this
->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each #lazy_builder callback exist.');
}
/**
* Generates an element with placeholders at 3 levels.
*
* @param array $args_1
* The arguments for the placeholder at level 1.
* @param array $args_2
* The arguments for the placeholder at level 2.
* @param array $args_3
* The arguments for the placeholder at level 3.
*
* @return array
* The generated render array for testing.
*/
protected function generatePlaceholdersWithChildrenTestElement(array $args_1, array $args_2, array $args_3) {
$test_element = [
'#type' => 'details',
'#cache' => [
'keys' => [
'simpletest',
'renderer',
'children_placeholders',
],
],
'#title' => 'Parent',
'#attached' => [
'drupalSettings' => [
'foo' => 'bar',
],
'placeholders' => [
'parent-x-parent' => [
'#lazy_builder' => [
__NAMESPACE__ . '\\PlaceholdersTest::callback',
$args_1,
],
],
],
],
];
$test_element['child'] = [
'#type' => 'details',
'#attached' => [
'placeholders' => [
'child-x-child' => [
'#lazy_builder' => [
__NAMESPACE__ . '\\PlaceholdersTest::callback',
$args_2,
],
],
],
],
'#title' => 'Child',
];
$test_element['child']['subchild'] = [
'#attached' => [
'placeholders' => [
'subchild-x-subchild' => [
'#lazy_builder' => [
__NAMESPACE__ . '\\PlaceholdersTest::callback',
$args_3,
],
],
],
],
'#markup' => 'Subchild',
];
return $test_element;
}
/**
* @return \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit\Framework\MockObject\Builder\InvocationMocker
*/
protected function setupThemeManagerForDetails() {
return $this->themeManager
->expects($this
->any())
->method('render')
->willReturnCallback(function ($theme, array $vars) {
$output = <<<'EOS'
<details>
<summary>{{ title }}</summary>
<div class="details-wrapper">{{ children }}</div>
</details>
EOS;
$output = str_replace([
'{{ title }}',
'{{ children }}',
], [
$vars['#title'],
$vars['#children'],
], $output);
return $output;
});
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
PhpunitCompatibilityTrait:: |
public | function | Returns a mock object for the specified class using the available method. | |
PhpunitCompatibilityTrait:: |
public | function | Compatibility layer for PHPUnit 6 to support PHPUnit 4 code. | |
RendererPlaceholdersTest:: |
protected | function | ||
RendererPlaceholdersTest:: |
protected | function | Generates an element with a placeholder. | |
RendererPlaceholdersTest:: |
protected | function | Generates an element with placeholders at 3 levels. | |
RendererPlaceholdersTest:: |
public | function | Provides the two classes of placeholders: cacheable and uncacheable. | |
RendererPlaceholdersTest:: |
protected | function |
Overrides RendererTestBase:: |
|
RendererPlaceholdersTest:: |
protected | function | ||
RendererPlaceholdersTest:: |
public | function | @covers ::render @covers ::doRender @covers \Drupal\Core\Render\RenderCache::get @covers \Drupal\Core\Render\RenderCache::set @covers \Drupal\Core\Render\RenderCache::createCacheID | |
RendererPlaceholdersTest:: |
public | function | @covers ::render @covers ::doRender @covers \Drupal\Core\Render\RenderCache::get @covers ::replacePlaceholders | |
RendererPlaceholdersTest:: |
public | function | @covers ::render @covers ::doRender | |
RendererPlaceholdersTest:: |
public | function | @covers ::render @covers ::doRender | |
RendererPlaceholdersTest:: |
public | function | @covers ::render @covers ::doRender | |
RendererPlaceholdersTest:: |
public | function | @covers ::render @covers ::doRender | |
RendererPlaceholdersTest:: |
public | function | @covers ::render @covers ::doRender | |
RendererPlaceholdersTest:: |
public | function | @covers ::render @covers ::doRender @covers \Drupal\Core\Render\RenderCache::get @covers \Drupal\Core\Render\PlaceholderingRenderCache::get @covers \Drupal\Core\Render\PlaceholderingRenderCache::set @covers ::replacePlaceholders | |
RendererPlaceholdersTest:: |
public | function | @covers ::render @covers ::doRender | |
RendererPlaceholdersTest:: |
public | function | Tests a placeholder that adds another placeholder. | |
RendererPlaceholdersTest:: |
public | function | Create an element with a child and subchild. Each element has the same #lazy_builder callback, but with different contexts. They don't modify markup, only attach additional drupalSettings. | |
RendererPlaceholdersTest:: |
public | function | @covers ::render @covers ::doRender | |
RendererPlaceholdersTest:: |
public | function | @covers ::render @covers ::doRender | |
RendererTestBase:: |
protected | property | ||
RendererTestBase:: |
protected | property | ||
RendererTestBase:: |
protected | property | The mocked controller resolver. | |
RendererTestBase:: |
protected | property | The simulated "current" user role, for use in tests with cache contexts. | |
RendererTestBase:: |
protected | property | The mocked element info. | |
RendererTestBase:: |
protected | property | ||
RendererTestBase:: |
protected | property | The tested placeholder generator. | 1 |
RendererTestBase:: |
protected | property | The tested render cache. | |
RendererTestBase:: |
protected | property | The tested renderer. | |
RendererTestBase:: |
protected | property | The mocked renderer configuration. | |
RendererTestBase:: |
protected | property | ||
RendererTestBase:: |
protected | property | The mocked theme manager. | |
RendererTestBase:: |
protected | function | Asserts a render cache item. | |
RendererTestBase:: |
protected | function | Generates a random context value for the placeholder tests. | |
RendererTestBase:: |
protected | function | Sets up a memory-based render cache back-end. | |
RendererTestBase:: |
protected | function | Sets up a request object on the request stack. | |
RendererTestBase:: |
protected | function | Sets up a render cache back-end that is asserted to be never used. | |
UnitTestCase:: |
protected | property | The random generator. | |
UnitTestCase:: |
protected | property | The app root. | 1 |
UnitTestCase:: |
protected | function | Asserts if two arrays are equal by sorting them first. | |
UnitTestCase:: |
protected | function | Mocks a block with a block plugin. | 1 |
UnitTestCase:: |
protected | function | Returns a stub class resolver. | |
UnitTestCase:: |
public | function | Returns a stub config factory that behaves according to the passed array. | |
UnitTestCase:: |
public | function | Returns a stub config storage that returns the supplied configuration. | |
UnitTestCase:: |
protected | function | Sets up a container with a cache tags invalidator. | |
UnitTestCase:: |
protected | function | Gets the random generator for the utility methods. | |
UnitTestCase:: |
public | function | Returns a stub translation manager that just returns the passed string. | |
UnitTestCase:: |
public | function | Generates a unique random string containing letters and numbers. |