View source
<?php
namespace Drupal\Tests\Core\Render;
use Drupal\Core\Cache\MemoryBackend;
use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
use Drupal\Core\Render\Element;
use Drupal\Core\State\State;
use Drupal\Core\Cache\Cache;
class RendererBubblingTest extends RendererTestBase {
protected function setUp() {
$this->rendererConfig['required_cache_contexts'] = [];
parent::setUp();
}
public function testBubblingWithoutPreRender() {
$this
->setUpRequest();
$this
->setupMemoryCache();
$this->cacheContextsManager
->expects($this
->any())
->method('convertTokensToKeys')
->willReturnArgument(0);
$element = [
'#type' => 'container',
'#cache' => [
'keys' => [
'simpletest',
'drupal_render',
'children_attached',
],
],
'#attached' => [
'library' => [
'test/parent',
],
],
'#title' => 'Parent',
];
$element['child'] = [
'#type' => 'container',
'#attached' => [
'library' => [
'test/child',
],
],
'#title' => 'Child',
];
$element['child']['subchild'] = [
'#attached' => [
'library' => [
'test/subchild',
],
],
'#markup' => 'Subchild',
];
$this->renderer
->renderRoot($element);
$expected_libraries = [
'test/parent',
'test/child',
'test/subchild',
];
$this
->assertEquals($element['#attached']['library'], $expected_libraries, 'The element, child and subchild #attached libraries are included.');
$element = [
'#cache' => [
'keys' => [
'simpletest',
'drupal_render',
'children_attached',
],
],
];
$this
->assertTrue(strlen($this->renderer
->renderRoot($element)) > 0, 'The element was retrieved from cache.');
$this
->assertEquals($element['#attached']['library'], $expected_libraries, 'The element, child and subchild #attached libraries are included.');
}
public function testContextBubblingCustomCacheBin() {
$bin = $this
->randomMachineName();
$this
->setUpRequest();
$this->memoryCache = new MemoryBackend('render');
$custom_cache = new MemoryBackend($bin);
$this->cacheFactory
->expects($this
->atLeastOnce())
->method('get')
->with($bin)
->willReturnCallback(function ($requested_bin) use ($bin, $custom_cache) {
if ($requested_bin === $bin) {
return $custom_cache;
}
else {
throw new \Exception();
}
});
$this->cacheContextsManager
->expects($this
->any())
->method('convertTokensToKeys')
->willReturnArgument(0);
$build = [
'#cache' => [
'keys' => [
'parent',
],
'contexts' => [
'foo',
],
'bin' => $bin,
],
'#markup' => 'parent',
'child' => [
'#cache' => [
'contexts' => [
'bar',
],
'max-age' => 3600,
],
],
];
$this->renderer
->renderRoot($build);
$this
->assertRenderCacheItem('parent:foo', [
'#cache_redirect' => TRUE,
'#cache' => [
'keys' => [
'parent',
],
'contexts' => [
'bar',
'foo',
],
'tags' => [],
'bin' => $bin,
'max-age' => 3600,
],
], $bin);
}
public function testContextBubblingEdgeCases(array $element, array $expected_top_level_contexts, array $expected_cache_items) {
$this
->setUpRequest();
$this
->setupMemoryCache();
$this->cacheContextsManager
->expects($this
->any())
->method('convertTokensToKeys')
->willReturnArgument(0);
$this->renderer
->renderRoot($element);
$this
->assertEquals($expected_top_level_contexts, $element['#cache']['contexts'], 'Expected cache contexts found.');
foreach ($expected_cache_items as $cid => $expected_cache_item) {
$this
->assertRenderCacheItem($cid, $expected_cache_item);
}
}
public function providerTestContextBubblingEdgeCases() {
$data = [];
$test_element = [
'#cache' => [
'keys' => [
'parent',
],
'contexts' => [],
],
'#markup' => 'parent',
'child' => [
'#access' => FALSE,
'#cache' => [
'contexts' => [
'foo',
],
],
],
];
$expected_cache_items = [
'parent' => [
'#attached' => [],
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => Cache::PERMANENT,
],
'#markup' => 'parent',
],
];
$data[] = [
$test_element,
[],
$expected_cache_items,
];
$test_element = [
'#cache' => [
'keys' => [
'set_test',
],
'contexts' => [],
],
];
$expected_cache_items = [
'set_test:bar:baz:foo' => [
'#attached' => [],
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => Cache::PERMANENT,
],
'#markup' => '',
],
];
$context_orders = [
[
'foo',
'bar',
'baz',
],
[
'foo',
'baz',
'bar',
],
[
'bar',
'foo',
'baz',
],
[
'bar',
'baz',
'foo',
],
[
'baz',
'foo',
'bar',
],
[
'baz',
'bar',
'foo',
],
];
foreach ($context_orders as $context_order) {
$test_element['#cache']['contexts'] = $context_order;
sort($context_order);
$expected_cache_items['set_test:bar:baz:foo']['#cache']['contexts'] = $context_order;
$data[] = [
$test_element,
$context_order,
$expected_cache_items,
];
}
$test_element = [
'#cache' => [
'keys' => [
'parent',
],
'contexts' => [
'foo',
'bar',
'baz',
],
],
'#markup' => 'parent',
'child' => [
'#cache' => [
'contexts' => [
'foo',
'baz',
],
'max-age' => 3600,
],
],
];
$expected_cache_items = [
'parent:bar:baz:foo' => [
'#attached' => [],
'#cache' => [
'contexts' => [
'bar',
'baz',
'foo',
],
'tags' => [],
'max-age' => 3600,
],
'#markup' => 'parent',
],
];
$data[] = [
$test_element,
[
'bar',
'baz',
'foo',
],
$expected_cache_items,
];
$test_element = [
'#cache' => [
'keys' => [
'parent',
],
'contexts' => [
'foo',
],
'tags' => [
'yar',
'har',
],
],
'#markup' => 'parent',
'child' => [
'#cache' => [
'contexts' => [
'bar',
],
'tags' => [
'fiddle',
'dee',
],
],
'#markup' => '',
],
];
$expected_cache_items = [
'parent:foo' => [
'#cache_redirect' => TRUE,
'#cache' => [
'keys' => [
'parent',
],
'contexts' => [
'bar',
'foo',
],
'tags' => [
'dee',
'fiddle',
'har',
'yar',
],
'bin' => 'render',
'max-age' => Cache::PERMANENT,
],
],
'parent:bar:foo' => [
'#attached' => [],
'#cache' => [
'contexts' => [
'bar',
'foo',
],
'tags' => [
'dee',
'fiddle',
'har',
'yar',
],
'max-age' => Cache::PERMANENT,
],
'#markup' => 'parent',
],
];
$data[] = [
$test_element,
[
'bar',
'foo',
],
$expected_cache_items,
];
return $data;
}
public function testConditionalCacheContextBubblingSelfHealing() {
$current_user_role =& $this->currentUserRole;
$this
->setUpRequest();
$this
->setupMemoryCache();
$test_element = [
'#cache' => [
'keys' => [
'parent',
],
'tags' => [
'a',
],
],
'#markup' => 'parent',
'child' => [
'#cache' => [
'contexts' => [
'user.roles',
],
'tags' => [
'b',
],
],
'grandchild' => [
'#access_callback' => function () use (&$current_user_role) {
return $current_user_role !== 'A';
},
'#cache' => [
'contexts' => [
'foo',
],
'tags' => [
'c',
],
'max-age' => 1800,
],
'grandgrandchild' => [
'#access_callback' => function () use (&$current_user_role) {
return $current_user_role === 'C';
},
'#cache' => [
'contexts' => [
'bar',
],
'tags' => [
'd',
],
'max-age' => 300,
],
],
],
],
];
$element = $test_element;
$current_user_role = 'A';
$this->renderer
->renderRoot($element);
$this
->assertRenderCacheItem('parent', [
'#cache_redirect' => TRUE,
'#cache' => [
'keys' => [
'parent',
],
'contexts' => [
'user.roles',
],
'tags' => [
'a',
'b',
],
'bin' => 'render',
'max-age' => Cache::PERMANENT,
],
]);
$this
->assertRenderCacheItem('parent:r.A', [
'#attached' => [],
'#cache' => [
'contexts' => [
'user.roles',
],
'tags' => [
'a',
'b',
],
'max-age' => Cache::PERMANENT,
],
'#markup' => 'parent',
]);
$element = $test_element;
$current_user_role = 'B';
$this->renderer
->renderRoot($element);
$this
->assertRenderCacheItem('parent', [
'#cache_redirect' => TRUE,
'#cache' => [
'keys' => [
'parent',
],
'contexts' => [
'foo',
'user.roles',
],
'tags' => [
'a',
'b',
'c',
],
'bin' => 'render',
'max-age' => 1800,
],
]);
$this
->assertRenderCacheItem('parent:foo:r.B', [
'#attached' => [],
'#cache' => [
'contexts' => [
'foo',
'user.roles',
],
'tags' => [
'a',
'b',
'c',
],
'max-age' => 1800,
],
'#markup' => 'parent',
]);
$element = $test_element;
$current_user_role = 'A';
$this->renderer
->renderRoot($element);
$this
->assertRenderCacheItem('parent', [
'#cache_redirect' => TRUE,
'#cache' => [
'keys' => [
'parent',
],
'contexts' => [
'foo',
'user.roles',
],
'tags' => [
'a',
'b',
'c',
],
'bin' => 'render',
'max-age' => 1800,
],
]);
$this
->assertRenderCacheItem('parent:foo:r.A', [
'#attached' => [],
'#cache' => [
'contexts' => [
'foo',
'user.roles',
],
'tags' => [
'a',
'b',
],
'max-age' => Cache::PERMANENT,
],
'#markup' => 'parent',
]);
$element = $test_element;
$current_user_role = 'C';
$this->renderer
->renderRoot($element);
$final_parent_cache_item = [
'#cache_redirect' => TRUE,
'#cache' => [
'keys' => [
'parent',
],
'contexts' => [
'bar',
'foo',
'user.roles',
],
'tags' => [
'a',
'b',
'c',
'd',
],
'bin' => 'render',
'max-age' => 300,
],
];
$this
->assertRenderCacheItem('parent', $final_parent_cache_item);
$this
->assertRenderCacheItem('parent:bar:foo:r.C', [
'#attached' => [],
'#cache' => [
'contexts' => [
'bar',
'foo',
'user.roles',
],
'tags' => [
'a',
'b',
'c',
'd',
],
'max-age' => 300,
],
'#markup' => 'parent',
]);
$element = $test_element;
$current_user_role = 'A';
$this->renderer
->renderRoot($element);
$this
->assertRenderCacheItem('parent', $final_parent_cache_item);
$this
->assertRenderCacheItem('parent:bar:foo:r.A', [
'#attached' => [],
'#cache' => [
'contexts' => [
'bar',
'foo',
'user.roles',
],
'tags' => [
'a',
'b',
],
'max-age' => Cache::PERMANENT,
],
'#markup' => 'parent',
]);
$element = $test_element;
$current_user_role = 'B';
$this->renderer
->renderRoot($element);
$this
->assertRenderCacheItem('parent', $final_parent_cache_item);
$this
->assertRenderCacheItem('parent:bar:foo:r.B', [
'#attached' => [],
'#cache' => [
'contexts' => [
'bar',
'foo',
'user.roles',
],
'tags' => [
'a',
'b',
'c',
],
'max-age' => 1800,
],
'#markup' => 'parent',
]);
}
public function testBubblingWithPrerender($test_element) {
$this
->setUpRequest();
$this
->setupMemoryCache();
$memory_state = new State(new KeyValueMemoryFactory());
\Drupal::getContainer()
->set('state', $memory_state);
$this->controllerResolver
->expects($this
->any())
->method('getControllerFromDefinition')
->willReturnArgument(0);
$this->themeManager
->expects($this
->any())
->method('render')
->willReturnCallback(function ($hook, $vars) {
return $this->renderer
->render($vars['foo']);
});
\Drupal::state()
->set('bubbling_nested_pre_render_cached', FALSE);
\Drupal::state()
->set('bubbling_nested_pre_render_uncached', FALSE);
$this->memoryCache
->set('cached_nested', [
'#markup' => 'Cached nested!',
'#attached' => [],
'#cache' => [
'contexts' => [],
'tags' => [],
],
]);
$output = $this->renderer
->renderRoot($test_element);
$this
->assertEquals('Cache context!Cache tag!Asset!Placeholder!barquxNested!Cached nested!', trim($output), 'Expected HTML generated.');
$this
->assertEquals([
'child.cache_context',
], $test_element['#cache']['contexts'], 'Expected cache contexts found.');
$this
->assertEquals([
'child:cache_tag',
], $test_element['#cache']['tags'], 'Expected cache tags found.');
$expected_attached = [
'drupalSettings' => [
'foo' => 'bar',
],
'placeholders' => [],
];
$this
->assertEquals($expected_attached, $test_element['#attached'], 'Expected attachments found.');
$this
->assertTrue(\Drupal::state()
->get('bubbling_nested_pre_render_uncached'));
$this
->assertFalse(\Drupal::state()
->get('bubbling_nested_pre_render_cached'));
}
public function providerTestBubblingWithPrerender() {
$data = [];
$data[] = [
[
'foo' => [
'#pre_render' => [
__NAMESPACE__ . '\\BubblingTest::bubblingPreRender',
],
],
],
];
$data[] = [
[
'#theme' => 'common_test_render_element',
'foo' => [
'#pre_render' => [
__NAMESPACE__ . '\\BubblingTest::bubblingPreRender',
],
],
],
];
return $data;
}
public function testOverWriteCacheKeys() {
$this
->setUpRequest();
$this
->setupMemoryCache();
$data = [
'#cache' => [
'keys' => [
'llama',
'bar',
],
],
'#pre_render' => [
__NAMESPACE__ . '\\BubblingTest::bubblingCacheOverwritePrerender',
],
];
$this->renderer
->renderRoot($data);
}
}
class BubblingTest {
public static function bubblingPreRender($elements) {
$elements += [
'child_cache_context' => [
'#cache' => [
'contexts' => [
'child.cache_context',
],
],
'#markup' => 'Cache context!',
],
'child_cache_tag' => [
'#cache' => [
'tags' => [
'child:cache_tag',
],
],
'#markup' => 'Cache tag!',
],
'child_asset' => [
'#attached' => [
'drupalSettings' => [
'foo' => 'bar',
],
],
'#markup' => 'Asset!',
],
'child_placeholder' => [
'#create_placeholder' => TRUE,
'#lazy_builder' => [
__CLASS__ . '::bubblingPlaceholder',
[
'bar',
'qux',
],
],
],
'child_nested_pre_render_uncached' => [
'#cache' => [
'keys' => [
'uncached_nested',
],
],
'#pre_render' => [
__CLASS__ . '::bubblingNestedPreRenderUncached',
],
],
'child_nested_pre_render_cached' => [
'#cache' => [
'keys' => [
'cached_nested',
],
],
'#pre_render' => [
__CLASS__ . '::bubblingNestedPreRenderCached',
],
],
];
return $elements;
}
public static function bubblingNestedPreRenderUncached($elements) {
\Drupal::state()
->set('bubbling_nested_pre_render_uncached', TRUE);
$elements['#markup'] = 'Nested!';
return $elements;
}
public static function bubblingNestedPreRenderCached($elements) {
\Drupal::state()
->set('bubbling_nested_pre_render_cached', TRUE);
return $elements;
}
public static function bubblingPlaceholder($foo, $baz) {
return [
'#markup' => 'Placeholder!' . $foo . $baz,
];
}
public static function bubblingCacheOverwritePrerender($elements) {
$elements['#cache'] = [
'keys' => [
'llama',
'foo',
],
];
$elements['#markup'] = 'Setting cache keys just now!';
return $elements;
}
}