class RendererBubblingTest in Drupal 9
Same name and namespace in other branches
- 8 core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php \Drupal\Tests\Core\Render\RendererBubblingTest
@coversDefaultClass \Drupal\Core\Render\Renderer @group Render
Hierarchy
- class \Drupal\Tests\UnitTestCase extends \PHPUnit\Framework\TestCase uses \Symfony\Bridge\PhpUnit\ExpectDeprecationTrait, PhpUnitCompatibilityTrait, PhpUnitWarnings
- class \Drupal\Tests\Core\Render\RendererTestBase
- class \Drupal\Tests\Core\Render\RendererBubblingTest
- class \Drupal\Tests\Core\Render\RendererTestBase
Expanded class hierarchy of RendererBubblingTest
File
- core/
tests/ Drupal/ Tests/ Core/ Render/ RendererBubblingTest.php, line 20 - Contains \Drupal\Tests\Core\Render\RendererBubblingTest.
Namespace
Drupal\Tests\Core\RenderView source
class RendererBubblingTest extends RendererTestBase {
/**
* {@inheritdoc}
*/
protected function setUp() : void {
// Disable the required cache contexts, so that this test can test just the
// bubbling behavior.
$this->rendererConfig['required_cache_contexts'] = [];
parent::setUp();
}
/**
* Tests bubbling of assets when NOT using #pre_render callbacks.
*/
public function testBubblingWithoutPreRender() {
$this
->setUpRequest();
$this
->setupMemoryCache();
$this->cacheContextsManager
->expects($this
->any())
->method('convertTokensToKeys')
->willReturnArgument(0);
// Create an element with a child and subchild. Each element loads a
// different library using #attached.
$element = [
'#type' => 'container',
'#cache' => [
'keys' => [
'simpletest',
'renderer',
'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',
];
// Render the element and verify the presence of #attached JavaScript.
$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.');
// Load the element from cache and verify the presence of the #attached
// JavaScript.
$element = [
'#cache' => [
'keys' => [
'simpletest',
'renderer',
'children_attached',
],
],
];
// Verify that the element was retrieved from the cache.
$this
->assertNotEmpty($this->renderer
->renderRoot($element));
$this
->assertEquals($element['#attached']['library'], $expected_libraries, 'The element, child and subchild #attached libraries are included.');
}
/**
* Tests cache context bubbling with a custom cache bin.
*/
public function testContextBubblingCustomCacheBin() {
$bin = $this
->randomMachineName();
$this
->setUpRequest();
$this->memoryCache = new MemoryBackend();
$custom_cache = new MemoryBackend();
$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' => [
'foo',
'bar',
],
'tags' => [],
'bin' => $bin,
'max-age' => 3600,
],
], $bin);
}
/**
* Tests cache context bubbling in edge cases, because it affects the CID.
*
* ::testBubblingWithPrerender() already tests the common case.
*
* @dataProvider providerTestContextBubblingEdgeCases
*/
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
->assertEqualsCanonicalizing($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 = [];
// Cache contexts of inaccessible children aren't bubbled (because those
// children are not rendered at all).
$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,
];
// Assert cache contexts are sorted when they are used to generate a CID.
// (Necessary to ensure that different render arrays where the same keys +
// set of contexts are present point to the same cache item. Regardless of
// the contexts' order. A sad necessity because PHP doesn't have sets.)
$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;
$expected_cache_items['set_test:bar:baz:foo']['#cache']['contexts'] = $context_order;
$data[] = [
$test_element,
$context_order,
$expected_cache_items,
];
}
// A parent with a certain set of cache contexts is unaffected by a child
// that has a subset of those contexts.
$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' => [
'foo',
'bar',
'baz',
],
'tags' => [],
'max-age' => 3600,
],
'#markup' => 'parent',
],
];
$data[] = [
$test_element,
[
'bar',
'baz',
'foo',
],
$expected_cache_items,
];
// A parent with a certain set of cache contexts that is a subset of the
// cache contexts of a child gets a redirecting cache item for the cache ID
// created pre-bubbling (without the child's additional cache contexts). It
// points to a cache item with a post-bubbling cache ID (i.e. with the
// child's additional cache contexts).
// Furthermore, the redirecting cache item also includes the children's
// cache tags, since changes in the children may cause those children to get
// different cache contexts and therefore cause different cache contexts to
// be stored in the redirecting cache item.
$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' => [
// The keys + contexts this redirects to.
'keys' => [
'parent',
],
'contexts' => [
'foo',
'bar',
],
'tags' => [
'yar',
'har',
'fiddle',
'dee',
],
'bin' => 'render',
'max-age' => Cache::PERMANENT,
],
],
'parent:bar:foo' => [
'#attached' => [],
'#cache' => [
'contexts' => [
'foo',
'bar',
],
'tags' => [
'yar',
'har',
'fiddle',
'dee',
],
'max-age' => Cache::PERMANENT,
],
'#markup' => 'parent',
],
];
$data[] = [
$test_element,
[
'bar',
'foo',
],
$expected_cache_items,
];
// Ensure that bubbleable metadata has been collected from children and set
// correctly to the main level of the render array. That ensures that correct
// bubbleable metadata exists if render array gets rendered multiple times.
$test_element = [
'#cache' => [
'keys' => [
'parent',
],
'tags' => [
'yar',
'har',
],
],
'#markup' => 'parent',
'child' => [
'#render_children' => TRUE,
'subchild' => [
'#cache' => [
'contexts' => [
'foo',
],
'tags' => [
'fiddle',
'dee',
],
],
'#attached' => [
'library' => [
'foo/bar',
],
],
'#markup' => '',
],
],
];
$expected_cache_items = [
'parent:foo' => [
'#attached' => [
'library' => [
'foo/bar',
],
],
'#cache' => [
'contexts' => [
'foo',
],
'tags' => [
'yar',
'har',
'fiddle',
'dee',
],
'max-age' => Cache::PERMANENT,
],
'#markup' => 'parent',
],
];
$data[] = [
$test_element,
[
'foo',
],
$expected_cache_items,
];
return $data;
}
/**
* Tests the self-healing of the redirect with conditional cache contexts.
*/
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) {
// Only role A cannot access this subtree.
return $current_user_role !== 'A';
},
'#cache' => [
'contexts' => [
'foo',
],
'tags' => [
'c',
],
// A lower max-age; the redirecting cache item should be updated.
'max-age' => 1800,
],
'grandgrandchild' => [
'#access_callback' => function () use (&$current_user_role) {
// Only role C can access this subtree.
return $current_user_role === 'C';
},
'#cache' => [
'contexts' => [
'bar',
],
'tags' => [
'd',
],
// A lower max-age; the redirecting cache item should be updated.
'max-age' => 300,
],
],
],
],
];
// Request 1: role A, the grandchild isn't accessible => bubbled cache
// contexts: user.roles.
$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',
]);
// Request 2: role B, the grandchild is accessible => bubbled cache
// contexts: foo, user.roles + merged max-age: 1800.
$element = $test_element;
$current_user_role = 'B';
$this->renderer
->renderRoot($element);
$this
->assertRenderCacheItem('parent', [
'#cache_redirect' => TRUE,
'#cache' => [
'keys' => [
'parent',
],
'contexts' => [
'user.roles',
'foo',
],
'tags' => [
'a',
'b',
'c',
],
'bin' => 'render',
'max-age' => 1800,
],
]);
$this
->assertRenderCacheItem('parent:foo:r.B', [
'#attached' => [],
'#cache' => [
'contexts' => [
'user.roles',
'foo',
],
'tags' => [
'a',
'b',
'c',
],
'max-age' => 1800,
],
'#markup' => 'parent',
]);
// Request 3: role A again, the grandchild is inaccessible again => bubbled
// cache contexts: user.roles; but that's a subset of the already-bubbled
// cache contexts, so nothing is actually changed in the redirecting cache
// item. However, the cache item we were looking for in request 1 is
// technically the same one we're looking for now (it's the exact same
// request), but with one additional cache context. This is necessary to
// avoid "cache ping-pong". (Requests 1 and 3 are identical, but without the
// right merging logic to handle request 2, the redirecting cache item would
// toggle between only the 'user.roles' cache context and both the 'foo'
// and 'user.roles' cache contexts, resulting in a cache miss every time.)
$element = $test_element;
$current_user_role = 'A';
$this->renderer
->renderRoot($element);
$this
->assertRenderCacheItem('parent', [
'#cache_redirect' => TRUE,
'#cache' => [
'keys' => [
'parent',
],
'contexts' => [
'user.roles',
'foo',
],
'tags' => [
'a',
'b',
'c',
],
'bin' => 'render',
'max-age' => 1800,
],
]);
$this
->assertRenderCacheItem('parent:foo:r.A', [
'#attached' => [],
'#cache' => [
'contexts' => [
'user.roles',
'foo',
],
'tags' => [
'a',
'b',
],
// Note that the max-age here is unaffected. When role A, the grandchild
// is never rendered, so neither is its max-age of 1800 present here,
// despite 1800 being the max-age of the redirecting cache item.
'max-age' => Cache::PERMANENT,
],
'#markup' => 'parent',
]);
// Request 4: role C, both the grandchild and the grandgrandchild are
// accessible => bubbled cache contexts: foo, bar, user.roles + merged
// max-age: 300.
$element = $test_element;
$current_user_role = 'C';
$this->renderer
->renderRoot($element);
$final_parent_cache_item = [
'#cache_redirect' => TRUE,
'#cache' => [
'keys' => [
'parent',
],
'contexts' => [
'user.roles',
'foo',
'bar',
],
'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' => [
'user.roles',
'foo',
'bar',
],
'tags' => [
'a',
'b',
'c',
'd',
],
'max-age' => 300,
],
'#markup' => 'parent',
]);
// Request 5: role A again, verifying the merging like we did for request 3.
$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' => [
'user.roles',
'foo',
'bar',
],
'tags' => [
'a',
'b',
],
// Note that the max-age here is unaffected. When role A, the grandchild
// is never rendered, so neither is its max-age of 1800 present here,
// nor the grandgrandchild's max-age of 300, despite 300 being the
// max-age of the redirecting cache item.
'max-age' => Cache::PERMANENT,
],
'#markup' => 'parent',
]);
// Request 6: role B again, verifying the merging like we did for request 3.
$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' => [
'user.roles',
'foo',
'bar',
],
'tags' => [
'a',
'b',
'c',
],
// Note that the max-age here is unaffected. When role B, the
// grandgrandchild is never rendered, so neither is its max-age of 300
// present here, despite 300 being the max-age of the redirecting cache
// item.
'max-age' => 1800,
],
'#markup' => 'parent',
]);
}
/**
* Tests bubbling of bubbleable metadata added by #pre_render callbacks.
*
* @dataProvider providerTestBubblingWithPrerender
*/
public function testBubblingWithPrerender($test_element) {
$this
->setUpRequest();
$this
->setupMemoryCache();
// Mock the State service.
$memory_state = new State(new KeyValueMemoryFactory());
\Drupal::getContainer()
->set('state', $memory_state);
$this->controllerResolver
->expects($this
->any())
->method('getControllerFromDefinition')
->willReturnArgument(0);
// Simulate the theme system/Twig: a recursive call to Renderer::render(),
// just like the theme system or a Twig template would have done.
$this->themeManager
->expects($this
->any())
->method('render')
->willReturnCallback(function ($hook, $vars) {
return $this->renderer
->render($vars['foo']);
});
// ::bubblingPreRender() verifies that a #pre_render callback for a render
// array that is cacheable and …
// - … is cached does NOT get called. (Also mock a render cache item.)
// - … is not cached DOES get called.
\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' => [],
],
]);
// Simulate the rendering of an entire response (i.e. a root call).
$output = $this->renderer
->renderRoot($test_element);
// First, assert the render array is of the expected form.
$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.');
// Second, assert that #pre_render callbacks are only executed if they don't
// have a render cache hit (and hence a #pre_render callback for a render
// cached item cannot bubble more metadata).
$this
->assertTrue(\Drupal::state()
->get('bubbling_nested_pre_render_uncached'));
$this
->assertFalse(\Drupal::state()
->get('bubbling_nested_pre_render_cached'));
}
/**
* Provides two test elements: one without, and one with the theme system.
*
* @return array
*/
public function providerTestBubblingWithPrerender() {
$data = [];
// Test element without theme.
$data[] = [
[
'foo' => [
'#pre_render' => [
__NAMESPACE__ . '\\BubblingTest::bubblingPreRender',
],
],
],
];
// Test element with theme.
$data[] = [
[
'#theme' => 'common_test_render_element',
'foo' => [
'#pre_render' => [
__NAMESPACE__ . '\\BubblingTest::bubblingPreRender',
],
],
],
];
return $data;
}
/**
* Tests that an element's cache keys cannot be changed during its rendering.
*/
public function testOverWriteCacheKeys() {
$this
->setUpRequest();
$this
->setupMemoryCache();
// Ensure a logic exception
$data = [
'#cache' => [
'keys' => [
'llama',
'bar',
],
],
'#pre_render' => [
__NAMESPACE__ . '\\BubblingTest::bubblingCacheOverwritePrerender',
],
];
$this
->expectException(\LogicException::class);
$this
->expectExceptionMessage('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.');
$this->renderer
->renderRoot($data);
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
PhpUnitWarnings:: |
private static | property | Deprecation warnings from PHPUnit to raise with @trigger_error(). | |
PhpUnitWarnings:: |
public | function | Converts PHPUnit deprecation warnings to E_USER_DEPRECATED. | |
RendererBubblingTest:: |
public | function | Provides two test elements: one without, and one with the theme system. | |
RendererBubblingTest:: |
public | function | ||
RendererBubblingTest:: |
protected | function |
Overrides RendererTestBase:: |
|
RendererBubblingTest:: |
public | function | Tests bubbling of assets when NOT using #pre_render callbacks. | |
RendererBubblingTest:: |
public | function | Tests bubbling of bubbleable metadata added by #pre_render callbacks. | |
RendererBubblingTest:: |
public | function | Tests the self-healing of the redirect with conditional cache contexts. | |
RendererBubblingTest:: |
public | function | Tests cache context bubbling with a custom cache bin. | |
RendererBubblingTest:: |
public | function | Tests cache context bubbling in edge cases, because it affects the CID. | |
RendererBubblingTest:: |
public | function | Tests that an element's cache keys cannot be changed during its rendering. | |
RendererTestBase:: |
protected | property | ||
RendererTestBase:: |
protected | property | ||
RendererTestBase:: |
protected | property | The mocked controller resolver. | |
RendererTestBase:: |
protected | property | The simulated "current" user role, for use in tests with cache contexts. | |
RendererTestBase:: |
protected | property | The mocked element info. | |
RendererTestBase:: |
protected | property | ||
RendererTestBase:: |
protected | property | The tested placeholder generator. | 1 |
RendererTestBase:: |
protected | property | The tested render cache. | |
RendererTestBase:: |
protected | property | The tested renderer. | |
RendererTestBase:: |
protected | property | The mocked renderer configuration. | |
RendererTestBase:: |
protected | property | ||
RendererTestBase:: |
protected | property | The mocked theme manager. | |
RendererTestBase:: |
protected | function | Asserts a render cache item. | |
RendererTestBase:: |
protected | function | Generates a random context value for the placeholder tests. | |
RendererTestBase:: |
protected | function | Sets up a memory-based render cache back-end. | |
RendererTestBase:: |
protected | function | Sets up a request object on the request stack. | |
RendererTestBase:: |
protected | function | Sets up a render cache back-end that is asserted to be never used. | |
UnitTestCase:: |
protected | property | The random generator. | |
UnitTestCase:: |
protected | property | The app root. | 1 |
UnitTestCase:: |
protected | function | Asserts if two arrays are equal by sorting them first. | |
UnitTestCase:: |
protected | function | Returns a stub class resolver. | |
UnitTestCase:: |
public | function | Returns a stub config factory that behaves according to the passed array. | |
UnitTestCase:: |
public | function | Returns a stub config storage that returns the supplied configuration. | |
UnitTestCase:: |
protected | function | Sets up a container with a cache tags invalidator. | |
UnitTestCase:: |
protected | function | Gets the random generator for the utility methods. | |
UnitTestCase:: |
public | function | Returns a stub translation manager that just returns the passed string. | |
UnitTestCase:: |
public | function | Generates a unique random string containing letters and numbers. | |
UnitTestCase:: |
public static | function |