Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

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 navigation.
*
* @group navigation
*/
class GenericTest extends GenericModuleTestBase {}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
// cspell:ignore foobarbaz baznew
/**
* Tests for navigation content_top section.
*
* @group navigation
*/
class NavigationContentTopTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['navigation', 'navigation_test', 'test_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->createUser([
'access navigation',
]));
}
/**
* Tests behavior of content_top section hooks.
*/
public function testNavigationContentTop(): void {
$test_page_url = Url::fromRoute('test_page_test.test_page');
$this->drupalGet($test_page_url);
$this->assertSession()->elementNotExists('css', '.admin-toolbar__content-top');
\Drupal::keyValue('navigation_test')->set('content_top', 1);
Cache::invalidateTags(['navigation_test']);
$this->drupalGet($test_page_url);
$this->assertSession()->elementTextContains('css', '.admin-toolbar__content-top', 'foobarbaz');
\Drupal::keyValue('navigation_test')->set('content_top_alter', 1);
Cache::invalidateTags(['navigation_test']);
$this->drupalGet($test_page_url);
$this->assertSession()->elementTextContains('css', '.admin-toolbar__content-top', 'baznew bar');
}
}

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the default block provider logic.
*
* @group navigation
*/
class NavigationDefaultBlockDefinitionTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['test_page_test', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the default block flow enabling Navigation module first.
*/
public function testNavigationDefaultAfterNavigation(): void {
$test_page_url = Url::fromRoute('test_page_test.test_page');
$module_installer = \Drupal::service('module_installer');
// After installing Navigation, the bar is present, but not the block.
$module_installer->install(['navigation']);
$this->drupalLogin($this->drupalCreateUser(['access navigation']));
$this->drupalGet($test_page_url);
$this->assertSession()->elementExists('css', '.admin-toolbar');
$this->assertSession()->elementNotExists('css', '.toolbar-button--icon--test-block');
// After installing Navigation Test Block, both elements are present.
$module_installer->install(['navigation_test_block']);
$this->drupalGet($test_page_url);
$this->assertSession()->elementExists('css', '.admin-toolbar');
$this->assertSession()->elementContains('css', '.toolbar-button--icon--test-block', 'Test Navigation Block');
}
/**
* Tests the default block flow enabling the block provider module first.
*/
public function testNavigationDefaultBeforeNavigation(): void {
$test_page_url = Url::fromRoute('test_page_test.test_page');
$module_installer = \Drupal::service('module_installer');
// After installing Navigation Test Block, none of the elements are present.
$module_installer->install(['navigation_test_block']);
$this->drupalGet($test_page_url);
$this->assertSession()->elementNotExists('css', '.admin-toolbar');
$this->assertSession()->elementNotExists('css', '.toolbar-button--icon--test-block');
// After installing Navigation, both elements are present.
$module_installer->install(['navigation']);
$this->drupalLogin($this->drupalCreateUser(['access navigation']));
$this->drupalGet($test_page_url);
$this->assertSession()->elementExists('css', '.admin-toolbar');
$this->assertSession()->elementContains('css', '.toolbar-button--icon--test-block', 'Test Navigation Block');
}
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Tests\BrowserTestBase;
/**
* Tests Navigation Icon behavior.
*
* @group navigation
*/
class NavigationIconTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['navigation', 'navigation_test', 'test_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->createUser([
'access navigation',
]));
}
/**
* Tests the behavior of custom icons.
*/
public function testNavigationIcon(): void {
$this->drupalGet('/test-page');
$this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--star svg', 'width', '25');
$this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--star svg', 'class', 'toolbar-button__icon foo');
$this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--pencil svg', 'width', '20');
$this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--pencil svg', 'class', 'toolbar-button__icon');
$this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--navigation-media svg', 'width', '20');
$this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--navigation-media svg', 'class', 'toolbar-button__icon');
$this->assertSession()->elementExists('css', 'a.toolbar-button--icon--navigation-test-navigation__no-icon');
$this->assertSession()->elementNotExists('css', 'a.toolbar-button--icon--navigation-test-navigation__no-icon svg');
// Rebuild menu with alterations and reload the page to check them.
\Drupal::keyValue('navigation_test')->set('menu_links_discovered_alter', 1);
\Drupal::service(MenuLinkManagerInterface::class)->rebuild();
$this->drupalGet('/test-page');
$this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--star svg', 'width', '25');
$this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--star svg', 'class', 'toolbar-button__icon foo');
$this->assertSession()->elementNotExists('css', 'a.toolbar-button--icon--pencil svg');
$this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--navigation-media svg', 'width', '20');
$this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--navigation-media svg', 'class', 'toolbar-button__icon');
$this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--radioactive svg', 'width', '20');
$this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--radioactive svg', 'class', 'toolbar-button__icon');
}
}

View File

@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Url;
use Drupal\layout_builder\SectionComponent;
use Drupal\Tests\system\Functional\Cache\PageCacheTagsTestBase;
/**
* Tests for \Drupal\navigation\Plugin\Block\NavigationLinkBlockTest.
*
* @group navigation
*/
class NavigationLinkBlockTest extends PageCacheTagsTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['navigation', 'test_page_test', 'entity_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* User with permission to administer navigation blocks and access navigation.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* An authenticated user to test navigation block caching.
*
* @var \Drupal\user\UserInterface
*/
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',
'administer site configuration',
'access navigation',
'view test entity',
]);
// Create additional users to test caching modes.
$this->normalUser = $this->drupalCreateUser([
'access navigation',
]);
}
/**
* Test output of the link navigation with regards to caching and contents.
*/
public function testNavigationLinkBlockCache(): void {
$label = 'Admin Main Page';
$link_title = 'Navigation Settings';
$link_uri = '/admin/config/user-interface/navigation/settings';
$link_icon = 'admin-link';
$this->appendNavigationLinkBlock($label, $link_title, 'internal:' . $link_uri, $link_icon);
// 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 not see the admin page link in the page.
$link_selector = '.admin-toolbar__item .toolbar-button--icon--' . $link_icon;
$this->assertSession()->elementNotExists('css', $link_selector);
$this->assertSession()->pageTextNotContains($link_title);
$this->assertSession()->pageTextNotContains($label);
// Login as a different user, UI should update.
$this->drupalLogin($this->adminUser);
$this->verifyDynamicPageCache($test_page_url, 'MISS');
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->drupalGet(Url::fromRoute('navigation.settings'));
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->elementExists('css', $link_selector);
$this->assertSession()->pageTextContains($link_title);
$this->assertSession()->pageTextContains($label);
$this->assertSession()
->elementTextContains('css', $link_selector, $link_title);
// The link should link to the admin page.
$link = $this->getSession()->getPage()->find('named', [
'link',
$link_title,
]);
$this->assertStringContainsString('/admin/config/user-interface/navigation/settings', $link->getAttribute('href'));
}
/**
* Test block visibility based on the link access logic.
*/
public function testNavigationLinkBlockVisibility(): void {
// Add a link to an external URL.
$external_label = 'External Link Block';
$external_link_title = 'Link to example';
$this->appendNavigationLinkBlock($external_label, $external_link_title, 'http://example.com', 'external');
// Create an entity and create a link to it.
$entity_type_manager = \Drupal::entityTypeManager();
$entity_test_storage = $entity_type_manager->getStorage('entity_test');
$entity_test_link = $entity_test_storage->create(['name' => 'test']);
$entity_test_link->save();
$entity_label = 'Entity Link BLock';
$entity_link_title = 'Link to entity';
$this->appendNavigationLinkBlock($entity_label, $entity_link_title, 'entity:entity_test/' . $entity_test_link->id(), 'entity');
// Link to admin page.
$admin_label = 'Admin Main Page';
$admin_link_title = 'Navigation Settings';
$this->appendNavigationLinkBlock($admin_label, $admin_link_title, 'internal:/admin/config/user-interface/navigation/settings', 'admin');
// Link to generic internal page (Help Link).
$help_label = 'Help Block';
$help_link_title = 'Link to help';
$this->appendNavigationLinkBlock($help_label, $help_link_title, 'internal:/admin/help', 'internal');
// Admin user should be capable to access to all the links but the internal
// one, since Help module is not enabled.
$test_page_url = Url::fromRoute('test_page_test.test_page');
$this->drupalLogin($this->adminUser);
$this->drupalGet($test_page_url);
$this->assertSession()->pageTextContains($external_label);
$this->assertSession()->pageTextContains($external_link_title);
$this->assertSession()->pageTextContains($entity_label);
$this->assertSession()->pageTextContains($entity_link_title);
$this->assertSession()->pageTextContains($admin_label);
$this->assertSession()->pageTextContains($admin_link_title);
$this->assertSession()->pageTextNotContains($help_label);
$this->assertSession()->pageTextNotContains($help_link_title);
// Normal user should not have access only to the external link.
$this->drupalLogin($this->normalUser);
$this->drupalGet($test_page_url);
$this->assertSession()->pageTextContains($external_label);
$this->assertSession()->pageTextContains($external_link_title);
$this->assertSession()->pageTextNotContains($entity_label);
$this->assertSession()->pageTextNotContains($entity_link_title);
$this->assertSession()->pageTextNotContains($admin_label);
$this->assertSession()->pageTextNotContains($admin_link_title);
$this->assertSession()->pageTextNotContains($help_label);
$this->assertSession()->pageTextNotContains($help_link_title);
// Enable Help module and grant permissions to admin user.
// Admin user should be capable to access to all the links
\Drupal::service('module_installer')->install(['help']);
$this->adminUser->addRole($this->drupalCreateRole(['access help pages']))->save();
$this->drupalLogin($this->adminUser);
$this->drupalGet($test_page_url);
$this->assertSession()->pageTextContains($external_label);
$this->assertSession()->pageTextContains($external_link_title);
$this->assertSession()->pageTextContains($entity_label);
$this->assertSession()->pageTextContains($entity_link_title);
$this->assertSession()->pageTextContains($admin_label);
$this->assertSession()->pageTextContains($admin_link_title);
$this->assertSession()->pageTextContains($help_label);
$this->assertSession()->pageTextContains($help_link_title);
// Normal user should not have access only to the external link.
$this->drupalLogin($this->normalUser);
$this->drupalGet($test_page_url);
$this->assertSession()->pageTextContains($external_label);
$this->assertSession()->pageTextContains($external_link_title);
$this->assertSession()->pageTextNotContains($entity_label);
$this->assertSession()->pageTextNotContains($entity_link_title);
$this->assertSession()->pageTextNotContains($admin_label);
$this->assertSession()->pageTextNotContains($admin_link_title);
$this->assertSession()->pageTextNotContains($help_label);
$this->assertSession()->pageTextNotContains($help_link_title);
}
/**
* Adds a Navigation Link Block to the sidebar.
*
* @param string $label
* The block label.
* @param string $link_title
* The link title.
* @param string $link_uri
* The link uri.
* @param string $link_icon
* The link icon CSS class.
*/
protected function appendNavigationLinkBlock(string $label, string $link_title, string $link_uri, string $link_icon): void {
$section_storage_manager = \Drupal::service('plugin.manager.layout_builder.section_storage');
$cacheability = new CacheableMetadata();
$contexts = [
'navigation' => new Context(ContextDefinition::create('string'), 'navigation'),
];
/** @var \Drupal\layout_builder\SectionListInterface $section_list */
$section_list = $section_storage_manager->findByContext($contexts, $cacheability);
$section = $section_list->getSection(0);
$section->appendComponent(new SectionComponent(\Drupal::service('uuid')->generate(), 'content', [
'id' => 'navigation_link',
'label' => $label,
'label_display' => '1',
'provider' => 'navigation',
'context_mapping' => [],
'title' => $link_title,
'uri' => $link_uri,
'icon_class' => $link_icon,
]));
$section_list->save();
}
}

