BlockViewBuilderTest.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. <?php
  2. namespace Drupal\Tests\block\Kernel;
  3. use Drupal\Component\Utility\Html;
  4. use Drupal\Core\Cache\Cache;
  5. use Drupal\Core\Language\LanguageInterface;
  6. use Drupal\KernelTests\KernelTestBase;
  7. use Drupal\block\Entity\Block;
  8. /**
  9. * Tests the block view builder.
  10. *
  11. * @group block
  12. */
  13. class BlockViewBuilderTest extends KernelTestBase {
  14. /**
  15. * Modules to install.
  16. *
  17. * @var array
  18. */
  19. public static $modules = ['block', 'block_test', 'system', 'user'];
  20. /**
  21. * The block being tested.
  22. *
  23. * @var \Drupal\block\Entity\BlockInterface
  24. */
  25. protected $block;
  26. /**
  27. * The block storage.
  28. *
  29. * @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
  30. */
  31. protected $controller;
  32. /**
  33. * The renderer.
  34. *
  35. * @var \Drupal\Core\Render\RendererInterface
  36. */
  37. protected $renderer;
  38. /**
  39. * {@inheritdoc}
  40. */
  41. protected function setUp() {
  42. parent::setUp();
  43. $this->controller = $this->container
  44. ->get('entity_type.manager')
  45. ->getStorage('block');
  46. \Drupal::state()->set('block_test.content', 'Llamas &gt; unicorns!');
  47. // Create a block with only required values.
  48. $this->block = $this->controller->create([
  49. 'id' => 'test_block',
  50. 'theme' => 'stark',
  51. 'plugin' => 'test_cache',
  52. ]);
  53. $this->block->save();
  54. $this->container->get('cache.render')->deleteAll();
  55. $this->renderer = $this->container->get('renderer');
  56. }
  57. /**
  58. * Tests the rendering of blocks.
  59. */
  60. public function testBasicRendering() {
  61. \Drupal::state()->set('block_test.content', '');
  62. $entity = $this->controller->create([
  63. 'id' => 'test_block1',
  64. 'theme' => 'stark',
  65. 'plugin' => 'test_html',
  66. ]);
  67. $entity->save();
  68. // Test the rendering of a block.
  69. $entity = Block::load('test_block1');
  70. $output = entity_view($entity, 'block');
  71. $expected = [];
  72. $expected[] = '<div id="block-test-block1">';
  73. $expected[] = ' ';
  74. $expected[] = ' ';
  75. $expected[] = ' ';
  76. $expected[] = ' </div>';
  77. $expected[] = '';
  78. $expected_output = implode("\n", $expected);
  79. $this->assertEqual($this->renderer->renderRoot($output), $expected_output);
  80. // Reset the HTML IDs so that the next render is not affected.
  81. Html::resetSeenIds();
  82. // Test the rendering of a block with a given title.
  83. $entity = $this->controller->create([
  84. 'id' => 'test_block2',
  85. 'theme' => 'stark',
  86. 'plugin' => 'test_html',
  87. 'settings' => [
  88. 'label' => 'Powered by Bananas',
  89. ],
  90. ]);
  91. $entity->save();
  92. $output = entity_view($entity, 'block');
  93. $expected = [];
  94. $expected[] = '<div id="block-test-block2">';
  95. $expected[] = ' ';
  96. $expected[] = ' <h2>Powered by Bananas</h2>';
  97. $expected[] = ' ';
  98. $expected[] = ' ';
  99. $expected[] = ' </div>';
  100. $expected[] = '';
  101. $expected_output = implode("\n", $expected);
  102. $this->assertEqual($this->renderer->renderRoot($output), $expected_output);
  103. }
  104. /**
  105. * Tests block render cache handling.
  106. */
  107. public function testBlockViewBuilderCache() {
  108. // Verify cache handling for a non-empty block.
  109. $this->verifyRenderCacheHandling();
  110. // Create an empty block.
  111. $this->block = $this->controller->create([
  112. 'id' => 'test_block',
  113. 'theme' => 'stark',
  114. 'plugin' => 'test_cache',
  115. ]);
  116. $this->block->save();
  117. \Drupal::state()->set('block_test.content', NULL);
  118. // Verify cache handling for an empty block.
  119. $this->verifyRenderCacheHandling();
  120. }
  121. /**
  122. * Verifies render cache handling of the block being tested.
  123. *
  124. * @see ::testBlockViewBuilderCache()
  125. */
  126. protected function verifyRenderCacheHandling() {
  127. // Force a request via GET so we can test the render cache.
  128. $request = \Drupal::request();
  129. $request_method = $request->server->get('REQUEST_METHOD');
  130. $request->setMethod('GET');
  131. // Test that a cache entry is created.
  132. $build = $this->getBlockRenderArray();
  133. $cid = 'entity_view:block:test_block:' . implode(':', \Drupal::service('cache_contexts_manager')->convertTokensToKeys(['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'])->getKeys());
  134. $this->renderer->renderRoot($build);
  135. $this->assertTrue($this->container->get('cache.render')->get($cid), 'The block render element has been cached.');
  136. // Re-save the block and check that the cache entry has been deleted.
  137. $this->block->save();
  138. $this->assertFalse($this->container->get('cache.render')->get($cid), 'The block render cache entry has been cleared when the block was saved.');
  139. // Rebuild the render array (creating a new cache entry in the process) and
  140. // delete the block to check the cache entry is deleted.
  141. unset($build['#printed']);
  142. // Re-add the block because \Drupal\block\BlockViewBuilder::buildBlock()
  143. // removes it.
  144. $build['#block'] = $this->block;
  145. $this->renderer->renderRoot($build);
  146. $this->assertTrue($this->container->get('cache.render')->get($cid), 'The block render element has been cached.');
  147. $this->block->delete();
  148. $this->assertFalse($this->container->get('cache.render')->get($cid), 'The block render cache entry has been cleared when the block was deleted.');
  149. // Restore the previous request method.
  150. $request->setMethod($request_method);
  151. }
  152. /**
  153. * Tests block view altering.
  154. *
  155. * @see hook_block_view_alter()
  156. * @see hook_block_view_BASE_BLOCK_ID_alter()
  157. */
  158. public function testBlockViewBuilderViewAlter() {
  159. // Establish baseline.
  160. $build = $this->getBlockRenderArray();
  161. $this->setRawContent((string) $this->renderer->renderRoot($build));
  162. $this->assertIdentical(trim((string) $this->cssSelect('div')[0]), 'Llamas > unicorns!');
  163. // Enable the block view alter hook that adds a foo=bar attribute.
  164. \Drupal::state()->set('block_test_view_alter_suffix', TRUE);
  165. Cache::invalidateTags($this->block->getCacheTagsToInvalidate());
  166. $build = $this->getBlockRenderArray();
  167. $this->setRawContent((string) $this->renderer->renderRoot($build));
  168. $this->assertIdentical(trim((string) $this->cssSelect('[foo=bar]')[0]), 'Llamas > unicorns!');
  169. \Drupal::state()->set('block_test_view_alter_suffix', FALSE);
  170. \Drupal::state()->set('block_test.content', NULL);
  171. Cache::invalidateTags($this->block->getCacheTagsToInvalidate());
  172. // Advanced: cached block, but an alter hook adds a #pre_render callback to
  173. // alter the eventual content.
  174. \Drupal::state()->set('block_test_view_alter_append_pre_render_prefix', TRUE);
  175. $build = $this->getBlockRenderArray();
  176. $this->assertFalse(isset($build['#prefix']), 'The appended #pre_render callback has not yet run before rendering.');
  177. $this->assertIdentical((string) $this->renderer->renderRoot($build), 'Hiya!<br>');
  178. $this->assertTrue(isset($build['#prefix']) && $build['#prefix'] === 'Hiya!<br>', 'A cached block without content is altered.');
  179. }
  180. /**
  181. * Tests block build altering.
  182. *
  183. * @see hook_block_build_alter()
  184. * @see hook_block_build_BASE_BLOCK_ID_alter()
  185. */
  186. public function testBlockViewBuilderBuildAlter() {
  187. // Force a request via GET so we can test the render cache.
  188. $request = \Drupal::request();
  189. $request_method = $request->server->get('REQUEST_METHOD');
  190. $request->setMethod('GET');
  191. $default_keys = ['entity_view', 'block', 'test_block'];
  192. $default_contexts = [];
  193. $default_tags = ['block_view', 'config:block.block.test_block'];
  194. $default_max_age = Cache::PERMANENT;
  195. // hook_block_build_alter() adds an additional cache key.
  196. $alter_add_key = $this->randomMachineName();
  197. \Drupal::state()->set('block_test_block_alter_cache_key', $alter_add_key);
  198. $this->assertBlockRenderedWithExpectedCacheability(array_merge($default_keys, [$alter_add_key]), $default_contexts, $default_tags, $default_max_age);
  199. \Drupal::state()->set('block_test_block_alter_cache_key', NULL);
  200. // hook_block_build_alter() adds an additional cache context.
  201. $alter_add_context = 'url.query_args:' . $this->randomMachineName();
  202. \Drupal::state()->set('block_test_block_alter_cache_context', $alter_add_context);
  203. $this->assertBlockRenderedWithExpectedCacheability($default_keys, Cache::mergeContexts($default_contexts, [$alter_add_context]), $default_tags, $default_max_age);
  204. \Drupal::state()->set('block_test_block_alter_cache_context', NULL);
  205. // hook_block_build_alter() adds an additional cache tag.
  206. $alter_add_tag = $this->randomMachineName();
  207. \Drupal::state()->set('block_test_block_alter_cache_tag', $alter_add_tag);
  208. $this->assertBlockRenderedWithExpectedCacheability($default_keys, $default_contexts, Cache::mergeTags($default_tags, [$alter_add_tag]), $default_max_age);
  209. \Drupal::state()->set('block_test_block_alter_cache_tag', NULL);
  210. // hook_block_build_alter() alters the max-age.
  211. $alter_max_age = 300;
  212. \Drupal::state()->set('block_test_block_alter_cache_max_age', $alter_max_age);
  213. $this->assertBlockRenderedWithExpectedCacheability($default_keys, $default_contexts, $default_tags, $alter_max_age);
  214. \Drupal::state()->set('block_test_block_alter_cache_max_age', NULL);
  215. // hook_block_build_alter() alters cache keys, contexts, tags and max-age.
  216. \Drupal::state()->set('block_test_block_alter_cache_key', $alter_add_key);
  217. \Drupal::state()->set('block_test_block_alter_cache_context', $alter_add_context);
  218. \Drupal::state()->set('block_test_block_alter_cache_tag', $alter_add_tag);
  219. \Drupal::state()->set('block_test_block_alter_cache_max_age', $alter_max_age);
  220. $this->assertBlockRenderedWithExpectedCacheability(array_merge($default_keys, [$alter_add_key]), Cache::mergeContexts($default_contexts, [$alter_add_context]), Cache::mergeTags($default_tags, [$alter_add_tag]), $alter_max_age);
  221. \Drupal::state()->set('block_test_block_alter_cache_key', NULL);
  222. \Drupal::state()->set('block_test_block_alter_cache_context', NULL);
  223. \Drupal::state()->set('block_test_block_alter_cache_tag', NULL);
  224. \Drupal::state()->set('block_test_block_alter_cache_max_age', NULL);
  225. // hook_block_build_alter() sets #create_placeholder.
  226. foreach ([TRUE, FALSE] as $value) {
  227. \Drupal::state()->set('block_test_block_alter_create_placeholder', $value);
  228. $build = $this->getBlockRenderArray();
  229. $this->assertTrue(isset($build['#create_placeholder']));
  230. $this->assertIdentical($value, $build['#create_placeholder']);
  231. }
  232. \Drupal::state()->set('block_test_block_alter_create_placeholder', NULL);
  233. // Restore the previous request method.
  234. $request->setMethod($request_method);
  235. }
  236. /**
  237. * Asserts that a block is built/rendered/cached with expected cacheability.
  238. *
  239. * @param string[] $expected_keys
  240. * The expected cache keys.
  241. * @param string[] $expected_contexts
  242. * The expected cache contexts.
  243. * @param string[] $expected_tags
  244. * The expected cache tags.
  245. * @param int $expected_max_age
  246. * The expected max-age.
  247. */
  248. protected function assertBlockRenderedWithExpectedCacheability(array $expected_keys, array $expected_contexts, array $expected_tags, $expected_max_age) {
  249. $required_cache_contexts = ['languages:' . LanguageInterface::TYPE_INTERFACE, 'theme', 'user.permissions'];
  250. // Check that the expected cacheability metadata is present in:
  251. // - the built render array;
  252. $this->pass('Built render array');
  253. $build = $this->getBlockRenderArray();
  254. $this->assertIdentical($expected_keys, $build['#cache']['keys']);
  255. $this->assertIdentical($expected_contexts, $build['#cache']['contexts']);
  256. $this->assertIdentical($expected_tags, $build['#cache']['tags']);
  257. $this->assertIdentical($expected_max_age, $build['#cache']['max-age']);
  258. $this->assertFalse(isset($build['#create_placeholder']));
  259. // - the rendered render array;
  260. $this->pass('Rendered render array');
  261. $this->renderer->renderRoot($build);
  262. // - the render cache item.
  263. $this->pass('Render cache item');
  264. $final_cache_contexts = Cache::mergeContexts($expected_contexts, $required_cache_contexts);
  265. $cid = implode(':', $expected_keys) . ':' . implode(':', \Drupal::service('cache_contexts_manager')->convertTokensToKeys($final_cache_contexts)->getKeys());
  266. $cache_item = $this->container->get('cache.render')->get($cid);
  267. $this->assertTrue($cache_item, 'The block render element has been cached with the expected cache ID.');
  268. $this->assertIdentical(Cache::mergeTags($expected_tags, ['rendered']), $cache_item->tags);
  269. $this->assertIdentical($final_cache_contexts, $cache_item->data['#cache']['contexts']);
  270. $this->assertIdentical($expected_tags, $cache_item->data['#cache']['tags']);
  271. $this->assertIdentical($expected_max_age, $cache_item->data['#cache']['max-age']);
  272. $this->container->get('cache.render')->delete($cid);
  273. }
  274. /**
  275. * Get a fully built render array for a block.
  276. *
  277. * @return array
  278. * The render array.
  279. */
  280. protected function getBlockRenderArray() {
  281. return $this->container->get('entity_type.manager')->getViewBuilder('block')->view($this->block, 'block');
  282. }
  283. }