1<?php
2
3/**
4 * @file
5 * Contains \Drupal\Tests\Core\Render\RendererTest.
6 */
7
8namespace Drupal\Tests\Core\Render;
9
10use Drupal\Component\Render\MarkupInterface;
11use Drupal\Core\Access\AccessResult;
12use Drupal\Core\Access\AccessResultInterface;
13use Drupal\Core\Cache\Cache;
14use Drupal\Core\Security\TrustedCallbackInterface;
15use Drupal\Core\Render\Element;
16use Drupal\Core\Render\Markup;
17use Drupal\Core\Template\Attribute;
18
19/**
20 * @coversDefaultClass \Drupal\Core\Render\Renderer
21 * @group Render
22 */
23class RendererTest extends RendererTestBase {
24
25  protected $defaultThemeVars = [
26    '#cache' => [
27      'contexts' => [
28        'languages:language_interface',
29        'theme',
30      ],
31      'tags' => [],
32      'max-age' => Cache::PERMANENT,
33    ],
34    '#attached' => [],
35    '#children' => '',
36  ];
37
38  /**
39   * @covers ::render
40   * @covers ::doRender
41   *
42   * @dataProvider providerTestRenderBasic
43   */
44  public function testRenderBasic($build, $expected, callable $setup_code = NULL) {
45    if (isset($setup_code)) {
46      $setup_code = $setup_code->bindTo($this);
47      $setup_code();
48    }
49
50    if (isset($build['#markup'])) {
51      $this->assertNotInstanceOf(MarkupInterface::class, $build['#markup']);
52    }
53    $render_output = $this->renderer->renderRoot($build);
54    $this->assertSame($expected, (string) $render_output);
55    if ($render_output !== '') {
56      $this->assertInstanceOf(MarkupInterface::class, $render_output);
57      $this->assertInstanceOf(MarkupInterface::class, $build['#markup']);
58    }
59  }
60
61  /**
62   * Provides a list of render arrays to test basic rendering.
63   *
64   * @return array
65   */
66  public function providerTestRenderBasic() {
67    $data = [];
68
69    // Part 1: the most simplistic render arrays possible, none using #theme.
70
71    // Pass a NULL.
72    $data[] = [NULL, ''];
73    // Pass an empty string.
74    $data[] = ['', ''];
75    // Previously printed, see ::renderTwice for a more integration-like test.
76    $data[] = [
77      ['#markup' => 'foo', '#printed' => TRUE],
78      '',
79    ];
80    // Printed in pre_render.
81    $data[] = [
82      [
83        '#markup' => 'foo',
84        '#pre_render' => [[new TestCallables(), 'preRenderPrinted']],
85      ],
86      '',
87    ];
88    // Basic #markup based renderable array.
89    $data[] = [
90      ['#markup' => 'foo'],
91      'foo',
92    ];
93    // Basic #markup based renderable array with value '0'.
94    $data[] = [
95      ['#markup' => '0'],
96      '0',
97    ];
98    // Basic #markup based renderable array with value 0.
99    $data[] = [
100      ['#markup' => 0],
101      '0',
102    ];
103    // Basic #markup based renderable array with value ''.
104    $data[] = [
105      ['#markup' => ''],
106      '',
107    ];
108    // Basic #markup based renderable array with value NULL.
109    $data[] = [
110      ['#markup' => NULL],
111      '',
112    ];
113    // Basic #plain_text based renderable array.
114    $data[] = [
115      ['#plain_text' => 'foo'],
116      'foo',
117    ];
118    // Mixing #plain_text and #markup based renderable array.
119    $data[] = [
120      ['#plain_text' => '<em>foo</em>', '#markup' => 'bar'],
121      '&lt;em&gt;foo&lt;/em&gt;',
122    ];
123    // Safe strings in #plain_text are still escaped.
124    $data[] = [
125      ['#plain_text' => Markup::create('<em>foo</em>')],
126      '&lt;em&gt;foo&lt;/em&gt;',
127    ];
128    // #plain_text based renderable array with value '0'.
129    $data[] = [
130      ['#plain_text' => '0'],
131      '0',
132    ];
133    // #plain_text based renderable array with value 0.
134    $data[] = [
135      ['#plain_text' => 0],
136      '0',
137    ];
138    // #plain_text based renderable array with value ''.
139    $data[] = [
140      ['#plain_text' => ''],
141      '',
142    ];
143    // #plain_text based renderable array with value NULL.
144    $data[] = [
145      ['#plain_text' => NULL],
146      '',
147    ];
148    // Renderable child element.
149    $data[] = [
150      ['child' => ['#markup' => 'bar']],
151      'bar',
152    ];
153    // XSS filtering test.
154    $data[] = [
155      ['child' => ['#markup' => "This is <script>alert('XSS')</script> test"]],
156      "This is alert('XSS') test",
157    ];
158    // XSS filtering test.
159    $data[] = [
160      [
161        'child' => [
162          '#markup' => "This is <script>alert('XSS')</script> test",
163          '#allowed_tags' => ['script'],
164        ],
165      ],
166      "This is <script>alert('XSS')</script> test",
167    ];
168    // XSS filtering test.
169    $data[] = [
170      [
171        'child' => [
172          '#markup' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>",
173          '#allowed_tags' => ['em', 'strong'],
174        ],
175      ],
176      "This is <em>alert('XSS')</em> <strong>test</strong>",
177    ];
178    // Html escaping test.
179    $data[] = [
180      [
181        'child' => [
182          '#plain_text' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>",
183        ],
184      ],
185      "This is &lt;script&gt;&lt;em&gt;alert(&#039;XSS&#039;)&lt;/em&gt;&lt;/script&gt; &lt;strong&gt;test&lt;/strong&gt;",
186    ];
187    // XSS filtering by default test.
188    $data[] = [
189      [
190        'child' => [
191          '#markup' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>",
192        ],
193      ],
194      "This is <em>alert('XSS')</em> <strong>test</strong>",
195    ];
196    // Ensure non-XSS tags are not filtered out.
197    $data[] = [
198      [
199        'child' => [
200          '#markup' => "This is <strong><script>alert('not a giraffe')</script></strong> test",
201        ],
202      ],
203      "This is <strong>alert('not a giraffe')</strong> test",
204    ];
205    // #children set but empty, and renderable children.
206    $data[] = [
207      ['#children' => '', 'child' => ['#markup' => 'bar']],
208      'bar',
209    ];
210    // #children set, not empty, and renderable children. #children will be
211    // assumed oto be the rendered child elements, even though the #markup for
212    // 'child' differs.
213    $data[] = [
214      ['#children' => 'foo', 'child' => ['#markup' => 'bar']],
215      'foo',
216    ];
217    // Ensure that content added to #markup via a #pre_render callback is safe.
218    $data[] = [
219      [
220        '#markup' => 'foo',
221        '#pre_render' => [function ($elements) {
222          $elements['#markup'] .= '<script>alert("bar");</script>';
223          return $elements;
224        },
225        ],
226      ],
227      'fooalert("bar");',
228    ];
229    // Test #allowed_tags in combination with #markup and #pre_render.
230    $data[] = [
231      [
232        '#markup' => 'foo',
233        '#allowed_tags' => ['script'],
234        '#pre_render' => [function ($elements) {
235          $elements['#markup'] .= '<script>alert("bar");</script>';
236          return $elements;
237        },
238        ],
239      ],
240      'foo<script>alert("bar");</script>',
241    ];
242    // Ensure output is escaped when adding content to #check_plain through
243    // a #pre_render callback.
244    $data[] = [
245      [
246        '#plain_text' => 'foo',
247        '#pre_render' => [function ($elements) {
248          $elements['#plain_text'] .= '<script>alert("bar");</script>';
249          return $elements;
250        },
251        ],
252      ],
253      'foo&lt;script&gt;alert(&quot;bar&quot;);&lt;/script&gt;',
254    ];
255
256    // Part 2: render arrays using #theme and #theme_wrappers.
257
258    // Tests that #theme and #theme_wrappers can co-exist on an element.
259    $build = [
260      '#theme' => 'common_test_foo',
261      '#foo' => 'foo',
262      '#bar' => 'bar',
263      '#theme_wrappers' => ['container'],
264      '#attributes' => ['class' => ['baz']],
265    ];
266    $setup_code_type_link = function () {
267      $this->themeManager->expects($this->exactly(2))
268        ->method('render')
269        ->with($this->logicalOr('common_test_foo', 'container'))
270        ->willReturnCallback(function ($theme, $vars) {
271          if ($theme == 'container') {
272            return '<div' . (string) (new Attribute($vars['#attributes'])) . '>' . $vars['#children'] . "</div>\n";
273          }
274          return $vars['#foo'] . $vars['#bar'];
275        });
276    };
277    $data[] = [$build, '<div class="baz">foobar</div>' . "\n", $setup_code_type_link];
278
279    // Tests that #theme_wrappers can disambiguate element attributes shared
280    // with rendering methods that build #children by using the alternate
281    // #theme_wrappers attribute override syntax.
282    $build = [
283      '#type' => 'link',
284      '#theme_wrappers' => [
285        'container' => [
286          '#attributes' => ['class' => ['baz']],
287        ],
288      ],
289      '#attributes' => ['id' => 'foo'],
290      '#url' => 'https://www.drupal.org',
291      '#title' => 'bar',
292    ];
293    $setup_code_type_link = function () {
294      $this->themeManager->expects($this->exactly(2))
295        ->method('render')
296        ->with($this->logicalOr('link', 'container'))
297        ->willReturnCallback(function ($theme, $vars) {
298          if ($theme == 'container') {
299            return '<div' . (string) (new Attribute($vars['#attributes'])) . '>' . $vars['#children'] . "</div>\n";
300          }
301          $attributes = new Attribute(['href' => $vars['#url']] + (isset($vars['#attributes']) ? $vars['#attributes'] : []));
302          return '<a' . (string) $attributes . '>' . $vars['#title'] . '</a>';
303        });
304    };
305    $data[] = [$build, '<div class="baz"><a href="https://www.drupal.org" id="foo">bar</a></div>' . "\n", $setup_code_type_link];
306
307    // Tests that #theme_wrappers can disambiguate element attributes when the
308    // "base" attribute is not set for #theme.
309    $build = [
310      '#type' => 'link',
311      '#url' => 'https://www.drupal.org',
312      '#title' => 'foo',
313      '#theme_wrappers' => [
314        'container' => [
315          '#attributes' => ['class' => ['baz']],
316        ],
317      ],
318    ];
319    $data[] = [$build, '<div class="baz"><a href="https://www.drupal.org">foo</a></div>' . "\n", $setup_code_type_link];
320
321    // Tests two 'container' #theme_wrappers, one using the "base" attributes
322    // and one using an override.
323    $build = [
324      '#attributes' => ['class' => ['foo']],
325      '#theme_wrappers' => [
326        'container' => [
327          '#attributes' => ['class' => ['bar']],
328        ],
329        'container',
330      ],
331    ];
332    $setup_code = function () {
333      $this->themeManager->expects($this->exactly(2))
334        ->method('render')
335        ->with('container')
336        ->willReturnCallback(function ($theme, $vars) {
337          return '<div' . (string) (new Attribute($vars['#attributes'])) . '>' . $vars['#children'] . "</div>\n";
338        });
339    };
340    $data[] = [$build, '<div class="foo"><div class="bar"></div>' . "\n" . '</div>' . "\n", $setup_code];
341
342    // Tests array syntax theme hook suggestion in #theme_wrappers.
343    $build = [
344      '#theme_wrappers' => [['container']],
345      '#attributes' => ['class' => ['foo']],
346    ];
347    $setup_code = function () {
348      $this->themeManager->expects($this->once())
349        ->method('render')
350        ->with(['container'])
351        ->willReturnCallback(function ($theme, $vars) {
352          return '<div' . (string) (new Attribute($vars['#attributes'])) . '>' . $vars['#children'] . "</div>\n";
353        });
354    };
355    $data[] = [$build, '<div class="foo"></div>' . "\n", $setup_code];
356
357    // Part 3: render arrays using #markup as a fallback for #theme hooks.
358
359    // Theme suggestion is not implemented, #markup should be rendered.
360    $build = [
361      '#theme' => ['suggestionnotimplemented'],
362      '#markup' => 'foo',
363    ];
364    $setup_code = function () {
365      $this->themeManager->expects($this->once())
366        ->method('render')
367        ->with(['suggestionnotimplemented'], $this->anything())
368        ->willReturn(FALSE);
369    };
370    $data[] = [$build, 'foo', $setup_code];
371
372    // Tests unimplemented theme suggestion, child #markup should be rendered.
373    $build = [
374      '#theme' => ['suggestionnotimplemented'],
375      'child' => [
376        '#markup' => 'foo',
377      ],
378    ];
379    $setup_code = function () {
380      $this->themeManager->expects($this->once())
381        ->method('render')
382        ->with(['suggestionnotimplemented'], $this->anything())
383        ->willReturn(FALSE);
384    };
385    $data[] = [$build, 'foo', $setup_code];
386
387    // Tests implemented theme suggestion: #markup should not be rendered.
388    $build = [
389      '#theme' => ['common_test_empty'],
390      '#markup' => 'foo',
391    ];
392    $theme_function_output = $this->randomContextValue();
393    $setup_code = function () use ($theme_function_output) {
394      $this->themeManager->expects($this->once())
395        ->method('render')
396        ->with(['common_test_empty'], $this->anything())
397        ->willReturn($theme_function_output);
398    };
399    $data[] = [$build, $theme_function_output, $setup_code];
400
401    // Tests implemented theme suggestion: children should not be rendered.
402    $build = [
403      '#theme' => ['common_test_empty'],
404      'child' => [
405        '#markup' => 'foo',
406      ],
407    ];
408    $data[] = [$build, $theme_function_output, $setup_code];
409
410    // Part 4: handling of #children and child renderable elements.
411
412    // #theme is implemented so the values of both #children and 'child' will
413    // be ignored - it is the responsibility of the theme hook to render these
414    // if appropriate.
415    $build = [
416      '#theme' => 'common_test_foo',
417      '#children' => 'baz',
418      'child' => ['#markup' => 'boo'],
419    ];
420    $setup_code = function () {
421      $this->themeManager->expects($this->once())
422        ->method('render')
423        ->with('common_test_foo', $this->anything())
424        ->willReturn('foobar');
425    };
426    $data[] = [$build, 'foobar', $setup_code];
427
428    // #theme is implemented but #render_children is TRUE. As in the case where
429    // #theme is not set, empty #children means child elements are rendered
430    // recursively.
431    $build = [
432      '#theme' => 'common_test_foo',
433      '#children' => '',
434      '#render_children' => TRUE,
435      'child' => [
436        '#markup' => 'boo',
437      ],
438    ];
439    $setup_code = function () {
440      $this->themeManager->expects($this->never())
441        ->method('render');
442    };
443    $data[] = [$build, 'boo', $setup_code];
444
445    // #theme is implemented but #render_children is TRUE. As in the case where
446    // #theme is not set, #children will take precedence over 'child'.
447    $build = [
448      '#theme' => 'common_test_foo',
449      '#children' => 'baz',
450      '#render_children' => TRUE,
451      'child' => [
452        '#markup' => 'boo',
453      ],
454    ];
455    $setup_code = function () {
456      $this->themeManager->expects($this->never())
457        ->method('render');
458    };
459    $data[] = [$build, 'baz', $setup_code];
460
461    // #theme is implemented but #render_children is TRUE. In this case the
462    // calling code is expecting only the children to be rendered. #prefix and
463    // #suffix should not be inherited for the children.
464    $build = [
465      '#theme' => 'common_test_foo',
466      '#children' => '',
467      '#prefix' => 'kangaroo',
468      '#suffix' => 'unicorn',
469      '#render_children' => TRUE,
470      'child' => [
471        '#markup' => 'kitten',
472      ],
473    ];
474    $setup_code = function () {
475      $this->themeManager->expects($this->never())
476        ->method('render');
477    };
478    $data[] = [$build, 'kitten', $setup_code];
479
480    return $data;
481  }
482
483  /**
484   * @covers ::render
485   * @covers ::doRender
486   */
487  public function testRenderSorting() {
488    $first = $this->randomMachineName();
489    $second = $this->randomMachineName();
490    // Build an array with '#weight' set for each element.
491    $elements = [
492      'second' => [
493        '#weight' => 10,
494        '#markup' => $second,
495      ],
496      'first' => [
497        '#weight' => 0,
498        '#markup' => $first,
499      ],
500    ];
501    $output = $this->renderer->renderRoot($elements);
502
503    // The lowest weight element should appear last in $output.
504    $this->assertGreaterThan(strpos($output, $first), strpos($output, $second));
505
506    // Confirm that the $elements array has '#sorted' set to TRUE.
507    $this->assertTrue($elements['#sorted'], "'#sorted' => TRUE was added to the array");
508
509    // Pass $elements through \Drupal\Core\Render\Element::children() and
510    // ensure it remains sorted in the correct order.
511    // \Drupal::service('renderer')->render() will return an empty string if
512    // used on the same array in the same request.
513    $children = Element::children($elements);
514    $this->assertSame('first', array_shift($children), 'Child found in the correct order.');
515    $this->assertSame('second', array_shift($children), 'Child found in the correct order.');
516  }
517
518  /**
519   * @covers ::render
520   * @covers ::doRender
521   */
522  public function testRenderSortingWithSetHashSorted() {
523    $first = $this->randomMachineName();
524    $second = $this->randomMachineName();
525    // The same array structure again, but with #sorted set to TRUE.
526    $elements = [
527      'second' => [
528        '#weight' => 10,
529        '#markup' => $second,
530      ],
531      'first' => [
532        '#weight' => 0,
533        '#markup' => $first,
534      ],
535      '#sorted' => TRUE,
536    ];
537    $output = $this->renderer->renderRoot($elements);
538
539    // The elements should appear in output in the same order as the array.
540    $this->assertLessThan(strpos($output, $first), strpos($output, $second));
541  }
542
543  /**
544   * @covers ::render
545   * @covers ::doRender
546   *
547   * @dataProvider providerAccessValues
548   */
549  public function testRenderWithPresetAccess($access) {
550    $build = [
551      '#access' => $access,
552    ];
553
554    $this->assertAccess($build, $access);
555  }
556
557  /**
558   * @covers ::render
559   * @covers ::doRender
560   *
561   * @dataProvider providerAccessValues
562   */
563  public function testRenderWithAccessCallbackCallable($access) {
564    $build = [
565      '#access_callback' => function () use ($access) {
566        return $access;
567      },
568    ];
569
570    $this->assertAccess($build, $access);
571  }
572
573  /**
574   * Ensures that the #access property wins over the callable.
575   *
576   * @covers ::render
577   * @covers ::doRender
578   *
579   * @dataProvider providerAccessValues
580   */
581  public function testRenderWithAccessPropertyAndCallback($access) {
582    $build = [
583      '#access' => $access,
584      '#access_callback' => function () {
585        return TRUE;
586      },
587    ];
588
589    $this->assertAccess($build, $access);
590  }
591
592  /**
593   * @covers ::render
594   * @covers ::doRender
595   *
596   * @dataProvider providerAccessValues
597   */
598  public function testRenderWithAccessControllerResolved($access) {
599
600    switch ($access) {
601      case AccessResult::allowed():
602        $method = 'accessResultAllowed';
603        break;
604
605      case AccessResult::forbidden():
606        $method = 'accessResultForbidden';
607        break;
608
609      case FALSE:
610        $method = 'accessFalse';
611        break;
612
613      case TRUE:
614        $method = 'accessTrue';
615        break;
616    }
617
618    $build = [
619      '#access_callback' => 'Drupal\Tests\Core\Render\TestAccessClass::' . $method,
620    ];
621
622    $this->assertAccess($build, $access);
623  }
624
625  /**
626   * @covers ::render
627   * @covers ::doRender
628   */
629  public function testRenderAccessCacheabilityDependencyInheritance() {
630    $build = [
631      '#access' => AccessResult::allowed()->addCacheContexts(['user']),
632    ];
633
634    $this->renderer->renderPlain($build);
635
636    $this->assertEquals(['languages:language_interface', 'theme', 'user'], $build['#cache']['contexts']);
637  }
638
639  /**
640   * Tests rendering same render array twice.
641   *
642   * Tests that a first render returns the rendered output and a second doesn't
643   * because of the #printed property. Also tests that correct metadata has been
644   * set for re-rendering.
645   *
646   * @covers ::render
647   * @covers ::doRender
648   *
649   * @dataProvider providerRenderTwice
650   */
651  public function testRenderTwice($build) {
652    $this->assertEquals('kittens', $this->renderer->renderRoot($build));
653    $this->assertEquals('kittens', $build['#markup']);
654    $this->assertEquals(['kittens-147'], $build['#cache']['tags']);
655    $this->assertTrue($build['#printed']);
656
657    // We don't want to reprint already printed render arrays.
658    $this->assertEquals('', $this->renderer->renderRoot($build));
659  }
660
661  /**
662   * Provides a list of render array iterations.
663   *
664   * @return array
665   */
666  public function providerRenderTwice() {
667    return [
668      [
669        [
670          '#markup' => 'kittens',
671          '#cache' => [
672            'tags' => ['kittens-147'],
673          ],
674        ],
675      ],
676      [
677        [
678          'child' => [
679            '#markup' => 'kittens',
680            '#cache' => [
681              'tags' => ['kittens-147'],
682            ],
683          ],
684        ],
685      ],
686      [
687        [
688          '#render_children' => TRUE,
689          'child' => [
690            '#markup' => 'kittens',
691            '#cache' => [
692              'tags' => ['kittens-147'],
693            ],
694          ],
695        ],
696      ],
697    ];
698  }
699
700  /**
701   * Ensures that #access is taken in account when rendering #render_children.
702   */
703  public function testRenderChildrenAccess() {
704    $build = [
705      '#access' => FALSE,
706      '#render_children' => TRUE,
707      'child' => [
708        '#markup' => 'kittens',
709      ],
710    ];
711
712    $this->assertEquals('', $this->renderer->renderRoot($build));
713  }
714
715  /**
716   * Provides a list of both booleans.
717   *
718   * @return array
719   */
720  public function providerAccessValues() {
721    return [
722      [FALSE],
723      [TRUE],
724      [AccessResult::forbidden()],
725      [AccessResult::allowed()],
726    ];
727  }
728
729  /**
730   * Asserts that a render array with access checking renders correctly.
731   *
732   * @param array $build
733   *   A render array with either #access or #access_callback.
734   * @param bool $access
735   *   Whether the render array is accessible or not.
736   */
737  protected function assertAccess($build, $access) {
738    $sensitive_content = $this->randomContextValue();
739    $build['#markup'] = $sensitive_content;
740    if (($access instanceof AccessResultInterface && $access->isAllowed()) || $access === TRUE) {
741      $this->assertSame($sensitive_content, (string) $this->renderer->renderRoot($build));
742    }
743    else {
744      $this->assertSame('', (string) $this->renderer->renderRoot($build));
745    }
746  }
747
748  /**
749   * @covers ::render
750   * @covers ::doRender
751   */
752  public function testRenderWithoutThemeArguments() {
753    $element = [
754      '#theme' => 'common_test_foo',
755    ];
756
757    $this->themeManager->expects($this->once())
758      ->method('render')
759      ->with('common_test_foo', $this->defaultThemeVars + $element)
760      ->willReturn('foobar');
761
762    // Test that defaults work.
763    $this->assertEquals('foobar', $this->renderer->renderRoot($element), 'Defaults work');
764  }
765
766  /**
767   * @covers ::render
768   * @covers ::doRender
769   */
770  public function testRenderWithThemeArguments() {
771    $element = [
772      '#theme' => 'common_test_foo',
773      '#foo' => $this->randomMachineName(),
774      '#bar' => $this->randomMachineName(),
775    ];
776
777    $this->themeManager->expects($this->once())
778      ->method('render')
779      ->with('common_test_foo', $this->defaultThemeVars + $element)
780      ->willReturnCallback(function ($hook, $vars) {
781        return $vars['#foo'] . $vars['#bar'];
782      });
783
784    // Tests that passing arguments to the theme function works.
785    $this->assertEquals($this->renderer->renderRoot($element), $element['#foo'] . $element['#bar'], 'Passing arguments to theme functions works');
786  }
787
788  /**
789   * @covers ::render
790   * @covers ::doRender
791   * @covers \Drupal\Core\Render\RenderCache::get
792   * @covers \Drupal\Core\Render\RenderCache::set
793   * @covers \Drupal\Core\Render\RenderCache::createCacheID
794   */
795  public function testRenderCache() {
796    $this->setUpRequest();
797    $this->setupMemoryCache();
798
799    // Create an empty element.
800    $test_element = [
801      '#cache' => [
802        'keys' => ['render_cache_test'],
803        'tags' => ['render_cache_tag'],
804      ],
805      '#markup' => '',
806      'child' => [
807        '#cache' => [
808          'keys' => ['render_cache_test_child'],
809          'tags' => ['render_cache_tag_child:1', 'render_cache_tag_child:2'],
810        ],
811        '#markup' => '',
812      ],
813    ];
814
815    // Render the element and confirm that it goes through the rendering
816    // process (which will set $element['#printed']).
817    $element = $test_element;
818    $this->renderer->renderRoot($element);
819    $this->assertTrue(isset($element['#printed']), 'No cache hit');
820
821    // Render the element again and confirm that it is retrieved from the cache
822    // instead (so $element['#printed'] will not be set).
823    $element = $test_element;
824    $this->renderer->renderRoot($element);
825    $this->assertFalse(isset($element['#printed']), 'Cache hit');
826
827    // Test that cache tags are correctly collected from the render element,
828    // including the ones from its subchild.
829    $expected_tags = [
830      'render_cache_tag',
831      'render_cache_tag_child:1',
832      'render_cache_tag_child:2',
833    ];
834    $this->assertEquals($expected_tags, $element['#cache']['tags'], 'Cache tags were collected from the element and its subchild.');
835
836    // The cache item also has a 'rendered' cache tag.
837    $cache_item = $this->cacheFactory->get('render')->get('render_cache_test:en:stark');
838    $this->assertSame(Cache::mergeTags($expected_tags, ['rendered']), $cache_item->tags);
839  }
840
841  /**
842   * @covers ::render
843   * @covers ::doRender
844   * @covers \Drupal\Core\Render\RenderCache::get
845   * @covers \Drupal\Core\Render\RenderCache::set
846   * @covers \Drupal\Core\Render\RenderCache::createCacheID
847   *
848   * @dataProvider providerTestRenderCacheMaxAge
849   */
850  public function testRenderCacheMaxAge($max_age, $is_render_cached, $render_cache_item_expire) {
851    $this->setUpRequest();
852    $this->setupMemoryCache();
853
854    $element = [
855      '#cache' => [
856        'keys' => ['render_cache_test'],
857        'max-age' => $max_age,
858      ],
859      '#markup' => '',
860    ];
861    $this->renderer->renderRoot($element);
862
863    $cache_item = $this->cacheFactory->get('render')->get('render_cache_test:en:stark');
864    if (!$is_render_cached) {
865      $this->assertFalse($cache_item);
866    }
867    else {
868      $this->assertNotFalse($cache_item);
869      $this->assertSame($render_cache_item_expire, $cache_item->expire);
870    }
871  }
872
873  public function providerTestRenderCacheMaxAge() {
874    return [
875      [0, FALSE, NULL],
876      [60, TRUE, (int) $_SERVER['REQUEST_TIME'] + 60],
877      [Cache::PERMANENT, TRUE, -1],
878    ];
879  }
880
881  /**
882   * Tests that #cache_properties are properly handled.
883   *
884   * @param array $expected_results
885   *   An associative array of expected results keyed by property name.
886   *
887   * @covers ::render
888   * @covers ::doRender
889   * @covers \Drupal\Core\Render\RenderCache::get
890   * @covers \Drupal\Core\Render\RenderCache::set
891   * @covers \Drupal\Core\Render\RenderCache::createCacheID
892   * @covers \Drupal\Core\Render\RenderCache::getCacheableRenderArray
893   *
894   * @dataProvider providerTestRenderCacheProperties
895   */
896  public function testRenderCacheProperties(array $expected_results) {
897    $this->setUpRequest();
898    $this->setupMemoryCache();
899
900    $element = $original = [
901      '#cache' => [
902        'keys' => ['render_cache_test'],
903      ],
904      // Collect expected property names.
905      '#cache_properties' => array_keys(array_filter($expected_results)),
906      'child1' => ['#markup' => Markup::create('1')],
907      'child2' => ['#markup' => Markup::create('2')],
908      // Mark the value as safe.
909      '#custom_property' => Markup::create('custom_value'),
910      '#custom_property_array' => ['custom value'],
911    ];
912
913    $this->renderer->renderRoot($element);
914
915    $cache = $this->cacheFactory->get('render');
916    $data = $cache->get('render_cache_test:en:stark')->data;
917
918    // Check that parent markup is ignored when caching children's markup.
919    $this->assertEquals($data['#markup'] === '', (bool) Element::children($data));
920
921    // Check that the element properties are cached as specified.
922    foreach ($expected_results as $property => $expected) {
923      $cached = !empty($data[$property]);
924      $this->assertEquals($cached, (bool) $expected);
925      // Check that only the #markup key is preserved for children.
926      if ($cached) {
927        $this->assertEquals($data[$property], $original[$property]);
928      }
929    }
930    // #custom_property_array can not be a safe_cache_property.
931    $safe_cache_properties = array_diff(Element::properties(array_filter($expected_results)), ['#custom_property_array']);
932    foreach ($safe_cache_properties as $cache_property) {
933      $this->assertInstanceOf(MarkupInterface::class, $data[$cache_property]);
934    }
935  }
936
937  /**
938   * Data provider for ::testRenderCacheProperties().
939   *
940   * @return array
941   *   An array of associative arrays of expected results keyed by property
942   *   name.
943   */
944  public function providerTestRenderCacheProperties() {
945    return [
946      [[]],
947      [['child1' => 0, 'child2' => 0, '#custom_property' => 0, '#custom_property_array' => 0]],
948      [['child1' => 0, 'child2' => 0, '#custom_property' => 1, '#custom_property_array' => 0]],
949      [['child1' => 0, 'child2' => 1, '#custom_property' => 0, '#custom_property_array' => 0]],
950      [['child1' => 0, 'child2' => 1, '#custom_property' => 1, '#custom_property_array' => 0]],
951      [['child1' => 1, 'child2' => 0, '#custom_property' => 0, '#custom_property_array' => 0]],
952      [['child1' => 1, 'child2' => 0, '#custom_property' => 1, '#custom_property_array' => 0]],
953      [['child1' => 1, 'child2' => 1, '#custom_property' => 0, '#custom_property_array' => 0]],
954      [['child1' => 1, 'child2' => 1, '#custom_property' => 1, '#custom_property_array' => 0]],
955      [['child1' => 1, 'child2' => 1, '#custom_property' => 1, '#custom_property_array' => 1]],
956    ];
957  }
958
959  /**
960   * @covers ::addCacheableDependency
961   *
962   * @dataProvider providerTestAddCacheableDependency
963   */
964  public function testAddCacheableDependency(array $build, $object, array $expected) {
965    $this->renderer->addCacheableDependency($build, $object);
966    $this->assertEquals($build, $expected);
967  }
968
969  public function providerTestAddCacheableDependency() {
970    return [
971      // Empty render array, typical default cacheability.
972      [
973        [],
974        new TestCacheableDependency([], [], Cache::PERMANENT),
975        [
976          '#cache' => [
977            'contexts' => [],
978            'tags' => [],
979            'max-age' => Cache::PERMANENT,
980          ],
981        ],
982      ],
983      // Empty render array, some cacheability.
984      [
985        [],
986        new TestCacheableDependency(['user.roles'], ['foo'], Cache::PERMANENT),
987        [
988          '#cache' => [
989            'contexts' => ['user.roles'],
990            'tags' => ['foo'],
991            'max-age' => Cache::PERMANENT,
992          ],
993        ],
994      ],
995      // Cacheable render array, some cacheability.
996      [
997        [
998          '#cache' => [
999            'contexts' => ['theme'],
1000            'tags' => ['bar'],
1001            'max-age' => 600,
1002          ],
1003        ],
1004        new TestCacheableDependency(['user.roles'], ['foo'], Cache::PERMANENT),
1005        [
1006          '#cache' => [
1007            'contexts' => ['theme', 'user.roles'],
1008            'tags' => ['bar', 'foo'],
1009            'max-age' => 600,
1010          ],
1011        ],
1012      ],
1013      // Cacheable render array, no cacheability.
1014      [
1015        [
1016          '#cache' => [
1017            'contexts' => ['theme'],
1018            'tags' => ['bar'],
1019            'max-age' => 600,
1020          ],
1021        ],
1022        new \stdClass(),
1023        [
1024          '#cache' => [
1025            'contexts' => ['theme'],
1026            'tags' => ['bar'],
1027            'max-age' => 0,
1028          ],
1029        ],
1030      ],
1031    ];
1032  }
1033
1034}
1035
1036class TestAccessClass implements TrustedCallbackInterface {
1037
1038  public static function accessTrue() {
1039    return TRUE;
1040  }
1041
1042  public static function accessFalse() {
1043    return FALSE;
1044  }
1045
1046  public static function accessResultAllowed() {
1047    return AccessResult::allowed();
1048  }
1049
1050  public static function accessResultForbidden() {
1051    return AccessResult::forbidden();
1052  }
1053
1054  /**
1055   * {@inheritdoc}
1056   */
1057  public static function trustedCallbacks() {
1058    return ['accessTrue', 'accessFalse', 'accessResultAllowed', 'accessResultForbidden'];
1059  }
1060
1061}
1062
1063class TestCallables implements TrustedCallbackInterface {
1064
1065  public function preRenderPrinted($elements) {
1066    $elements['#printed'] = TRUE;
1067    return $elements;
1068  }
1069
1070  /**
1071   * {@inheritdoc}
1072   */
1073  public static function trustedCallbacks() {
1074    return ['preRenderPrinted'];
1075  }
1076
1077}
1078