View File

@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\file\Entity\File;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests for \Drupal\navigation\Form\SettingsForm.
*
* @group navigation
*/
class NavigationLogoTest extends BrowserTestBase {
use StringTranslationTrait;
use TestFileCreationTrait;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* A user with administrative permissions.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected static $modules = [
'navigation',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Inject the file_system service.
$this->fileSystem = $this->container->get('file_system');
// Create and log in an administrative user.
$this->adminUser = $this->drupalCreateUser([
'administer site configuration',
'access navigation',
]);
$this->drupalLogin($this->adminUser);
}
/**
* Tests Navigation logo configuration base options.
*/
public function testSettingsLogoOptionsForm(): void {
$test_files = $this->getTestFiles('image');
// Navigate to the settings form.
$this->drupalGet('/admin/config/user-interface/navigation/settings');
$this->assertSession()->statusCodeEquals(200);
// Option 1. Check the default logo provider.
$this->assertSession()->fieldValueEquals('logo_provider', 'default');
$this->assertSession()->elementExists('css', 'a.admin-toolbar__logo > svg');
// Option 2: Set the logo provider to hide and check.
$edit = [
'logo_provider' => 'hide',
];
$this->submitForm($edit, 'Save configuration');
$this->assertSession()->pageTextContains('The configuration options have been saved.');
$this->assertSession()->elementNotExists('css', 'a.admin-toolbar__logo');
// Option 3: Set the logo provider to custom and upload a logo.
$file = reset($test_files);
$logo_file = File::create((array) $file + ['status' => 1]);
$logo_file->save();
$this->assertNotEmpty($logo_file, 'File entity is not empty.');
$edit = [
'logo_provider' => 'custom',
'logo_path' => $logo_file->getFileUri(),
];
$this->submitForm($edit, 'Save configuration');
// Refresh the page to verify custom logo is placed.
$this->drupalGet('/admin/config/user-interface/navigation/settings');
$this->assertSession()->elementExists('css', 'a.admin-toolbar__logo > img');
$this->assertSession()->elementAttributeContains('css', 'a.admin-toolbar__logo > img', 'src', $logo_file->getFilename());
// Option 4: Set the custom logo to an image in the source code.
$edit = [
'logo_provider' => 'custom',
'logo_path' => 'core/misc/logo/drupal-logo.svg',
];
$this->submitForm($edit, 'Save configuration');
// Refresh the page to verify custom logo is placed.
$this->drupalGet('/admin/config/user-interface/navigation/settings');
$this->assertSession()->elementExists('css', 'a.admin-toolbar__logo > img');
$this->assertSession()->elementAttributeContains('css', 'a.admin-toolbar__logo > img', 'src', 'drupal-logo.svg');
// Option 5: Upload custom logo.
$file = end($test_files);
$edit = [
'logo_provider' => 'custom',
'files[logo_upload]' => $this->fileSystem->realpath($file->uri),
];
$this->submitForm($edit, 'Save configuration');
$this->assertSession()->statusMessageContains('The image was resized to fit within the navigation logo expected dimensions of 40x40 pixels. The new dimensions of the resized image are 40x27 pixels.');
// Refresh the page to verify custom logo is placed.
$this->drupalGet('/admin/config/user-interface/navigation/settings');
$this->assertSession()->elementExists('css', 'a.admin-toolbar__logo > img');
$this->assertSession()->elementAttributeContains('css', 'a.admin-toolbar__logo > img', 'src', $file->name);
}
}

View File

@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the definition of navigation safe blocks.
*
* @group navigation
*/
class NavigationSafeBlockDefinitionTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['navigation', 'navigation_test', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* User with permission to administer navigation blocks and access navigation.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create an admin user, log in and enable test navigation blocks.
$this->adminUser = $this->drupalCreateUser([
'configure navigation layout',
'access navigation',
'administer blocks',
]);
$this->drupalLogin($this->adminUser);
}
/**
* Tests logic to include blocks in Navigation Layout UI.
*/
public function testNavigationSafeBlockDefinition(): void {
// Confirm that default blocks are available.
$layout_url = '/layout_builder/choose/block/navigation/navigation.block_layout/0/content';
$this->drupalGet($layout_url);
$this->assertSession()->linkExists('Administration');
$this->assertSession()->linkExists('Content');
$this->assertSession()->linkExists('Footer');
$this->assertSession()->linkExists('Navigation Shortcuts');
$this->assertSession()->linkExists('User');
$this->assertSession()->linkNotExists('Link');
// Apply changes, clear cache and confirm that changes are applied.
\Drupal::state()->set('navigation_safe_alter', TRUE);
\Drupal::cache('discovery')->delete('block_plugins');
$this->drupalGet($this->getUrl());
$this->assertSession()->linkExists('Administration');
$this->assertSession()->linkExists('Content');
$this->assertSession()->linkExists('Footer');
$this->assertSession()->linkExists('Link');
$this->assertSession()->linkNotExists('Navigation Shortcuts');
}
/**
* Tests logic to exclude blocks in Block Layout UI.
*/
public function testNavigationBlocksHiddenInBlockLayout(): void {
$block_url = '/admin/structure/block';
$this->drupalGet($block_url);
$this->clickLink('Place block');
$this->assertSession()->linkByHrefNotExists('/admin/structure/block/add/navigation_menu%3Aadmin/stark');
$this->assertSession()->linkByHrefNotExists('/admin/structure/block/add/navigation_menu%3Acontent/stark');
$this->assertSession()->linkByHrefNotExists('/admin/structure/block/add/navigation_shortcuts/stark');
$this->assertSession()->linkByHrefNotExists('/admin/structure/block/add/navigation_user/stark');
$this->assertSession()->linkByHrefNotExists('/admin/structure/block/add/navigation_link/stark');
}
}

View File

