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 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M92,136H40a16,16,0,0,1-11.76-5.21,16.21,16.21,0,0,1-4.17-12.37A103.83,103.83,0,0,1,67.65,42.93,16,16,0,0,1,90.75,48l26,45a8,8,0,1,1-13.86,8L76.89,56A87.83,87.83,0,0,0,40,119.86a.19.19,0,0,0,.07.16L92,120a8,8,0,0,1,0,16Zm139.93-17.58a103.83,103.83,0,0,0-43.58-75.49A16,16,0,0,0,165.25,48L139.3,93a8,8,0,0,0,13.86,8l26-45A87.87,87.87,0,0,1,216,119.86c0,.07,0,.12,0,.14H164a8,8,0,0,0,0,16h52a16,16,0,0,0,11.76-5.21A16.21,16.21,0,0,0,231.93,118.42Zm-79,36.76a8,8,0,1,0-13.86,8l25.84,44.73a88.22,88.22,0,0,1-73.81,0l25.83-44.73a8,8,0,1,0-13.86-8L77.25,199.91a16,16,0,0,0,7.12,22.52,104.24,104.24,0,0,0,87.26,0,16,16,0,0,0,7.12-22.52ZM128,140a12,12,0,1,0-12-12A12,12,0,0,0,128,140Z"></path></svg>

After

Width:  |  Height:  |  Size: 799 B

View File

