first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
name: Navigation test
description: 'Provides test functionality for navigation module'
package: Testing
# version: VERSION
type: module
dependencies:
- drupal:navigation
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,19 @@
<?php
/**
* @file
* Contains main module functions.
*/
declare(strict_types=1);
use Drupal\Component\Utility\Html;
/**
* Implements hook_preprocess_HOOK().
*/
function navigation_test_preprocess_block__navigation(&$variables) {
// Add some additional classes so we can target the correct contextual link
// in tests.
$variables['attributes']['class'][] = Html::cleanCssIdentifier('block-' . $variables['elements']['#plugin_id']);
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for contextual.
*
* @group contextual
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,226 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Drupal\Core\Url;
use Drupal\shortcut\Entity\Shortcut;
use Drupal\shortcut\Entity\ShortcutSet;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\Tests\system\Functional\Cache\PageCacheTagsTestBase;
/**
* Tests for \Drupal\navigation\Plugin\Block\NavigationShortcutsBlock.
*
* @group navigation
*/
class NavigationShortcutsBlockTest extends PageCacheTagsTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* Modules to install.
*
* @var array
*/
protected static $modules = ['navigation', 'shortcut', 'test_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests visibility and cacheability of shortcuts in the navigation bar.
*/
public function testNavigationBlock(): void {
$this->drupalPlaceBlock('page_title_block', ['id' => 'title']);
$test_page_url = Url::fromRoute('test_page_test.test_page');
$this->verifyPageCache($test_page_url, 'MISS');
$this->verifyPageCache($test_page_url, 'HIT');
// Ensure that without enabling the shortcuts-in-page-title-link feature
// in the theme, the shortcut_list cache tag is not added to the page.
$this->drupalLogin($this->drupalCreateUser([
'administer site configuration',
'access navigation',
'administer shortcuts',
'access shortcuts',
]));
$this->drupalGet('admin/config/system/cron');
$expected_cache_tags = [
'CACHE_MISS_IF_UNCACHEABLE_HTTP_METHOD:form',
'block_view',
'config:block.block.title',
'config:block_list',
'config:navigation.settings',
'config:navigation.block_layout',
'config:shortcut.set.default',
'config:system.menu.admin',
'config:system.menu.content',
'http_response',
'rendered',
];
$this->assertCacheTags($expected_cache_tags);
\Drupal::configFactory()
->getEditable('stark.settings')
->set('third_party_settings.shortcut.module_link', TRUE)
->save(TRUE);
// Add cron to the default shortcut set, now the shortcut list cache tag
// is expected.
$this->drupalGet('admin/config/system/cron');
$this->clickLink('Add to Default shortcuts');
$expected_cache_tags[] = 'config:shortcut_set_list';
$this->assertCacheTags($expected_cache_tags);
// Verify that users without the 'access shortcuts' permission can't see the
// shortcuts.
$this->drupalLogin($this->drupalCreateUser(['access navigation']));
$this->assertSession()->pageTextNotContains('Shortcuts');
$this->verifyDynamicPageCache($test_page_url, 'MISS');
$this->verifyDynamicPageCache($test_page_url, 'HIT');
// Verify that users without the 'administer site configuration' permission
// can't see the cron shortcut nor the shortcuts navigation item.
$this->drupalLogin($this->drupalCreateUser([
'access navigation',
'access shortcuts',
]));
$this->verifyDynamicPageCache($test_page_url, 'MISS');
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->elementNotExists('css', '#menu--shortcuts');
$this->assertSession()->pageTextNotContains('Shortcuts');
$this->assertSession()->linkNotExists('Cron');
// Create a role with access to shortcuts as well as the necessary
// permissions to see specific shortcuts.
$site_configuration_role = $this->drupalCreateRole([
'access navigation',
'access shortcuts',
'administer site configuration',
'access administration pages',
'configure navigation layout',
]);
// Create two different users with the same role to assert that the second
// user has a cache hit despite the user cache context, as
// the returned cache contexts include those from lazy-builder content.
$site_configuration_user1 = $this->drupalCreateUser();
$site_configuration_user1->addRole($site_configuration_role)->save();
$site_configuration_user2 = $this->drupalCreateUser();
$site_configuration_user2->addRole($site_configuration_role)->save();
$this->drupalLogin($site_configuration_user1);
$this->verifyDynamicPageCache($test_page_url, 'MISS');
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertCacheContexts(['user', 'url.query_args:_wrapper_format', 'session']);
$this->assertSession()->elementExists('css', '#menu--shortcuts');
$this->assertSession()->pageTextContains('Shortcuts');
$this->assertSession()->linkExists('Cron');
$this->drupalLogin($site_configuration_user2);
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertCacheContexts(['user', 'url.query_args:_wrapper_format', 'session']);
$this->assertSession()->pageTextContains('Shortcuts');
$this->assertSession()->linkExists('Cron');
// Add another shortcut.
$shortcut = Shortcut::create([
'shortcut_set' => 'default',
'title' => 'Llama',
'weight' => 0,
'link' => [['uri' => 'internal:/admin/config']],
]);
$shortcut->save();
// The shortcuts are displayed in a lazy builder, so the page is still a
// cache HIT but shows the new shortcut immediately.
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkExists('Llama');
// Update the shortcut title and assert that it is updated.
$shortcut->set('title', 'Alpaca');
$shortcut->save();
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkExists('Alpaca');
// Delete the shortcut and assert that the link is gone.
$shortcut->delete();
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkNotExists('Alpaca');
// Add a new Shortcut Set with a single link.
$new_set = ShortcutSet::create([
'id' => 'llama-set',
'label' => 'Llama Set',
]);
$new_set->save();
$new_shortcut = Shortcut::create([
'shortcut_set' => 'llama-set',
'title' => 'New Llama',
'weight' => 0,
'link' => [['uri' => 'internal:/admin/config']],
]);
$new_shortcut->save();
// Assign the new shortcut set to user 2 and confirm that links are changed
// automatically.
\Drupal::entityTypeManager()
->getStorage('shortcut_set')
->assignUser($new_set, $site_configuration_user2);
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkExists('New Llama');
// Confirm that links for user 1 have not been affected.
$this->drupalLogin($site_configuration_user1);
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkNotExists('New Llama');
// Confirm that removing assignment automatically changes the links too.
$this->drupalLogin($site_configuration_user2);
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkExists('New Llama');
\Drupal::entityTypeManager()
->getStorage('shortcut_set')
->unassignUser($site_configuration_user2);
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkNotExists('New Llama');
// Confirm that deleting a shortcut set automatically changes the links too.
\Drupal::entityTypeManager()
->getStorage('shortcut_set')
->assignUser($new_set, $site_configuration_user2);
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkExists('New Llama');
\Drupal::entityTypeManager()
->getStorage('shortcut_set')
->delete([$new_set]);
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->linkExists('Cron');
$this->assertSession()->linkNotExists('New Llama');
// Verify that block disappears gracefully when shortcut module is disabled.
// Shortcut entities has to be removed first.
$link_storage = \Drupal::entityTypeManager()->getStorage('shortcut');
$link_storage->delete($link_storage->loadMultiple());
\Drupal::service('module_installer')->uninstall(['shortcut']);
$this->verifyDynamicPageCache($test_page_url, 'MISS');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextNotContains('Shortcuts');
// Confirm that Navigation Blocks page is working.
// @see https://www.drupal.org/project/drupal/issues/3445184
$this->drupalGet('/admin/config/user-interface/navigation-block');
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the top bar functionality.
*
* @group navigation
*/
class NavigationTopBarTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'navigation',
'node',
'layout_builder',
'field_ui',
'file',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* An admin user to configure the test environment.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* Node used to check top bar options.
*
* @var \Drupal\node\NodeInterface
*/
protected $node;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create and log in an administrative user.
$this->adminUser = $this->drupalCreateUser([
'administer site configuration',
'access administration pages',
'access navigation',
'bypass node access',
]);
$this->drupalLogin($this->adminUser);
// Create a new content type and enable Layout Builder for it.
$node_type = $this->createContentType(['type' => 'node_type']);
LayoutBuilderEntityViewDisplay::load('node.node_type.default')
->enableLayoutBuilder()
->setOverridable()
->save();
// Place the tabs block to check its presence.
$this->drupalPlaceBlock('local_tasks_block', ['id' => 'tabs']);
// Enable some test blocks.
$this->node = $this->drupalCreateNode(['type' => $node_type->id()]);
}
/**
* Tests the top bar visibility.
*/
public function testTopBarVisibility(): void {
$this->drupalGet($this->node->toUrl());
// Top Bar is not visible if the feature flag module is disabled.
$this->assertSession()->elementNotExists('xpath', "//div[contains(@class, 'top-bar__content')]/button/span");
$this->assertSession()->elementExists('xpath', '//div[@id="block-tabs"]');
\Drupal::service('module_installer')->install(['navigation_top_bar']);
// Top Bar is visible once the feature flag module is enabled.
$this->drupalGet($this->node->toUrl());
$this->assertSession()->elementExists('xpath', "//div[contains(@class, 'top-bar__content')]/button/span");
$this->assertSession()->elementTextEquals('xpath', "//div[contains(@class, 'top-bar__content')]/button/span", 'More actions');
$this->assertSession()->elementNotExists('xpath', '//div[@id="block-tabs"]');
// Find all the dropdown links and check if the top bar is there as well.
$toolbar_links = $this->mink->getSession()->getPage()->find('xpath', '//*[@id="admin-local-tasks"]/ul');
foreach ($toolbar_links->findAll('css', 'li') as $toolbar_link) {
$this->clickLink($toolbar_link->getText());
$this->assertSession()->elementExists('xpath', "//div[contains(@class, 'top-bar__content')]/button/span");
$this->assertSession()->elementTextEquals('xpath', "//div[contains(@class, 'top-bar__content')]/button/span", 'More actions');
$this->assertSession()->elementNotExists('xpath', '//div[@id="block-tabs"]');
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Drupal\Core\Url;
use Drupal\Tests\system\Functional\Cache\PageCacheTagsTestBase;
/**
* Tests for \Drupal\navigation\Plugin\NavigationBlock\NavigationUserBlock.
*
* @group navigation
*/
class NavigationUserBlockTest extends PageCacheTagsTestBase {
/**
* Modules to install.
*
* @var array
*/
protected static $modules = ['navigation', 'test_page_test', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* User with permission to administer navigation blocks and access navigation.
*
* @var object
*/
protected $adminUser;
/**
* An authenticated user to test navigation block caching.
*
* @var object
*/
protected $normalUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create an admin user, log in and enable test navigation blocks.
$this->adminUser = $this->drupalCreateUser([
'access administration pages',
'access navigation',
]);
// Create additional users to test caching modes.
$this->normalUser = $this->drupalCreateUser([
'access navigation',
]);
// Note that we don't need to setup a user navigation block b/c it's
// installed by default.
}
/**
* Test output of user navigation block with regards to caching and contents.
*/
public function testNavigationUserBlock(): void {
// Verify some basic cacheability metadata. Ensures that we're not doing
// anything so egregious as to upset expected caching behavior. In this
// case, as an anonymous user, we should have zero effect on the page.
$test_page_url = Url::fromRoute('test_page_test.test_page');
$this->verifyPageCache($test_page_url, 'MISS');
$this->verifyPageCache($test_page_url, 'HIT');
// Login as a limited access user, and verify that the dynamic page cache
// is working as expected.
$this->drupalLogin($this->normalUser);
$this->verifyDynamicPageCache($test_page_url, 'MISS');
$this->verifyDynamicPageCache($test_page_url, 'HIT');
// We should see the users name in the navigation menu.
$rendered_user_name = $this->cssSelect('[aria-controls="admin-toolbar-user-menu"] > .toolbar-button__label')[0]->getText();
$this->assertEquals((string) $this->normalUser->getDisplayName(), $rendered_user_name);
// We should see all three user links in the page.
$link_labels = ['View profile', 'Edit profile', 'Log out'];
$block = $this->assertSession()->elementExists('css', sprintf('.toolbar-block:contains("%s")', $rendered_user_name));
foreach ($link_labels as $link_label) {
$links = $block->findAll('named', ['link', $link_label]);
$this->assertCount(1, $links, sprintf('Found %s links with label %s.', count($links), $link_label));
}
// The Edit profile link should link to the users edit profile page.
$links = $this->getSession()->getPage()->findAll('named', ['link', 'Edit profile']);
$this->assertStringContainsString(sprintf('/user/%s/edit', $this->normalUser->id()), $links[0]->getAttribute('href'));
// Login as a different user, UI should update.
$this->drupalLogin($this->adminUser);
$this->verifyDynamicPageCache($test_page_url, 'MISS');
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$rendered_user_name = $this->cssSelect('[aria-controls="admin-toolbar-user-menu"] > .toolbar-button__label')[0]->getText();
$this->assertEquals((string) $this->adminUser->getDisplayName(), $rendered_user_name);
// The Edit profile link should link to the users edit profile page.
$links = $this->getSession()->getPage()->findAll('named', ['link', 'Edit profile']);
$this->assertStringContainsString(sprintf('/user/%s/edit', $this->adminUser->id()), $links[0]->getAttribute('href'));
}
}

View File

@@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\FunctionalJavascript;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\block\Traits\BlockCreationTrait;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
use Drupal\Tests\layout_builder\FunctionalJavascript\LayoutBuilderSortTrait;
use Drupal\Tests\system\Traits\OffCanvasTestTrait;
use Drupal\user\UserInterface;
/**
* Tests that the navigation block UI exists and stores data correctly.
*
* @group navigation
*/
class NavigationBlockUiTest extends WebDriverTestBase {
use BlockCreationTrait;
use ContextualLinkClickTrait;
use LayoutBuilderSortTrait;
use OffCanvasTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'navigation',
'block_content',
'layout_builder',
'layout_test',
'node',
'field_ui',
'shortcut',
'off_canvas_test',
'navigation_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* An administrative user to configure the test environment.
*
* @var \Drupal\user\UserInterface
*/
protected UserInterface $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('page_title_block', ['id' => 'title']);
// Create an administrative user.
$this->adminUser = $this->drupalCreateUser([
'configure navigation layout',
'access administration pages',
'access navigation',
'access shortcuts',
'access contextual links',
'administer shortcuts',
'administer site configuration',
'access administration pages',
]);
}
/**
* Tests navigation block admin page exists and functions correctly.
*/
public function testNavigationBlockAdminUiPage(): void {
$layout_url = '/admin/config/user-interface/navigation-block';
$this->drupalGet($layout_url);
$this->assertSession()->pageTextContains('Access denied');
// Add at least one shortcut.
$shortcut_set = \Drupal::entityTypeManager()
->getStorage('shortcut_set')
->getDisplayedToUser($this->adminUser);
$shortcut = \Drupal::entityTypeManager()->getStorage('shortcut')->create([
'title' => 'Run cron',
'shortcut_set' => $shortcut_set->id(),
'link' => [
'uri' => 'internal:/admin/config/system/cron',
],
]);
$shortcut->save();
$this->drupalLogin($this->adminUser);
$this->drupalGet($layout_url);
$page = $this->getSession()->getPage();
// Add section should not be present
$this->assertSession()->linkNotExists('Add section');
// Configure section should not be present.
$this->assertSession()->linkNotExists('Configure Section 1');
// Remove section should not be present.
$this->assertSession()->linkNotExists('Remove Section 1');
// Remove the shortcut block.
$this->assertSession()->pageTextContains('Shortcuts');
$this->clickContextualLink('form .block-navigation-shortcuts', 'Remove block');
$this->assertOffCanvasFormAfterWait('layout_builder_remove_block');
$this->assertSession()->pageTextContains('Are you sure you want to remove the Shortcuts block?');
$this->assertSession()->pageTextContains('This action cannot be undone.');
$page->pressButton('Remove');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertSession()->assertNoElementAfterWait('css', '#drupal-off-canvas');
$this->assertSession()->elementNotExists('css', 'form .block-navigation-shortcuts');
// Add a new block.
$this->getSession()->getPage()->uncheckField('toggle_content_preview');
$this->openAddBlockForm('Navigation Shortcuts');
$page->fillField('settings[label]', 'New Shortcuts');
$page->checkField('settings[label_display]');
// Save the new block, and ensure it is displayed on the page.
$page->pressButton('Add block');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertSession()->assertNoElementAfterWait('css', '#drupal-off-canvas');
$this->assertSession()->addressEquals($layout_url);
$this->assertSession()->pageTextContains('Shortcuts');
$this->assertSession()->pageTextContains('New Shortcuts');
// Until the layout is saved, the new block is not visible on the node page.
$front = Url::fromRoute('<front>');
$this->drupalGet($front);
$this->assertSession()->pageTextNotContains('New Shortcuts');
// When returning to the layout edit mode, the new block is visible.
$this->drupalGet($layout_url);
$this->assertSession()->pageTextContains('New Shortcuts');
// Save the layout, and the new block is visible.
$page->pressButton('Save');
$this->drupalGet($front);
$this->assertSession()->pageTextContains('New Shortcuts');
// Reconfigure a block and ensure that the layout content is updated.
$this->drupalGet($layout_url);
$this->clickContextualLink('form .block-navigation-shortcuts', 'Configure');
$this->assertOffCanvasFormAfterWait('layout_builder_update_block');
$page->fillField('settings[label]', 'Newer Shortcuts');
$page->pressButton('Update');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertSession()->assertNoElementAfterWait('css', '#drupal-off-canvas');
$this->assertSession()->addressEquals($layout_url);
$this->assertSession()->pageTextContains('Newer Shortcuts');
$this->assertSession()->elementTextNotContains('css', 'form', 'New Shortcuts');
}
/**
* Opens the add block form in the off-canvas dialog.
*
* @param string $block_title
* The block title which will be the link text.
*
* @todo move this from into a trait from
* \Drupal\Tests\layout_builder\FunctionalJavascript\LayoutBuilderTest
*/
private function openAddBlockForm($block_title) {
$this->assertSession()->linkExists('Add block');
$this->clickLink('Add block');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('named', ['link', $block_title]));
$this->clickLink($block_title);
$this->assertOffCanvasFormAfterWait('layout_builder_add_block');
}
/**
* Waits for the specified form and returns it when available and visible.
*
* @param string $expected_form_id
* The expected form ID.
*
* @todo move this from into a trait from
* \Drupal\Tests\layout_builder\FunctionalJavascript\LayoutBuilderTest
*/
private function assertOffCanvasFormAfterWait(string $expected_form_id): void {
$this->assertSession()->assertWaitOnAjaxRequest();
$this->waitForOffCanvasArea();
$off_canvas = $this->assertSession()->elementExists('css', '#drupal-off-canvas');
$this->assertNotNull($off_canvas);
$form_id_element = $off_canvas->find('hidden_field_selector', ['hidden_field', 'form_id']);
// Ensure the form ID has the correct value and that the form is visible.
$this->assertNotEmpty($form_id_element);
$this->assertSame($expected_form_id, $form_id_element->getValue());
$this->assertTrue($form_id_element->getParent()->isVisible());
}
}

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Kernel;
use Drupal\Core\Render\MetadataBubblingUrlGenerator;
use Drupal\Core\Routing\UrlGenerator;
use Drupal\KernelTests\KernelTestBase;
use Drupal\navigation\Plugin\Block\NavigationMenuBlock;
use Drupal\system\Entity\Menu;
use Drupal\system\Tests\Routing\MockRouteProvider;
use Drupal\Tests\Core\Menu\MenuLinkMock;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Tests \Drupal\navigation\Plugin\Block\NavigationMenuBlock.
*
* @group navigation
* @see \Drupal\navigation\Plugin\Derivative\SystemMenuNavigationBlock
* @see \Drupal\navigation\Plugin\Block\NavigationMenuBlock
*/
class NavigationMenuMarkupTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'system',
'navigation',
'menu_test',
'menu_link_content',
'field',
'block',
'user',
'link',
'layout_builder',
];
/**
* The menu for testing.
*
* @var \Drupal\system\MenuInterface
*/
protected $menu;
/**
* The menu link tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTree
*/
protected $linkTree;
/**
* The menu link plugin manager service.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected $menuLinkManager;
/**
* The block manager service.
*
* @var \Drupal\Core\Block\BlockManager
*/
protected $blockManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('menu_link_content');
$this->menuLinkManager = $this->container->get('plugin.manager.menu.link');
$this->linkTree = $this->container->get('menu.link_tree');
$this->blockManager = $this->container->get('plugin.manager.block');
$routes = new RouteCollection();
$requirements = ['_access' => 'TRUE'];
$options = ['_access_checks' => ['access_check.default']];
$routes->add('example1', new Route('/example1', [], $requirements, $options));
$routes->add('example2', new Route('/example2', [], $requirements, $options));
$routes->add('example3', new Route('/example3', [], $requirements, $options));
// Define our RouteProvider mock.
$mock_route_provider = new MockRouteProvider($routes);
$this->container->set('router.route_provider', $mock_route_provider);
// Define our UrlGenerator service that use the new RouteProvider.
$url_generator_non_bubbling = new UrlGenerator(
$mock_route_provider,
$this->container->get('path_processor_manager'),
$this->container->get('route_processor_manager'),
$this->container->get('request_stack'),
$this->container->getParameter('filter_protocols')
);
$url_generator = new MetadataBubblingUrlGenerator($url_generator_non_bubbling, $this->container->get('renderer'));
$this->container->set('url_generator', $url_generator);
// Add a new custom menu.
$menu_name = 'mock';
$label = $this->randomMachineName(16);
$this->menu = Menu::create([
'id' => $menu_name,
'label' => $label,
'description' => 'Description text',
]);
$this->menu->save();
// This creates a tree with the following structure:
// - 1
// - 2
// - 3
// phpcs:disable
$links = [
1 => MenuLinkMock::create(['id' => 'test.example1', 'route_name' => 'example1', 'title' => 'title 1', 'parent' => '', 'weight' => 0]),
2 => MenuLinkMock::create(['id' => 'test.example2', 'route_name' => 'example2', 'title' => 'Another title', 'parent' => '', 'route_parameters' => ['foo' => 'bar'], 'weight' => 1]),
3 => MenuLinkMock::create(['id' => 'test.example3', 'route_name' => 'example3', 'title' => 'A menu link', 'parent' => 'test.example2', 'weight' => 2]),
];
// phpcs:enable
foreach ($links as $instance) {
$this->menuLinkManager->addDefinition($instance->getPluginId(), $instance->getPluginDefinition());
}
}
/**
* Tests the generated HTML markup.
*/
public function testToolbarButtonAttributes(): void {
$block = $this->blockManager->createInstance('navigation_menu:' . $this->menu->id(), [
'region' => 'content',
'id' => 'machine_name',
'level' => 1,
'depth' => NavigationMenuBlock::NAVIGATION_MAX_DEPTH - 1,
]);
$block_build = $block->build();
$render = \Drupal::service('renderer')->renderRoot($block_build);
$dom = new \DOMDocument();
$dom->loadHTML((string) $render);
$xpath = new \DOMXPath($dom);
$items_query = [
"//li[contains(@class,'toolbar-block__list-item')]/a[@data-index-text='t']",
"//li[contains(@class,'toolbar-block__list-item')]/a[@data-icon-text='ti']",
"//li[contains(@class,'toolbar-block__list-item')]/button[@data-index-text='a']",
"//li[contains(@class,'toolbar-block__list-item')]/button[@data-icon-text='An']",
"//li[contains(@class,'toolbar-menu__item--level-1')]/a[@data-index-text='a']",
"//li[contains(@class,'toolbar-menu__item--level-1')]/a[not(@data-icon-text)]",
];
foreach ($items_query as $query) {
$span = $xpath->query($query);
$this->assertEquals(1, $span->length, $query);
}
}
}