@ -0,0 +1,224 @@
<?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;
/**
* {@inheritdoc}
*/
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.
$admin_user = $this->drupalCreateUser([
'administer site configuration',
'access navigation',
'administer shortcuts',
'access shortcuts',
]);
$this->drupalLogin($admin_user);
$this->drupalGet('admin/config/system/cron');
$expected_cache_tags = array_merge([
'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',
'config:system.menu.navigation-user-links',
'http_response',
'rendered',
], $admin_user->getCacheTags());
$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()->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', 'route']);
$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', 'route']);
$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,81 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Drupal\node\NodeInterface;
use Drupal\Tests\content_moderation\Functional\ModerationStateTestBase;
/**
* Tests the top bar behavior along with content moderation.
*
* @group navigation
*/
class NavigationTopBarContentModerationTest extends ModerationStateTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_moderation',
'node',
'navigation',
];
/**
* Node used to check top bar options.
*
* @var \Drupal\node\NodeInterface
*/
protected NodeInterface $node;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser
->addRole($this->drupalCreateRole(['access navigation']))
->save();
$this->drupalLogin($this->adminUser);
$this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE);
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
$this->node = $this->drupalCreateNode([
'type' => 'moderated_content',
'moderation_state' => 'published',
]);
}
/**
* Tests the interaction of page actions and content moderation.
*/
public function testContentModerationPageActions(): void {
$this->drupalGet($this->node->toUrl());
$this->assertSession()->elementNotContains('xpath', '//*[@id="top-bar-page-actions"]/ul', 'Latest version');
// Publish a new draft.
$this->node->setNewRevision(TRUE);
$this->node->setTitle($this->node->getTitle() . ' - draft');
$this->node->moderation_state->value = 'draft';
$this->node->save();
$this->drupalGet($this->node->toUrl());
$this->assertSession()->elementContains('xpath', '//*[@id="top-bar-page-actions"]/ul', 'Latest version');
$this->assertSession()->elementContains('css', '.toolbar-badge--success', 'Published (Draft available)');
// Confirm that Edit option is featured in Latest version page.
$this->clickLink('Latest version');
$this->assertSession()->elementNotContains('xpath', '//*[@id="top-bar-page-actions"]/ul', 'Edit');
$this->assertSession()->elementContains('css', '.toolbar-badge--info', 'Draft');
$this->assertSession()->elementTextEquals('xpath', "//div[contains(@class, 'top-bar__content')]/div[contains(@class, 'top-bar__actions')]/a[contains(@class, 'toolbar-button--icon--pencil')]", "Edit");
}
}

View File

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\user\UserInterface;
/**
* Tests the PageContext top bar item functionality.
*
* @group navigation
*/
class NavigationTopBarPageContextTest extends BrowserTestBase {
use ContentTypeCreationTrait;
use NodeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'navigation',
'test_page_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* An admin user to configure the test environment.
*
* @var \Drupal\user\UserInterface
*/
protected UserInterface $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create and log in an administrative user.
$this->adminUser = $this->drupalCreateUser([
'access navigation',
'bypass node access',
]);
$this->drupalLogin($this->adminUser);
// Ensure the 'article' content type exists.
$this->createContentType(['type' => 'article', 'name' => 'Article']);
}
/**
* Tests the PageContext top bar item output for a published node.
*/
public function testPageContextTopBarItemNode(): void {
// Create a published node entity.
$node = $this->createNode([
'type' => 'article',
'title' => 'No easy twist on the bow',
'status' => 1,
'uid' => $this->adminUser->id(),
]);
$test_page_url = Url::fromRoute('test_page_test.test_page');
$this->drupalGet($test_page_url);
// Ensure the top bar item is not present.
$this->assertSession()->elementNotExists('css', '.top-bar .top-bar__context .toolbar-title');
// Test the PageContext output for the published node.
$this->drupalGet($node->toUrl());
// Ensure the top bar exists and is valid.
$this->assertSession()->elementTextEquals('css', '.top-bar .top-bar__context .toolbar-title', 'No easy twist on the bow');
$this->assertSession()->elementTextEquals('css', '.top-bar .top-bar__context .toolbar-badge', 'Published');
$this->drupalGet($node->toUrl('edit-form'));
// Ensure the top bar exists and is valid.
$this->assertSession()->elementTextEquals('css', '.top-bar .top-bar__context .toolbar-title', 'No easy twist on the bow');
$this->assertSession()->elementTextEquals('css', '.top-bar .top-bar__context .toolbar-badge', 'Published');
// Unpublish the node.
$node->setUnpublished();
$node->save();
// Test the PageContext output for the unpublished node.
$this->drupalGet($node->toUrl());
// Ensure the top bar exists and is valid.
$this->assertSession()->elementTextEquals('css', '.top-bar .top-bar__context .toolbar-title', 'No easy twist on the bow');
$this->assertSession()->elementTextEquals('css', '.top-bar .top-bar__context .toolbar-badge', 'Unpublished');
$this->drupalGet($node->toUrl('edit-form'));
// Ensure the top bar exists and is valid.
$this->assertSession()->elementTextEquals('css', '.top-bar .top-bar__context .toolbar-title', 'No easy twist on the bow');
$this->assertSession()->elementTextEquals('css', '.top-bar .top-bar__context .toolbar-badge', 'Unpublished');
}
}

View File

@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Behat\Mink\Element\NodeElement;
use Drupal\Component\Utility\SortArray;
use Drupal\Core\Url;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\system\Functional\Cache\PageCacheTagsTestBase;
/**
* Tests the top bar functionality.
*
* @group navigation
*/
class NavigationTopBarTest extends PageCacheTagsTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'navigation',
'navigation_test_top_bar',
'node',
'layout_builder',
'test_page_test',
];
/**
* {@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',
'configure any layout',
]);
$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 {
// Test page does not include the Top Bar.
$test_page_url = Url::fromRoute('test_page_test.test_page');
$this->verifyDynamicPageCache($test_page_url, 'MISS');
$this->verifyDynamicPageCache($test_page_url, 'HIT');
$this->assertSession()->elementNotExists('xpath', "//div[contains(@class, 'top-bar__content')]/div[contains(@class, 'top-bar__actions')]/button");
// Top Bar is visible on node pages.
$this->verifyDynamicPageCache($this->node->toUrl(), 'MISS');
$this->verifyDynamicPageCache($this->node->toUrl(), 'HIT');
$this->assertSession()->elementExists('xpath', "(//div[contains(@class, 'top-bar__content')]/div[contains(@class, 'top-bar__actions')]/button)[1]");
$this->assertSession()->elementTextEquals('xpath', "//div[contains(@class, 'top-bar__content')]/div[contains(@class, 'top-bar__actions')]/a[contains(@class, 'toolbar-button--icon--pencil')]", "Edit");
$this->assertSession()->elementAttributeContains('xpath', "(//div[contains(@class, 'top-bar__content')]/div[contains(@class, 'top-bar__actions')]/button)[1]", 'class', 'toolbar-button--icon--dots');
// Verify that the action link contains an extra attribute.
$this->assertSession()->elementTextEquals('xpath', "//div[contains(@class, 'top-bar__content')]/div[contains(@class, 'top-bar__actions')]/a[contains(@class, 'toolbar-button--icon--database')]", "Test link");
$this->assertSession()->elementAttributeContains('xpath', "//div[contains(@class, 'top-bar__content')]/div[contains(@class, 'top-bar__actions')]/a[contains(@class, 'toolbar-button--icon--database')]", 'data-dialog-type', 'modal');
// Find all the dropdown links and check if the top bar is there as well.
$toolbar_links = $this->mink->getSession()->getPage()->find('xpath', '//*[@id="top-bar-page-actions"]/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')]/div[contains(@class, 'top-bar__actions')]/button)[1]");
$this->assertSession()->elementAttributeContains('xpath', "(//div[contains(@class, 'top-bar__content')]/div[contains(@class, 'top-bar__actions')]/button)[1]", 'class', 'toolbar-button--icon--dots');
// Ensure that link to current page is not included in the dropdown.
$url = $this->getSession()->getCurrentUrl();
$this->assertSession()->linkByHrefNotExistsExact(parse_url($url, PHP_URL_PATH));
// Ensure that the actions are displayed in the correct order.
$this->assertActionsWeight($toolbar_links);
}
// Regular tabs are visible for user that cannot access to navigation.
$this->drupalLogin($this->drupalCreateUser([
'bypass node access',
]));
$this->drupalGet($this->node->toUrl());
$this->assertSession()->elementNotExists('xpath', "//div[contains(@class, 'top-bar__content')]/div[contains(@class, 'top-bar__actions')]/button");
$this->assertSession()->elementExists('xpath', '//div[@id="block-tabs"]');
}
/**
* Asserts that top bar actions respect local tasks weights.
*
* @param \Behat\Mink\Element\NodeElement $toolbar_links
* Action links to assert.
*/
protected function assertActionsWeight(NodeElement $toolbar_links): void {
// Displayed action links in the top bar.
$displayed_links = array_map(
fn($link) => $link->getText(),
$toolbar_links->findAll('css', 'li')
);
// Extract the route name from the URL.
$current_url = $this->getSession()->getCurrentUrl();
// Convert alias to system path.
$path = parse_url($current_url, PHP_URL_PATH);
if ($GLOBALS['base_path'] !== '/') {
$path = str_replace($GLOBALS['base_path'], '/', $path);
}
// Get local tasks for the current route.
$entity_local_tasks = \Drupal::service('plugin.manager.menu.local_task')->getLocalTasks(Url::fromUserInput($path)->getRouteName());
// Sort order of tabs based on their weights.
uasort($entity_local_tasks['tabs'], [SortArray::class, 'sortByWeightProperty']);
// Extract the expected order based on sorted weights.
$expected_order = array_values(array_map(fn($task) => $task['#link']['title'], $entity_local_tasks['tabs']));
// Filter out elements not in displayed_links.
$expected_order = array_values(array_filter($expected_order, fn($title) => in_array($title, $displayed_links, TRUE)));
// Ensure that the displayed links match the expected order.
$this->assertSame($expected_order, $displayed_links, 'Local tasks are displayed in the correct order based on their weights.');
}
}

