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,327 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: contextual_recent
label: 'Recent content'
module: node
description: 'Recent content.'
tag: default
base_table: node_field_data
base_field: nid
display:
default:
id: default
display_title: Default
display_plugin: default
position: 0
display_options:
title: 'Recent content'
fields:
title:
id: title
table: node_field_data
field: title
relationship: none
group_type: group
admin_label: ''
entity_type: node
entity_field: title
plugin_id: field
label: ''
exclude: false
alter:
alter_text: false
make_link: false
absolute: false
word_boundary: false
ellipsis: false
strip_tags: false
trim: false
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
type: string
settings:
link_to_entity: true
changed:
id: changed
table: node_field_data
field: changed
relationship: none
group_type: group
admin_label: ''
entity_type: node
entity_field: changed
plugin_id: field
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: timestamp_ago
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
pager:
type: some
options:
offset: 0
items_per_page: 10
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
empty:
area_text_custom:
id: area_text_custom
table: views
field: area_text_custom
relationship: none
group_type: group
admin_label: ''
plugin_id: text_custom
empty: true
content: 'No content available.'
tokenize: false
sorts:
changed:
id: changed
table: node_field_data
field: changed
relationship: none
group_type: group
admin_label: ''
entity_type: node
entity_field: changed
plugin_id: date
order: DESC
expose:
label: ''
exposed: false
granularity: second
arguments: { }
filters:
status_extra:
id: status_extra
table: node_field_data
field: status_extra
relationship: none
group_type: group
admin_label: ''
entity_type: node
plugin_id: node_status
operator: '='
value: false
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
operator_limit_selection: false
operator_list: { }
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
langcode:
id: langcode
table: node_field_data
field: langcode
relationship: none
group_type: group
admin_label: ''
entity_type: node
entity_field: langcode
plugin_id: language
operator: in
value:
'***LANGUAGE_language_content***': '***LANGUAGE_language_content***'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
operator_limit_selection: false
operator_list: { }
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
style:
type: html_list
options:
grouping: { }
row_class: ''
default_row_class: true
type: ul
wrapper_class: item-list
class: ''
row:
type: fields
query:
type: views_query
options:
query_comment: ''
disable_sql_rewrite: false
distinct: false
replica: false
query_tags: { }
relationships:
uid:
id: uid
table: node_field_data
field: uid
relationship: none
group_type: group
admin_label: author
entity_type: node
entity_field: uid
plugin_id: standard
required: true
use_more: false
use_more_always: false
use_more_text: More
link_display: '0'
link_url: ''
header: { }
footer: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- user
- 'user.node_grants:view'
- user.permissions
tags: { }
block_1:
id: block_1
display_title: Block
display_plugin: block
position: 2
display_options:
row:
type: 'entity:node'
options:
relationship: none
view_mode: teaser
defaults:
style: false
row: false
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- user
- 'user.node_grants:view'
- user.permissions
tags: { }

View File

@ -0,0 +1,7 @@
name: 'Contextual Test'
type: module
description: 'Provides test contextual links.'
package: Testing
version: VERSION
dependencies:
- drupal:contextual

View File

@ -0,0 +1,12 @@
contextual_test:
title: 'Test Link'
route_name: 'contextual_test'
group: 'contextual_test'
contextual_test_ajax:
title: 'Test Link with Ajax'
route_name: 'contextual_test'
group: 'contextual_test'
options:
attributes:
class: ['use-ajax']
data-dialog-type: 'modal'

View File

