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