View File

@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Drupal\Core\Url;
use Drupal\Tests\system\Functional\Cache\PageCacheTagsTestBase;
// cspell:ignore navigationuser linksuserwrapper
/**
* Tests for \Drupal\navigation\Plugin\NavigationBlock\NavigationUserBlock.
*
* @group navigation
*/
class NavigationUserBlockTest extends PageCacheTagsTestBase {
/**
* {@inheritdoc}
*/
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="navigation-link-navigationuser-linksuserwrapper"] > .toolbar-button__label')[0]->getText();
$this->assertEquals($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('/user/edit', $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="navigation-link-navigationuser-linksuserwrapper"] > .toolbar-button__label')[0]->getText();
$this->assertEquals($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('/user/edit', $links[0]->getAttribute('href'));
}
/**
* Test output of user navigation block when there are no menu items.
*/
public function testNavigationUserBlockFallback(): void {
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
$items = [
'navigation.user_links.user.page',
'navigation.user_links.user.edit',
'navigation.user_links.user.logout',
];
foreach ($items as $item) {
$front_page_link = $menu_link_manager->getDefinition($item);
$front_page_link['enabled'] = FALSE;
$menu_link_manager->updateDefinition($item, $front_page_link);
}
$this->drupalLogin($this->normalUser);
// We should see the users name in the navigation menu in a link.
$rendered_user_name = $this->cssSelect('a.toolbar-button--icon--navigation-user-links-user-wrapper')[0]->getText();
$this->assertEquals($this->normalUser->getDisplayName(), $rendered_user_name);
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests for \Drupal\navigation\WorkspacesLazyBuilder.
*
* @group navigation
*/
class NavigationWorkspacesUiTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['navigation', 'workspaces_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$admin_user = $this->drupalCreateUser([
'access navigation',
'administer workspaces',
]);
$this->drupalLogin($admin_user);
}
/**
* Tests the Workspaces button in the navigation bar.
*/
public function testWorkspacesNavigationButton(): void {
$this->drupalGet('<front>');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--workspaces svg', 'width', '20');
$this->assertSession()->elementAttributeContains('css', 'a.toolbar-button--icon--workspaces svg', 'class', 'toolbar-button__icon');
}
}

View File

@ -0,0 +1,245 @@
<?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',
'layout_builder_form_block_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 testNavigationBlockAdminUiPageNestedForm(): void {
$layout_url = '/admin/config/user-interface/navigation-block';
$this->drupalLogin($this->adminUser);
// Edit the layout and add a block that contains a form.
$this->drupalGet($layout_url);
$this->getSession()->getPage()->pressButton('Enable edit mode');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->openAddBlockForm('Layout Builder form block test form api form block');
$this->getSession()->getPage()->checkField('settings[label_display]');
// Save the new block, and ensure it is displayed on the page.
$this->getSession()->getPage()->pressButton('Add block');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertSession()->assertNoElementAfterWait('css', '#drupal-off-canvas');
$this->assertSession()->addressEquals($layout_url);
$this->assertSession()->pageTextContains('Layout Builder form block test form api form block');
$this->getSession()->getPage()->pressButton('Save');
$unexpected_save_message = 'You have unsaved changes';
$expected_save_message = 'Saved navigation blocks';
$this->assertSession()->statusMessageNotContains($unexpected_save_message);
$this->assertSession()->statusMessageContains($expected_save_message);
// Try to save the layout again and confirm it can save because there are no
// nested form tags.
$this->drupalGet($layout_url);
$this->getSession()->getPage()->pressButton('Enable edit mode');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->getSession()->getPage()->checkField('toggle_content_preview');
$this->getSession()->getPage()->pressButton('Save');
$this->assertSession()->statusMessageNotContains($unexpected_save_message);
$this->assertSession()->statusMessageContains($expected_save_message);
}
/**
* 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();
$this->getSession()->getPage()->pressButton('Enable edit mode');
$this->assertSession()->assertWaitOnAjaxRequest();
// 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('.layout-builder .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', '.layout-builder .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 page, the new block is not visible.
$this->drupalGet($layout_url);
$this->assertSession()->pageTextNotContains('New Shortcuts');
// When returning to the layout edit mode, the new block is visible.
$this->getSession()->getPage()->pressButton('Enable edit mode');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertSession()->pageTextContains('New Shortcuts');
// Save the layout, and the new block is visible in the front page.
$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->getSession()->getPage()->pressButton('Enable edit mode');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->clickContextualLink('.layout-builder .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): void {
$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,111 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\FunctionalJavascript;
use Behat\Mink\Element\Element;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
// cspell:ignore navigationuser linksuserwrapper
/**
* Tests for \Drupal\navigation\Plugin\NavigationBlock\NavigationUserBlock.
*
* @group navigation
*/
class NavigationUserBlockTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
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',
'change own username',
]);
// 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 contents.
*/
public function testNavigationUserBlock(): void {
$test_page_url = Url::fromRoute('test_page_test.test_page');
// Login as a limited access user, and verify that the username is displayed
// correctly.
$this->drupalLogin($this->normalUser);
$this->drupalGet($test_page_url);
// Wait for the default 'My Account' text to be replaced.
$this->getSession()->getPage()->waitFor(10, function (Element $page) {
return $page->find('css', '[aria-controls="navigation-link-navigationuser-linksuserwrapper"] > .toolbar-button__label')->getText() !== 'My Account';
});
// We should see the users name in the navigation menu.
$rendered_user_name = $this->cssSelect('[aria-controls="navigation-link-navigationuser-linksuserwrapper"] > .toolbar-button__label')[0]->getText();
$this->assertEquals($this->normalUser->getDisplayName(), $rendered_user_name);
// Login as an admin access user, and verify that the username is displayed
// correctly.
$this->drupalLogin($this->adminUser);
$this->drupalGet($test_page_url);
// Wait for the default 'My Account' text to be replaced.
$this->getSession()->getPage()->waitFor(10, function (Element $page) {
return $page->find('css', '[aria-controls="navigation-link-navigationuser-linksuserwrapper"] > .toolbar-button__label')->getText() !== 'My Account';
});
// We should see the users name in the navigation menu.
$rendered_user_name = $this->cssSelect('[aria-controls="navigation-link-navigationuser-linksuserwrapper"] > .toolbar-button__label')[0]->getText();
$this->assertEquals($this->adminUser->getDisplayName(), $rendered_user_name);
// Change the users name, assert that the changes reflect in the navigation.
$new_username = $this->randomMachineName();
$this->drupalGet('user/' . $this->adminUser->id() . '/edit');
$this->submitForm(['name' => $new_username], 'Save');
// Wait for the default 'My Account' text to be replaced.
$this->getSession()->getPage()->waitFor(10, function (Element $page) {
return $page->find('css', '[aria-controls="navigation-link-navigationuser-linksuserwrapper"] > .toolbar-button__label')->getText() !== 'My Account';
});
// We should see the users name in the navigation menu.
$rendered_user_name = $this->cssSelect('[aria-controls="navigation-link-navigationuser-linksuserwrapper"] > .toolbar-button__label')[0]->getText();
$this->assertEquals($new_username, $rendered_user_name);
}
}

View File

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\PerformanceTestBase;
/**
* Tests performance with the navigation toolbar enabled.
*
* Stark is used as the default theme so that this test is not Olivero specific.
*
* @todo move this coverage to StandardPerformanceTest when Navigation is
* enabled by default.
*
* @group Common
* @group #slow
* @requires extension apcu
*/
class PerformanceTest extends PerformanceTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $profile = 'standard';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Uninstall the toolbar.
\Drupal::service('module_installer')->uninstall(['toolbar']);
\Drupal::service('module_installer')->install(['navigation']);
}
/**
* Tests performance of the navigation toolbar.
*/
public function testLogin(): void {
$user = $this->drupalCreateUser();
$user->addRole('administrator');
$user->save();
$this->drupalLogin($user);
// Request the front page twice to ensure all cache collectors are fully
// warmed. The exact contents of cache collectors depends on the order in
// which requests complete so this ensures that the second request completes
// after asset aggregates are served.
$this->drupalGet('');
sleep(1);
$this->drupalGet('');
// Flush the dynamic page cache to simulate visiting a page that is not
// already fully cached.
\Drupal::cache('dynamic_page_cache')->deleteAll();
$performance_data = $this->collectPerformanceData(function () {
$this->drupalGet('');
}, 'navigation');
$expected_queries = [
'SELECT "session" FROM "sessions" WHERE "sid" = "SESSION_ID" LIMIT 0, 1',
'SELECT * FROM "users_field_data" "u" WHERE "u"."uid" = "2" AND "u"."default_langcode" = 1',
'SELECT "roles_target_id" FROM "user__roles" WHERE "entity_id" = "2"',
'SELECT "name", "value" FROM "key_value" WHERE "name" IN ( "theme:stark" ) AND "collection" = "config.entity.key_store.block"',
];
$recorded_queries = $performance_data->getQueries();
$this->assertSame($expected_queries, $recorded_queries);
$expected = [
'QueryCount' => 4,
'CacheGetCount' => 47,
'CacheGetCountByBin' => [
'config' => 11,
'data' => 4,
'discovery' => 10,
'bootstrap' => 6,
'dynamic_page_cache' => 1,
'render' => 14,
'menu' => 1,
],
'CacheSetCount' => 2,
'CacheSetCountByBin' => [
'dynamic_page_cache' => 2,
],
'CacheDeleteCount' => 0,
'CacheTagInvalidationCount' => 0,
'CacheTagLookupQueryCount' => 13,
'ScriptCount' => 3,
'ScriptBytes' => 167569,
'StylesheetCount' => 2,
'StylesheetBytes' => 46000,
];
$this->assertMetrics($expected, $performance_data);
// Check that the navigation toolbar is cached without any high-cardinality
// cache contexts (user, route, query parameters etc.).
$this->assertIsObject(\Drupal::cache('render')->get('navigation:navigation:[languages:language_interface]=en:[theme]=stark:[user.permissions]=is-admin'));
}
}