View File

@@ -0,0 +1,304 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Kernel;
use Drupal\block\Entity\Block;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteObjectInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\navigation\Plugin\Block\NavigationMenuBlock;
use Drupal\system\Entity\Menu;
use Drupal\system\Tests\Routing\MockRouteProvider;
use Drupal\Tests\Core\Menu\MenuLinkMock;
use Drupal\user\Entity\User;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Tests \Drupal\navigation\Plugin\Block\SystemMenuNavigationBlock.
*
* @group navigation
* @see \Drupal\navigation\Plugin\Derivative\SystemMenuNavigationBlock
* @see \Drupal\navigation\Plugin\Block\NavigationMenuBlock
* @todo Expand test coverage to all SystemMenuNavigationBlock functionality,
* including block_menu_delete().
*/
class SystemMenuNavigationBlockTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'system',
'navigation',
'menu_test',
'menu_link_content',
'field',
'block',
'user',
'link',
'layout_builder',
];
/**
* The navigation block under test.
*
* @var \Drupal\navigation\Plugin\Block\NavigationMenuBlock
*/
protected $navigationBlock;
/**
* The menu for testing.
*
* @var \Drupal\system\MenuInterface
*/
protected $menu;
/**
* The menu link tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTree
*/
protected $linkTree;
/**
* The menu link plugin manager service.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected $menuLinkManager;
/**
* The block manager service.
*
* @var \Drupal\Core\Block\BlockManager
*/
protected $blockManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('menu_link_content');
$account = User::create([
'name' => $this->randomMachineName(),
'status' => 1,
]);
$account->save();
$this->container->get('current_user')->setAccount($account);
$this->menuLinkManager = $this->container->get('plugin.manager.menu.link');
$this->linkTree = $this->container->get('menu.link_tree');
$this->blockManager = $this->container->get('plugin.manager.block');
$routes = new RouteCollection();
$requirements = ['_access' => 'TRUE'];
$options = ['_access_checks' => ['access_check.default']];
$routes->add('example1', new Route('/example1', [], $requirements, $options));
$routes->add('example2', new Route('/example2', [], $requirements, $options));
$routes->add('example3', new Route('/example3', [], $requirements, $options));
$routes->add('example4', new Route('/example4', [], $requirements, $options));
$routes->add('example5', new Route('/example5', [], $requirements, $options));
$routes->add('example6', new Route('/example6', [], $requirements, $options));
$routes->add('example7', new Route('/example7', [], $requirements, $options));
$routes->add('example8', new Route('/example8', [], $requirements, $options));
$routes->add('example9', new Route('/example9', [], $requirements, $options));
$mock_route_provider = new MockRouteProvider($routes);
$this->container->set('router.route_provider', $mock_route_provider);
// Add a new custom menu.
$menu_name = 'mock';
$label = $this->randomMachineName(16);
$this->menu = Menu::create([
'id' => $menu_name,
'label' => $label,
'description' => 'Description text',
]);
$this->menu->save();
// This creates a tree with the following structure:
// - 1
// - 2
// - 3
// - 4
// - 9
// - 5
// - 7
// - 6
// - 8
// With link 6 being the only external link.
// phpcs:disable
$links = [
1 => MenuLinkMock::create(['id' => 'test.example1', 'route_name' => 'example1', 'title' => 'title 1', 'parent' => '', 'weight' => 0]),
2 => MenuLinkMock::create(['id' => 'test.example2', 'route_name' => 'example2', 'title' => 'title 2', 'parent' => '', 'route_parameters' => ['foo' => 'bar'], 'weight' => 1]),
3 => MenuLinkMock::create(['id' => 'test.example3', 'route_name' => 'example3', 'title' => 'title 3', 'parent' => 'test.example2', 'weight' => 2]),
4 => MenuLinkMock::create(['id' => 'test.example4', 'route_name' => 'example4', 'title' => 'title 4', 'parent' => 'test.example3', 'weight' => 3]),
5 => MenuLinkMock::create(['id' => 'test.example5', 'route_name' => 'example5', 'title' => 'title 5', 'parent' => '', 'expanded' => TRUE, 'weight' => 4]),
6 => MenuLinkMock::create(['id' => 'test.example6', 'route_name' => '', 'url' => 'https://www.drupal.org/', 'title' => 'title 6', 'parent' => '', 'weight' => 5]),
7 => MenuLinkMock::create(['id' => 'test.example7', 'route_name' => 'example7', 'title' => 'title 7', 'parent' => 'test.example5', 'weight' => 6]),
8 => MenuLinkMock::create(['id' => 'test.example8', 'route_name' => 'example8', 'title' => 'title 8', 'parent' => '', 'weight' => 7]),
9 => MenuLinkMock::create(['id' => 'test.example9', 'route_name' => 'example9', 'title' => 'title 9', 'parent' => 'test.example4', 'weight' => 7]),
];
// phpcs:enable
foreach ($links as $instance) {
$this->menuLinkManager->addDefinition($instance->getPluginId(), $instance->getPluginDefinition());
}
}
/**
* Tests calculation of a system navigation menu block's config dependencies.
*/
public function testSystemMenuBlockConfigDependencies(): void {
$block = Block::create([
'plugin' => 'navigation_menu:' . $this->menu->id(),
'region' => 'content',
'id' => 'machine_name',
'theme' => 'stark',
]);
$dependencies = $block->calculateDependencies()->getDependencies();
$expected = [
'config' => [
'system.menu.' . $this->menu->id(),
],
'module' => [
'navigation',
'system',
],
'theme' => [
'stark',
],
];
$this->assertSame($expected, $dependencies);
}
/**
* Tests the config start level and depth.
*/
public function testConfigLevelDepth(): void {
// Helper function to generate a configured navigation block instance.
$place_block = function ($level, $depth) {
return $this->blockManager->createInstance('navigation_menu:' . $this->menu->id(), [
'region' => 'content',
'id' => 'machine_name',
'level' => $level,
'depth' => $depth,
]);
};
// All the different navigation block instances we're going to test.
$blocks = [
'level_1_only' => $place_block(1, 0),
'level_2_only' => $place_block(2, 0),
'level_3_only' => $place_block(3, 0),
'level_1_and_beyond' => $place_block(1, NavigationMenuBlock::NAVIGATION_MAX_DEPTH - 1),
'level_2_and_beyond' => $place_block(2, NavigationMenuBlock::NAVIGATION_MAX_DEPTH - 1),
'level_3_and_beyond' => $place_block(3, NavigationMenuBlock::NAVIGATION_MAX_DEPTH - 1),
];
// Expectations are independent of the active trail.
$expectations = [];
$expectations['level_1_only'] = [
'test.example1' => [],
'test.example2' => [],
'test.example5' => [],
'test.example6' => [],
'test.example8' => [],
];
$expectations['level_2_only'] = [
'test.example3' => [],
'test.example7' => [],
];
$expectations['level_3_only'] = [
'test.example4' => [],
];
$expectations['level_1_and_beyond'] = [
'test.example1' => [],
'test.example2' => [
'test.example3' => [
'test.example4' => [],
],
],
'test.example5' => [
'test.example7' => [],
],
'test.example6' => [],
'test.example8' => [],
];
$expectations['level_2_and_beyond'] = [
'test.example3' => [
'test.example4' => [
'test.example9' => [],
],
],
'test.example7' => [],
];
$expectations['level_3_and_beyond'] = [
'test.example4' => [
'test.example9' => [],
],
];
// Scenario 1: test all navigation block instances when there's no active
// trail.
foreach ($blocks as $id => $block) {
$block_build = $block->build();
$items = $block_build['#items'] ?? [];
$this->assertSame($expectations[$id], $this->convertBuiltMenuToIdTree($items), "Menu block $id with no active trail renders the expected tree.");
}
// Scenario 2: test all navigation block instances when there's an active
// trail.
$route = $this->container->get('router.route_provider')->getRouteByName('example3');
$request = new Request();
$request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'example3');
$request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, $route);
$request->setSession(new Session(new MockArraySessionStorage()));
$this->container->get('request_stack')->push($request);
// \Drupal\Core\Menu\MenuActiveTrail uses the cache collector pattern, which
// includes static caching. Since this second scenario simulates a second
// request, we must also simulate it for the MenuActiveTrail service, by
// clearing the cache collector's static cache.
\Drupal::service('menu.active_trail')->clear();
foreach ($blocks as $id => $block) {
$block_build = $block->build();
$items = $block_build['#items'] ?? [];
$this->assertSame($expectations[$id], $this->convertBuiltMenuToIdTree($items), "Menu navigation block $id with an active trail renders the expected tree.");
}
}
/**
* Helper method to allow for easy menu link tree structure assertions.
*
* Converts the result of MenuLinkTree::build() in a "menu link ID tree".
*
* @param array $build
* The return value of MenuLinkTree::build()
*
* @return array
* The "menu link ID tree" representation of the given render array.
*/
protected function convertBuiltMenuToIdTree(array $build) {
$level = [];
foreach (Element::children($build) as $id) {
$level[$id] = [];
if (isset($build[$id]['below'])) {
$level[$id] = $this->convertBuiltMenuToIdTree($build[$id]['below']);
}
}
return $level;
}
}