@ -0,0 +1,6 @@
contextual_test:
path: '/contextual-tests'
defaults:
_controller: '\Drupal\contextual_test\Controller\TestController::render'
requirements:
_access: 'TRUE'

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Drupal\contextual_test\Controller;
/**
* Test controller to provide a callback for the contextual link.
*/
class TestController {
/**
* Callback for the contextual link.
*
* @return array
* Render array.
*/
public function render() {
return [
'#type' => 'markup',
'#markup' => 'Everything is contextual!',
];
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Drupal\contextual_test\Hook;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for contextual_test.
*/
class ContextualTestHooks {
/**
* Implements hook_block_view_alter().
*/
#[Hook('block_view_alter')]
public function blockViewAlter(array &$build, BlockPluginInterface $block): void {
$build['#contextual_links']['contextual_test'] = ['route_parameters' => []];
}
/**
* Implements hook_contextual_links_view_alter().
*
* @todo Apparently this too late to attach the library?
* It won't work without contextual_test_page_attachments_alter()
* Is that a problem? Should the contextual module itself do the attaching?
*/
#[Hook('contextual_links_view_alter')]
public function contextualLinksViewAlter(&$element, $items): void {
if (isset($element['#links']['contextual-test-ajax'])) {
$element['#attached']['library'][] = 'core/drupal.dialog.ajax';
}
}
/**
* Implements hook_page_attachments_alter().
*/
#[Hook('page_attachments_alter')]
public function pageAttachmentsAlter(array &$attachments): void {
$attachments['#attached']['library'][] = 'core/drupal.dialog.ajax';
}
}

View File

@ -0,0 +1,298 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\contextual\Functional;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
use Psr\Http\Message\ResponseInterface;
/**
* Tests contextual link display on the front page based on permissions.
*
* @group contextual
*/
class ContextualDynamicContextTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to access contextual links and edit content.
*
* @var \Drupal\user\UserInterface
*/
protected $editorUser;
/**
* An authenticated user with permission to access contextual links.
*
* @var \Drupal\user\UserInterface
*/
protected $authenticatedUser;
/**
* A simulated anonymous user with access only to node content.
*
* @var \Drupal\user\UserInterface
*/
protected $anonymousUser;
/**
* {@inheritdoc}
*/
protected static $modules = [
'contextual',
'node',
'views',
'views_ui',
'language',
'menu_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
ConfigurableLanguage::createFromLangcode('it')->save();
$this->rebuildContainer();
$this->editorUser = $this->drupalCreateUser([
'access content',
'access contextual links',
'edit any article content',
]);
$this->authenticatedUser = $this->drupalCreateUser([
'access content',
'access contextual links',
]);
$this->anonymousUser = $this->drupalCreateUser(['access content']);
}
/**
* Tests contextual links with different permissions.
*
* Ensures that contextual link placeholders always exist, even if the user is
* not allowed to use contextual links.
*/
public function testDifferentPermissions(): void {
$this->drupalLogin($this->editorUser);
// Create three nodes in the following order:
// - An article, which should be user-editable.
// - A page, which should not be user-editable.
// - A second article, which should also be user-editable.
$node1 = $this->drupalCreateNode(['type' => 'article', 'promote' => 1]);
$node2 = $this->drupalCreateNode(['type' => 'page', 'promote' => 1]);
$node3 = $this->drupalCreateNode(['type' => 'article', 'promote' => 1]);
// Now, on the front page, all article nodes should have contextual links
// placeholders, as should the view that contains them.
$ids = [
'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en',
'node:node=' . $node2->id() . ':changed=' . $node2->getChangedTime() . '&langcode=en',
'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=en',
'entity.view.edit_form:view=frontpage:location=page&name=frontpage&display_id=page_1&langcode=en',
];
// Editor user: can access contextual links and can edit articles.
$this->drupalGet('node');
for ($i = 0; $i < count($ids); $i++) {
$this->assertContextualLinkPlaceHolder($ids[$i]);
}
$response = $this->renderContextualLinks([], 'node');
$this->assertSame(400, $response->getStatusCode());
$this->assertStringContainsString('No contextual ids specified.', (string) $response->getBody());
$response = $this->renderContextualLinks($ids, 'node');
$this->assertSame(200, $response->getStatusCode());
$json = Json::decode((string) $response->getBody());
$this->assertSame('<ul class="contextual-links"><li><a href="' . base_path() . 'node/1/edit">Edit</a></li></ul>', $json[$ids[0]]);
$this->assertSame('', $json[$ids[1]]);
$this->assertSame('<ul class="contextual-links"><li><a href="' . base_path() . 'node/3/edit">Edit</a></li></ul>', $json[$ids[2]]);
$this->assertSame('', $json[$ids[3]]);
// Verify that link language is properly handled.
$node3->addTranslation('it')->set('title', $this->randomString())->save();
$id = 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=it';
$this->drupalGet('node', ['language' => ConfigurableLanguage::createFromLangcode('it')]);
$this->assertContextualLinkPlaceHolder($id);
// Authenticated user: can access contextual links, cannot edit articles.
$this->drupalLogin($this->authenticatedUser);
$this->drupalGet('node');
for ($i = 0; $i < count($ids); $i++) {
$this->assertContextualLinkPlaceHolder($ids[$i]);
}
$response = $this->renderContextualLinks([], 'node');
$this->assertSame(400, $response->getStatusCode());
$this->assertStringContainsString('No contextual ids specified.', (string) $response->getBody());
$response = $this->renderContextualLinks($ids, 'node');
$this->assertSame(200, $response->getStatusCode());
$json = Json::decode((string) $response->getBody());
$this->assertSame('', $json[$ids[0]]);
$this->assertSame('', $json[$ids[1]]);
$this->assertSame('', $json[$ids[2]]);
$this->assertSame('', $json[$ids[3]]);
// Anonymous user: cannot access contextual links.
$this->drupalLogin($this->anonymousUser);
$this->drupalGet('node');
for ($i = 0; $i < count($ids); $i++) {
$this->assertNoContextualLinkPlaceHolder($ids[$i]);
}
$response = $this->renderContextualLinks([], 'node');
$this->assertSame(403, $response->getStatusCode());
$this->renderContextualLinks($ids, 'node');
$this->assertSame(403, $response->getStatusCode());
// Get a page where contextual links are directly rendered.
$this->drupalGet(Url::fromRoute('menu_test.contextual_test'));
$this->assertSession()->assertEscaped("<script>alert('Welcome to the jungle!')</script>");
$this->assertSession()->responseContains('<li><a href="' . base_path() . 'menu-test-contextual/1/edit" class="use-ajax" data-dialog-type="modal" data-is-something>Edit menu - contextual</a></li>');
// Test contextual links respects the weight set in *.links.contextual.yml.
$firstLink = $this->assertSession()->elementExists('css', 'ul.contextual-links li:nth-of-type(1) a');
$secondLink = $this->assertSession()->elementExists('css', 'ul.contextual-links li:nth-of-type(2) a');
$this->assertEquals(base_path() . 'menu-test-contextual/1/edit', $firstLink->getAttribute('href'));
$this->assertEquals(base_path() . 'menu-test-contextual/1', $secondLink->getAttribute('href'));
}
/**
* Tests the contextual placeholder content is protected by a token.
*/
public function testTokenProtection(): void {
$this->drupalLogin($this->editorUser);
// Create a node that will have a contextual link.
$node1 = $this->drupalCreateNode(['type' => 'article', 'promote' => 1]);
// Now, on the front page, all article nodes should have contextual links
// placeholders, as should the view that contains them.
$id = 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en';
// Editor user: can access contextual links and can edit articles.
$this->drupalGet('node');
$this->assertContextualLinkPlaceHolder($id);
$http_client = $this->getHttpClient();
$url = Url::fromRoute('contextual.render', [], [
'query' => [
'_format' => 'json',
'destination' => 'node',
],
])->setAbsolute()->toString();
$response = $http_client->request('POST', $url, [
'cookies' => $this->getSessionCookies(),
'form_params' => ['ids' => [$id], 'tokens' => []],
'http_errors' => FALSE,
]);
$this->assertEquals('400', $response->getStatusCode());
$this->assertStringContainsString('No contextual ID tokens specified.', (string) $response->getBody());
$response = $http_client->request('POST', $url, [
'cookies' => $this->getSessionCookies(),
'form_params' => ['ids' => [$id], 'tokens' => ['wrong_token']],
'http_errors' => FALSE,
]);
$this->assertEquals('400', $response->getStatusCode());
$this->assertStringContainsString('Invalid contextual ID specified.', (string) $response->getBody());
$response = $http_client->request('POST', $url, [
'cookies' => $this->getSessionCookies(),
'form_params' => ['ids' => [$id], 'tokens' => ['wrong_key' => $this->createContextualIdToken($id)]],
'http_errors' => FALSE,
]);
$this->assertEquals('400', $response->getStatusCode());
$this->assertStringContainsString('Invalid contextual ID specified.', (string) $response->getBody());
$response = $http_client->request('POST', $url, [
'cookies' => $this->getSessionCookies(),
'form_params' => ['ids' => [$id], 'tokens' => [$this->createContextualIdToken($id)]],
'http_errors' => FALSE,
]);
$this->assertEquals('200', $response->getStatusCode());
}
/**
* Asserts that a contextual link placeholder with the given id exists.
*
* @param string $id
* A contextual link id.
*
* @internal
*/
protected function assertContextualLinkPlaceHolder(string $id): void {
$this->assertSession()->elementAttributeContains(
'css',
'div[data-contextual-id="' . $id . '"]',
'data-contextual-token',
$this->createContextualIdToken($id)
);
}
/**
* Asserts that a contextual link placeholder with a given id does not exist.
*
* @param string $id
* A contextual link id.
*
* @internal
*/
protected function assertNoContextualLinkPlaceHolder(string $id): void {
$this->assertSession()->elementNotExists('css', 'div[data-contextual-id="' . $id . '"]');
}
/**
* Get server-rendered contextual links for the given contextual link ids.
*
* @param array $ids
* An array of contextual link ids.
* @param string $current_path
* The Drupal path for the page for which the contextual links are rendered.
*
* @return \Psr\Http\Message\ResponseInterface
* The response object.
*/
protected function renderContextualLinks($ids, $current_path): ResponseInterface {
$tokens = array_map([$this, 'createContextualIdToken'], $ids);
$http_client = $this->getHttpClient();
$url = Url::fromRoute('contextual.render', [], [
'query' => [
'_format' => 'json',
'destination' => $current_path,
],
]);
return $http_client->request('POST', $this->buildUrl($url), [
'cookies' => $this->getSessionCookies(),
'form_params' => ['ids' => $ids, 'tokens' => $tokens],
'http_errors' => FALSE,
]);
}
/**
* Creates a contextual ID token.
*
* @param string $id
* The contextual ID to create a token for.
*
* @return string
* The contextual ID token.
*/
protected function createContextualIdToken($id) {
return Crypt::hmacBase64($id, Settings::getHashSalt() . $this->container->get('private_key')->get());
}
}

View File

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

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\contextual\FunctionalJavascript;
/**
* Functions for testing contextual links.
*/
trait ContextualLinkClickTrait {
/**
* Clicks a contextual link.
*
* @param string $selector
* The selector for the element that contains the contextual link.
* @param string $link_locator
* The link id, title, or text.
* @param bool $force_visible
* If true then the button will be forced to visible so it can be clicked.
*/
protected function clickContextualLink($selector, $link_locator, $force_visible = TRUE) {
$page = $this->getSession()->getPage();
$page->waitFor(10, function () use ($page, $selector) {
return $page->find('css', "$selector .contextual-links");
});
if ($force_visible) {
$this->toggleContextualTriggerVisibility($selector);
}
$element = $this->getSession()->getPage()->find('css', $selector);
$element->find('css', '.contextual button')->press();
$element->findLink($link_locator)->click();
if ($force_visible) {
$this->toggleContextualTriggerVisibility($selector);
}
}
/**
* Toggles the visibility of a contextual trigger.
*
* @param string $selector
* The selector for the element that contains the contextual link.
*/
protected function toggleContextualTriggerVisibility($selector) {
// Hovering over the element itself with should be enough, but does not
// work. Manually remove the visually-hidden class.
$this->getSession()->executeScript("jQuery('{$selector} .contextual .trigger').toggleClass('visually-hidden');");
}
}

View File

@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\contextual\FunctionalJavascript;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\user\Entity\Role;
/**
* Tests the UI for correct contextual links.
*
* @group contextual
*/
class ContextualLinksTest extends WebDriverTestBase {
use ContextualLinkClickTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'contextual'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->createUser(['access contextual links']));
$this->placeBlock('system_branding_block', [
'id' => 'branding',
]);
}
/**
* Tests the visibility of contextual links.
*/
public function testContextualLinksVisibility(): void {
$this->drupalGet('user');
$contextualLinks = $this->assertSession()->waitForElement('css', '.contextual button');
$this->assertEmpty($contextualLinks);
// Ensure visibility remains correct after cached paged load.
$this->drupalGet('user');
$contextualLinks = $this->assertSession()->waitForElement('css', '.contextual button');
$this->assertEmpty($contextualLinks);
// Grant permissions to use contextual links on blocks.
$this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), [
'access contextual links',
'administer blocks',
]);
$this->drupalGet('user');
$contextualLinks = $this->assertSession()->waitForElement('css', '.contextual button');
$this->assertNotEmpty($contextualLinks);
// Confirm touchevents detection is loaded with Contextual Links
$this->assertSession()->elementExists('css', 'html.no-touchevents');
// Ensure visibility remains correct after cached paged load.
$this->drupalGet('user');
$contextualLinks = $this->assertSession()->waitForElement('css', '.contextual button');
$this->assertNotEmpty($contextualLinks);
}
/**
* Tests clicking contextual links.
*/
public function testContextualLinksClick(): void {
$this->container->get('module_installer')->install(['contextual_test']);
// Test clicking contextual link without toolbar.
$this->drupalGet('user');
$this->clickContextualLink('#block-branding', 'Test Link');
$this->assertSession()->pageTextContains('Everything is contextual!');
// Test click a contextual link that uses ajax.
$this->drupalGet('user');
$current_page_string = 'NOT_RELOADED_IF_ON_PAGE';
$this->getSession()->executeScript('document.body.appendChild(document.createTextNode("' . $current_page_string . '"));');
// Move the pointer over the branding block so the contextual link appears
// as it would with a real user interaction. Otherwise clickContextualLink()
// does not open the dialog in a manner that is opener-aware, and it isn't
// possible to reliably test focus management.
$this->getSession()->getDriver()->mouseOver('.//*[@id="block-branding"]');
$this->clickContextualLink('#block-branding', 'Test Link with Ajax', FALSE);
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '#drupal-modal'));
$this->assertSession()->elementContains('css', '#drupal-modal', 'Everything is contextual!');
$this->getSession()->executeScript('document.querySelector("#block-branding .trigger").addEventListener("focus", (e) => e.target.classList.add("i-am-focused"))');
$this->getSession()->getPage()->pressButton('Close');
$this->assertSession()->assertNoElementAfterWait('css', 'ui.dialog');
// When the dialog is closed, the opening contextual link is now inside a
// collapsed container, so focus should be routed to the contextual link
// toggle button.
$this->assertNotNull($this->assertSession()->waitForElement('css', '.trigger.i-am-focused'), $this->getSession()->getPage()->find('css', '#block-branding')->getOuterHtml());
$this->assertJsCondition('document.activeElement === document.querySelector("#block-branding button.trigger")', 10000, 'Focus should be on the contextual trigger, but instead is at ' . $this->getSession()->evaluateScript('document.activeElement.outerHTML'));
// Check to make sure that page was not reloaded.
$this->assertSession()->pageTextContains($current_page_string);
// Test clicking contextual link with toolbar.
$this->container->get('module_installer')->install(['toolbar']);
$this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), ['access toolbar']);
$this->drupalGet('user');
$this->assertSession()->assertExpectedAjaxRequest(1);
// Click "Edit" in toolbar to show contextual links.
$this->getSession()->getPage()->find('css', '.contextual-toolbar-tab button')->press();
$this->clickContextualLink('#block-branding', 'Test Link', FALSE);
$this->assertSession()->pageTextContains('Everything is contextual!');
}
/**
* Tests the contextual links destination.
*/
public function testContextualLinksDestination(): void {
$this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), [
'access contextual links',
'administer blocks',
]);
$this->drupalGet('user');
$this->assertSession()->waitForElement('css', '.contextual button');
$expected_destination_value = (string) $this->loggedInUser->toUrl()->toString();
$contextual_link_url_parsed = parse_url($this->getSession()->getPage()->findLink('Configure block')->getAttribute('href'));
$this->assertEquals("destination=$expected_destination_value", $contextual_link_url_parsed['query']);
}
/**
* Tests the contextual links destination with query.
*/
public function testContextualLinksDestinationWithQuery(): void {
$this->grantPermissions(Role::load(Role::AUTHENTICATED_ID), [
'access contextual links',
'administer blocks',
]);
$this->drupalGet('admin/structure/block', ['query' => ['foo' => 'bar']]);
$this->assertSession()->waitForElement('css', '.contextual button');
$expected_destination_value = Url::fromRoute('block.admin_display')->toString();
$contextual_link_url_parsed = parse_url($this->getSession()->getPage()->findLink('Configure block')->getAttribute('href'));
$this->assertEquals("destination=$expected_destination_value%3Ffoo%3Dbar", $contextual_link_url_parsed['query']);
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\contextual\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the UI for correct contextual links.
*
* @group contextual
*/
class DuplicateContextualLinksTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'contextual',
'node',
'views',
'views_ui',
'contextual_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the contextual links with same id.
*/
public function testSameContextualLinks(): void {
$this->drupalPlaceBlock('views_block:contextual_recent-block_1', ['id' => 'first']);
$this->drupalPlaceBlock('views_block:contextual_recent-block_1', ['id' => 'second']);
$this->drupalCreateContentType(['type' => 'page']);
$this->drupalCreateNode();
$this->drupalLogin($this->drupalCreateUser([
'access content',
'access contextual links',
'administer nodes',
'administer blocks',
'administer views',
'edit any page content',
]));
// Ensure same contextual links work correct with fresh and cached page.
foreach (['fresh', 'cached'] as $state) {
$this->drupalGet('user');
$contextual_id = '[data-contextual-id^="node:node=1"]';
$this->assertJsCondition("(typeof jQuery !== 'undefined' && jQuery('[data-contextual-id]:empty').length === 0)");
$this->getSession()->executeScript("jQuery('#block-first $contextual_id .trigger').trigger('click');");
$contextual_links = $this->assertSession()->waitForElementVisible('css', "#block-first $contextual_id .contextual-links");
$this->assertTrue($contextual_links->isVisible(), "Contextual links are visible with $state page.");
}
}
}