View File

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Kernel\ConfigAction;
use Drupal\Core\Config\Action\ConfigActionException;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\FunctionalTests\Core\Recipe\RecipeTestTrait;
use Drupal\KernelTests\KernelTestBase;
/**
* @covers \Drupal\navigation\Plugin\ConfigAction\AddNavigationBlock
* @group navigation
* @group Recipe
*/
class AddNavigationBlockConfigActionTest extends KernelTestBase {
use RecipeTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'navigation',
'layout_builder',
'layout_discovery',
'system',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig('navigation');
}
/**
* Tests add item logic.
*
* @testWith [0, 0]
* [1, 1]
* [3, 3]
* [7, 3]
*/
public function testAddBlockToNavigation($delta, $computed_delta): void {
// Load the navigation section storage.
$navigation_storage = \Drupal::service('plugin.manager.layout_builder.section_storage')->load('navigation', [
'navigation' => new Context(new ContextDefinition('string'), 'navigation'),
]);
$section = $navigation_storage->getSection(0);
$components = $section->getComponentsByRegion('content');
$this->assertCount(3, $components);
$data = [
'delta' => $delta,
'configuration' => [
'id' => 'navigation_menu:content',
'label' => 'Content From Recipe',
'label_display' => 1,
'provider' => 'navigation',
'level' => 1,
'depth' => 2,
],
];
// Use the action to add a new block to Navigation.
/** @var \Drupal\Core\Config\Action\ConfigActionManager $manager */
$manager = $this->container->get('plugin.manager.config_action');
$manager->applyAction('addNavigationBlock', 'navigation.block_layout', $data);
// Load the config after the execution.
$navigation_storage = \Drupal::service('plugin.manager.layout_builder.section_storage')->load('navigation', [
'navigation' => new Context(new ContextDefinition('string'), 'navigation'),
]);
$section = $navigation_storage->getSection(0);
$components = $section->getComponentsByRegion('content');
$this->assertCount(4, $components);
$component = array_values($components)[$computed_delta];
$this->assertSame('content', $component->getRegion());
$this->assertEquals($data['configuration'], $component->get('configuration'));
}
/**
* Checks invalid config exception.
*/
public function testActionOnlySupportsNavigationConfig(): void {
$this->expectException(ConfigActionException::class);
$this->expectExceptionMessage('addNavigationBlock can only be executed for the navigation.block_layout config.');
// Try to apply the Config Action against an unexpected config entity.
/** @var \Drupal\Core\Config\Action\ConfigActionManager $manager */
$manager = $this->container->get('plugin.manager.config_action');
$manager->applyAction('addNavigationBlock', 'navigation.settings', []);
}
}

View File

@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* Tests \Drupal\navigation\NavigationContentLinks.
*
* @group navigation
* @see \Drupal\navigation\NavigationContentLinks
*/
class NavigationContentLinksTest extends KernelTestBase {
use ContentTypeCreationTrait;
use MediaTypeCreationTrait;
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'navigation',
'node',
'file',
'media',
'layout_builder',
'system',
'views',
'user',
'field',
'media_test_source',
'image',
'text',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->installConfig(['node', 'file']);
$this->createContentType(['type' => 'article']);
$this->createContentType(['type' => 'blog']);
$this->createContentType(['type' => 'landing_page']);
$this->installEntitySchema('media');
$this->createMediaType('test', ['id' => 'document', 'label' => 'Document']);
$this->createMediaType('test', ['id' => 'image', 'label' => 'Image']);
$this->createMediaType('test', ['id' => 'special', 'label' => 'Special']);
}
/**
* Tests if the expected navigation content links are added/removed correctly.
*/
public function testNavigationContentLinks(): void {
$module_installer = \Drupal::service('module_installer');
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
$links = $menu_link_manager->getDefinitions();
// Assert that the "Create" link is added to the menu.
$this->assertArrayHasKey('navigation.create', $links);
$this->assertEquals('node.add_page', $links['navigation.create']['route_name']);
$this->assertEquals('Create', $links['navigation.create']['title']);
// Assert that the "Content" link is added to the menu.
$this->assertArrayHasKey('navigation.content', $links);
$this->assertEquals('system.admin_content', $links['navigation.content']['route_name']);
$this->assertEquals('Content', $links['navigation.content']['title']);
// Assert that the "Article" submenu link is added to the menu.
$this->assertArrayHasKey('navigation.content.node_type.article', $links);
$this->assertEquals('node.add', $links['navigation.content.node_type.article']['route_name']);
$this->assertEquals('article', $links['navigation.content.node_type.article']['title']);
// Assert that the "Blog" submenu link is added to the menu.
$this->assertArrayHasKey('navigation.content.node_type.blog', $links);
$this->assertEquals('node.add', $links['navigation.content.node_type.blog']['route_name']);
$this->assertEquals('blog', $links['navigation.content.node_type.blog']['title']);
// Assert that the "Landing Page" submenu link is added to the menu.
$this->assertArrayHasKey('navigation.content.node_type.landing_page', $links);
$this->assertEquals('node.add', $links['navigation.content.node_type.landing_page']['route_name']);
$this->assertEquals('landing_page', $links['navigation.content.node_type.landing_page']['title']);
// Assert that the "Create User" submenu link is added to the menu.
$this->assertArrayHasKey('navigation.create.user', $links);
$this->assertEquals('user.admin_create', $links['navigation.create.user']['route_name']);
$this->assertEquals('User', $links['navigation.create.user']['title']);
// Assert that the "Document" media type link is added to the menu.
$this->assertArrayHasKey('navigation.content.media_type.document', $links);
$this->assertEquals('entity.media.add_form', $links['navigation.content.media_type.document']['route_name']);
$this->assertEquals('Document', $links['navigation.content.media_type.document']['title']);
// Assert that the "Image" media type link is added to the menu.
$this->assertArrayHasKey('navigation.content.media_type.image', $links);
$this->assertEquals('entity.media.add_form', $links['navigation.content.media_type.image']['route_name']);
$this->assertEquals('Image', $links['navigation.content.media_type.image']['title']);
// Assert that the "Special" media type link is not added to the menu.
$this->assertArrayNotHasKey('navigation.content.media_type.special', $links);
// Assert that the "Media" link is added.
$this->assertArrayHasKey('navigation.media', $links);
$this->assertEquals('entity.media.collection', $links['navigation.media']['route_name']);
$this->assertEquals('Media', $links['navigation.media']['title']);
// Assert that the "Files" link is added.
$this->assertArrayHasKey('navigation.files', $links);
$this->assertEquals('view.files.page_1', $links['navigation.files']['route_name']);
$this->assertEquals('Files', $links['navigation.files']['title']);
// Assert that "Blocks" link is not added.
$this->assertArrayNotHasKey('navigation.blocks', $links);
// Install the block_content module and rebuild the menu links.
$module_installer->install(['block_content']);
// Rebuild the links after module installation.
$menu_link_manager->rebuild();
$links = $menu_link_manager->getDefinitions();
// Assert that "Blocks" link is added.
$this->assertArrayHasKey('navigation.blocks', $links);
$this->assertEquals('entity.block_content.collection', $links['navigation.blocks']['route_name']);
$this->assertEquals('Blocks', $links['navigation.blocks']['title']);
}
}

View File

