BlockUiTest.php 17.5 KB
Newer Older
1 2
<?php

3
namespace Drupal\Tests\block\Functional;
4

5
use Drupal\Component\Utility\Html;
6 7
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
8
use Drupal\Tests\BrowserTestBase;
9

10 11
// cspell:ignore scriptalertxsssubjectscript

12
/**
13 14 15
 * Tests that the block configuration UI exists and stores data correctly.
 *
 * @group block
16
 */
17
class BlockUiTest extends BrowserTestBase {
18 19

  /**
20
   * Modules to install.
21 22 23
   *
   * @var array
   */
24 25 26 27 28 29
  protected static $modules = [
    'block',
    'block_test',
    'help',
    'condition_test',
  ];
30

31 32 33 34 35
  /**
   * {@inheritdoc}
   */
  protected $defaultTheme = 'classy';

36 37
  protected $regions;

38 39 40 41 42 43 44 45 46 47 48 49 50 51
  /**
   * The submitted block values used by this test.
   *
   * @var array
   */
  protected $blockValues;

  /**
   * The block entities used by this test.
   *
   * @var \Drupal\block\BlockInterface[]
   */
  protected $blocks;

52 53 54 55 56
  /**
   * An administrative user to configure the test environment.
   */
  protected $adminUser;

57
  protected function setUp(): void {
58 59
    parent::setUp();
    // Create and log in an administrative user.
60
    $this->adminUser = $this->drupalCreateUser([
61 62
      'administer blocks',
      'access administration pages',
63
    ]);
64
    $this->drupalLogin($this->adminUser);
65 66

    // Enable some test blocks.
67 68
    $this->blockValues = [
      [
69 70
        'label' => 'Tools',
        'tr' => '5',
71
        'plugin_id' => 'system_menu_block:tools',
72
        'settings' => ['region' => 'sidebar_second', 'id' => 'tools'],
73
        'test_weight' => '-1',
74 75
      ],
      [
76
        'label' => 'Powered by Drupal',
77
        'tr' => '16',
78
        'plugin_id' => 'system_powered_by_block',
79
        'settings' => ['region' => 'footer', 'id' => 'powered'],
80
        'test_weight' => '0',
81 82 83
      ],
    ];
    $this->blocks = [];
84 85
    foreach ($this->blockValues as $values) {
      $this->blocks[] = $this->drupalPlaceBlock($values['plugin_id'], $values['settings']);
86
    }
87 88
  }

89 90 91 92
  /**
   * Test block demo page exists and functions correctly.
   */
  public function testBlockDemoUiPage() {
93
    $this->drupalPlaceBlock('help_block', ['region' => 'help']);
94
    $this->drupalGet('admin/structure/block');
95 96
    $this->clickLink(t('Demonstrate block regions (@theme)', ['@theme' => 'Classy']));
    $elements = $this->xpath('//div[contains(@class, "region-highlighted")]/div[contains(@class, "block-region") and contains(text(), :title)]', [':title' => 'Highlighted']);
97
    $this->assertTrue(!empty($elements), 'Block demo regions are shown.');
98

99
    // Ensure that other themes can use the block demo page.
100
    \Drupal::service('theme_installer')->install(['test_theme']);
101
    $this->drupalGet('admin/structure/block/demo/test_theme');
102
    $this->assertSession()->assertEscaped('<strong>Test theme</strong>');
103

104
    // Ensure that a hidden theme cannot use the block demo page.
105
    \Drupal::service('theme_installer')->install(['stable']);
106
    $this->drupalGet('admin/structure/block/demo/stable');
107
    $this->assertSession()->statusCodeEquals(404);
108 109
  }

110
  /**
111
   * Test block admin page exists and functions correctly.
112
   */
113
  public function testBlockAdminUiPage() {
114 115 116 117 118 119
    // Visit the blocks admin ui.
    $this->drupalGet('admin/structure/block');
    // Look for the blocks table.
    $blocks_table = $this->xpath("//table[@id='blocks']");
    $this->assertTrue(!empty($blocks_table), 'The blocks table is being rendered.');
    // Look for test blocks in the table.
120 121 122
    foreach ($this->blockValues as $delta => $values) {
      $block = $this->blocks[$delta];
      $label = $block->label();
123
      $element = $this->xpath('//*[@id="blocks"]/tbody/tr[' . $values['tr'] . ']/td[1]/text()');
124
      $this->assertEquals($element[0]->getText(), $label, 'The "' . $label . '" block title is set inside the ' . $values['settings']['region'] . ' region.');
125
      // Look for a test block region select form element.
126
      $this->assertSession()->fieldExists('blocks[' . $values['settings']['id'] . '][region]');
127
      // Move the test block to the header region.
128
      $edit['blocks[' . $values['settings']['id'] . '][region]'] = 'header';
129
      // Look for a test block weight select form element.
130
      $this->assertSession()->fieldExists('blocks[' . $values['settings']['id'] . '][weight]');
131
      // Change the test block's weight.
132
      $edit['blocks[' . $values['settings']['id'] . '][weight]'] = $values['test_weight'];
133
    }
134
    $this->drupalPostForm('admin/structure/block', $edit, t('Save blocks'));
135
    foreach ($this->blockValues as $values) {
136
      // Check if the region and weight settings changes have persisted.
137 138
      $this->assertTrue($this->assertSession()->optionExists('edit-blocks-' . $values['settings']['id'] . '-region', 'header')->isSelected());
      $this->assertTrue($this->assertSession()->optionExists('edit-blocks-' . $values['settings']['id'] . '-weight', $values['test_weight'])->isSelected());
139
    }
140 141 142 143 144 145

    // Add a block with a machine name the same as a region name.
    $this->drupalPlaceBlock('system_powered_by_block', ['region' => 'header', 'id' => 'header']);
    $this->drupalGet('admin/structure/block');
    $element = $this->xpath('//tr[contains(@class, :class)]', [':class' => 'region-title-header']);
    $this->assertTrue(!empty($element));
146 147 148 149 150 151 152 153

    // Ensure hidden themes do not appear in the UI. Enable another non base
    // theme and place the local tasks block.
    $this->assertTrue(\Drupal::service('theme_handler')->themeExists('classy'), 'The classy base theme is enabled');
    $this->drupalPlaceBlock('local_tasks_block', ['region' => 'header']);
    \Drupal::service('theme_installer')->install(['stable', 'stark']);
    $this->drupalGet('admin/structure/block');
    $theme_handler = \Drupal::service('theme_handler');
154 155 156
    $this->assertSession()->linkExists($theme_handler->getName('classy'));
    $this->assertSession()->linkExists($theme_handler->getName('stark'));
    $this->assertSession()->linkNotExists($theme_handler->getName('stable'));
157

158
    // Ensure that a hidden theme cannot use the block demo page.
159
    $this->drupalGet('admin/structure/block/list/stable');
160
    $this->assertSession()->statusCodeEquals(404);
161

162 163
    // Ensure that a hidden theme set as the admin theme can use the block demo
    // page.
164 165 166 167
    \Drupal::configFactory()->getEditable('system.theme')->set('admin', 'stable')->save();
    \Drupal::service('router.builder')->rebuildIfNeeded();
    $this->drupalPlaceBlock('local_tasks_block', ['region' => 'header', 'theme' => 'stable']);
    $this->drupalGet('admin/structure/block');
168
    $this->assertSession()->linkExists($theme_handler->getName('stable'));
169
    $this->drupalGet('admin/structure/block/list/stable');
170
    $this->assertSession()->statusCodeEquals(200);
171
  }
172 173