View File

@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\contextual\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests edit mode.
*
* @group contextual
* @group #slow
*/
class EditModeTest extends WebDriverTestBase {
/**
* CSS selector for Drupal's announce element.
*/
const ANNOUNCE_SELECTOR = '#drupal-live-announce';
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'block',
'user',
'system',
'breakpoint',
'toolbar',
'contextual',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The administration theme name.
*
* @var string
*/
protected $adminTheme = 'claro';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
\Drupal::service('theme_installer')->install([$this->adminTheme]);
\Drupal::configFactory()
->getEditable('system.theme')
->set('admin', $this->adminTheme)
->save();
$this->drupalLogin($this->createUser([
'administer blocks',
'access contextual links',
'access toolbar',
'view the administration theme',
]));
$this->placeBlock('system_powered_by_block', ['id' => 'powered']);
}
/**
* Tests enabling and disabling edit mode.
*/
public function testEditModeEnableDisable(): void {
$web_assert = $this->assertSession();
$page = $this->getSession()->getPage();
// Get the page twice to ensure edit mode remains enabled after a new page
// request.
$this->drupalGet('user');
$expected_restricted_tab_count = 1 + count($page->findAll('css', '[data-contextual-id]'));
// After the page loaded we need to additionally wait until the settings
// tray Ajax activity is done.
$web_assert->assertWaitOnAjaxRequest();
$unrestricted_tab_count = $this->getTabbableElementsCount();
$this->assertGreaterThan($expected_restricted_tab_count, $unrestricted_tab_count);
// Enable edit mode.
// After the first page load the page will be in edit mode when loaded.
$this->pressToolbarEditButton();
$this->assertAnnounceEditMode();
$this->assertSame($expected_restricted_tab_count, $this->getTabbableElementsCount());
// Disable edit mode.
$this->pressToolbarEditButton();
$this->assertAnnounceLeaveEditMode();
$this->assertSame($unrestricted_tab_count, $this->getTabbableElementsCount());
// Enable edit mode again.
$this->pressToolbarEditButton();
// Finally assert that the 'edit mode enabled' announcement is still
// correct after toggling the edit mode at least once.
$this->assertAnnounceEditMode();
$this->assertSame($expected_restricted_tab_count, $this->getTabbableElementsCount());
// Test while Edit Mode is enabled it doesn't interfere with pages with
// no contextual links.
$this->drupalGet('admin/structure/block');
$web_assert->elementContains('css', 'h1.page-title', 'Block layout');
$this->assertEquals(0, count($page->findAll('css', '[data-contextual-id]')));
$this->assertGreaterThan(0, $this->getTabbableElementsCount());
}
/**
* Presses the toolbar edit mode.
*/
protected function pressToolbarEditButton(): void {
$edit_button = $this->getSession()->getPage()->find('css', '#toolbar-bar div.contextual-toolbar-tab button');
$edit_button->press();
}
/**
* Asserts that the correct message was announced when entering edit mode.
*
* @internal
*/
protected function assertAnnounceEditMode(): void {
$web_assert = $this->assertSession();
// Wait for contextual trigger button.
$web_assert->waitForElementVisible('css', '.contextual trigger');
$web_assert->elementContains('css', static::ANNOUNCE_SELECTOR, 'Tabbing is constrained to a set of');
$web_assert->elementNotContains('css', static::ANNOUNCE_SELECTOR, 'Tabbing is no longer constrained by the Contextual module.');
}
/**
* Assert that the correct message was announced when leaving edit mode.
*
* @internal
*/
protected function assertAnnounceLeaveEditMode(): void {
$web_assert = $this->assertSession();
$page = $this->getSession()->getPage();
// Wait till all the contextual links are hidden.
$page->waitFor(1, function () use ($page) {
return empty($page->find('css', '.contextual .trigger.visually-hidden'));
});
$web_assert->elementContains('css', static::ANNOUNCE_SELECTOR, 'Tabbing is no longer constrained by the Contextual module.');
$web_assert->elementNotContains('css', static::ANNOUNCE_SELECTOR, 'Tabbing is constrained to a set of');
}
/**
* Gets the number of elements that are tabbable.
*
* @return int
* The number of tabbable elements.
*/
protected function getTabbableElementsCount(): int {
// Mark all tabbable elements.
$this->getSession()->executeScript("jQuery(window.tabbable.tabbable(document.body)).attr('data-marked', '');");
// Count all marked elements.
$count = count($this->getSession()->getPage()->findAll('css', "[data-marked]"));
// Remove set attributes.
$this->getSession()->executeScript("jQuery('[data-marked]').removeAttr('data-marked');");
return $count;
}
}