@ -0,0 +1,434 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Kernel;
use Drupal\block\Entity\Block;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\MetadataBubblingUrlGenerator;
use Drupal\Core\Routing\RouteObjectInterface;
use Drupal\Core\Routing\UrlGenerator;
use Drupal\KernelTests\KernelTestBase;
use Drupal\navigation\Plugin\Block\NavigationMenuBlock;
use Drupal\system\Controller\SystemController;
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\NavigationMenuBlock.
*
* @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 NavigationMenuBlockTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
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']];
$special_options = $options + ['_no_path' => TRUE];
$routes->add('example2', new Route('/example2', [], $requirements, $options));
$routes->add('example4', new Route('/example4', ['_controller' => SystemController::class . '::systemAdminMenuBlockPage'], $requirements, $options));
$routes->add('example9', new Route('/example9', [], $requirements, $options));
$routes->add('example11', new Route('/example11', ['_controller' => SystemController::class . '::systemAdminMenuBlockPage'], $requirements, $options));
$routes->add('example13', new Route('/example13', [], $requirements, $options));
$routes->add('example14', new Route('/example14', [], $requirements, $options));
$routes->add('example15', new Route('/example15', [], $requirements, $options));
$routes->add('example16', new Route('/example16', [], $requirements, $options));
$routes->add('example17', new Route('/example17', [], $requirements, $options));
$routes->add('example18', new Route('/example18', [], $requirements, $options));
$routes->add('example19', new Route('/example19', [], ['_access' => 'FALSE'], $options));
// Mock special routes defined in system.routing.yml.
$routes->add('<nolink>', new Route('', [], $requirements, $special_options));
$routes->add('<button>', new Route('', [], $requirements, $special_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 (nolink)
// - 2
// - 3 (nolink)
// - 4 (list of child links)
// - 9
// - 5 (button)
// - 7 (button)
// - 10 (nolink)
// - 6
// - 8 (nolink)
// - 11 (list of child links)
// - 12 (button)
// - 13
// - 14 (not a list of child links)
// - 15
// - 16
// - 17
// - 18 (disabled)
// - 19 (access denied)
// - 20 (links to same routed URL as 17)
// With link 6 being the only external link.
// phpcs:disable
$links = [
1 => MenuLinkMock::create(['id' => 'test.example1', 'route_name' => '<nolink>', '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' => '<nolink>', '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' => '<button>', '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, 'options' => ['attributes' => ['target' => '_blank', 'class' => ['external-link']]]]),
7 => MenuLinkMock::create(['id' => 'test.example7', 'route_name' => '<button>', 'title' => 'title 7', 'parent' => 'test.example5', 'weight' => 6]),
8 => MenuLinkMock::create(['id' => 'test.example8', 'route_name' => '<nolink>', 'title' => 'title 8', 'parent' => '', 'weight' => 7]),
9 => MenuLinkMock::create(['id' => 'test.example9', 'route_name' => 'example9', 'title' => 'title 9', 'parent' => 'test.example4', 'weight' => 7]),
10 => MenuLinkMock::create(['id' => 'test.example10', 'route_name' => '<nolink>', 'title' => 'title 10', 'parent' => 'test.example7', 'weight' => 7]),
11 => MenuLinkMock::create(['id' => 'test.example11', 'route_name' => 'example11', 'title' => 'title 11', 'parent' => 'test.example8', 'weight' => 7]),
12 => MenuLinkMock::create(['id' => 'test.example12', 'route_name' => '<button>', 'title' => 'title 12', 'parent' => 'test.example11', 'weight' => 7]),
13 => MenuLinkMock::create(['id' => 'test.example13', 'route_name' => 'example13', 'title' => 'title 13', 'parent' => '', 'weight' => 8]),
14 => MenuLinkMock::create(['id' => 'test.example14', 'route_name' => 'example14', 'title' => 'title 14', 'parent' => 'test.example13', 'weight' => 8]),
15 => MenuLinkMock::create(['id' => 'test.example15', 'route_name' => 'example15', 'title' => 'title 15', 'parent' => 'test.example14', 'weight' => 8]),
16 => MenuLinkMock::create(['id' => 'test.example16', 'route_name' => 'example16', 'title' => 'title 16', 'parent' => '', 'weight' => 9]),
17 => MenuLinkMock::create(['id' => 'test.example17', 'route_name' => 'example17', 'title' => 'title 17', 'parent' => 'test.example16', 'weight' => 9]),
18 => MenuLinkMock::create(['id' => 'test.example18', 'route_name' => 'example18', 'title' => 'title 18', 'parent' => 'test.example17', 'weight' => 9, 'enabled' => FALSE]),
19 => MenuLinkMock::create(['id' => 'test.example19', 'route_name' => 'example19', 'title' => 'title 19', 'parent' => 'test.example17', 'weight' => 9]),
20 => MenuLinkMock::create(['id' => 'test.example20', 'route_name' => 'example17', 'title' => 'title 20', 'parent' => 'test.example17', 'weight' => 9]),
];
// 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' => [],
'test.example13' => [],
'test.example16' => [],
];
$expectations['level_2_only'] = [
'test.example3' => [],
'test.example7' => [],
'test.example11' => [],
'test.example14' => [],
'test.example17' => [],
];
$expectations['level_3_only'] = [
'test.example4' => [],
'test.example10' => [],
'test.example12' => [],
'test.example15' => [],
'test.example20' => [],
];
$expectations['level_1_and_beyond'] = [
'test.example1' => [],
'test.example2' => [
'test.example3' => [
'test.example4' => [],
],
],
'test.example5' => [
'test.example7' => [
'test.example10' => [],
],
],
'test.example6' => [],
'test.example8' => [
'test.example11' => [
'test.example12' => [],
],
],
'test.example13' => [
'test.example14' => [
'test.example14.navigation_overview' => [],
'test.example15' => [],
],
],
'test.example16' => [
// 17 only has inaccessible and disabled child links, and a child item
// that links to the same url as 17, so there should be no overview link
// child added.
'test.example17' => [
'test.example20' => [],
],
],
];
$expectations['level_2_and_beyond'] = [
'test.example3' => [
'test.example4' => [
'test.example9' => [],
],
],
'test.example7' => [
'test.example10' => [],
],
'test.example11' => [
'test.example12' => [],
],
'test.example14' => [
'test.example15' => [],
],
'test.example17' => [
'test.example20' => [],
],
];
$expectations['level_3_and_beyond'] = [
'test.example4' => [
'test.example9' => [],
],
'test.example10' => [],
'test.example12' => [],
'test.example15' => [],
'test.example20' => [],
];
// 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.");
}
}
/**
* Tests the generated HTML markup.
*/
public function testHtmlMarkup(): 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')]/span/span[text()='title 1']",
"//li[contains(@class,'toolbar-block__list-item')]/button/span[text()='title 2']",
"//li[contains(@class,'toolbar-menu__item--level-1')]/button/span[text()='title 3']",
"//li[contains(@class,'toolbar-menu__item--level-2')]/a[text()='title 4']",
"//li[contains(@class,'toolbar-block__list-item')]/button/span[text()='title 5']",
"//li[contains(@class,'toolbar-block__list-item')]/a/span[text()='title 6']",
"//li[contains(@class,'toolbar-block__list-item')]/a[contains(@class, 'external-link')]",
"//li[contains(@class,'toolbar-block__list-item')]/a[contains(@class, 'external-link')]",
"//li[contains(@class,'toolbar-block__list-item')]/a[@target='_blank']",
"//li[contains(@class,'toolbar-menu__item--level-1')]/button/span[text()='title 7']",
"//li[contains(@class,'toolbar-block__list-item')]/button/span[text()='title 8']",
"//li[contains(@class,'toolbar-menu__item--level-2')]/span[text()='title 10']",
"//li[contains(@class,'toolbar-menu__item--level-1')]/button/span[text()='title 11']",
"//li[contains(@class,'toolbar-menu__item--level-2')]/button[text()='title 12']",
"//li[contains(@class,'toolbar-block__list-item')]/button/span[text()='title 13']",
"//li[contains(@class,'toolbar-menu__item--level-1')]/button/span[text()='title 14']",
"//li[contains(@class,'toolbar-menu__item--level-2')]/a[text()='Overview']",
"//li[contains(@class,'toolbar-menu__item--level-1')]/button/span[text()='title 17']",
];
foreach ($items_query as $query) {
$span = $xpath->query($query);
$this->assertEquals(1, $span->length, $query);
}
}
/**
* 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): array {
$level = [];
foreach (Element::children($build) as $id) {
$level[$id] = [];
if (isset($build[$id]['below'])) {
$level[$id] = $this->convertBuiltMenuToIdTree($build[$id]['below']);
}
}
return $level;
}
}

View File

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Kernel;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\KernelTests\KernelTestBase;
use Drupal\navigation\Menu\NavigationMenuLinkTree;
/**
* Tests \Drupal\navigation\Menu\NavigationMenuLinkTree.
*
* @group navigation
*
* @see \Drupal\navigation\Menu\NavigationMenuLinkTree
*/
class NavigationMenuLinkTreeTest extends KernelTestBase {
/**
* The tested navigation menu link tree.
*
* @var \Drupal\navigation\Menu\NavigationMenuLinkTree
*/
protected NavigationMenuLinkTree $linkTree;
/**
* {@inheritdoc}
*/
protected static $modules = [
'field',
'layout_builder',
'layout_discovery',
'link',
'menu_link_content',
'menu_test',
'navigation',
'navigation_test',
'system',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('menu_link_content');
$this->linkTree = $this->container->get('navigation.menu_tree');
}
/**
* Tests the hook_navigation_menu_link_tree_alter logic.
*/
public function testNavigationMenuLinkTreeAlter(): void {
/** @var \Drupal\system\MenuStorage $storage */
$storage = \Drupal::entityTypeManager()->getStorage('menu');
$storage->create(['id' => 'menu1', 'label' => 'Menu 1'])->save();
$storage->create(['id' => 'menu2', 'label' => 'Menu 2'])->save();
\Drupal::entityTypeManager()
->getStorage('menu_link_content')
->create([
'link' => ['uri' => 'internal:/menu_name_test'],
'menu_name' => 'menu1',
'bundle' => 'menu_link_content',
'title' => 'Link test',
])->save();
\Drupal::entityTypeManager()
->getStorage('menu_link_content')
->create([
'link' => ['uri' => 'internal:/menu_name_test'],
'menu_name' => 'menu1',
'bundle' => 'menu_link_content',
'title' => 'Link test',
])->save();
\Drupal::entityTypeManager()
->getStorage('menu_link_content')
->create([
'link' => ['uri' => 'internal:/menu_name_test'],
'menu_name' => 'menu2',
'bundle' => 'menu_link_content',
'title' => 'Link test',
])->save();
$output = $this->linkTree->load('menu1', new MenuTreeParameters());
$this->assertCount(2, $output);
$output = $this->linkTree->transform($output, []);
$this->assertCount(0, $output);
$output = $this->linkTree->load('menu2', new MenuTreeParameters());
$this->assertCount(1, $output);
$output = $this->linkTree->transform($output, []);
$this->assertCount(1, $output);
$item = reset($output);
$this->assertSame('New Link Title', $item->link->getTitle());
}
}