@ -0,0 +1,3 @@
<svg width="22" height="21" viewBox="0 0 22 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.4251 8.121C21.3342 7.84104 21.1631 7.594 20.9328 7.41063C20.7026 7.22726 20.4236 7.11566 20.1304 7.08975L14.5626 6.60974L12.3801 1.41974C12.2664 1.14742 12.0748 0.914795 11.8292 0.751174C11.5836 0.587552 11.2952 0.500244 11.0001 0.500244C10.705 0.500244 10.4165 0.587552 10.1709 0.751174C9.92537 0.914795 9.7337 1.14742 9.62007 1.41974L7.44413 6.60974L1.86976 7.09256C1.57542 7.11729 1.29493 7.22838 1.06347 7.41188C0.832008 7.59539 0.65988 7.84315 0.568668 8.1241C0.477457 8.40504 0.471222 8.70666 0.550747 8.99113C0.630272 9.2756 0.792015 9.53027 1.01569 9.72318L5.24476 13.4188L3.97726 18.9069C3.91023 19.1941 3.92936 19.4947 4.03224 19.7711C4.13512 20.0475 4.31719 20.2874 4.55569 20.4609C4.79419 20.6344 5.07853 20.7337 5.37317 20.7464C5.66781 20.7592 5.95967 20.6848 6.21226 20.5326L10.9935 17.6263L15.7851 20.5326C16.0377 20.6848 16.3295 20.7592 16.6242 20.7464C16.9188 20.7337 17.2031 20.6344 17.4416 20.4609C17.6801 20.2874 17.8622 20.0475 17.9651 19.7711C18.068 19.4947 18.0871 19.1941 18.0201 18.9069L16.7535 13.4132L20.9816 9.72318C21.2053 9.5296 21.3667 9.27421 21.4456 8.98914C21.5245 8.70406 21.5174 8.40202 21.4251 8.121ZM19.9982 8.58975L15.7701 12.2797C15.5643 12.4587 15.4112 12.6905 15.3273 12.95C15.2434 13.2095 15.2318 13.487 15.2938 13.7526L16.5641 19.2501L11.7763 16.3438C11.5427 16.2016 11.2745 16.1263 11.001 16.1263C10.7275 16.1263 10.4593 16.2016 10.2257 16.3438L5.44444 19.2501L6.70632 13.7563C6.76834 13.4907 6.75676 13.2132 6.67285 12.9537C6.58893 12.6942 6.43585 12.4625 6.23007 12.2835L2.00007 8.59537C1.99973 8.59257 1.99973 8.58973 2.00007 8.58693L7.57257 8.10506C7.84463 8.08108 8.10499 7.98327 8.32555 7.82219C8.54611 7.6611 8.7185 7.44286 8.82413 7.191L11.0001 2.00756L13.1751 7.191C13.2807 7.44286 13.4531 7.6611 13.6737 7.82219C13.8942 7.98327 14.1546 8.08108 14.4266 8.10506L20.0001 8.58693C20.0001 8.58693 20.0001 8.59256 20.0001 8.59349L19.9982 8.58975Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,36 @@
navigation_test:
enabled: true
label: "Drupal Navigation Test"
description: "Icons available within Drupal Navigation Test module."
version: 11.x
license:
name: GPL2-or-later
url: https://api.drupal.org/api/drupal/core%21LICENSE.txt/11.x
gpl-compatible: true
extractor: svg
config:
sources:
- assets/icons/*.svg
settings:
size:
title: "Size"
description: "Set a size for this icon."
type: "integer"
default: 20
class:
title: "Class"
description: "Set a class for this icon."
type: "string"
default: ""
template: >
<svg
{{ attributes
.setAttribute('viewBox', attributes.viewBox|default('0 0 24 24'))
.setAttribute('class', class)
.setAttribute('width', size|default('20'))
.setAttribute('height', size|default('20'))
.setAttribute('aria-hidden', 'true')
}}
>
{{ content }}
</svg>

View File

@ -0,0 +1,7 @@
name: Navigation test
description: 'Provides test functionality for navigation module'
package: Testing
version: VERSION
type: module
dependencies:
- drupal:navigation

View File

@ -0,0 +1,37 @@
navigation_test.navigation__custom_item:
title: "Test Custom Icon"
weight: 0
menu_name: admin
parent: system.admin
route_name: "<front>"
options:
icon:
pack_id: navigation_test
icon_id: star
settings:
class: 'toolbar-button__icon foo'
size: 25
navigation_test.navigation__default_item:
title: "Test Default Icon"
weight: 1
menu_name: admin
parent: system.admin
route_name: "<front>"
options:
icon:
icon_id: pencil
navigation_test.navigation__no_icon:
title: "Test No Icon"
weight: 2
menu_name: admin
parent: system.admin
route_name: "<front>"
navigation.media:
title: "Test Default Logic Icon"
weight: 3
menu_name: admin
parent: system.admin
route_name: "<front>"

View File

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

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation_test\Hook;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\State\StateInterface;
/**
* Hooks implementations for navigation_test module.
*/
class NavigationTestHooks {
/**
* NavigationTestHooks constructor.
*
* @param \Drupal\Core\State\StateInterface $state
* The state service.
*/
public function __construct(
protected StateInterface $state,
) {
}
/**
* Implements hook_block_alter().
*/
#[Hook('block_alter')]
public function blockAlter(&$definitions): void {
if ($this->state->get('navigation_safe_alter')) {
$definitions['navigation_link']['allow_in_navigation'] = TRUE;
$definitions['navigation_shortcuts']['allow_in_navigation'] = FALSE;
}
}
/**
* Implements hook_navigation_content_top().
*/
#[Hook('navigation_content_top')]
public function navigationContentTop(): array {
if (\Drupal::keyValue('navigation_test')->get('content_top')) {
$items = [
'navigation_foo' => [
'#markup' => 'foo',
],
'navigation_bar' => [
'#markup' => 'bar',
],
'navigation_baz' => [
'#markup' => 'baz',
],
];
}
else {
$items = [
'navigation_foo' => [],
'navigation_bar' => [],
'navigation_baz' => [],
];
}
// Add cache tags to our items to express a made up dependency to test
// cacheability. Note that as we're always returning the same items,
// sometimes only with cacheability metadata. By doing this we're testing
// conditional rendering of content_top items.
foreach ($items as &$element) {
CacheableMetadata::createFromRenderArray($element)
->addCacheTags(['navigation_test'])
->applyTo($element);
}
return $items;
}
/**
* Implements hook_navigation_content_top_alter().
*/
#[Hook('navigation_content_top_alter')]
public function navigationContentTopAlter(&$content_top): void {
if (\Drupal::keyValue('navigation_test')->get('content_top_alter')) {
unset($content_top['navigation_foo']);
$content_top['navigation_bar']['#markup'] = 'new bar';
$content_top['navigation_baz']['#weight'] = '-100';
}
}
/**
* Implements hook_navigation_menu_link_tree_alter().
*/
#[Hook('navigation_menu_link_tree_alter')]
public function navigationMenuLinkTreeAlter(array &$tree): void {
foreach ($tree as $key => $item) {
// Skip elements where menu is not the 'admin' one.
$menu_name = $item->link->getMenuName();
// Removes all items from menu1.
if ($menu_name == 'menu1') {
unset($tree[$key]);
}
// Updates title for items in menu2
if ($menu_name == 'menu2') {
$item->link->updateLink(['title' => 'New Link Title'], FALSE);
}
}
}
/**
* Implements hook_menu_links_discovered_alter().
*/
#[Hook('menu_links_discovered_alter')]
public function menuLinksDiscoveredAlter(array &$links): void {
if (\Drupal::keyValue('navigation_test')->get('menu_links_discovered_alter')) {
$links['navigation_test.navigation__no_icon']['options']['icon'] = [
'icon_id' => 'radioactive',
'pack_id' => 'navigation_test',
];
$links['navigation_test.navigation__default_item']['options']['icon'] = [
'icon_id' => 'foo',
'pack_id' => 'bar',
];
}
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation_test\Plugin\TopBarItem;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\navigation\Attribute\TopBarItem;
use Drupal\navigation\TopBarItemBase;
use Drupal\navigation\TopBarRegion;
/**
* Provides a top bar item plugin for testing the top bar.
*/
#[TopBarItem(
id: 'test_item',
region: TopBarRegion::Actions,
label: new TranslatableMarkup('Test Item'),
)]
class TopBarItemInstantiation extends TopBarItemBase {
/**
* {@inheritdoc}
*/
public function build(): array {
return [
'#markup' => 'Top Bar Item',
];
}
}