  /**
174
   * Tests the block categories on the listing page.
175
   */
176
  public function testCandidateBlockList() {
177
    $arguments = [
178 179
      ':title' => 'Display message',
      ':category' => 'Block test',
180
      ':href' => 'admin/structure/block/add/test_block_instantiation/classy',
181
    ];
182
    $pattern = '//tr[.//td/div[text()=:title] and .//td[text()=:category] and .//td//a[contains(@href, :href)]]';
183 184

    $this->drupalGet('admin/structure/block');
185
    $this->clickLink('Place block');
186
    $elements = $this->xpath($pattern, $arguments);
187 188 189 190 191 192 193
    $this->assertTrue(!empty($elements), 'The test block appears in the category for its module.');

    // Trigger the custom category addition in block_test_block_alter().
    $this->container->get('state')->set('block_test_info_alter', TRUE);
    $this->container->get('plugin.manager.block')->clearCachedDefinitions();

    $this->drupalGet('admin/structure/block');
194
    $this->clickLink('Place block');
195 196
    $arguments[':category'] = 'Custom category';
    $elements = $this->xpath($pattern, $arguments);
197
    $this->assertTrue(!empty($elements), 'The test block appears in a custom category controlled by block_test_block_alter().');
198
  }
199

200 201 202 203
  /**
   * Tests the behavior of unsatisfied context-aware blocks.
   */
  public function testContextAwareUnsatisfiedBlocks() {
204
    $arguments = [
205 206 207
      ':category' => 'Block test',
      ':href' => 'admin/structure/block/add/test_context_aware_unsatisfied/classy',
      ':text' => 'Test context-aware unsatisfied block',
208
    ];
209 210

    $this->drupalGet('admin/structure/block');
211
    $this->clickLink('Place block');
212 213 214 215 216 217 218
    $elements = $this->xpath('//tr[.//td/div[text()=:text] and .//td[text()=:category] and .//td//a[contains(@href, :href)]]', $arguments);
    $this->assertTrue(empty($elements), 'The context-aware test block does not appear.');

    $definition = \Drupal::service('plugin.manager.block')->getDefinition('test_context_aware_unsatisfied');
    $this->assertTrue(!empty($definition), 'The context-aware test block does not exist.');
  }

219 220 221 222
  /**
   * Tests the behavior of context-aware blocks.
   */
  public function testContextAwareBlocks() {
223
    $expected_text = '<div id="test_context_aware--username">' . \Drupal::currentUser()->getAccountName() . '</div>';
224 225 226 227 228
    $this->drupalGet('');
    $this->assertNoText('Test context-aware block');
    $this->assertNoRaw($expected_text);

    $block_url = 'admin/structure/block/add/test_context_aware/classy';
229
    $arguments = [
230 231 232
      ':title' => 'Test context-aware block',
      ':category' => 'Block test',
      ':href' => $block_url,
233
    ];
234
    $pattern = '//tr[.//td/div[text()=:title] and .//td[text()=:category] and .//td//a[contains(@href, :href)]]';
235 236

    $this->drupalGet('admin/structure/block');
237
    $this->clickLink('Place block');
238 239
    $elements = $this->xpath($pattern, $arguments);
    $this->assertTrue(!empty($elements), 'The context-aware test block appears.');
240 241
    $definition = \Drupal::service('plugin.manager.block')->getDefinition('test_context_aware');
    $this->assertTrue(!empty($definition), 'The context-aware test block exists.');
242 243
    $edit = [
      'region' => 'content',
244
      'settings[context_mapping][user]' => '@block_test.multiple_static_context:userB',
245 246 247 248 249
    ];
    $this->drupalPostForm($block_url, $edit, 'Save block');

    $this->drupalGet('');
    $this->assertText('Test context-aware block');
250
    $this->assertText('User context found.');
251
    $this->assertRaw($expected_text);
252

253 254 255 256 257 258 259
    // Test context mapping form element is not visible if there are no valid
    // context options for the block (the test_context_aware_no_valid_context_options
    // block has one context defined which is not available for it on the
    // Block Layout interface).
    $this->drupalGet('admin/structure/block/add/test_context_aware_no_valid_context_options/classy');
    $this->assertSession()->fieldNotExists('edit-settings-context-mapping-email');

260 261 262 263 264 265 266 267
    // Test context mapping allows empty selection for optional contexts.
    $this->drupalGet('admin/structure/block/manage/testcontextawareblock');
    $edit = [
      'settings[context_mapping][user]' => '',
    ];
    $this->drupalPostForm(NULL, $edit, 'Save block');
    $this->drupalGet('');
    $this->assertText('No context mapping selected.');
268
    $this->assertNoText('User context found.');
269 270 271 272

    // Tests that conditions with missing context are not displayed.
    $this->drupalGet('admin/structure/block/manage/testcontextawareblock');
    $this->assertNoRaw('No existing type');
273
    $this->assertSession()->elementNotExists('xpath', '//*[@name="visibility[condition_test_no_existing_type][negate]"]');
274 275
  }

276
  /**
277
   * Tests that the BlockForm populates machine name correctly.
278 279
   */
  public function testMachineNameSuggestion() {
280 281
    // Check the form uses the raw machine name suggestion when no instance
    // already exists.
282
    $url = 'admin/structure/block/add/test_block_instantiation/classy';
283
    $this->drupalGet($url);
284
    $this->assertSession()->fieldValueEquals('id', 'displaymessage');
285 286 287
    $edit = ['region' => 'content'];
    $this->drupalPostForm($url, $edit, 'Save block');
    $this->assertText('The block configuration has been saved.');
288 289 290

    // Now, check to make sure the form starts by autoincrementing correctly.
    $this->drupalGet($url);
291
    $this->assertSession()->fieldValueEquals('id', 'displaymessage_2');
292 293
    $this->drupalPostForm($url, $edit, 'Save block');
    $this->assertText('The block configuration has been saved.');
294 295 296

    // And verify that it continues working beyond just the first two.
    $this->drupalGet($url);
297
    $this->assertSession()->fieldValueEquals('id', 'displaymessage_3');
298 299
  }

300 301 302 303
  /**
   * Tests the block placement indicator.
   */
  public function testBlockPlacementIndicator() {
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
    // Test the block placement indicator with using the domain as URL language
    // indicator. This causes destination query parameters to be absolute URLs.
    \Drupal::service('module_installer')->install(['language', 'locale']);
    $this->container = \Drupal::getContainer();
    ConfigurableLanguage::createFromLangcode('it')->save();
    $config = $this->config('language.types');
    $config->set('negotiation.language_interface.enabled', [
      LanguageNegotiationUrl::METHOD_ID => -10,
    ]);
    $config->save();
    $config = $this->config('language.negotiation');
    $config->set('url.source', LanguageNegotiationUrl::CONFIG_DOMAIN);
    $config->set('url.domains', [
      'en' => \Drupal::request()->getHost(),
      'it' => 'it.example.com',
    ]);
    $config->save();

322
    // Select the 'Powered by Drupal' block to be placed.
323
    $block = [];
324
    $block['id'] = strtolower($this->randomMachineName());
325
    $block['theme'] = 'classy';
326 327 328 329
    $block['region'] = 'content';

    // After adding a block, it will indicate which block was just added.
    $this->drupalPostForm('admin/structure/block/add/system_powered_by_block', $block, t('Save block'));
330
    $this->assertSession()->addressEquals('admin/structure/block/list/classy?block-placement=' . Html::getClass($block['id']));
331

332
    // Resaving the block page will remove the block placement indicator.
333
    $this->drupalPostForm(NULL, [], t('Save blocks'));
334 335 336 337 338 339
    $this->assertSession()->addressEquals('admin/structure/block/list/classy');

    // Place another block and test the remove functionality works with the
    // block placement indicator. Click the first 'Place block' link to bring up
    // the list of blocks to place in the first available region.
    $this->clickLink('Place block');
340 341 342
    // Select the first available block, which is the 'test_xss_title' plugin,
    // with a default machine name 'scriptalertxsssubjectscript' that is used
    // for the 'block-placement' querystring parameter.
343 344
    $this->clickLink('Place block');
    $this->submitForm([], 'Save block');
345
    $this->assertSession()->addressEquals('admin/structure/block/list/classy?block-placement=scriptalertxsssubjectscript');
346 347 348 349 350 351 352

    // Removing a block will remove the block placement indicator.
    $this->clickLink('Remove');
    $this->submitForm([], 'Remove');
    // @todo https://www.drupal.org/project/drupal/issues/2980527 this should be
    //   'admin/structure/block/list/classy' but there is a bug.
    $this->assertSession()->addressEquals('admin/structure/block');
353 354
  }

355 356 357 358
  /**
   * Tests if validation errors are passed plugin form to the parent form.
   */
  public function testBlockValidateErrors() {
359
    $this->drupalPostForm('admin/structure/block/add/test_settings_validation/classy', ['region' => 'content', 'settings[digits]' => 'abc'], t('Save block'));
360 361 362 363

    $arguments = [':message' => 'Only digits are allowed'];
    $pattern = '//div[contains(@class,"messages messages--error")]/div[contains(text()[2],:message)]';
    $elements = $this->xpath($pattern, $arguments);
364
    $this->assertNotEmpty($elements, 'Plugin error message found in parent form.');
365 366 367

    $error_class_pattern = '//div[contains(@class,"form-item-settings-digits")]/input[contains(@class,"error")]';
    $error_class = $this->xpath($error_class_pattern);
368
    $this->assertNotEmpty($error_class, 'Plugin error class found in parent form.');
369 370
  }

371 372 373 374 375 376 377 378 379
  /**
   * Tests that the enable/disable routes are protected from CSRF.
   */
  public function testRouteProtection() {
    // Get the first block generated in our setUp method.
    /** @var \Drupal\block\BlockInterface $block */
    $block = reset($this->blocks);
    // Ensure that the enable and disable routes are protected.
    $this->drupalGet('admin/structure/block/manage/' . $block->id() . '/disable');
380
    $this->assertSession()->statusCodeEquals(403);
381
    $this->drupalGet('admin/structure/block/manage/' . $block->id() . '/enable');
382
    $this->assertSession()->statusCodeEquals(403);
383 384
  }

385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415
  /**
   * Tests that users without permission are not able to view broken blocks.
   */
  public function testBrokenBlockVisibility() {
    $assert_session = $this->assertSession();

    $this->drupalPlaceBlock('broken');

    // Login as an admin user to the site.
    $this->drupalLogin($this->adminUser);
    $this->drupalGet('');
    $assert_session->statusCodeEquals(200);
    // Check that this user can view the Broken Block message.
    $assert_session->pageTextContains('This block is broken or missing. You may be missing content or you might need to enable the original module.');
    $this->drupalLogout();

    // Visit the same page as anonymous.
    $this->drupalGet('');
    $assert_session->statusCodeEquals(200);
    // Check that this user cannot view the Broken Block message.
    $assert_session->pageTextNotContains('This block is broken or missing. You may be missing content or you might need to enable the original module.');

    // Visit same page as an authorized user that does not have access to
    // administer blocks.
    $this->drupalLogin($this->drupalCreateUser(['access administration pages']));
    $this->drupalGet('');
    $assert_session->statusCodeEquals(200);
    // Check that this user cannot view the Broken Block message.
    $assert_session->pageTextNotContains('This block is broken or missing. You may be missing content or you might need to enable the original module.');
  }

416
}