BlockUiTest.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. <?php
  2. namespace Drupal\Tests\block\Functional;
  3. use Drupal\Component\Utility\Html;
  4. use Drupal\Tests\BrowserTestBase;
  5. /**
  6. * Tests that the block configuration UI exists and stores data correctly.
  7. *
  8. * @group block
  9. */
  10. class BlockUiTest extends BrowserTestBase {
  11. /**
  12. * Modules to install.
  13. *
  14. * @var array
  15. */
  16. public static $modules = ['block', 'block_test', 'help', 'condition_test'];
  17. protected $regions;
  18. /**
  19. * The submitted block values used by this test.
  20. *
  21. * @var array
  22. */
  23. protected $blockValues;
  24. /**
  25. * The block entities used by this test.
  26. *
  27. * @var \Drupal\block\BlockInterface[]
  28. */
  29. protected $blocks;
  30. /**
  31. * An administrative user to configure the test environment.
  32. */
  33. protected $adminUser;
  34. protected function setUp() {
  35. parent::setUp();
  36. // Create and log in an administrative user.
  37. $this->adminUser = $this->drupalCreateUser([
  38. 'administer blocks',
  39. 'access administration pages',
  40. ]);
  41. $this->drupalLogin($this->adminUser);
  42. // Enable some test blocks.
  43. $this->blockValues = [
  44. [
  45. 'label' => 'Tools',
  46. 'tr' => '5',
  47. 'plugin_id' => 'system_menu_block:tools',
  48. 'settings' => ['region' => 'sidebar_second', 'id' => 'tools'],
  49. 'test_weight' => '-1',
  50. ],
  51. [
  52. 'label' => 'Powered by Drupal',
  53. 'tr' => '16',
  54. 'plugin_id' => 'system_powered_by_block',
  55. 'settings' => ['region' => 'footer', 'id' => 'powered'],
  56. 'test_weight' => '0',
  57. ],
  58. ];
  59. $this->blocks = [];
  60. foreach ($this->blockValues as $values) {
  61. $this->blocks[] = $this->drupalPlaceBlock($values['plugin_id'], $values['settings']);
  62. }
  63. }
  64. /**
  65. * Test block demo page exists and functions correctly.
  66. */
  67. public function testBlockDemoUiPage() {
  68. $this->drupalPlaceBlock('help_block', ['region' => 'help']);
  69. $this->drupalGet('admin/structure/block');
  70. $this->clickLink(t('Demonstrate block regions (@theme)', ['@theme' => 'Classy']));
  71. $elements = $this->xpath('//div[contains(@class, "region-highlighted")]/div[contains(@class, "block-region") and contains(text(), :title)]', [':title' => 'Highlighted']);
  72. $this->assertTrue(!empty($elements), 'Block demo regions are shown.');
  73. \Drupal::service('theme_handler')->install(['test_theme']);
  74. $this->drupalGet('admin/structure/block/demo/test_theme');
  75. $this->assertEscaped('<strong>Test theme</strong>');
  76. \Drupal::service('theme_handler')->install(['stable']);
  77. $this->drupalGet('admin/structure/block/demo/stable');
  78. $this->assertResponse(404, 'Hidden themes that are not the default theme are not supported by the block demo screen');
  79. }
  80. /**
  81. * Test block admin page exists and functions correctly.
  82. */
  83. public function testBlockAdminUiPage() {
  84. // Visit the blocks admin ui.
  85. $this->drupalGet('admin/structure/block');
  86. // Look for the blocks table.
  87. $blocks_table = $this->xpath("//table[@id='blocks']");
  88. $this->assertTrue(!empty($blocks_table), 'The blocks table is being rendered.');
  89. // Look for test blocks in the table.
  90. foreach ($this->blockValues as $delta => $values) {
  91. $block = $this->blocks[$delta];
  92. $label = $block->label();
  93. $element = $this->xpath('//*[@id="blocks"]/tbody/tr[' . $values['tr'] . ']/td[1]/text()');
  94. $this->assertEquals($element[0]->getText(), $label, 'The "' . $label . '" block title is set inside the ' . $values['settings']['region'] . ' region.');
  95. // Look for a test block region select form element.
  96. $this->assertField('blocks[' . $values['settings']['id'] . '][region]', 'The block "' . $values['label'] . '" has a region assignment field.');
  97. // Move the test block to the header region.
  98. $edit['blocks[' . $values['settings']['id'] . '][region]'] = 'header';
  99. // Look for a test block weight select form element.
  100. $this->assertField('blocks[' . $values['settings']['id'] . '][weight]', 'The block "' . $values['label'] . '" has a weight assignment field.');
  101. // Change the test block's weight.
  102. $edit['blocks[' . $values['settings']['id'] . '][weight]'] = $values['test_weight'];
  103. }
  104. $this->drupalPostForm('admin/structure/block', $edit, t('Save blocks'));
  105. foreach ($this->blockValues as $values) {
  106. // Check if the region and weight settings changes have persisted.
  107. $this->assertOptionSelected(
  108. 'edit-blocks-' . $values['settings']['id'] . '-region',
  109. 'header',
  110. 'The block "' . $label . '" has the correct region assignment (header).'
  111. );
  112. $this->assertOptionSelected(
  113. 'edit-blocks-' . $values['settings']['id'] . '-weight',
  114. $values['test_weight'],
  115. 'The block "' . $label . '" has the correct weight assignment (' . $values['test_weight'] . ').'
  116. );
  117. }
  118. // Add a block with a machine name the same as a region name.
  119. $this->drupalPlaceBlock('system_powered_by_block', ['region' => 'header', 'id' => 'header']);
  120. $this->drupalGet('admin/structure/block');
  121. $element = $this->xpath('//tr[contains(@class, :class)]', [':class' => 'region-title-header']);
  122. $this->assertTrue(!empty($element));
  123. // Ensure hidden themes do not appear in the UI. Enable another non base
  124. // theme and place the local tasks block.
  125. $this->assertTrue(\Drupal::service('theme_handler')->themeExists('classy'), 'The classy base theme is enabled');
  126. $this->drupalPlaceBlock('local_tasks_block', ['region' => 'header']);
  127. \Drupal::service('theme_installer')->install(['stable', 'stark']);
  128. $this->drupalGet('admin/structure/block');
  129. $theme_handler = \Drupal::service('theme_handler');
  130. $this->assertLink($theme_handler->getName('classy'));
  131. $this->assertLink($theme_handler->getName('stark'));
  132. $this->assertNoLink($theme_handler->getName('stable'));
  133. $this->drupalGet('admin/structure/block/list/stable');
  134. $this->assertResponse(404, 'Placing blocks through UI is not possible for a hidden base theme.');
  135. \Drupal::configFactory()->getEditable('system.theme')->set('admin', 'stable')->save();
  136. \Drupal::service('router.builder')->rebuildIfNeeded();
  137. $this->drupalPlaceBlock('local_tasks_block', ['region' => 'header', 'theme' => 'stable']);
  138. $this->drupalGet('admin/structure/block');
  139. $this->assertLink($theme_handler->getName('stable'));
  140. $this->drupalGet('admin/structure/block/list/stable');
  141. $this->assertResponse(200, 'Placing blocks through UI is possible for a hidden base theme that is the admin theme.');
  142. }
  143. /**
  144. * Tests the block categories on the listing page.
  145. */
  146. public function testCandidateBlockList() {
  147. $arguments = [
  148. ':title' => 'Display message',
  149. ':category' => 'Block test',
  150. ':href' => 'admin/structure/block/add/test_block_instantiation/classy',
  151. ];
  152. $pattern = '//tr[.//td/div[text()=:title] and .//td[text()=:category] and .//td//a[contains(@href, :href)]]';
  153. $this->drupalGet('admin/structure/block');
  154. $this->clickLink('Place block');
  155. $elements = $this->xpath($pattern, $arguments);
  156. $this->assertTrue(!empty($elements), 'The test block appears in the category for its module.');
  157. // Trigger the custom category addition in block_test_block_alter().
  158. $this->container->get('state')->set('block_test_info_alter', TRUE);
  159. $this->container->get('plugin.manager.block')->clearCachedDefinitions();
  160. $this->drupalGet('admin/structure/block');
  161. $this->clickLink('Place block');
  162. $arguments[':category'] = 'Custom category';
  163. $elements = $this->xpath($pattern, $arguments);
  164. $this->assertTrue(!empty($elements), 'The test block appears in a custom category controlled by block_test_block_alter().');
  165. }
  166. /**
  167. * Tests the behavior of unsatisfied context-aware blocks.
  168. */
  169. public function testContextAwareUnsatisfiedBlocks() {
  170. $arguments = [
  171. ':category' => 'Block test',
  172. ':href' => 'admin/structure/block/add/test_context_aware_unsatisfied/classy',
  173. ':text' => 'Test context-aware unsatisfied block',
  174. ];
  175. $this->drupalGet('admin/structure/block');
  176. $this->clickLink('Place block');
  177. $elements = $this->xpath('//tr[.//td/div[text()=:text] and .//td[text()=:category] and .//td//a[contains(@href, :href)]]', $arguments);
  178. $this->assertTrue(empty($elements), 'The context-aware test block does not appear.');
  179. $definition = \Drupal::service('plugin.manager.block')->getDefinition('test_context_aware_unsatisfied');
  180. $this->assertTrue(!empty($definition), 'The context-aware test block does not exist.');
  181. }
  182. /**
  183. * Tests the behavior of context-aware blocks.
  184. */
  185. public function testContextAwareBlocks() {
  186. $expected_text = '<div id="test_context_aware--username">' . \Drupal::currentUser()->getUsername() . '</div>';
  187. $this->drupalGet('');
  188. $this->assertNoText('Test context-aware block');
  189. $this->assertNoRaw($expected_text);
  190. $block_url = 'admin/structure/block/add/test_context_aware/classy';
  191. $arguments = [
  192. ':title' => 'Test context-aware block',
  193. ':category' => 'Block test',
  194. ':href' => $block_url,
  195. ];
  196. $pattern = '//tr[.//td/div[text()=:title] and .//td[text()=:category] and .//td//a[contains(@href, :href)]]';
  197. $this->drupalGet('admin/structure/block');
  198. $this->clickLink('Place block');
  199. $elements = $this->xpath($pattern, $arguments);
  200. $this->assertTrue(!empty($elements), 'The context-aware test block appears.');
  201. $definition = \Drupal::service('plugin.manager.block')->getDefinition('test_context_aware');
  202. $this->assertTrue(!empty($definition), 'The context-aware test block exists.');
  203. $edit = [
  204. 'region' => 'content',
  205. 'settings[context_mapping][user]' => '@block_test.multiple_static_context:userB',
  206. ];
  207. $this->drupalPostForm($block_url, $edit, 'Save block');
  208. $this->drupalGet('');
  209. $this->assertText('Test context-aware block');
  210. $this->assertText('User context found.');
  211. $this->assertRaw($expected_text);
  212. // Test context mapping allows empty selection for optional contexts.
  213. $this->drupalGet('admin/structure/block/manage/testcontextawareblock');
  214. $edit = [
  215. 'settings[context_mapping][user]' => '',
  216. ];
  217. $this->drupalPostForm(NULL, $edit, 'Save block');
  218. $this->drupalGet('');
  219. $this->assertText('No context mapping selected.');
  220. $this->assertNoText('User context found.');
  221. // Tests that conditions with missing context are not displayed.
  222. $this->drupalGet('admin/structure/block/manage/testcontextawareblock');
  223. $this->assertNoRaw('No existing type');
  224. $this->assertNoFieldByXPath('//*[@name="visibility[condition_test_no_existing_type][negate]"]');
  225. }
  226. /**
  227. * Tests that the BlockForm populates machine name correctly.
  228. */
  229. public function testMachineNameSuggestion() {
  230. $url = 'admin/structure/block/add/test_block_instantiation/classy';
  231. $this->drupalGet($url);
  232. $this->assertFieldByName('id', 'displaymessage', 'Block form uses raw machine name suggestion when no instance already exists.');
  233. $edit = ['region' => 'content'];
  234. $this->drupalPostForm($url, $edit, 'Save block');
  235. $this->assertText('The block configuration has been saved.');
  236. // Now, check to make sure the form starts by autoincrementing correctly.
  237. $this->drupalGet($url);
  238. $this->assertFieldByName('id', 'displaymessage_2', 'Block form appends _2 to plugin-suggested machine name when an instance already exists.');
  239. $this->drupalPostForm($url, $edit, 'Save block');
  240. $this->assertText('The block configuration has been saved.');
  241. // And verify that it continues working beyond just the first two.
  242. $this->drupalGet($url);
  243. $this->assertFieldByName('id', 'displaymessage_3', 'Block form appends _3 to plugin-suggested machine name when two instances already exist.');
  244. }
  245. /**
  246. * Tests the block placement indicator.
  247. */
  248. public function testBlockPlacementIndicator() {
  249. // Select the 'Powered by Drupal' block to be placed.
  250. $block = [];
  251. $block['id'] = strtolower($this->randomMachineName());
  252. $block['theme'] = 'classy';
  253. $block['region'] = 'content';
  254. // After adding a block, it will indicate which block was just added.
  255. $this->drupalPostForm('admin/structure/block/add/system_powered_by_block', $block, t('Save block'));
  256. $this->assertUrl('admin/structure/block/list/classy?block-placement=' . Html::getClass($block['id']));
  257. // Resaving the block page will remove the block indicator.
  258. $this->drupalPostForm(NULL, [], t('Save blocks'));
  259. $this->assertUrl('admin/structure/block/list/classy');
  260. }
  261. /**
  262. * Tests if validation errors are passed plugin form to the parent form.
  263. */
  264. public function testBlockValidateErrors() {
  265. $this->drupalPostForm('admin/structure/block/add/test_settings_validation/classy', ['region' => 'content', 'settings[digits]' => 'abc'], t('Save block'));
  266. $arguments = [':message' => 'Only digits are allowed'];
  267. $pattern = '//div[contains(@class,"messages messages--error")]/div[contains(text()[2],:message)]';
  268. $elements = $this->xpath($pattern, $arguments);
  269. $this->assertTrue($elements, 'Plugin error message found in parent form.');
  270. $error_class_pattern = '//div[contains(@class,"form-item-settings-digits")]/input[contains(@class,"error")]';
  271. $error_class = $this->xpath($error_class_pattern);
  272. $this->assertTrue($error_class, 'Plugin error class found in parent form.');
  273. }
  274. /**
  275. * Tests that the enable/disable routes are protected from CSRF.
  276. */
  277. public function testRouteProtection() {
  278. // Get the first block generated in our setUp method.
  279. /** @var \Drupal\block\BlockInterface $block */
  280. $block = reset($this->blocks);
  281. // Ensure that the enable and disable routes are protected.
  282. $this->drupalGet('admin/structure/block/manage/' . $block->id() . '/disable');
  283. $this->assertResponse(403);
  284. $this->drupalGet('admin/structure/block/manage/' . $block->id() . '/enable');
  285. $this->assertResponse(403);
  286. }
  287. }