View File

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\contextual\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests edge cases for converting between contextual links and IDs.
*
* @group contextual
*/
class ContextualUnitTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['contextual'];
/**
* Provides test cases for both test functions.
*
* Used in testContextualLinksToId() and testContextualIdToLinks().
*
* @return array[]
* Test cases.
*/
public static function contextualLinksDataProvider(): array {
$tests['one group, one dynamic path argument, no metadata'] = [
[
'node' => [
'route_parameters' => [
'node' => '14031991',
],
'metadata' => ['langcode' => 'en'],
],
],
'node:node=14031991:langcode=en',
];
$tests['one group, multiple dynamic path arguments, no metadata'] = [
[
'foo' => [
'route_parameters' => [
0 => 'bar',
'key' => 'baz',
1 => 'qux',
],
'metadata' => ['langcode' => 'en'],
],
],
'foo:0=bar&key=baz&1=qux:langcode=en',
];
$tests['one group, one dynamic path argument, metadata'] = [
[
'views_ui_edit' => [
'route_parameters' => [
'view' => 'frontpage',
],
'metadata' => [
'location' => 'page',
'display' => 'page_1',
'langcode' => 'en',
],
],
],
'views_ui_edit:view=frontpage:location=page&display=page_1&langcode=en',
];
$tests['multiple groups, multiple dynamic path arguments'] = [
[
'node' => [
'route_parameters' => [
'node' => '14031991',
],
'metadata' => ['langcode' => 'en'],
],
'foo' => [
'route_parameters' => [
0 => 'bar',
'key' => 'baz',
1 => 'qux',
],
'metadata' => ['langcode' => 'en'],
],
'edge' => [
'route_parameters' => ['20011988'],
'metadata' => ['langcode' => 'en'],
],
],
'node:node=14031991:langcode=en|foo:0=bar&key=baz&1=qux:langcode=en|edge:0=20011988:langcode=en',
];
return $tests;
}
/**
* Tests the conversion from contextual links to IDs.
*
* @param array $links
* The #contextual_links property value array.
* @param string $id
* The serialized representation of the passed links.
*
* @covers ::_contextual_links_to_id
*
* @dataProvider contextualLinksDataProvider
*/
public function testContextualLinksToId(array $links, string $id): void {
$this->assertSame($id, _contextual_links_to_id($links));
}
/**
* Tests the conversion from contextual ID to links.
*
* @param array $links
* The #contextual_links property value array.
* @param string $id
* The serialized representation of the passed links.
*
* @covers ::_contextual_id_to_links
*
* @dataProvider contextualLinksDataProvider
*/
public function testContextualIdToLinks(array $links, string $id): void {
$this->assertSame($links, _contextual_id_to_links($id));
}
}