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 array $expected_data
551 * FALSE if no render cache item is expected, a render array with the
552 * expected values if a render cache item is expected.
553 * @param string[] $bubbled_cache_contexts
554 * Additional cache contexts that were bubbled when the placeholder was
557 protected function assertPlaceholderRenderCache($cid_parts, array $bubbled_cache_contexts, array $expected_data) {
558 if ($cid_parts !== FALSE) {
559 if ($bubbled_cache_contexts) {
560 // Verify render cached placeholder.
561 $cached_element = $this->memoryCache->get(implode(':', $cid_parts))->data;
562 $expected_redirect_element = [
563 '#cache_redirect' => TRUE,
564 '#cache' => $expected_data['#cache'] + [
565 'keys' => $cid_parts,
569 $this->assertEquals($expected_redirect_element, $cached_element, 'The correct cache redirect exists.');
572 // Verify render cached placeholder.
573 $cached = $this->memoryCache->get(implode(':', array_merge($cid_parts, $bubbled_cache_contexts)));
574 $cached_element = $cached->data;
575 $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.');
582 * @dataProvider providerPlaceholders
584 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) {
585 if ($placeholder_cid_parts) {
586 $this->setupMemoryCache();
589 $this->setUpUnusedCache();
592 $this->setUpRequest('GET');
594 // No #cache on parent element.
595 $element['#prefix'] = '<p>#cache disabled</p>';
596 $output = $this->renderer->renderRoot($element);
597 $this->assertSame('<p>#cache disabled</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
598 $this->assertSame('<p>#cache disabled</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
599 $expected_js_settings = [
601 'dynamic_animal' => $args[0],
603 $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.');
604 $this->assertPlaceholderRenderCache($placeholder_cid_parts, $bubbled_cache_contexts, $placeholder_expected_render_cache_array);
610 * @covers \Drupal\Core\Render\RenderCache::get
611 * @covers \Drupal\Core\Render\RenderCache::set
612 * @covers \Drupal\Core\Render\RenderCache::createCacheID
614 * @dataProvider providerPlaceholders
616 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) {
617 $element = $test_element;
618 $this->setupMemoryCache();
620 $this->setUpRequest('GET');
622 $token = Crypt::hashBase64(serialize($expected_placeholder_render_array));
623 $placeholder_callback = $expected_placeholder_render_array['#lazy_builder'][0];
624 $expected_placeholder_markup = '<drupal-render-placeholder callback="' . $placeholder_callback . '" arguments="0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>';
625 $this->assertSame($expected_placeholder_markup, Html::normalize($expected_placeholder_markup), 'Placeholder unaltered by Html::normalize() which is used by FilterHtmlCorrector.');
627 // GET request: #cache enabled, cache miss.
628 $element['#cache'] = ['keys' => ['placeholder_test_GET']];
629 $element['#prefix'] = '<p>#cache enabled, GET</p>';
630 $output = $this->renderer->renderRoot($element);
631 $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
632 $this->assertTrue(isset($element['#printed']), 'No cache hit');
633 $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
634 $expected_js_settings = [
636 'dynamic_animal' => $args[0],
638 $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.');
639 $this->assertPlaceholderRenderCache($placeholder_cid_parts, $bubbled_cache_contexts, $placeholder_expected_render_cache_array);
641 // GET request: validate cached data.
642 $cached = $this->memoryCache->get('placeholder_test_GET');
643 // There are three edge cases, where the shape of the render cache item for
644 // the parent (with CID 'placeholder_test_GET') is vastly different. These
645 // are the cases where:
646 // - the placeholder is uncacheable (because it has no #cache[keys]), and;
647 // - cacheability metadata that meets auto_placeholder_conditions is bubbled
648 $has_uncacheable_lazy_builder = !isset($test_element['placeholder']['#cache']['keys']) && isset($test_element['placeholder']['#lazy_builder']);
649 // Edge cases: always where both bubbling of an auto-placeholdering
650 // condition happens from within a #lazy_builder that is uncacheable.
651 // - uncacheable + A5 (cache max-age)
652 // @todo in https://www.drupal.org/node/2559847
653 // - uncacheable + A6 (cache context)
654 $edge_case_a6_uncacheable = $has_uncacheable_lazy_builder && $test_element['placeholder']['#lazy_builder'][0] === 'Drupal\Tests\Core\Render\PlaceholdersTest::callbackPerUser';
655 // - uncacheable + A7 (cache tag)
656 $edge_case_a7_uncacheable = $has_uncacheable_lazy_builder && $test_element['placeholder']['#lazy_builder'][0] === 'Drupal\Tests\Core\Render\PlaceholdersTest::callbackTagCurrentTemperature';
657 // The redirect-cacheable edge case: a high-cardinality cache context is
658 // bubbled from a #lazy_builder callback for an uncacheable placeholder. The
659 // element containing the uncacheable placeholder has cache keys set, and
660 // due to the bubbled cache contexts it creates a cache redirect.
661 if ($edge_case_a6_uncacheable) {
662 $cached_element = $cached->data;
663 $expected_redirect = [
664 '#cache_redirect' => TRUE,
666 'keys' => ['placeholder_test_GET'],
667 'contexts' => ['user'],
669 'max-age' => Cache::PERMANENT,
673 $this->assertEquals($expected_redirect, $cached_element);
674 // Follow the redirect.
675 $cached_element = $this->memoryCache->get('placeholder_test_GET:' . implode(':', $bubbled_cache_contexts))->data;
676 $expected_element = [
677 '#markup' => '<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>',
679 'drupalSettings' => [
681 'dynamic_animal' => $args[0],
685 'contexts' => $bubbled_cache_contexts,
687 'max-age' => Cache::PERMANENT,
690 $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.');
692 // The normally cacheable edge case: a high-invalidation frequency cache tag
693 // is bubbled from a #lazy_builder callback for an uncacheable placeholder.
694 // The element containing the uncacheable placeholder has cache keys set,
695 // and also has the bubbled cache tags.
696 elseif ($edge_case_a7_uncacheable) {
697 $cached_element = $cached->data;
698 $expected_element = [
699 '#markup' => '<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>',
701 'drupalSettings' => [
703 'dynamic_animal' => $args[0],
708 'tags' => $bubbled_cache_tags,
709 'max-age' => Cache::PERMANENT,
712 $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.');
716 $cached_element = $cached->data;
717 $expected_element = [
718 '#markup' => '<p>#cache enabled, GET</p>' . $expected_placeholder_markup,
720 'drupalSettings' => [
724 $expected_placeholder_markup => [
725 '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
731 'tags' => $bubbled_cache_tags,
732 'max-age' => Cache::PERMANENT,
735 $expected_element['#attached']['placeholders'][$expected_placeholder_markup] = $expected_placeholder_render_array;
736 $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.');
739 // GET request: #cache enabled, cache hit.
740 $element = $test_element;
741 $element['#cache'] = ['keys' => ['placeholder_test_GET']];
742 $element['#prefix'] = '<p>#cache enabled, GET</p>';
743 $output = $this->renderer->renderRoot($element);
744 $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
745 $this->assertFalse(isset($element['#printed']), 'Cache hit');
746 $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
747 $expected_js_settings = [
749 'dynamic_animal' => $args[0],
751 $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.');
757 * @covers \Drupal\Core\Render\RenderCache::get
758 * @covers ::replacePlaceholders
760 * @dataProvider providerPlaceholders
762 public function testCacheableParentWithPostRequest($test_element, $args) {
763 $this->setUpUnusedCache();
765 // Verify behavior when handling a non-GET request, e.g. a POST request:
766 // also in that case, placeholders must be replaced.
767 $this->setUpRequest('POST');
769 // POST request: #cache enabled, cache miss.
770 $element = $test_element;
771 $element['#cache'] = ['keys' => ['placeholder_test_POST']];
772 $element['#prefix'] = '<p>#cache enabled, POST</p>';
773 $output = $this->renderer->renderRoot($element);
774 $this->assertSame('<p>#cache enabled, POST</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
775 $this->assertTrue(isset($element['#printed']), 'No cache hit');
776 $this->assertSame('<p>#cache enabled, POST</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
777 $expected_js_settings = [
779 'dynamic_animal' => $args[0],
781 $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.');
783 // Even when the child element's placeholder is cacheable, it should not
784 // generate a render cache item.
785 $this->assertPlaceholderRenderCache(FALSE, [], []);
791 * @covers \Drupal\Core\Render\RenderCache::get
792 * @covers \Drupal\Core\Render\PlaceholderingRenderCache::get
793 * @covers \Drupal\Core\Render\PlaceholderingRenderCache::set
794 * @covers ::replacePlaceholders
796 * @dataProvider providerPlaceholders
798 public function testPlaceholderingDisabledForPostRequests($test_element, $args) {
799 $this->setUpUnusedCache();
800 $this->setUpRequest('POST');
802 $element = $test_element;
804 // Render without replacing placeholders, to allow this test to see which
805 // #attached[placeholders] there are, if any.
806 $this->renderer->executeInRenderContext(new RenderContext(), function () use (&$element) {
807 return $this->renderer->render($element);
809 // Only test cases where the placeholders have been specified manually are
810 // allowed to have placeholders. This means that of the different situations
811 // listed in providerPlaceholders(), only type B can have attached
812 // placeholders. Everything else, whether:
813 // 1. manual placeholdering
814 // 2. automatic placeholdering via already-present cacheability metadata
815 // 3. automatic placeholdering via bubbled cacheability metadata
816 // All three of those should NOT result in placeholders.
817 if (!isset($test_element['#attached']['placeholders'])) {
818 $this->assertFalse(isset($element['#attached']['placeholders']), 'No placeholders created.');
823 * Tests a placeholder that adds another placeholder.
825 * E.g. when rendering a node in a placeholder the rendering of that node
826 * needs a placeholder of its own to be executed (to render the node links).
830 * @covers ::replacePlaceholders
832 public function testRecursivePlaceholder() {
833 $args = [$this->randomContextValue()];
835 $element['#create_placeholder'] = TRUE;
836 $element['#lazy_builder'] = ['Drupal\Tests\Core\Render\RecursivePlaceholdersTest::callback', $args];
838 $output = $this->renderer->renderRoot($element);
839 $this->assertEquals('<p>This is a rendered placeholder!</p>', $output, 'The output has been modified by the indirect, recursive placeholder #lazy_builder callback.');
840 $this->assertSame((string) $element['#markup'], '<p>This is a rendered placeholder!</p>', '#markup is overridden by the indirect, recursive placeholder #lazy_builder callback.');
841 $expected_js_settings = [
842 'dynamic_animal' => $args[0],
844 $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified by the indirect, recursive placeholder #lazy_builder callback.');
851 public function testInvalidLazyBuilder() {
853 $element['#lazy_builder'] = '\Drupal\Tests\Core\Render\PlaceholdersTest::callback';
855 $this->setExpectedException(\DomainException::class, 'The #lazy_builder property must have an array as a value.');
856 $this->renderer->renderRoot($element);
863 public function testInvalidLazyBuilderArguments() {
865 $element['#lazy_builder'] = ['\Drupal\Tests\Core\Render\PlaceholdersTest::callback', 'arg1', 'arg2'];
867 $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.');
868 $this->renderer->renderRoot($element);
875 * @see testNonScalarLazybuilderCallbackContext
877 public function testScalarLazybuilderCallbackContext() {
879 $element['#lazy_builder'] = ['\Drupal\Tests\Core\Render\PlaceholdersTest::callback', [
887 $result = $this->renderer->renderRoot($element);
888 $this->assertInstanceOf('\Drupal\Core\Render\Markup', $result);
889 $this->assertEquals('<p>This is a rendered placeholder!</p>', (string) $result);
896 public function testNonScalarLazybuilderCallbackContext() {
898 $element['#lazy_builder'] = ['\Drupal\Tests\Core\Render\PlaceholdersTest::callback', [
904 // array is not one of the scalar types.
908 $this->setExpectedException(\DomainException::class, "A #lazy_builder callback's context may only contain scalar values or NULL.");
909 $this->renderer->renderRoot($element);
916 public function testChildrenPlusBuilder() {
918 $element['#lazy_builder'] = ['Drupal\Tests\Core\Render\RecursivePlaceholdersTest::callback', []];
919 $element['child_a']['#markup'] = 'Oh hai!';
920 $element['child_b']['#markup'] = 'kthxbai';
922 $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.');
923 $this->renderer->renderRoot($element);
930 public function testPropertiesPlusBuilder() {
932 $element['#lazy_builder'] = ['Drupal\Tests\Core\Render\RecursivePlaceholdersTest::callback', []];
933 $element['#llama'] = '#awesome';
934 $element['#piglet'] = '#cute';
936 $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.');
937 $this->renderer->renderRoot($element);
944 public function testCreatePlaceholderPropertyWithoutLazyBuilder() {
946 $element['#create_placeholder'] = TRUE;
948 $this->setExpectedException(\LogicException::class, 'When #create_placeholder is set, a #lazy_builder callback must be present as well.');
949 $this->renderer->renderRoot($element);
953 * Create an element with a child and subchild. Each element has the same
954 * #lazy_builder callback, but with different contexts. They don't modify
955 * markup, only attach additional drupalSettings.
959 * @covers \Drupal\Core\Render\RenderCache::get
960 * @covers ::replacePlaceholders
962 public function testRenderChildrenPlaceholdersDifferentArguments() {
963 $this->setUpRequest();
964 $this->setupMemoryCache();
965 $this->cacheContextsManager->expects($this->any())
966 ->method('convertTokensToKeys')
967 ->willReturnArgument(0);
968 $this->controllerResolver->expects($this->any())
969 ->method('getControllerFromDefinition')
970 ->willReturnArgument(0);
971 $this->setupThemeManagerForDetails();
973 $args_1 = ['foo', TRUE];
974 $args_2 = ['bar', TRUE];
975 $args_3 = ['baz', TRUE];
976 $test_element = $this->generatePlaceholdersWithChildrenTestElement($args_1, $args_2, $args_3);
978 $element = $test_element;
979 $output = $this->renderer->renderRoot($element);
980 $expected_output = <<<HTML
982 <summary>Parent</summary>
983 <div class="details-wrapper"><details>
984 <summary>Child</summary>
985 <div class="details-wrapper">Subchild</div>
989 $this->assertSame($expected_output, (string) $output, 'Output is not overridden.');
990 $this->assertTrue(isset($element['#printed']), 'No cache hit');
991 $this->assertSame($expected_output, (string) $element['#markup'], '#markup is not overridden.');
992 $expected_js_settings = [
994 'dynamic_animal' => [$args_1[0] => TRUE, $args_2[0] => TRUE, $args_3[0] => TRUE],
996 $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.');
998 // GET request: validate cached data.
999 $cached_element = $this->memoryCache->get('simpletest:drupal_render:children_placeholders')->data;
1000 $expected_element = [
1002 'drupalSettings' => [
1006 'parent-x-parent' => [
1007 '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_1],
1009 'child-x-child' => [
1010 '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_2],
1012 'subchild-x-subchild' => [
1013 '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_3],
1020 'max-age' => Cache::PERMANENT,
1024 $dom = Html::load($cached_element['#markup']);
1025 $xpath = new \DOMXPath($dom);
1026 $parent = $xpath->query('//details/summary[text()="Parent"]')->length;
1027 $child = $xpath->query('//details/div[@class="details-wrapper"]/details/summary[text()="Child"]')->length;
1028 $subchild = $xpath->query('//details/div[@class="details-wrapper"]/details/div[@class="details-wrapper" and text()="Subchild"]')->length;
1029 $this->assertTrue($parent && $child && $subchild, 'The correct data is cached: the stored #markup is not affected by placeholder #lazy_builder callbacks.');
1031 // Remove markup because it's compared above in the xpath.
1032 unset($cached_element['#markup']);
1033 $this->assertEquals($cached_element, $expected_element, 'The correct data is cached: the stored #attached properties are not affected by placeholder #lazy_builder callbacks.');
1035 // GET request: #cache enabled, cache hit.
1036 $element = $test_element;
1037 $output = $this->renderer->renderRoot($element);
1038 $this->assertSame($expected_output, (string) $output, 'Output is not overridden.');
1039 $this->assertFalse(isset($element['#printed']), 'Cache hit');
1040 $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.');
1042 // Use the exact same element, but now unset #cache; ensure we get the same
1044 unset($test_element['#cache']);
1045 $element = $test_element;
1046 $output = $this->renderer->renderRoot($element);
1047 $this->assertSame($expected_output, (string) $output, 'Output is not overridden.');
1048 $this->assertSame($expected_output, (string) $element['#markup'], '#markup is not overridden.');
1049 $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.');
1053 * Generates an element with placeholders at 3 levels.
1055 * @param array $args_1
1056 * The arguments for the placeholder at level 1.
1057 * @param array $args_2
1058 * The arguments for the placeholder at level 2.
1059 * @param array $args_3
1060 * The arguments for the placeholder at level 3.
1063 * The generated render array for testing.
1065 protected function generatePlaceholdersWithChildrenTestElement(array $args_1, array $args_2, array $args_3) {
1067 '#type' => 'details',
1069 'keys' => ['simpletest', 'drupal_render', 'children_placeholders'],
1071 '#title' => 'Parent',
1073 'drupalSettings' => [
1077 'parent-x-parent' => [
1078 '#lazy_builder' => [ __NAMESPACE__ . '\\PlaceholdersTest::callback', $args_1],
1083 $test_element['child'] = [
1084 '#type' => 'details',
1087 'child-x-child' => [
1088 '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_2],
1092 '#title' => 'Child',
1094 $test_element['child']['subchild'] = [
1097 'subchild-x-subchild' => [
1098 '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_3],
1102 '#markup' => 'Subchild',
1104 return $test_element;
1108 * @return \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_Builder_InvocationMocker
1110 protected function setupThemeManagerForDetails() {
1111 return $this->themeManager->expects($this->any())
1113 ->willReturnCallback(function ($theme, array $vars) {
1116 <summary>{{ title }}</summary>
1117 <div class="details-wrapper">{{ children }}</div>
1120 $output = str_replace([
1123 ], [$vars['#title'], $vars['#children']], $output);
1131 * @see \Drupal\Tests\Core\Render\RendererPlaceholdersTest::testRecursivePlaceholder()
1133 class RecursivePlaceholdersTest {
1136 * #lazy_builder callback; bubbles another placeholder.
1138 * @param string $animal
1142 * A renderable array.
1144 public static function callback($animal) {
1147 '#create_placeholder' => TRUE,
1148 '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', [$animal]],