first commit

This commit is contained in:
2020-06-08 23:57:36 +02:00
commit 6277454f7a
16057 changed files with 1715382 additions and 0 deletions
@@ -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:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
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
pager:
type: some
options:
items_per_page: 10
offset: 0
style:
type: html_list
options:
grouping: { }
row_class: ''
default_row_class: true
type: ul
wrapper_class: item-list
class: ''
row:
type: fields
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
entity_field: title
label: ''
exclude: false
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
relationship: none
group_type: group
admin_label: ''
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_alter_empty: true
type: string
settings:
link_to_entity: true
plugin_id: field
changed:
id: changed
table: node_field_data
field: changed
relationship: none
group_type: group
admin_label: ''
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
entity_type: node
entity_field: changed
plugin_id: field
filters:
status_extra:
id: status_extra
table: node_field_data
field: status_extra
relationship: none
group_type: group
admin_label: ''
operator: '='
value: false
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
operator_limit_selection: false
operator_list: { }
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: node
plugin_id: node_status
langcode:
id: langcode
table: node_field_data
field: langcode
relationship: none
group_type: group
admin_label: ''
operator: in
value:
'***LANGUAGE_language_content***': '***LANGUAGE_language_content***'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
reduce: false
operator_limit_selection: false
operator_list: { }
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: node
entity_field: langcode
plugin_id: language
sorts:
changed:
id: changed
table: node_field_data
field: changed
relationship: none
group_type: group
admin_label: ''
order: DESC
exposed: false
expose:
label: ''
granularity: second
entity_type: node
entity_field: changed
plugin_id: date
title: 'Recent content'
header: { }
footer: { }
empty:
area_text_custom:
id: area_text_custom
table: views
field: area_text_custom
relationship: none
group_type: group
admin_label: ''
empty: true
tokenize: false
content: 'No content available.'
plugin_id: text_custom
relationships:
uid:
id: uid
table: node_field_data
field: uid
relationship: none
group_type: group
admin_label: author
required: true
entity_type: node
entity_field: uid
plugin_id: standard
arguments: { }
display_extenders: { }
use_more: false
use_more_always: false
use_more_text: More
link_url: ''
link_display: '0'
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- user
- 'user.node_grants:view'
- user.permissions
max-age: -1
tags: { }
block_1:
display_plugin: block
id: block_1
display_title: Block
position: 2
display_options:
display_extenders: { }
defaults:
style: false
row: false
row:
type: 'entity:node'
options:
relationship: none
view_mode: teaser
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- user
- 'user.node_grants:view'
- user.permissions
max-age: -1
tags: { }
@@ -0,0 +1,8 @@
name: 'Contextual Test'
type: module
description: 'Provides test contextual links.'
package: Testing
version: VERSION
core: 8.x
dependencies:
- drupal:contextual
@@ -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'
@@ -0,0 +1,37 @@
<?php
/**
* @file
* Provides test contextual link on blocks.
*/
use Drupal\Core\Block\BlockPluginInterface;
/**
* Implements hook_block_view_alter().
*/
function contextual_test_block_view_alter(array &$build, BlockPluginInterface $block) {
$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?
*/
function contextual_test_contextual_links_view_alter(&$element, $items) {
if (isset($element['#links']['contextual-test-ajax'])) {
$element['#attached']['library'][] = 'core/drupal.dialog.ajax';
}
}
/**
* Implements hook_page_attachments_alter().
*/
function contextual_test_page_attachments_alter(array &$attachments) {
$attachments['#attached']['library'][] = 'core/drupal.dialog.ajax';
}
@@ -0,0 +1,6 @@
contextual_test:
path: '/contextual-tests'
defaults:
_controller: '\Drupal\contextual_test\Controller\TestController::render'
requirements:
_access: 'TRUE'
@@ -0,0 +1,23 @@
<?php
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!',
];
}
}
@@ -0,0 +1,279 @@
<?php
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;
/**
* Tests if contextual links are showing on the front page depending on
* permissions.
*
* @group contextual
*/
class ContextualDynamicContextTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'classy';
/**
* 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;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'contextual',
'node',
'views',
'views_ui',
'language',
'menu_test',
];
protected function setUp() {
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() {
$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->assertIdentical($json[$ids[0]], '<ul class="contextual-links"><li class="entitynodeedit-form"><a href="' . base_path() . 'node/1/edit">Edit</a></li></ul>');
$this->assertIdentical($json[$ids[1]], '');
$this->assertIdentical($json[$ids[2]], '<ul class="contextual-links"><li class="entitynodeedit-form"><a href="' . base_path() . 'node/3/edit">Edit</a></li></ul>');
$this->assertIdentical($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->assertIdentical($json[$ids[0]], '');
$this->assertIdentical($json[$ids[1]], '');
$this->assertIdentical($json[$ids[2]], '');
$this->assertIdentical($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->assertEscaped("<script>alert('Welcome to the jungle!')</script>");
$this->assertRaw('<li class="menu-testcontextual-hidden-manage-edit"><a href="' . base_path() . 'menu-test-contextual/1/edit" class="use-ajax" data-dialog-type="modal" data-is-something>Edit menu - contextual</a></li>');
}
/**
* Tests the contextual placeholder content is protected by a token.
*/
public function testTokenProtection() {
$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.
*/
protected function assertContextualLinkPlaceHolder($id) {
$this->assertSession()->elementAttributeContains(
'css',
'div[data-contextual-id="' . $id . '"]',
'data-contextual-token',
$this->createContextualIdToken($id)
);
}
/**
* Asserts that a contextual link placeholder with the given id does not exist.
*
* @param string $id
* A contextual link id.
*/
protected function assertNoContextualLinkPlaceHolder($id) {
$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) {
$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,51 @@
<?php
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,115 @@
<?php
namespace Drupal\Tests\contextual\FunctionalJavascript;
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}
*/
public static $modules = ['block', 'contextual'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
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() {
$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);
// Ensure visibility remains correct after cached paged load.
$this->drupalGet('user');
$contextualLinks = $this->assertSession()->waitForElement('css', '.contextual button');
$this->assertNotEmpty($contextualLinks);
}
/**
* Test clicking contextual links.
*/
public function testContextualLinksClick() {
$this->container->get('module_installer')->install(['contextual_test']);
// Test clicking contextual link without toolbar.
$this->drupalGet('user');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->clickContextualLink('#block-branding', 'Test Link');
$this->assertSession()->pageTextContains('Everything is contextual!');
// Test click a contextual link that uses ajax.
$this->drupalGet('user');
$this->assertSession()->assertWaitOnAjaxRequest();
$current_page_string = 'NOT_RELOADED_IF_ON_PAGE';
$this->getSession()->executeScript('document.body.appendChild(document.createTextNode("' . $current_page_string . '"));');
$this->clickContextualLink('#block-branding', 'Test Link with Ajax');
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '#drupal-modal'));
$this->assertSession()->elementContains('css', '#drupal-modal', 'Everything is contextual!');
// 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()->assertWaitOnAjaxRequest();
// 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!');
}
/**
* Test the contextual links destination.
*/
public function testContextualLinksDestination() {
$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']);
}
}
@@ -0,0 +1,58 @@
<?php
namespace Drupal\Tests\contextual\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the UI for correct contextual links.
*
* @group contextual
*/
class DuplicateContextualLinksTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'block',
'contextual',
'node',
'views',
'views_ui',
'contextual_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the contextual links with same id.
*/
public function testSameContextualLinks() {
$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,142 @@
<?php
namespace Drupal\Tests\contextual\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests edit mode.
*
* @group contextual
*/
class EditModeTest extends WebDriverTestBase {
/**
* CSS selector for Drupal's announce element.
*/
const ANNOUNCE_SELECTOR = '#drupal-live-announce';
/**
* {@inheritdoc}
*/
public static $modules = [
'node',
'block',
'user',
'system',
'breakpoint',
'toolbar',
'contextual',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->createUser([
'administer blocks',
'access contextual links',
'access toolbar',
]));
$this->placeBlock('system_powered_by_block', ['id' => 'powered']);
}
/**
* Tests enabling and disabling edit mode.
*/
public function testEditModeEnableDisable() {
$web_assert = $this->assertSession();
$page = $this->getSession()->getPage();
// Get the page twice to ensure edit mode remains enabled after a new page
// request.
for ($page_get_count = 0; $page_get_count < 2; $page_get_count++) {
$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();
if ($page_get_count == 0) {
$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());
}
}
/**
* Presses the toolbar edit mode.
*/
protected function pressToolbarEditButton() {
$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.
*/
protected function assertAnnounceEditMode() {
$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.
*/
protected function assertAnnounceLeaveEditMode() {
$web_assert = $this->assertSession();
$page = $this->getSession()->getPage();
// Wait till all the contextual links are hidden.
$page->waitFor(1, function () use ($page, $web_assert) {
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() {
// Mark all tabbable elements.
$this->getSession()->executeScript("jQuery(':tabbable').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,130 @@
<?php
namespace Drupal\Tests\contextual\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests all edge cases of converting from #contextual_links to ids and vice
* versa.
*
* @group contextual
*/
class ContextualUnitTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['contextual'];
/**
* Provides testcases for testContextualLinksToId() and
*/
public function _contextual_links_id_testcases() {
// Test branch conditions:
// - one group.
// - one dynamic path argument.
// - no metadata.
$tests[] = [
'links' => [
'node' => [
'route_parameters' => [
'node' => '14031991',
],
'metadata' => ['langcode' => 'en'],
],
],
'id' => 'node:node=14031991:langcode=en',
];
// Test branch conditions:
// - one group.
// - multiple dynamic path arguments.
// - no metadata.
$tests[] = [
'links' => [
'foo' => [
'route_parameters' => [
'bar',
'key' => 'baz',
'qux',
],
'metadata' => ['langcode' => 'en'],
],
],
'id' => 'foo:0=bar&key=baz&1=qux:langcode=en',
];
// Test branch conditions:
// - one group.
// - one dynamic path argument.
// - metadata.
$tests[] = [
'links' => [
'views_ui_edit' => [
'route_parameters' => [
'view' => 'frontpage',
],
'metadata' => [
'location' => 'page',
'display' => 'page_1',
'langcode' => 'en',
],
],
],
'id' => 'views_ui_edit:view=frontpage:location=page&display=page_1&langcode=en',
];
// Test branch conditions:
// - multiple groups.
// - multiple dynamic path arguments.
$tests[] = [
'links' => [
'node' => [
'route_parameters' => [
'node' => '14031991',
],
'metadata' => ['langcode' => 'en'],
],
'foo' => [
'route_parameters' => [
'bar',
'key' => 'baz',
'qux',
],
'metadata' => ['langcode' => 'en'],
],
'edge' => [
'route_parameters' => ['20011988'],
'metadata' => ['langcode' => 'en'],
],
],
'id' => 'node:node=14031991:langcode=en|foo:0=bar&key=baz&1=qux:langcode=en|edge:0=20011988:langcode=en',
];
return $tests;
}
/**
* Tests _contextual_links_to_id().
*/
public function testContextualLinksToId() {
$tests = $this->_contextual_links_id_testcases();
foreach ($tests as $test) {
$this->assertIdentical(_contextual_links_to_id($test['links']), $test['id']);
}
}
/**
* Tests _contextual_id_to_links().
*/
public function testContextualIdToLinks() {
$tests = $this->_contextual_links_id_testcases();
foreach ($tests as $test) {
$this->assertIdentical(_contextual_id_to_links($test['id']), $test['links']);
}
}
}