1<?php 2 3namespace Drupal\Tests\views\Functional\Plugin; 4 5use Drupal\language\Entity\ConfigurableLanguage; 6use Drupal\Tests\views\Functional\ViewTestBase; 7use Drupal\views\Views; 8use Drupal\views_test_data\Plugin\views\display\DisplayTest as DisplayTestPlugin; 9 10/** 11 * Tests the basic display plugin. 12 * 13 * @group views 14 */ 15class DisplayTest extends ViewTestBase { 16 17 /** 18 * Views used by this test. 19 * 20 * @var array 21 */ 22 public static $testViews = ['test_filter_groups', 'test_get_attach_displays', 'test_view', 'test_display_more', 'test_display_invalid', 'test_display_empty', 'test_exposed_relationship_admin_ui', 'test_simple_argument']; 23 24 /** 25 * Modules to enable. 26 * 27 * @var array 28 */ 29 protected static $modules = ['views_ui', 'node', 'block']; 30 31 /** 32 * {@inheritdoc} 33 */ 34 protected $defaultTheme = 'stark'; 35 36 protected function setUp($import_test_views = TRUE): void { 37 parent::setUp(); 38 39 $this->enableViewsTestModule(); 40 41 $this->adminUser = $this->drupalCreateUser(['administer views']); 42 $this->drupalLogin($this->adminUser); 43 44 // Create 10 nodes. 45 for ($i = 0; $i <= 10; $i++) { 46 $this->drupalCreateNode(['promote' => TRUE]); 47 } 48 } 49 50 /** 51 * Tests the display test plugin. 52 * 53 * @see \Drupal\views_test_data\Plugin\views\display\DisplayTest 54 */ 55 public function testDisplayPlugin() { 56 /** @var \Drupal\Core\Render\RendererInterface $renderer */ 57 $renderer = $this->container->get('renderer'); 58 $view = Views::getView('test_view'); 59 60 // Add a new 'display_test' display and test it's there. 61 $view->storage->addDisplay('display_test'); 62 $displays = $view->storage->get('display'); 63 64 $this->assertTrue(isset($displays['display_test_1']), 'Added display has been assigned to "display_test_1"'); 65 66 // Check the display options are like expected. 67 $options = [ 68 'display_options' => [], 69 'display_plugin' => 'display_test', 70 'id' => 'display_test_1', 71 'display_title' => 'Display test', 72 'position' => 1, 73 ]; 74 $this->assertEquals($options, $displays['display_test_1']); 75 76 // Add another one to ensure that position is counted up. 77 $view->storage->addDisplay('display_test'); 78 $displays = $view->storage->get('display'); 79 $options = [ 80 'display_options' => [], 81 'display_plugin' => 'display_test', 82 'id' => 'display_test_2', 83 'display_title' => 'Display test 2', 84 'position' => 2, 85 ]; 86 $this->assertEquals($options, $displays['display_test_2']); 87 88 // Move the second display before the first one in order to test custom 89 // sorting. 90 $displays['display_test_1']['position'] = 2; 91 $displays['display_test_2']['position'] = 1; 92 $view->storage->set('display', $displays); 93 $view->save(); 94 95 $view->setDisplay('display_test_1'); 96 97 $this->assertInstanceOf(DisplayTestPlugin::class, $view->display_handler); 98 99 // Check the test option. 100 $this->assertSame('', $view->display_handler->getOption('test_option')); 101 102 $style = $view->display_handler->getOption('style'); 103 $style['type'] = 'test_style'; 104 $view->display_handler->setOption('style', $style); 105 $view->initDisplay(); 106 $view->initStyle(); 107 $view->style_plugin->setUsesRowPlugin(FALSE); 108 109 $output = $view->preview(); 110 $output = $renderer->renderRoot($output); 111 112 $this->assertStringContainsString('<h1></h1>', $output, 'An empty value for test_option found in output.'); 113 114 // Change this option and check the title of out output. 115 $view->display_handler->overrideOption('test_option', 'Test option title'); 116 $view->save(); 117 118 $output = $view->preview(); 119 $output = $renderer->renderRoot($output); 120 121 // Test we have our custom <h1> tag in the output of the view. 122 $this->assertStringContainsString('<h1>Test option title</h1>', $output, 'The test_option value found in display output title.'); 123 124 // Test that the display category/summary is in the UI. 125 $this->drupalGet('admin/structure/views/view/test_view/edit/display_test_1'); 126 $this->assertSession()->pageTextContains('Display test settings'); 127 // Ensure that the order is as expected. 128 $result = $this->xpath('//ul[@id="views-display-menu-tabs"]/li/a/child::text()'); 129 $this->assertEquals('Display test 2', $result[0]->getText()); 130 $this->assertEquals('Display test', $result[1]->getText()); 131 132 $this->clickLink('Test option title'); 133 134 $test_option = $this->randomString(); 135 $this->submitForm(['test_option' => $test_option], 'Apply'); 136 137 // Check the new value has been saved by checking the UI summary text. 138 $this->drupalGet('admin/structure/views/view/test_view/edit/display_test_1'); 139 $this->assertSession()->linkExists($test_option); 140 141 // Test the enable/disable status of a display. 142 $view->display_handler->setOption('enabled', FALSE); 143 $this->assertFalse($view->display_handler->isEnabled(), 'Make sure that isEnabled returns FALSE on a disabled display.'); 144 $view->display_handler->setOption('enabled', TRUE); 145 $this->assertTrue($view->display_handler->isEnabled(), 'Make sure that isEnabled returns TRUE on a disabled display.'); 146 } 147 148 /** 149 * Tests the overriding of filter_groups. 150 */ 151 public function testFilterGroupsOverriding() { 152 $view = Views::getView('test_filter_groups'); 153 $view->initDisplay(); 154 155 // mark is as overridden, yes FALSE, means overridden. 156 $view->displayHandlers->get('page')->setOverride('filter_groups', FALSE); 157 $this->assertFalse($view->displayHandlers->get('page')->isDefaulted('filter_groups'), "Make sure that 'filter_groups' is marked as overridden."); 158 $this->assertFalse($view->displayHandlers->get('page')->isDefaulted('filters'), "Make sure that 'filters'' is marked as overridden."); 159 } 160 161 /** 162 * Tests the getAttachedDisplays method. 163 */ 164 public function testGetAttachedDisplays() { 165 $view = Views::getView('test_get_attach_displays'); 166 167 // Both the feed_1 and the feed_2 display are attached to the page display. 168 $view->setDisplay('page_1'); 169 $this->assertEquals(['feed_1', 'feed_2'], $view->display_handler->getAttachedDisplays()); 170 171 $view->setDisplay('feed_1'); 172 $this->assertEquals([], $view->display_handler->getAttachedDisplays()); 173 } 174 175 /** 176 * Tests the readmore validation. 177 */ 178 public function testReadMoreNoDisplay() { 179 $view = Views::getView('test_display_more'); 180 // Confirm that the view validates when there is a page display. 181 $errors = $view->validate(); 182 $this->assertTrue(empty($errors), 'More link validation has no errors.'); 183 184 // Confirm that the view does not validate when the page display is disabled. 185 $view->setDisplay('page_1'); 186 $view->display_handler->setOption('enabled', FALSE); 187 $view->setDisplay('default'); 188 $errors = $view->validate(); 189 $this->assertTrue(!empty($errors), 'More link validation has some errors.'); 190 $this->assertEquals('Display "Default" uses a "more" link but there are no displays it can link to. You need to specify a custom URL.', $errors['default'][0], 'More link validation has the right error.'); 191 192 // Confirm that the view does not validate when the page display does not exist. 193 $view = Views::getView('test_view'); 194 $view->setDisplay('default'); 195 $view->display_handler->setOption('use_more', 1); 196 $errors = $view->validate(); 197 $this->assertTrue(!empty($errors), 'More link validation has some errors.'); 198 $this->assertEquals('Display "Default" uses a "more" link but there are no displays it can link to. You need to specify a custom URL.', $errors['default'][0], 'More link validation has the right error.'); 199 } 200 201 /** 202 * Tests the readmore with custom URL. 203 */ 204 public function testReadMoreCustomURL() { 205 /** @var \Drupal\Core\Render\RendererInterface $renderer */ 206 $renderer = $this->container->get('renderer'); 207 208 $view = Views::getView('test_display_more'); 209 $view->setDisplay('default'); 210 $view->display_handler->setOption('use_more', 1); 211 $view->display_handler->setOption('use_more_always', 1); 212 $view->display_handler->setOption('link_display', 'custom_url'); 213 214 // Test more link without leading slash. 215 $view->display_handler->setOption('link_url', 'node'); 216 $this->executeView($view); 217 $output = $view->preview(); 218 $output = $renderer->renderRoot($output); 219 $this->assertStringContainsString('/node', $output, 'The read more link with href "/node" was found.'); 220 221 // Test more link with leading slash. 222 $view->display_handler->setOption('link_display', 'custom_url'); 223 $view->display_handler->setOption('link_url', '/node'); 224 $this->executeView($view); 225 $output = $view->preview(); 226 $output = $renderer->renderRoot($output); 227 $this->assertStringContainsString('/node', $output, 'The read more link with href "/node" was found.'); 228 229 // Test more link with absolute url. 230 $view->display_handler->setOption('link_display', 'custom_url'); 231 $view->display_handler->setOption('link_url', 'http://drupal.org'); 232 $this->executeView($view); 233 $output = $view->preview(); 234 $output = $renderer->renderRoot($output); 235 $this->assertStringContainsString('http://drupal.org', $output, 'The read more link with href "http://drupal.org" was found.'); 236 237 // Test more link with query parameters in the url. 238 $view->display_handler->setOption('link_display', 'custom_url'); 239 $view->display_handler->setOption('link_url', 'node?page=1&foo=bar'); 240 $this->executeView($view); 241 $output = $view->preview(); 242 $output = $renderer->renderRoot($output); 243 $this->assertStringContainsString('/node?page=1&foo=bar', $output, 'The read more link with href "/node?page=1&foo=bar" was found.'); 244 245 // Test more link with fragment in the url. 246 $view->display_handler->setOption('link_display', 'custom_url'); 247 $view->display_handler->setOption('link_url', 'node#target'); 248 $this->executeView($view); 249 $output = $view->preview(); 250 $output = $renderer->renderRoot($output); 251 $this->assertStringContainsString('/node#target', $output, 'The read more link with href "/node#target" was found.'); 252 253 // Test more link with arguments. 254 $view = Views::getView('test_simple_argument'); 255 $view->setDisplay('default'); 256 $view->display_handler->setOption('use_more', 1); 257 $view->display_handler->setOption('use_more_always', 1); 258 $view->display_handler->setOption('link_display', 'custom_url'); 259 $view->display_handler->setOption('link_url', 'node?date={{ raw_arguments.age }}&foo=bar'); 260 $view->setArguments([22]); 261 $this->executeView($view); 262 $output = $view->preview(); 263 $output = $renderer->renderRoot($output); 264 $this->assertStringContainsString('/node?date=22&foo=bar', $output, 'The read more link with href "/node?date=22&foo=bar" was found.'); 265 266 // Test more link with 1 dimension array query parameters with arguments. 267 $view = Views::getView('test_simple_argument'); 268 $view->setDisplay('default'); 269 $view->display_handler->setOption('use_more', 1); 270 $view->display_handler->setOption('use_more_always', 1); 271 $view->display_handler->setOption('link_display', 'custom_url'); 272 $view->display_handler->setOption('link_url', '/node?f[0]=foo:bar&f[1]=foo:{{ raw_arguments.age }}'); 273 $view->setArguments([22]); 274 $this->executeView($view); 275 $output = $view->preview(); 276 $output = $renderer->renderRoot($output); 277 $this->assertStringContainsString('/node?f%5B0%5D=foo%3Abar&f%5B1%5D=foo%3A22', $output, 'The read more link with href "/node?f[0]=foo:bar&f[1]=foo:22" was found.'); 278 279 // Test more link with arguments in path. 280 $view->display_handler->setOption('link_url', 'node/{{ raw_arguments.age }}?date={{ raw_arguments.age }}&foo=bar'); 281 $view->setArguments([22]); 282 $this->executeView($view); 283 $output = $view->preview(); 284 $output = $renderer->renderRoot($output); 285 $this->assertStringContainsString('/node/22?date=22&foo=bar', $output, 'The read more link with href "/node/22?date=22&foo=bar" was found.'); 286 287 // Test more link with arguments in fragment. 288 $view->display_handler->setOption('link_url', 'node?date={{ raw_arguments.age }}&foo=bar#{{ raw_arguments.age }}'); 289 $view->setArguments([22]); 290 $this->executeView($view); 291 $output = $view->preview(); 292 $output = $renderer->renderRoot($output); 293 $this->assertStringContainsString('/node?date=22&foo=bar#22', $output, 'The read more link with href "/node?date=22&foo=bar#22" was found.'); 294 } 295 296 /** 297 * Tests invalid display plugins. 298 */ 299 public function testInvalidDisplayPlugins() { 300 $this->drupalGet('test_display_invalid'); 301 $this->assertSession()->statusCodeEquals(200); 302 303 // Change the page plugin id to an invalid one. Bypass the entity system 304 // so no menu rebuild was executed (so the path is still available). 305 $config = $this->config('views.view.test_display_invalid'); 306 $config->set('display.page_1.display_plugin', 'invalid'); 307 $config->save(); 308 309 $this->drupalGet('test_display_invalid'); 310 $this->assertSession()->statusCodeEquals(200); 311 $this->assertSession()->pageTextContains('The "invalid" plugin does not exist.'); 312 313 // Rebuild the router, and ensure that the path is not accessible anymore. 314 views_invalidate_cache(); 315 \Drupal::service('router.builder')->rebuildIfNeeded(); 316 317 $this->drupalGet('test_display_invalid'); 318 $this->assertSession()->statusCodeEquals(404); 319 320 // Change the display plugin ID back to the correct ID. 321 $config = $this->config('views.view.test_display_invalid'); 322 $config->set('display.page_1.display_plugin', 'page'); 323 $config->save(); 324 325 // Place the block display. 326 $block = $this->drupalPlaceBlock('views_block:test_display_invalid-block_1', ['label' => 'Invalid display']); 327 328 $this->drupalGet('<front>'); 329 $this->assertSession()->statusCodeEquals(200); 330 $this->assertSession()->elementsCount('xpath', "//div[@id = 'block-{$block->id()}']", 1); 331 332 // Change the block plugin ID to an invalid one. 333 $config = $this->config('views.view.test_display_invalid'); 334 $config->set('display.block_1.display_plugin', 'invalid'); 335 $config->save(); 336 337 // Test the page is still displayed, the block not present, and has the 338 // plugin warning message. 339 $this->drupalGet('<front>'); 340 $this->assertSession()->statusCodeEquals(200); 341 $this->assertSession()->pageTextContains('The "invalid" plugin does not exist.'); 342 $this->assertSession()->elementNotExists('xpath', "//div[@id = 'block-{$block->id()}']"); 343 } 344 345 /** 346 * Tests display validation when a required relationship is missing. 347 */ 348 public function testMissingRelationship() { 349 $view = Views::getView('test_exposed_relationship_admin_ui'); 350 351 // Remove the relationship that is not used by other handlers. 352 $view->removeHandler('default', 'relationship', 'uid_1'); 353 $errors = $view->validate(); 354 // Check that no error message is shown. 355 $this->assertTrue(empty($errors['default']), 'No errors found when removing unused relationship.'); 356 357 // Unset cached relationships (see DisplayPluginBase::getHandlers()) 358 unset($view->display_handler->handlers['relationship']); 359 360 // Remove the relationship used by other handlers. 361 $view->removeHandler('default', 'relationship', 'uid'); 362 // Validate display 363 $errors = $view->validate(); 364 // Check that the error messages are shown. 365 $this->assertCount(2, $errors['default'], 'Error messages found for required relationship'); 366 $this->assertEquals(t('The %handler_type %handler uses a relationship that has been removed.', ['%handler_type' => 'field', '%handler' => 'User: Last login']), $errors['default'][0]); 367 $this->assertEquals(t('The %handler_type %handler uses a relationship that has been removed.', ['%handler_type' => 'field', '%handler' => 'User: Created']), $errors['default'][1]); 368 } 369 370 /** 371 * Tests the outputIsEmpty method on the display. 372 */ 373 public function testOutputIsEmpty() { 374 $view = Views::getView('test_display_empty'); 375 $this->executeView($view); 376 $this->assertNotEmpty($view->result); 377 $this->assertFalse($view->display_handler->outputIsEmpty(), 'Ensure the view output is marked as not empty.'); 378 $view->destroy(); 379 380 // Add a filter, so the view result is empty. 381 $view->setDisplay('default'); 382 $item = [ 383 'table' => 'views_test_data', 384 'field' => 'id', 385 'id' => 'id', 386 'value' => ['value' => 7297], 387 ]; 388 $view->setHandler('default', 'filter', 'id', $item); 389 $this->executeView($view); 390 $this->assertEmpty($view->result, 'Ensure the result of the view is empty.'); 391 $this->assertFalse($view->display_handler->outputIsEmpty(), 'Ensure the view output is marked as not empty, because the empty text still appears.'); 392 $view->destroy(); 393 394 // Remove the empty area, but mark the header area to still appear. 395 $view->removeHandler('default', 'empty', 'area'); 396 $item = $view->getHandler('default', 'header', 'area'); 397 $item['empty'] = TRUE; 398 $view->setHandler('default', 'header', 'area', $item); 399 $this->executeView($view); 400 $this->assertEmpty($view->result, 'Ensure the result of the view is empty.'); 401 $this->assertFalse($view->display_handler->outputIsEmpty(), 'Ensure the view output is marked as not empty, because the header text still appears.'); 402 $view->destroy(); 403 404 // Hide the header on empty results. 405 $item = $view->getHandler('default', 'header', 'area'); 406 $item['empty'] = FALSE; 407 $view->setHandler('default', 'header', 'area', $item); 408 $this->executeView($view); 409 $this->assertEmpty($view->result, 'Ensure the result of the view is empty.'); 410 $this->assertTrue($view->display_handler->outputIsEmpty(), 'Ensure the view output is marked as empty.'); 411 } 412 413 /** 414 * Tests translation rendering settings based on entity translatability. 415 */ 416 public function testTranslationSetting() { 417 \Drupal::service('module_installer')->install(['file']); 418 419 // By default there should be no language settings. 420 $this->checkTranslationSetting(); 421 \Drupal::service('module_installer')->install(['language']); 422 423 // Enabling the language module should not make a difference. 424 $this->checkTranslationSetting(); 425 426 // Making the site multilingual should let translatable entity types support 427 // translation rendering. 428 ConfigurableLanguage::createFromLangcode('it')->save(); 429 $this->checkTranslationSetting(TRUE); 430 } 431 432 /** 433 * Asserts a node and a file based view for the translation setting. 434 * 435 * The file based view should never expose that setting. The node based view 436 * should if the site is multilingual. 437 * 438 * @param bool $expected_node_translatability 439 * Whether the node based view should be expected to support translation 440 * settings. 441 */ 442 protected function checkTranslationSetting($expected_node_translatability = FALSE) { 443 $not_supported_text = 'The view is not based on a translatable entity type or the site is not multilingual.'; 444 $supported_text = 'All content that supports translations will be displayed in the selected language.'; 445 446 $this->drupalGet('admin/structure/views/nojs/display/content/page_1/rendering_language'); 447 if ($expected_node_translatability) { 448 $this->assertSession()->pageTextNotContains($not_supported_text); 449 $this->assertSession()->pageTextContains($supported_text); 450 } 451 else { 452 $this->assertSession()->pageTextContains($not_supported_text); 453 $this->assertSession()->pageTextNotContains($supported_text); 454 } 455 456 $this->drupalGet('admin/structure/views/nojs/display/files/page_1/rendering_language'); 457 $this->assertSession()->pageTextContains($not_supported_text); 458 $this->assertSession()->pageTextNotContains($supported_text); 459 } 460 461} 462