View File

@ -0,0 +1,5 @@
name: Navigation test block
description: 'Provides test block functionality for navigation module'
package: Testing
version: VERSION
type: module

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation_test_block\Hook;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hooks implementations for navigation_test_block module.
*/
class NavigationTestBlockHooks {
/**
* Implements hook_block_alter().
*/
#[Hook('block_alter')]
public function blockAlter(&$definitions): void {
$definitions['navigation_test']['allow_in_navigation'] = TRUE;
}
/**
* Implements hook_navigation_defaults().
*/
#[Hook('navigation_defaults')]
public function navigationDefaults(): array {
$blocks = [];
$blocks[] = [
'delta' => 1,
'configuration' => [
'id' => 'navigation_test',
'label' => 'My test block',
'label_display' => 1,
'provider' => 'navigation_test_block',
],
];
return $blocks;
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation_test_block\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
/**
* Defines a dummy navigation block for testing purposes.
*
* @internal
*/
#[Block(
id: 'navigation_test',
admin_label: new TranslatableMarkup('Navigation Test'),
)]
final class NavigationTestBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build(): array {
$config = $this->configuration;
$build = [];
return $build + [
'#title' => $config['label'],
'#theme' => 'navigation_menu',
'#menu_name' => 'test',
'#items' => [
[
'title' => 'Test Navigation Block',
'class' => 'test-block',
'icon' => [
'icon_id' => 'test-block',
],
'url' => Url::fromRoute('<front>'),
],
],
];
}
}

View File

@ -0,0 +1,7 @@
name: Navigation test top bar
description: 'Provides test functionality for top bar in navigation module'
package: Testing
version: VERSION
type: module
dependencies:
- drupal:navigation

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Drupal\navigation_test_top_bar\Plugin\TopBarItem;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\navigation\Attribute\TopBarItem;
use Drupal\navigation\TopBarItemBase;
use Drupal\navigation\TopBarRegion;
/**
* Provides a top bar item plugin for testing link attributes in the top bar.
*/
#[TopBarItem(
id: 'test_item_link_attribute',
region: TopBarRegion::Actions,
label: new TranslatableMarkup('Test Item Link Attribute'),
)]
class TopBarItemLinkAttribute extends TopBarItemBase {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
public function build(): array {
$links = [];
$featuredLinks['test_link'] = [
'page_action' => [
'#theme' => 'top_bar_page_action',
'#link' => [
'#type' => 'link',
'#title' => $this->t('Test link'),
'#url' => Url::fromRoute('entity.node.canonical', ['node' => 1]),
'#attributes' => [
'title' => $this->t('Test link with attributes'),
'class' => ['use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => json_encode(['width' => 700]),
],
],
],
'icon' => [
'icon_id' => 'database',
],
];
return [
'#theme' => 'top_bar_page_actions',
'#page_actions' => $links,
'#featured_page_actions' => $featuredLinks,
'#attached' => [
'library' => [
'core/drupal.dialog.ajax',
],
],
];
}
}

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));
}
}