View File

@ -0,0 +1,161 @@
<?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 {
/**
* {@inheritdoc}
*/
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' => 'Nested 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='n']",
"//li[contains(@class,'toolbar-menu__item--level-1')]/a[@data-icon-text='Ne']",
];
foreach ($items_query as $query) {
$element = $xpath->query($query);
$this->assertEquals(1, $element->length, $query);
}
}
}

View File

@ -0,0 +1,31 @@
const navigationTest = {
'@tags': ['core', 'a11y', 'a11y:admin', 'navigation'],
before(browser) {
browser
.drupalInstall({
installProfile: 'nightwatch_a11y_testing',
})
.drupalInstallModule('navigation', true)
.setWindowSize(1220, 800);
},
after(browser) {
browser.drupalUninstall();
},
};
const testCases = [{ name: 'Claro page', path: '/user/1/edit' }];
testCases.forEach((testCase) => {
navigationTest[`Accessibility - Navigation Module - ${testCase.name}`] = (
browser,
) => {
browser.drupalLoginAsAdmin(() => {
browser
.drupalRelativeURL(testCase.path)
.axeInject()
.axeRun('body', testCase.options || {});
});
};
});
module.exports = navigationTest;

View File

@ -0,0 +1,47 @@
const selectors = {
expandButton: {
expanded: '.admin-toolbar__expand-button[aria-expanded=true]',
collapsed: '.admin-toolbar__expand-button[aria-expanded=false]',
},
htmlAttribute: {
expanded: '[data-admin-toolbar="expanded"]',
collapsed: '[data-admin-toolbar="collapsed"]',
},
clearCacheButton: 'input[data-drupal-selector="edit-clear"]',
};
module.exports = {
'@tags': ['core', 'navigation'],
browser(browser) {
browser
.drupalInstall()
.drupalInstallModule('navigation', true)
.drupalInstallModule('big_pipe')
.setWindowSize(1220, 800);
},
after(browser) {
browser.drupalUninstall();
},
'Expand/Collapse': (browser) => {
browser.drupalLoginAsAdmin(() => {
browser
.drupalRelativeURL('/admin/config/development/performance')
.click(selectors.clearCacheButton)
.waitForElementPresent(
'[data-once="admin-toolbar-document-triggers-listener"]',
)
// This pause required to wait for first init event.
.waitForElementNotPresent(selectors.expandButton.expanded)
.waitForElementPresent(selectors.expandButton.collapsed)
.waitForElementPresent(selectors.htmlAttribute.collapsed)
.click(selectors.expandButton.collapsed)
.waitForElementPresent(selectors.expandButton.expanded)
.waitForElementPresent(selectors.htmlAttribute.expanded)
.click(selectors.expandButton.expanded)
.waitForElementNotPresent(selectors.expandButton.expanded)
.waitForElementPresent(selectors.expandButton.collapsed)
.waitForElementPresent(selectors.htmlAttribute.collapsed);
});
},
};

View File

@ -0,0 +1,26 @@
/**
* Verify that Drupal.displace() attribute is properly added by JavaScript.
*/
module.exports = {
'@tags': ['core', 'navigation'],
browser(browser) {
browser
.drupalInstall()
.drupalInstallModule('navigation', true)
.drupalInstallModule('big_pipe')
.setWindowSize(1220, 800);
},
after(browser) {
browser.drupalUninstall();
},
'Verify displace attribute': (browser) => {
browser.drupalLoginAsAdmin(() => {
browser
.drupalRelativeURL('/admin/')
.waitForElementPresent(
'.admin-toolbar__displace-placeholder[data-offset-left]',
);
});
},
};

View File

