Initial Drupal 11 with DDEV setup
This commit is contained in:
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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');");
|
||||
}
|
||||
|
||||
}
|
||||
@ -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']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user