1<?php
2
3namespace Drupal\Tests\block\Functional\Views;
4
5use Drupal\Component\Render\FormattableMarkup;
6use Drupal\Component\Serialization\Json;
7use Drupal\Component\Utility\Crypt;
8use Drupal\Core\Site\Settings;
9use Drupal\Core\Url;
10use Drupal\Tests\block\Functional\AssertBlockAppearsTrait;
11use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
12use Drupal\Tests\views\Functional\ViewTestBase;
13use Drupal\views\Entity\View;
14use Drupal\views\Views;
15use Drupal\views\Tests\ViewTestData;
16use Drupal\Core\Template\Attribute;
17
18/**
19 * Tests the block display plugin.
20 *
21 * @group block
22 * @see \Drupal\views\Plugin\views\display\Block
23 */
24class DisplayBlockTest extends ViewTestBase {
25
26  use AssertPageCacheContextsAndTagsTrait;
27  use AssertBlockAppearsTrait;
28
29  /**
30   * Modules to install.
31   *
32   * @var array
33   */
34  public static $modules = [
35    'node',
36    'block_test_views',
37    'test_page_test',
38    'contextual',
39    'views_ui',
40  ];
41
42  /**
43   * {@inheritdoc}
44   */
45  protected $defaultTheme = 'classy';
46
47  /**
48   * Views used by this test.
49   *
50   * @var array
51   */
52  public static $testViews = ['test_view_block', 'test_view_block2'];
53
54  /**
55   * {@inheritdoc}
56   */
57  protected function setUp($import_test_views = TRUE) {
58    parent::setUp($import_test_views);
59
60    ViewTestData::createTestViews(get_class($this), ['block_test_views']);
61    $this->enableViewsTestModule();
62  }
63
64  /**
65   * Tests default and custom block categories.
66   */
67  public function testBlockCategory() {
68    $this->drupalLogin($this->drupalCreateUser([
69      'administer views',
70      'administer blocks',
71    ]));
72
73    // Create a new view in the UI.
74    $edit = [];
75    $edit['label'] = $this->randomString();
76    $edit['id'] = strtolower($this->randomMachineName());
77    $edit['show[wizard_key]'] = 'standard:views_test_data';
78    $edit['description'] = $this->randomString();
79    $edit['block[create]'] = TRUE;
80    $edit['block[style][row_plugin]'] = 'fields';
81    $this->drupalPostForm('admin/structure/views/add', $edit, t('Save and edit'));
82
83    $pattern = '//tr[.//td[text()=:category] and .//td//a[contains(@href, :href)]]';
84
85    // Test that the block was given a default category corresponding to its
86    // base table.
87    $arguments = [
88      ':href' => Url::fromRoute('block.admin_add', [
89        'plugin_id' => 'views_block:' . $edit['id'] . '-block_1',
90        'theme' => 'classy',
91      ])->toString(),
92      ':category' => 'Lists (Views)',
93    ];
94    $this->drupalGet('admin/structure/block');
95    $this->clickLink('Place block');
96    $elements = $this->xpath($pattern, $arguments);
97    $this->assertTrue(!empty($elements), 'The test block appears in the category for its base table.');
98
99    // Duplicate the block before changing the category.
100    $this->drupalPostForm('admin/structure/views/view/' . $edit['id'] . '/edit/block_1', [], t('Duplicate @display_title', ['@display_title' => 'Block']));
101    $this->assertUrl('admin/structure/views/view/' . $edit['id'] . '/edit/block_2');
102
103    // Change the block category to a random string.
104    $this->drupalGet('admin/structure/views/view/' . $edit['id'] . '/edit/block_1');
105    $link = $this->xpath('//a[@id="views-block-1-block-category" and normalize-space(text())=:category]', $arguments);
106    $this->assertTrue(!empty($link));
107    $this->clickLink(t('Lists (Views)'));
108    $category = $this->randomString();
109    $this->drupalPostForm(NULL, ['block_category' => $category], t('Apply'));
110
111    // Duplicate the block after changing the category.
112    $this->drupalPostForm(NULL, [], t('Duplicate @display_title', ['@display_title' => 'Block']));
113    $this->assertUrl('admin/structure/views/view/' . $edit['id'] . '/edit/block_3');
114
115    $this->drupalPostForm(NULL, [], t('Save'));
116
117    // Test that the blocks are listed under the correct categories.
118    $arguments[':category'] = $category;
119    $this->drupalGet('admin/structure/block');
120    $this->clickLink('Place block');
121    $elements = $this->xpath($pattern, $arguments);
122    $this->assertTrue(!empty($elements), 'The test block appears in the custom category.');
123
124    $arguments = [
125      ':href' => Url::fromRoute('block.admin_add', [
126        'plugin_id' => 'views_block:' . $edit['id'] . '-block_2',
127        'theme' => 'classy',
128      ])->toString(),
129      ':category' => 'Lists (Views)',
130    ];
131    $elements = $this->xpath($pattern, $arguments);
132    $this->assertTrue(!empty($elements), 'The first duplicated test block remains in the original category.');
133
134    $arguments = [
135      ':href' => Url::fromRoute('block.admin_add', [
136        'plugin_id' => 'views_block:' . $edit['id'] . '-block_3',
137        'theme' => 'classy',
138      ])->toString(),
139      ':category' => $category,
140    ];
141    $elements = $this->xpath($pattern, $arguments);
142    $this->assertTrue(!empty($elements), 'The second duplicated test block appears in the custom category.');
143  }
144
145  /**
146   * Tests removing a block display.
147   */
148  public function testDeleteBlockDisplay() {
149    // To test all combinations possible we first place create two instances
150    // of the block display of the first view.
151    $block_1 = $this->drupalPlaceBlock('views_block:test_view_block-block_1', ['label' => 'test_view_block-block_1:1']);
152    $block_2 = $this->drupalPlaceBlock('views_block:test_view_block-block_1', ['label' => 'test_view_block-block_1:2']);
153
154    // Then we add one instance of blocks for each of the two displays of the
155    // second view.
156    $block_3 = $this->drupalPlaceBlock('views_block:test_view_block2-block_1', ['label' => 'test_view_block2-block_1']);
157    $block_4 = $this->drupalPlaceBlock('views_block:test_view_block2-block_2', ['label' => 'test_view_block2-block_2']);
158
159    $this->drupalGet('test-page');
160    $this->assertBlockAppears($block_1);
161    $this->assertBlockAppears($block_2);
162    $this->assertBlockAppears($block_3);
163    $this->assertBlockAppears($block_4);
164
165    $block_storage = $this->container->get('entity_type.manager')->getStorage('block');
166
167    // Remove the block display, so both block entities from the first view
168    // should both disappear.
169    $view = Views::getView('test_view_block');
170    $view->initDisplay();
171    $view->displayHandlers->remove('block_1');
172    $view->storage->save();
173
174    $this->assertNull($block_storage->load($block_1->id()), 'The block for this display was removed.');
175    $this->assertNull($block_storage->load($block_2->id()), 'The block for this display was removed.');
176    $this->assertNotEmpty($block_storage->load($block_3->id()), 'A block from another view was unaffected.');
177    $this->assertNotEmpty($block_storage->load($block_4->id()), 'A block from another view was unaffected.');
178    $this->drupalGet('test-page');
179    $this->assertNoBlockAppears($block_1);
180    $this->assertNoBlockAppears($block_2);
181    $this->assertBlockAppears($block_3);
182    $this->assertBlockAppears($block_4);
183
184    // Remove the first block display of the second view and ensure the block
185    // instance of the second block display still exists.
186    $view = Views::getView('test_view_block2');
187    $view->initDisplay();
188    $view->displayHandlers->remove('block_1');
189    $view->storage->save();
190
191    $this->assertNull($block_storage->load($block_3->id()), 'The block for this display was removed.');
192    $this->assertNotEmpty($block_storage->load($block_4->id()), 'A block from another display on the same view was unaffected.');
193    $this->drupalGet('test-page');
194    $this->assertNoBlockAppears($block_3);
195    $this->assertBlockAppears($block_4);
196  }
197
198  /**
199   * Test the block form for a Views block.
200   */
201  public function testViewsBlockForm() {
202    $this->drupalLogin($this->drupalCreateUser(['administer blocks']));
203    $default_theme = $this->config('system.theme')->get('default');
204    $this->drupalGet('admin/structure/block/add/views_block:test_view_block-block_1/' . $default_theme);
205    $elements = $this->xpath('//input[@name="label"]');
206    $this->assertTrue(empty($elements), 'The label field is not found for Views blocks.');
207    // Test that the machine name field is hidden from display and has been
208    // saved as expected from the default value.
209    $this->assertNoFieldById('edit-machine-name', 'views_block__test_view_block_1', 'The machine name is hidden on the views block form.');
210
211    // Save the block.
212    $edit = ['region' => 'content'];
213    $this->drupalPostForm(NULL, $edit, t('Save block'));
214    $storage = $this->container->get('entity_type.manager')->getStorage('block');
215    $block = $storage->load('views_block__test_view_block_block_1');
216    // This will only return a result if our new block has been created with the
217    // expected machine name.
218    $this->assertTrue(!empty($block), 'The expected block was loaded.');
219
220    for ($i = 2; $i <= 3; $i++) {
221      // Place the same block again and make sure we have a new ID.
222      $this->drupalPostForm('admin/structure/block/add/views_block:test_view_block-block_1/' . $default_theme, $edit, t('Save block'));
223      $block = $storage->load('views_block__test_view_block_block_1_' . $i);
224      // This will only return a result if our new block has been created with the
225      // expected machine name.
226      $this->assertTrue(!empty($block), 'The expected block was loaded.');
227    }
228
229    // Tests the override capability of items per page.
230    $this->drupalGet('admin/structure/block/add/views_block:test_view_block-block_1/' . $default_theme);
231    $edit = ['region' => 'content'];
232    $edit['settings[override][items_per_page]'] = 10;
233
234    $this->drupalPostForm('admin/structure/block/add/views_block:test_view_block-block_1/' . $default_theme, $edit, t('Save block'));
235
236    $block = $storage->load('views_block__test_view_block_block_1_4');
237    $config = $block->getPlugin()->getConfiguration();
238    $this->assertEqual(10, $config['items_per_page'], "'Items per page' is properly saved.");
239
240    $edit['settings[override][items_per_page]'] = 5;
241    $this->drupalPostForm('admin/structure/block/manage/views_block__test_view_block_block_1_4', $edit, t('Save block'));
242
243    $block = $storage->load('views_block__test_view_block_block_1_4');
244
245    $config = $block->getPlugin()->getConfiguration();
246    $this->assertEqual(5, $config['items_per_page'], "'Items per page' is properly saved.");
247
248    // Tests the override of the label capability.
249    $edit = ['region' => 'content'];
250    $edit['settings[views_label_checkbox]'] = 1;
251    $edit['settings[views_label]'] = 'Custom title';
252    $this->drupalPostForm('admin/structure/block/add/views_block:test_view_block-block_1/' . $default_theme, $edit, t('Save block'));
253
254    $block = $storage->load('views_block__test_view_block_block_1_5');
255    $config = $block->getPlugin()->getConfiguration();
256    $this->assertEqual('Custom title', $config['views_label'], "'Label' is properly saved.");
257  }
258
259  /**
260   * Tests the actual rendering of the views block.
261   */
262  public function testBlockRendering() {
263    // Create a block and set a custom title.
264    $block = $this->drupalPlaceBlock('views_block:test_view_block-block_1', ['label' => 'test_view_block-block_1:1', 'views_label' => 'Custom title']);
265    $this->drupalGet('');
266
267    $result = $this->xpath('//div[contains(@class, "region-sidebar-first")]/div[contains(@class, "block-views")]/h2');
268    $this->assertEqual($result[0]->getText(), 'Custom title');
269
270    // Don't override the title anymore.
271    $plugin = $block->getPlugin();
272    $plugin->setConfigurationValue('views_label', '');
273    $block->save();
274
275    $this->drupalGet('');
276    $result = $this->xpath('//div[contains(@class, "region-sidebar-first")]/div[contains(@class, "block-views")]/h2');
277    $this->assertEqual($result[0]->getText(), 'test_view_block');
278
279    // Hide the title.
280    $block->getPlugin()->setConfigurationValue('label_display', FALSE);
281    $block->save();
282
283    $this->drupalGet('');
284    $result = $this->xpath('//div[contains(@class, "region-sidebar-first")]/div[contains(@class, "block-views")]/h2');
285    $this->assertTrue(empty($result), 'The title is not visible.');
286
287    $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:system.site', 'config:views.view.test_view_block', 'http_response', 'rendered']));
288  }
289
290  /**
291   * Tests the various testcases of empty block rendering.
292   */
293  public function testBlockEmptyRendering() {
294    $url = new Url('test_page_test.test_page');
295    // Remove all views_test_data entries.
296    \Drupal::database()->truncate('views_test_data')->execute();
297    /** @var \Drupal\views\ViewEntityInterface $view */
298    $view = View::load('test_view_block');
299    $view->invalidateCaches();
300
301    $block = $this->drupalPlaceBlock('views_block:test_view_block-block_1', ['label' => 'test_view_block-block_1:1', 'views_label' => 'Custom title']);
302    $this->drupalGet('');
303    $this->assertCount(1, $this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]'));
304
305    $display = &$view->getDisplay('block_1');
306    $display['display_options']['block_hide_empty'] = TRUE;
307    $view->save();
308
309    $this->drupalGet($url);
310    $this->assertCount(0, $this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]'));
311    // Ensure that the view cacheability metadata is propagated even, for an
312    // empty block.
313    $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block', 'http_response', 'rendered']));
314    $this->assertCacheContexts(['url.query_args:_wrapper_format']);
315
316    // Add a header displayed on empty result.
317    $display = &$view->getDisplay('block_1');
318    $display['display_options']['defaults']['header'] = FALSE;
319    $display['display_options']['header']['example'] = [
320      'field' => 'area_text_custom',
321      'id' => 'area_text_custom',
322      'table' => 'views',
323      'plugin_id' => 'text_custom',
324      'content' => 'test header',
325      'empty' => TRUE,
326    ];
327    $view->save();
328
329    $this->drupalGet($url);
330    $this->assertCount(1, $this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]'));
331    $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block', 'http_response', 'rendered']));
332    $this->assertCacheContexts(['url.query_args:_wrapper_format']);
333
334    // Hide the header on empty results.
335    $display = &$view->getDisplay('block_1');
336    $display['display_options']['defaults']['header'] = FALSE;
337    $display['display_options']['header']['example'] = [
338      'field' => 'area_text_custom',
339      'id' => 'area_text_custom',
340      'table' => 'views',
341      'plugin_id' => 'text_custom',
342      'content' => 'test header',
343      'empty' => FALSE,
344    ];
345    $view->save();
346
347    $this->drupalGet($url);
348    $this->assertCount(0, $this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]'));
349    $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block', 'http_response', 'rendered']));
350    $this->assertCacheContexts(['url.query_args:_wrapper_format']);
351
352    // Add an empty text.
353    $display = &$view->getDisplay('block_1');
354    $display['display_options']['defaults']['empty'] = FALSE;
355    $display['display_options']['empty']['example'] = [
356      'field' => 'area_text_custom',
357      'id' => 'area_text_custom',
358      'table' => 'views',
359      'plugin_id' => 'text_custom',
360      'content' => 'test empty',
361    ];
362    $view->save();
363
364    $this->drupalGet($url);
365    $this->assertCount(1, $this->xpath('//div[contains(@class, "block-views-blocktest-view-block-block-1")]'));
366    $this->assertCacheTags(array_merge($block->getCacheTags(), ['block_view', 'config:block_list', 'config:views.view.test_view_block', 'http_response', 'rendered']));
367    $this->assertCacheContexts(['url.query_args:_wrapper_format']);
368  }
369
370  /**
371   * Tests the contextual links on a Views block.
372   */
373  public function testBlockContextualLinks() {
374    $this->drupalLogin($this->drupalCreateUser([
375      'administer views',
376      'access contextual links',
377      'administer blocks',
378    ]));
379    $block = $this->drupalPlaceBlock('views_block:test_view_block-block_1');
380    $cached_block = $this->drupalPlaceBlock('views_block:test_view_block-block_1');
381    $this->drupalGet('test-page');
382
383    $id = 'block:block=' . $block->id() . ':langcode=en|entity.view.edit_form:view=test_view_block:location=block&name=test_view_block&display_id=block_1&langcode=en';
384    $id_token = Crypt::hmacBase64($id, Settings::getHashSalt() . $this->container->get('private_key')->get());
385    $cached_id = 'block:block=' . $cached_block->id() . ':langcode=en|entity.view.edit_form:view=test_view_block:location=block&name=test_view_block&display_id=block_1&langcode=en';
386    $cached_id_token = Crypt::hmacBase64($cached_id, Settings::getHashSalt() . $this->container->get('private_key')->get());
387    // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder()
388    $this->assertRaw('<div' . new Attribute(['data-contextual-id' => $id, 'data-contextual-token' => $id_token]) . '></div>', new FormattableMarkup('Contextual link placeholder with id @id exists.', ['@id' => $id]));
389    $this->assertRaw('<div' . new Attribute(['data-contextual-id' => $cached_id, 'data-contextual-token' => $cached_id_token]) . '></div>', new FormattableMarkup('Contextual link placeholder with id @id exists.', ['@id' => $cached_id]));
390
391    // Get server-rendered contextual links.
392    // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:renderContextualLinks()
393    $post = ['ids[0]' => $id, 'ids[1]' => $cached_id, 'tokens[0]' => $id_token, 'tokens[1]' => $cached_id_token];
394    $url = 'contextual/render?_format=json,destination=test-page';
395    $this->getSession()->getDriver()->getClient()->request('POST', $url, $post);
396    $this->assertSession()->statusCodeEquals(200);
397    $json = Json::decode($this->getSession()->getPage()->getContent());
398    $this->assertIdentical($json[$id], '<ul class="contextual-links"><li class="block-configure"><a href="' . base_path() . 'admin/structure/block/manage/' . $block->id() . '">Configure block</a></li><li class="entityviewedit-form"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1">Edit view</a></li></ul>');
399    $this->assertIdentical($json[$cached_id], '<ul class="contextual-links"><li class="block-configure"><a href="' . base_path() . 'admin/structure/block/manage/' . $cached_block->id() . '">Configure block</a></li><li class="entityviewedit-form"><a href="' . base_path() . 'admin/structure/views/view/test_view_block/edit/block_1">Edit view</a></li></ul>');
400  }
401
402}
403