@ -0,0 +1,308 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Unit;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Menu\MenuLinkTreeElement;
use Drupal\Core\Menu\StaticMenuLinkOverridesInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\navigation\Menu\NavigationMenuLinkTreeManipulators;
use Drupal\system\Controller\SystemController;
use Drupal\Tests\Core\Menu\MenuLinkMock;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\Routing\Route;
/**
* Tests the navigation menu link tree manipulator.
*
* @group navigation
*
* @coversDefaultClass \Drupal\navigation\Menu\NavigationMenuLinkTreeManipulators
*/
class NavigationMenuLinkTreeManipulatorsTest extends UnitTestCase {
/**
* Tests the addSecondLevelOverviewLinks() tree manipulator.
*
* @covers ::addSecondLevelOverviewLinks
*/
public function testAddSecondLevelOverviewLinks(): void {
$routeProvider = $this->createMock(RouteProviderInterface::class);
// For only the route named 'child_list', return a route object with the
// SystemController::systemAdminMenuBlockPage as the controller.
$childListRoute = new Route('/test-child-list', ['_controller' => SystemController::class . '::systemAdminMenuBlockPage']);
$routeProvider->expects($this->any())
->method('getRouteByName')
->willReturnCallback(static fn ($name) => $name === 'child_list' ? $childListRoute : new Route("/$name"));
$overrides = $this->createMock(StaticMenuLinkOverridesInterface::class);
$translation = $this->createMock(TranslationInterface::class);
$translation
->method('translateString')
->willReturnCallback(static fn ($string) => $string);
$manipulator = new NavigationMenuLinkTreeManipulators($routeProvider, $overrides, $translation);
$originalTree = $this->mockTree();
// Make sure overview links do not already exist.
$this->assertArrayNotHasKey('test.example3.navigation_overview', $originalTree[2]->subtree[3]->subtree);
$this->assertArrayNotHasKey('test.example6.navigation_overview', $originalTree[5]->subtree[6]->subtree);
$tree = $manipulator->addSecondLevelOverviewLinks($originalTree);
// First level menu items should not have any children added.
$this->assertEmpty($tree[1]->subtree);
$this->assertEquals($originalTree[2]->subtree, $tree[2]->subtree);
$this->assertEquals($originalTree[5]->subtree, $tree[5]->subtree);
$this->assertEquals($originalTree[8]->subtree, $tree[8]->subtree);
$this->assertEquals($originalTree[11]->subtree, $tree[11]->subtree);
$this->assertEquals($originalTree[13]->subtree, $tree[13]->subtree);
$this->assertEquals($originalTree[16]->subtree, $tree[16]->subtree);
$this->assertEquals($originalTree[19]->subtree, $tree[19]->subtree);
// Leaves should not have any children added.
$this->assertEmpty($tree[2]->subtree[3]->subtree[4]->subtree);
$this->assertEmpty($tree[5]->subtree[6]->subtree[7]->subtree);
$this->assertEmpty($tree[8]->subtree[9]->subtree[10]->subtree);
$this->assertEmpty($tree[11]->subtree[12]->subtree);
$this->assertEmpty($tree[13]->subtree[14]->subtree[15]->subtree);
$this->assertEmpty($tree[16]->subtree[17]->subtree[18]->subtree);
$this->assertEmpty($tree[19]->subtree[20]->subtree[21]->subtree);
$this->assertEmpty($tree[19]->subtree[20]->subtree[22]->subtree);
// Links 3 and 6 should have overview children, even though 6 is unrouted.
$this->assertArrayHasKey('test.example3.navigation_overview', $tree[2]->subtree[3]->subtree);
$this->assertArrayHasKey('test.example6.navigation_overview', $tree[5]->subtree[6]->subtree);
// Link 9 is a child list page, so it should not have an overview child.
$this->assertArrayNotHasKey('test.example9.navigation_overview', $tree[8]->subtree[9]->subtree);
// Link 14 and Link 17 are <nolink> and <button> routes, so they should not
// have overview children.
$this->assertArrayNotHasKey('test.example14.navigation_overview', $tree[13]->subtree[14]->subtree);
$this->assertArrayNotHasKey('test.example17.navigation_overview', $tree[16]->subtree[17]->subtree);
// Link 20's child links are either inaccessible, disabled, or link to the
// same route as 20, so it should not have an overview child.
$this->assertArrayNotHasKey('test.example20.navigation_overview', $tree[19]->subtree[20]->subtree);
}
/**
* Creates a mock tree.
*
* This mocks a tree with the following structure:
* - 1
* - 2
* - 3
* - 4
* - 5
* - 6 (external)
* - 7
* - 8
* - 9
* - 10
* - 11
* - 12
* - 13
* - 14 (nolink)
* - 15
* - 16
* - 17 (button)
* - 18
* - 19
* - 20
* - 21 (disabled)
* - 22 (access denied)
* - 23 (links to same routed URL as 20)
*
* With link 9 linking to a page that contains a list of child menu links.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* The mock menu tree.
*/
protected function mockTree(): array {
$links = [
1 => MenuLinkMock::create([
'id' => 'test.example1',
'route_name' => 'example1',
'title' => 'foo',
'parent' => '',
]),
2 => MenuLinkMock::create([
'id' => 'test.example2',
'route_name' => 'example2',
'title' => 'foo',
'parent' => '',
]),
3 => MenuLinkMock::create([
'id' => 'test.example3',
'route_name' => 'example3',
'title' => 'baz',
'parent' => 'test.example2',
]),
4 => MenuLinkMock::create([
'id' => 'test.example4',
'route_name' => 'example4',
'title' => 'qux',
'parent' => 'test.example3',
]),
5 => MenuLinkMock::create([
'id' => 'test.example5',
'route_name' => 'example5',
'title' => 'title5',
'parent' => '',
]),
6 => MenuLinkMock::create([
'id' => 'test.example6',
'route_name' => '',
'url' => 'https://www.drupal.org/',
'title' => 'bar_bar',
'parent' => 'test.example5',
]),
7 => MenuLinkMock::create([
'id' => 'test.example7',
'route_name' => 'example7',
'title' => 'title7',
'parent' => 'test.example6',
]),
8 => MenuLinkMock::create([
'id' => 'test.example8',
'route_name' => 'example8',
'title' => 'title8',
'parent' => '',
]),
9 => MenuLinkMock::create([
'id' => 'test.example9',
'route_name' => 'child_list',
'title' => 'title9',
'parent' => 'test.example8',
]),
10 => MenuLinkMock::create([
'id' => 'test.example10',
'route_name' => 'example9',
'title' => 'title10',
'parent' => 'test.example9',
]),
11 => MenuLinkMock::create([
'id' => 'test.example11',
'route_name' => 'example11',
'title' => 'title11',
'parent' => '',
]),
12 => MenuLinkMock::create([
'id' => 'test.example12',
'route_name' => 'example12',
'title' => 'title12',
'parent' => 'text.example11',
]),
13 => MenuLinkMock::create([
'id' => 'test.example13',
'route_name' => 'example13',
'title' => 'title13',
'parent' => '',
]),
14 => MenuLinkMock::create([
'id' => 'test.example14',
'route_name' => '<nolink>',
'title' => 'title14',
'parent' => 'text.example13',
]),
15 => MenuLinkMock::create([
'id' => 'test.example15',
'route_name' => 'example15',
'title' => 'title15',
'parent' => 'text.example14',
]),
16 => MenuLinkMock::create([
'id' => 'test.example16',
'route_name' => 'example16',
'title' => 'title16',
'parent' => '',
]),
17 => MenuLinkMock::create([
'id' => 'test.example17',
'route_name' => '<button>',
'title' => 'title17',
'parent' => 'text.example16',
]),
18 => MenuLinkMock::create([
'id' => 'test.example18',
'route_name' => 'example18',
'title' => 'title18',
'parent' => 'text.example17',
]),
19 => MenuLinkMock::create([
'id' => 'test.example19',
'route_name' => 'example19',
'title' => 'title19',
'parent' => '',
]),
20 => MenuLinkMock::create([
'id' => 'test.example20',
'route_name' => 'example20',
'title' => 'title20',
'parent' => 'test.example19',
]),
21 => MenuLinkMock::create([
'id' => 'test.example21',
'route_name' => 'example21',
'title' => 'title21',
'parent' => 'test.example20',
'enabled' => FALSE,
]),
22 => MenuLinkMock::create([
'id' => 'test.example22',
'route_name' => 'no_access',
'title' => 'title22',
'parent' => 'test.example20',
]),
23 => MenuLinkMock::create([
'id' => 'test.example23',
'route_name' => 'example20',
'title' => 'title23',
'parent' => 'test.example20',
]),
];
$tree = [];
$tree[1] = new MenuLinkTreeElement($links[1], FALSE, 1, FALSE, []);
$tree[2] = new MenuLinkTreeElement($links[2], TRUE, 1, FALSE, [
3 => new MenuLinkTreeElement($links[3], TRUE, 2, FALSE, [
4 => new MenuLinkTreeElement($links[4], FALSE, 3, FALSE, []),
]),
]);
$tree[5] = new MenuLinkTreeElement($links[5], TRUE, 1, FALSE, [
6 => new MenuLinkTreeElement($links[6], TRUE, 2, FALSE, [
7 => new MenuLinkTreeElement($links[7], FALSE, 3, FALSE, []),
]),
]);
$tree[8] = new MenuLinkTreeElement($links[8], TRUE, 1, FALSE, [
9 => new MenuLinkTreeElement($links[9], TRUE, 2, FALSE, [
10 => new MenuLinkTreeElement($links[10], FALSE, 3, FALSE, []),
]),
]);
$tree[11] = new MenuLinkTreeElement($links[11], TRUE, 1, FALSE, [
12 => new MenuLinkTreeElement($links[12], FALSE, 2, FALSE, []),
]);
$tree[13] = new MenuLinkTreeElement($links[13], TRUE, 1, FALSE, [
14 => new MenuLinkTreeElement($links[14], TRUE, 2, FALSE, [
15 => new MenuLinkTreeElement($links[15], FALSE, 3, FALSE, []),
]),
]);
$tree[16] = new MenuLinkTreeElement($links[16], TRUE, 1, FALSE, [
17 => new MenuLinkTreeElement($links[17], TRUE, 2, FALSE, [
18 => new MenuLinkTreeElement($links[18], FALSE, 3, FALSE, []),
]),
]);
$tree[19] = new MenuLinkTreeElement($links[19], TRUE, 1, FALSE, [
20 => new MenuLinkTreeElement($links[20], TRUE, 2, FALSE, [
21 => new MenuLinkTreeElement($links[21], FALSE, 3, FALSE, []),
22 => new MenuLinkTreeElement($links[22], FALSE, 3, FALSE, []),
23 => new MenuLinkTreeElement($links[23], FALSE, 3, FALSE, []),
]),
]);
$tree[19]->subtree[20]->subtree[22]->access = AccessResult::forbidden();
return $tree;
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Unit;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\navigation\TopBarRegion;
use Drupal\navigation_test\Plugin\TopBarItem\TopBarItemInstantiation;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\navigation\TopBarItemBase
*
* @group navigation
*/
class TopBarItemBaseTest extends UnitTestCase {
/**
* @covers ::label
* @covers ::region
*/
public function testTopBarItemBase(): void {
$definition = [
'label' => new TranslatableMarkup('label'),
'region' => TopBarRegion::Tools,
];
$top_bar_item_base = new TopBarItemInstantiation([], 'test_top_bar_item_base', $definition);
$this->assertEquals($definition['label'], $top_bar_item_base->label());
$this->assertEquals($definition['region'], $top_bar_item_base->region());
}
}

View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\navigation\Unit;
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\navigation\TopBarItemManager;
use Drupal\navigation\TopBarItemManagerInterface;
use Drupal\navigation\TopBarRegion;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\navigation\TopBarItemManager
*
* @group navigation
*/
class TopBarItemManagerTest extends UnitTestCase {
use StringTranslationTrait;
/**
* The top bar item manager under test.
*
* @var \Drupal\navigation\TopBarItemManagerInterface
*/
protected TopBarItemManagerInterface $manager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$container = new ContainerBuilder();
$container->set('string_translation', $this->getStringTranslationStub());
\Drupal::setContainer($container);
$cache_backend = $this->prophesize(CacheBackendInterface::class);
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
$this->manager = new TopBarItemManager(new \ArrayObject(), $cache_backend->reveal(), $module_handler->reveal());
$discovery = $this->prophesize(DiscoveryInterface::class);
// Specify the 'broken' block, as well as 3 other blocks with admin labels
// that are purposefully not in alphabetical order.
$discovery->getDefinitions()->willReturn([
'tools' => [
'label' => 'Tools',
'region' => TopBarRegion::Tools,
],
'context' => [
'admin_label' => 'Context',
'region' => TopBarRegion::Context,
],
'actions' => [
'label' => 'Actions',
'region' => TopBarRegion::Actions,
],
'more_actions' => [
'label' => 'More Actions',
'region' => TopBarRegion::Actions,
],
]);
// Force the discovery object onto the block manager.
$property = new \ReflectionProperty(TopBarItemManager::class, 'discovery');
$property->setValue($this->manager, $discovery->reveal());
}
/**
* @covers ::getDefinitions
*/
public function testDefinitions(): void {
$definitions = $this->manager->getDefinitions();
$this->assertSame(['tools', 'context', 'actions', 'more_actions'], array_keys($definitions));
}
/**
* @covers ::getDefinitionsByRegion
*/
public function testGetDefinitionsByRegion(): void {
$tools = $this->manager->getDefinitionsByRegion(TopBarRegion::Tools);
$this->assertSame(['tools'], array_keys($tools));
$context = $this->manager->getDefinitionsByRegion(TopBarRegion::Context);
$this->assertSame(['context'], array_keys($context));
$actions = $this->manager->getDefinitionsByRegion(TopBarRegion::Actions);
$this->assertSame(['actions', 'more_actions'], array_keys($actions));
}
}