5 * Contains \Drupal\Tests\Core\Render\RendererPlaceholdersTest.
8 namespace Drupal\Tests\Core\Render;
10 use Drupal\Component\Utility\Crypt;
11 use Drupal\Component\Utility\Html;
12 use Drupal\Core\Cache\Cache;
13 use Drupal\Core\Render\Markup;
14 use Drupal\Core\Render\RenderContext;
17 * @coversDefaultClass \Drupal\Core\Render\Renderer
18 * @covers \Drupal\Core\Render\RenderCache
19 * @covers \Drupal\Core\Render\PlaceholderingRenderCache
22 class RendererPlaceholdersTest extends RendererTestBase {
27 protected function setUp() {
28 // Disable the required cache contexts, so that this test can test just the
29 // placeholder replacement behavior.
30 $this->rendererConfig['required_cache_contexts'] = [];
36 * Provides the two classes of placeholders: cacheable and uncacheable.
38 * i.e. with or without #cache[keys].
40 * Also, different types:
41 * - A) automatically generated placeholder
42 * - 1) manually triggered (#create_placeholder = TRUE)
43 * - 2) automatically triggered (based on max-age = 0 at the top level)
44 * - 3) automatically triggered (based on high cardinality cache contexts at
46 * - 4) automatically triggered (based on high-invalidation frequency cache
47 * tags at the top level)
48 * - 5) automatically triggered (based on max-age = 0 in its subtree, i.e.
50 * - 6) automatically triggered (based on high cardinality cache contexts in
51 * its subtree, i.e. via bubbling)
52 * - 7) automatically triggered (based on high-invalidation frequency cache
53 * tags in its subtree, i.e. via bubbling)
54 * - B) manually generated placeholder
56 * So, in total 2*8 = 16 permutations. (On one axis: uncacheable vs.
57 * uncacheable = 2; on the other axis: A1–7 and B = 8.)
59 * @todo Case A5 is not yet supported by core. So that makes for only 14
60 * permutations currently, instead of 16. That will be done in
61 * https://www.drupal.org/node/2559847
65 public function providerPlaceholders() {
66 $args = [$this->randomContextValue()];
68 $generate_placeholder_markup = function ($cache_keys = NULL) use ($args) {
69 $token_render_array = [
70 '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
72 if (is_array($cache_keys)) {
73 $token_render_array['#cache']['keys'] = $cache_keys;
75 $token = Crypt::hashBase64(serialize($token_render_array));
76 // \Drupal\Core\Render\Markup::create() is necessary as the render
77 // system would mangle this markup. As this is exactly what happens at
78 // runtime this is a valid use-case.
79 return Markup::create('<drupal-render-placeholder callback="Drupal\Tests\Core\Render\PlaceholdersTest::callback" arguments="' . '0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>');
82 $extract_placeholder_render_array = function ($placeholder_render_array) {
83 return array_intersect_key($placeholder_render_array, ['#lazy_builder' => TRUE, '#cache' => TRUE]);
86 // Note the presence of '#create_placeholder'.
97 '#create_placeholder' => TRUE,
98 '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
101 // Note the absence of '#create_placeholder', presence of max-age=0 at the
105 'drupalSettings' => [
114 '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
117 // Note the absence of '#create_placeholder', presence of high cardinality
118 // cache context at the top level.
121 'drupalSettings' => [
127 'contexts' => ['user'],
129 '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
132 // Note the absence of '#create_placeholder', presence of high-invalidation
133 // frequency cache tag at the top level.
136 'drupalSettings' => [
143 'tags' => ['current-temperature'],
145 '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
148 // Note the absence of '#create_placeholder', presence of max-age=0 created
149 // by the #lazy_builder callback.
150 // @todo in https://www.drupal.org/node/2559847
151 $base_element_a5 = [];
152 // Note the absence of '#create_placeholder', presence of high cardinality
153 // cache context created by the #lazy_builder callback.
154 // @see \Drupal\Tests\Core\Render\PlaceholdersTest::callbackPerUser()
157 'drupalSettings' => [
165 '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callbackPerUser', $args],
168 // Note the absence of '#create_placeholder', presence of high-invalidation
169 // frequency cache tag created by the #lazy_builder callback.
170 // @see \Drupal\Tests\Core\Render\PlaceholdersTest::callbackTagCurrentTemperature()
173 'drupalSettings' => [
181 '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callbackTagCurrentTemperature', $args],
184 // Note the absence of '#create_placeholder', but the presence of
185 // '#attached[placeholders]'.
187 '#markup' => $generate_placeholder_markup(),
189 'drupalSettings' => [
193 (string) $generate_placeholder_markup() => [
194 '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
200 $keys = ['placeholder', 'output', 'can', 'be', 'render', 'cached', 'too'];
204 // Case one: render array that has a placeholder that is:
205 // - automatically created, but manually triggered (#create_placeholder = TRUE)
207 $element_without_cache_keys = $base_element_a1;
208 $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a1['placeholder']);
210 $element_without_cache_keys,
212 $expected_placeholder_render_array,
219 // Case two: render array that has a placeholder that is:
220 // - automatically created, but manually triggered (#create_placeholder = TRUE)
222 $element_with_cache_keys = $base_element_a1;
223 $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
224 $expected_placeholder_render_array['#cache']['keys'] = $keys;
226 $element_with_cache_keys,
228 $expected_placeholder_render_array,
233 '#markup' => '<p>This is a rendered placeholder!</p>',
235 'drupalSettings' => [
236 'dynamic_animal' => $args[0],
242 'max-age' => Cache::PERMANENT,
247 // Case three: render array that has a placeholder that is:
248 // - automatically created, and automatically triggered due to max-age=0
250 $element_without_cache_keys = $base_element_a2;
251 $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a2['placeholder']);
253 $element_without_cache_keys,
255 $expected_placeholder_render_array,
262 // Case four: render array that has a placeholder that is:
263 // - automatically created, but automatically triggered due to max-age=0
265 $element_with_cache_keys = $base_element_a2;
266 $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
267 $expected_placeholder_render_array['#cache']['keys'] = $keys;
269 $element_with_cache_keys,
271 $expected_placeholder_render_array,
278 // Case five: render array that has a placeholder that is:
279 // - automatically created, and automatically triggered due to high
280 // cardinality cache contexts
282 $element_without_cache_keys = $base_element_a3;
283 $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a3['placeholder']);
285 $element_without_cache_keys,
287 $expected_placeholder_render_array,
294 // Case six: render array that has a placeholder that is:
295 // - automatically created, and automatically triggered due to high
296 // cardinality cache contexts
298 $element_with_cache_keys = $base_element_a3;
299 $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
300 $expected_placeholder_render_array['#cache']['keys'] = $keys;
301 // The CID parts here consist of the cache keys plus the 'user' cache
302 // context, which in this unit test is simply the given cache context token,
303 // see \Drupal\Tests\Core\Render\RendererTestBase::setUp().
304 $cid_parts = array_merge($keys, ['user']);
306 $element_with_cache_keys,
308 $expected_placeholder_render_array,
313 '#markup' => '<p>This is a rendered placeholder!</p>',
315 'drupalSettings' => [
316 'dynamic_animal' => $args[0],
320 'contexts' => ['user'],
322 'max-age' => Cache::PERMANENT,
327 // Case seven: render array that has a placeholder that is:
328 // - automatically created, and automatically triggered due to high
329 // invalidation frequency cache tags
331 $element_without_cache_keys = $base_element_a4;
332 $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a4['placeholder']);
334 $element_without_cache_keys,
336 $expected_placeholder_render_array,
343 // Case eight: render array that has a placeholder that is:
344 // - automatically created, and automatically triggered due to high
345 // invalidation frequency cache tags
347 $element_with_cache_keys = $base_element_a4;
348 $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
349 $expected_placeholder_render_array['#cache']['keys'] = $keys;
351 $element_with_cache_keys,
353 $expected_placeholder_render_array,
358 '#markup' => '<p>This is a rendered placeholder!</p>',
360 'drupalSettings' => [
361 'dynamic_animal' => $args[0],
366 'tags' => ['current-temperature'],
367 'max-age' => Cache::PERMANENT,
372 // Case nine: render array that DOES NOT have a placeholder that is:
373 // - NOT created, despite max-age=0 that is bubbled
375 // (because the render element with #lazy_builder does not have #cache[keys]
376 // and hence the max-age=0 bubbles up further)
377 // @todo in https://www.drupal.org/node/2559847
379 // Case ten: render array that has a placeholder that is:
380 // - automatically created, and automatically triggered due to max-age=0
383 // @todo in https://www.drupal.org/node/2559847
385 // Case eleven: render array that DOES NOT have a placeholder that is:
386 // - NOT created, despite high cardinality cache contexts that are bubbled
388 $element_without_cache_keys = $base_element_a6;
389 $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a6['placeholder']);
391 $element_without_cache_keys,
393 $expected_placeholder_render_array,
400 // Case twelve: render array that has a placeholder that is:
401 // - automatically created, and automatically triggered due to high
402 // cardinality cache contexts that are bubbled
404 $element_with_cache_keys = $base_element_a6;
405 $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
406 $expected_placeholder_render_array['#cache']['keys'] = $keys;
408 $element_with_cache_keys,
410 $expected_placeholder_render_array,
415 '#markup' => '<p>This is a rendered placeholder!</p>',
417 'drupalSettings' => [
418 'dynamic_animal' => $args[0],
422 'contexts' => ['user'],
424 'max-age' => Cache::PERMANENT,
429 // Case thirteen: render array that has a placeholder that is:
430 // - automatically created, and automatically triggered due to high
431 // invalidation frequency cache tags that are bubbled
433 $element_without_cache_keys = $base_element_a7;
434 $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a7['placeholder']);
436 $element_without_cache_keys,
438 $expected_placeholder_render_array,
441 ['current-temperature'],
445 // Case fourteen: render array that has a placeholder that is:
446 // - automatically created, and automatically triggered due to high
447 // invalidation frequency cache tags that are bubbled
449 $element_with_cache_keys = $base_element_a7;
450 $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
451 $expected_placeholder_render_array['#cache']['keys'] = $keys;
453 $element_with_cache_keys,
455 $expected_placeholder_render_array,
460 '#markup' => '<p>This is a rendered placeholder!</p>',
462 'drupalSettings' => [
463 'dynamic_animal' => $args[0],
468 'tags' => ['current-temperature'],
469 'max-age' => Cache::PERMANENT,
474 // Case fifteen: render array that has a placeholder that is:
475 // - manually created
477 $x = $base_element_b;
478 $expected_placeholder_render_array = $x['#attached']['placeholders'][(string) $generate_placeholder_markup()];
479 unset($x['#attached']['placeholders'][(string) $generate_placeholder_markup()]['#cache']);
483 $expected_placeholder_render_array,
490 // Case sixteen: render array that has a placeholder that is:
491 // - manually created
493 $x = $base_element_b;
494 $x['#markup'] = $placeholder_markup = $generate_placeholder_markup($keys);
495 $placeholder_markup = (string) $placeholder_markup;
496 $x['#attached']['placeholders'] = [
497 $placeholder_markup => [
498 '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
499 '#cache' => ['keys' => $keys],
502 $expected_placeholder_render_array = $x['#attached']['placeholders'][$placeholder_markup];
506 $expected_placeholder_render_array,
511 '#markup' => '<p>This is a rendered placeholder!</p>',
513 'drupalSettings' => [
514 'dynamic_animal' => $args[0],
520 'max-age' => Cache::PERMANENT,
529 * Generates an element with a placeholder.
532 * An array containing:
533 * - A render array containing a placeholder.
534 * - The context used for that #lazy_builder callback.
536 protected function generatePlaceholderElement() {
537 $args = [$this->randomContextValue()];
539 $test_element['#attached']['drupalSettings']['foo'] = 'bar';
540 $test_element['placeholder']['#cache']['keys'] = ['placeholder', 'output', 'can', 'be', 'render', 'cached', 'too'];
541 $test_element['placeholder']['#cache']['contexts'] = [];
542 $test_element['placeholder']['#create_placeholder'] = TRUE;
543 $test_element['placeholder']['#lazy_builder'] = ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args];
545 return [$test_element, $args];
549 * @param false|array $cid_parts
550 * @param string[] $bubbled_cache_contexts
551 * Additional cache contexts that were bubbled when the placeholder was
553 * @param array $expected_data
554 * A render array with the expected values.
556 protected function assertPlaceholderRenderCache($cid_parts, array $bubbled_cache_contexts, array $expected_data) {
557 if ($cid_parts !== FALSE) {
558 if ($bubbled_cache_contexts) {
559 // Verify render cached placeholder.
560 $cached_element = $this->memoryCache->get(implode(':', $cid_parts))->data;
561 $expected_redirect_element = [
562 '#cache_redirect' => TRUE,
563 '#cache' => $expected_data['#cache'] + [
564 'keys' => $cid_parts,
568 $this->assertEquals($expected_redirect_element, $cached_element, 'The correct cache redirect exists.');
571 // Verify render cached placeholder.
572 $cached = $this->memoryCache->get(implode(':', array_merge($cid_parts, $bubbled_cache_contexts)));
573 $cached_element = $cached->data;
574 $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.');
581 * @dataProvider providerPlaceholders
583 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) {
584 if ($placeholder_cid_parts) {
585 $this->setupMemoryCache();
588 $this->setUpUnusedCache();
591 $this->setUpRequest('GET');
593 // No #cache on parent element.
594 $element['#prefix'] = '<p>#cache disabled</p>';
595 $output = $this->renderer->renderRoot($element);
596 $this->assertSame('<p>#cache disabled</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
597 $this->assertSame('<p>#cache disabled</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
598 $expected_js_settings = [
600 'dynamic_animal' => $args[0],
602 $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.');
603 $this->assertPlaceholderRenderCache($placeholder_cid_parts, $bubbled_cache_contexts, $placeholder_expected_render_cache_array);
609 * @covers \Drupal\Core\Render\RenderCache::get
610 * @covers \Drupal\Core\Render\RenderCache::set
611 * @covers \Drupal\Core\Render\RenderCache::createCacheID
613 * @dataProvider providerPlaceholders
615 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) {
616 $element = $test_element;
617 $this->setupMemoryCache();
619 $this->setUpRequest('GET');
621 $token = Crypt::hashBase64(serialize($expected_placeholder_render_array));
622 $placeholder_callback = $expected_placeholder_render_array['#lazy_builder'][0];
623 $expected_placeholder_markup = '<drupal-render-placeholder callback="' . $placeholder_callback . '" arguments="0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>';
624 $this->assertSame($expected_placeholder_markup, Html::normalize($expected_placeholder_markup), 'Placeholder unaltered by Html::normalize() which is used by FilterHtmlCorrector.');
626 // GET request: #cache enabled, cache miss.
627 $element['#cache'] = ['keys' => ['placeholder_test_GET']];
628 $element['#prefix'] = '<p>#cache enabled, GET</p>';
629 $output = $this->renderer->renderRoot($element);
630 $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
631 $this->assertTrue(isset($element['#printed']), 'No cache hit');
632 $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
633 $expected_js_settings = [
635 'dynamic_animal' => $args[0],
637 $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.');
638 $this->assertPlaceholderRenderCache($placeholder_cid_parts, $bubbled_cache_contexts, $placeholder_expected_render_cache_array);
640 // GET request: validate cached data.
641 $cached = $this->memoryCache->get('placeholder_test_GET');
642 // There are three edge cases, where the shape of the render cache item for
643 // the parent (with CID 'placeholder_test_GET') is vastly different. These
644 // are the cases where:
645 // - the placeholder is uncacheable (because it has no #cache[keys]), and;
646 // - cacheability metadata that meets auto_placeholder_conditions is bubbled
647 $has_uncacheable_lazy_builder = !isset($test_element['placeholder']['#cache']['keys']) && isset($test_element['placeholder']['#lazy_builder']);
648 // Edge cases: always where both bubbling of an auto-placeholdering
649 // condition happens from within a #lazy_builder that is uncacheable.
650 // - uncacheable + A5 (cache max-age)
651 // @todo in https://www.drupal.org/node/2559847
652 // - uncacheable + A6 (cache context)
653 $edge_case_a6_uncacheable = $has_uncacheable_lazy_builder && $test_element['placeholder']['#lazy_builder'][0] === 'Drupal\Tests\Core\Render\PlaceholdersTest::callbackPerUser';
654 // - uncacheable + A7 (cache tag)
655 $edge_case_a7_uncacheable = $has_uncacheable_lazy_builder && $test_element['placeholder']['#lazy_builder'][0] === 'Drupal\Tests\Core\Render\PlaceholdersTest::callbackTagCurrentTemperature';
656 // The redirect-cacheable edge case: a high-cardinality cache context is
657 // bubbled from a #lazy_builder callback for an uncacheable placeholder. The
658 // element containing the uncacheable placeholder has cache keys set, and
659 // due to the bubbled cache contexts it creates a cache redirect.
660 if ($edge_case_a6_uncacheable) {
661 $cached_element = $cached->data;
662 $expected_redirect = [
663 '#cache_redirect' => TRUE,
665 'keys' => ['placeholder_test_GET'],
666 'contexts' => ['user'],
668 'max-age' => Cache::PERMANENT,
672 $this->assertEquals($expected_redirect, $cached_element);
673 // Follow the redirect.
674 $cached_element = $this->memoryCache->get('placeholder_test_GET:' . implode(':', $bubbled_cache_contexts))->data;
675 $expected_element = [
676 '#markup' => '<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>',
678 'drupalSettings' => [
680 'dynamic_animal' => $args[0],
684 'contexts' => $bubbled_cache_contexts,
686 'max-age' => Cache::PERMANENT,
689 $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.');
691 // The normally cacheable edge case: a high-invalidation frequency cache tag
692 // is bubbled from a #lazy_builder callback for an uncacheable placeholder.
693 // The element containing the uncacheable placeholder has cache keys set,
694 // and also has the bubbled cache tags.
695 elseif ($edge_case_a7_uncacheable) {
696 $cached_element = $cached->data;
697 $expected_element = [
698 '#markup' => '<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>',
700 'drupalSettings' => [
702 'dynamic_animal' => $args[0],
707 'tags' => $bubbled_cache_tags,
708 'max-age' => Cache::PERMANENT,
711 $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.');
715 $cached_element = $cached->data;
716 $expected_element = [
717 '#markup' => '<p>#cache enabled, GET</p>' . $expected_placeholder_markup,
719 'drupalSettings' => [
723 $expected_placeholder_markup => [
724 '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
730 'tags' => $bubbled_cache_tags,
731 'max-age' => Cache::PERMANENT,
734 $expected_element['#attached']['placeholders'][$expected_placeholder_markup] = $expected_placeholder_render_array;
735 $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.');
738 // GET request: #cache enabled, cache hit.
739 $element = $test_element;
740 $element['#cache'] = ['keys' => ['placeholder_test_GET']];
741 $element['#prefix'] = '<p>#cache enabled, GET</p>';
742 $output = $this->renderer->renderRoot($element);
743 $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
744 $this->assertFalse(isset($element['#printed']), 'Cache hit');
745 $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
746 $expected_js_settings = [
748 'dynamic_animal' => $args[0],
750 $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.');
756 * @covers \Drupal\Core\Render\RenderCache::get
757 * @covers ::replacePlaceholders
759 * @dataProvider providerPlaceholders
761 public function testCacheableParentWithPostRequest($test_element, $args) {
762 $this->setUpUnusedCache();
764 // Verify behavior when handling a non-GET request, e.g. a POST request:
765 // also in that case, placeholders must be replaced.
766 $this->setUpRequest('POST');
768 // POST request: #cache enabled, cache miss.
769 $element = $test_element;
770 $element['#cache'] = ['keys' => ['placeholder_test_POST']];
771 $element['#prefix'] = '<p>#cache enabled, POST</p>';
772 $output = $this->renderer->renderRoot($element);
773 $this->assertSame('<p>#cache enabled, POST</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
774 $this->assertTrue(isset($element['#printed']), 'No cache hit');
775 $this->assertSame('<p>#cache enabled, POST</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
776 $expected_js_settings = [
778 'dynamic_animal' => $args[0],
780 $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.');
782 // Even when the child element's placeholder is cacheable, it should not
783 // generate a render cache item.
784 $this->assertPlaceholderRenderCache(FALSE, [], []);
790 * @covers \Drupal\Core\Render\RenderCache::get
791 * @covers \Drupal\Core\Render\PlaceholderingRenderCache::get
792 * @covers \Drupal\Core\Render\PlaceholderingRenderCache::set
793 * @covers ::replacePlaceholders
795 * @dataProvider providerPlaceholders
797 public function testPlaceholderingDisabledForPostRequests($test_element, $args) {
798 $this->setUpUnusedCache();
799 $this->setUpRequest('POST');
801 $element = $test_element;
803 // Render without replacing placeholders, to allow this test to see which
804 // #attached[placeholders] there are, if any.
805 $this->renderer->executeInRenderContext(new RenderContext(), function () use (&$element) {
806 return $this->renderer->render($element);
808 // Only test cases where the placeholders have been specified manually are
809 // allowed to have placeholders. This means that of the different situations
810 // listed in providerPlaceholders(), only type B can have attached
811 // placeholders. Everything else, whether:
812 // 1. manual placeholdering
813 // 2. automatic placeholdering via already-present cacheability metadata
814 // 3. automatic placeholdering via bubbled cacheability metadata
815 // All three of those should NOT result in placeholders.
816 if (!isset($test_element['#attached']['placeholders'])) {
817 $this->assertFalse(isset($element['#attached']['placeholders']), 'No placeholders created.');
822 * Tests a placeholder that adds another placeholder.
824 * E.g. when rendering a node in a placeholder the rendering of that node
825 * needs a placeholder of its own to be executed (to render the node links).
829 * @covers ::replacePlaceholders
831 public function testRecursivePlaceholder() {
832 $args = [$this->randomContextValue()];
834 $element['#create_placeholder'] = TRUE;
835 $element['#lazy_builder'] = ['Drupal\Tests\Core\Render\RecursivePlaceholdersTest::callback', $args];
837 $output = $this->renderer->renderRoot($element);
838 $this->assertEquals('<p>This is a rendered placeholder!</p>', $output, 'The output has been modified by the indirect, recursive placeholder #lazy_builder callback.');
839 $this->assertSame((string) $element['#markup'], '<p>This is a rendered placeholder!</p>', '#markup is overridden by the indirect, recursive placeholder #lazy_builder callback.');
840 $expected_js_settings = [
841 'dynamic_animal' => $args[0],
843 $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified by the indirect, recursive placeholder #lazy_builder callback.');
850 public function testInvalidLazyBuilder() {
852 $element['#lazy_builder'] = '\Drupal\Tests\Core\Render\PlaceholdersTest::callback';
854 $this->setExpectedException(\DomainException::class, 'The #lazy_builder property must have an array as a value.');
855 $this->renderer->renderRoot($element);
862 public function testInvalidLazyBuilderArguments() {
864 $element['#lazy_builder'] = ['\Drupal\Tests\Core\Render\PlaceholdersTest::callback', 'arg1', 'arg2'];
866 $this->setExpectedException(\DomainException::class, 'The #lazy_builder property must have an array as a value, containing two values: the callback, and the arguments for the callback.');
867 $this->renderer->renderRoot($element);
874 * @see testNonScalarLazybuilderCallbackContext
876 public function testScalarLazybuilderCallbackContext() {
878 $element['#lazy_builder'] = [
879 '\Drupal\Tests\Core\Render\PlaceholdersTest::callback',
889 $result = $this->renderer->renderRoot($element);
890 $this->assertInstanceOf('\Drupal\Core\Render\Markup', $result);
891 $this->assertEquals('<p>This is a rendered placeholder!</p>', (string) $result);
898 public function testNonScalarLazybuilderCallbackContext() {
900 $element['#lazy_builder'] = [
901 '\Drupal\Tests\Core\Render\PlaceholdersTest::callback',
908 // array is not one of the scalar types.
913 $this->setExpectedException(\DomainException::class, "A #lazy_builder callback's context may only contain scalar values or NULL.");
914 $this->renderer->renderRoot($element);
921 public function testChildrenPlusBuilder() {
923 $element['#lazy_builder'] = ['Drupal\Tests\Core\Render\RecursivePlaceholdersTest::callback', []];
924 $element['child_a']['#markup'] = 'Oh hai!';
925 $element['child_b']['#markup'] = 'kthxbai';
927 $this->setExpectedException(\DomainException::class, '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.');
928 $this->renderer->renderRoot($element);
935 public function testPropertiesPlusBuilder() {
937 $element['#lazy_builder'] = ['Drupal\Tests\Core\Render\RecursivePlaceholdersTest::callback', []];
938 $element['#llama'] = '#awesome';
939 $element['#piglet'] = '#cute';
941 $this->setExpectedException(\DomainException::class, '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.');
942 $this->renderer->renderRoot($element);
949 public function testCreatePlaceholderPropertyWithoutLazyBuilder() {
951 $element['#create_placeholder'] = TRUE;
953 $this->setExpectedException(\LogicException::class, 'When #create_placeholder is set, a #lazy_builder callback must be present as well.');
954 $this->renderer->renderRoot($element);
958 * Create an element with a child and subchild. Each element has the same
959 * #lazy_builder callback, but with different contexts. They don't modify
960 * markup, only attach additional drupalSettings.
964 * @covers \Drupal\Core\Render\RenderCache::get
965 * @covers ::replacePlaceholders
967 public function testRenderChildrenPlaceholdersDifferentArguments() {
968 $this->setUpRequest();
969 $this->setupMemoryCache();
970 $this->cacheContextsManager->expects($this->any())
971 ->method('convertTokensToKeys')
972 ->willReturnArgument(0);
973 $this->controllerResolver->expects($this->any())
974 ->method('getControllerFromDefinition')
975 ->willReturnArgument(0);
976 $this->setupThemeManagerForDetails();
978 $args_1 = ['foo', TRUE];
979 $args_2 = ['bar', TRUE];
980 $args_3 = ['baz', TRUE];
981 $test_element = $this->generatePlaceholdersWithChildrenTestElement($args_1, $args_2, $args_3);
983 $element = $test_element;
984 $output = $this->renderer->renderRoot($element);
985 $expected_output = <<<HTML
987 <summary>Parent</summary>
988 <div class="details-wrapper"><details>
989 <summary>Child</summary>
990 <div class="details-wrapper">Subchild</div>
994 $this->assertSame($expected_output, (string) $output, 'Output is not overridden.');
995 $this->assertTrue(isset($element['#printed']), 'No cache hit');
996 $this->assertSame($expected_output, (string) $element['#markup'], '#markup is not overridden.');
997 $expected_js_settings = [
999 'dynamic_animal' => [$args_1[0] => TRUE, $args_2[0] => TRUE, $args_3[0] => TRUE],
1001 $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.');
1003 // GET request: validate cached data.
1004 $cached_element = $this->memoryCache->get('simpletest:renderer:children_placeholders')->data;
1005 $expected_element = [
1007 'drupalSettings' => [
1011 'parent-x-parent' => [
1012 '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_1],
1014 'child-x-child' => [
1015 '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_2],
1017 'subchild-x-subchild' => [
1018 '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_3],
1025 'max-age' => Cache::PERMANENT,
1029 $dom = Html::load($cached_element['#markup']);
1030 $xpath = new \DOMXPath($dom);
1031 $parent = $xpath->query('//details/summary[text()="Parent"]')->length;
1032 $child = $xpath->query('//details/div[@class="details-wrapper"]/details/summary[text()="Child"]')->length;
1033 $subchild = $xpath->query('//details/div[@class="details-wrapper"]/details/div[@class="details-wrapper" and text()="Subchild"]')->length;
1034 $this->assertTrue($parent && $child && $subchild, 'The correct data is cached: the stored #markup is not affected by placeholder #lazy_builder callbacks.');
1036 // Remove markup because it's compared above in the xpath.
1037 unset($cached_element['#markup']);
1038 $this->assertEquals($cached_element, $expected_element, 'The correct data is cached: the stored #attached properties are not affected by placeholder #lazy_builder callbacks.');
1040 // GET request: #cache enabled, cache hit.
1041 $element = $test_element;
1042 $output = $this->renderer->renderRoot($element);
1043 $this->assertSame($expected_output, (string) $output, 'Output is not overridden.');
1044 $this->assertFalse(isset($element['#printed']), 'Cache hit');
1045 $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.');
1047 // Use the exact same element, but now unset #cache; ensure we get the same
1049 unset($test_element['#cache']);
1050 $element = $test_element;
1051 $output = $this->renderer->renderRoot($element);
1052 $this->assertSame($expected_output, (string) $output, 'Output is not overridden.');
1053 $this->assertSame($expected_output, (string) $element['#markup'], '#markup is not overridden.');
1054 $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.');
1058 * Generates an element with placeholders at 3 levels.
1060 * @param array $args_1
1061 * The arguments for the placeholder at level 1.
1062 * @param array $args_2
1063 * The arguments for the placeholder at level 2.
1064 * @param array $args_3
1065 * The arguments for the placeholder at level 3.
1068 * The generated render array for testing.
1070 protected function generatePlaceholdersWithChildrenTestElement(array $args_1, array $args_2, array $args_3) {
1072 '#type' => 'details',
1074 'keys' => ['simpletest', 'renderer', 'children_placeholders'],
1076 '#title' => 'Parent',
1078 'drupalSettings' => [
1082 'parent-x-parent' => [
1083 '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_1],
1088 $test_element['child'] = [
1089 '#type' => 'details',
1092 'child-x-child' => [
1093 '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_2],
1097 '#title' => 'Child',
1099 $test_element['child']['subchild'] = [
1102 'subchild-x-subchild' => [
1103 '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_3],
1107 '#markup' => 'Subchild',
1109 return $test_element;
1113 * @return \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_Builder_InvocationMocker
1115 protected function setupThemeManagerForDetails() {
1116 return $this->themeManager->expects($this->any())
1118 ->willReturnCallback(function ($theme, array $vars) {
1121 <summary>{{ title }}</summary>
1122 <div class="details-wrapper">{{ children }}</div>
1125 $output = str_replace([
1128 ], [$vars['#title'], $vars['#children']], $output);
1136 * @see \Drupal\Tests\Core\Render\RendererPlaceholdersTest::testRecursivePlaceholder()
1138 class RecursivePlaceholdersTest {
1141 * #lazy_builder callback; bubbles another placeholder.
1143 * @param string $animal
1147 * A renderable array.
1149 public static function callback($animal) {
1152 '#create_placeholder' => TRUE,
1153 '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', [$animal]],