layout_builder_component_attributes-1.1.0/tests/src/FunctionalJavascript/ComponentAttributeTest.php
tests/src/FunctionalJavascript/ComponentAttributeTest.php
<?php
namespace Drupal\Tests\layout_builder_component_attributes\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\block\Traits\BlockCreationTrait;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
/**
* Tests UI and rendering of component attributes.
*
* @group layout_builder_component_attributes
*/
class ComponentAttributeTest extends WebDriverTestBase {
use BlockCreationTrait;
use ContextualLinkClickTrait {
clickContextualLink as protected contextualLinkClickTraitClickContextualLink;
}
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Path prefix for the field UI for the test bundle.
*
* @var string
*/
const FIELD_UI_PREFIX = 'admin/structure/types/manage/bundle_with_section_field';
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'field_ui',
'layout_builder',
'layout_builder_component_attributes',
'layout_builder_component_attributes_test',
'node',
'contextual',
];
/**
* The test administrative user.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* The test non-administrative user.
*
* @var \Drupal\user\UserInterface
*/
protected $authUser;
/**
* {@inheritdoc}
*/
protected function setUp() : void {
parent::setUp();
$this->createContentType(['type' => 'bundle_with_section_field']);
// Create an authenticated user.
$this->authUser = $this
->drupalCreateUser([
'access administration pages',
'access contextual links',
'administer node display',
'administer node fields',
'configure any layout',
]);
// Create an admin user.
$this->adminUser = $this
->drupalCreateUser([
'access administration pages',
'access contextual links',
'administer node display',
'administer node fields',
'bypass node access',
'configure any layout',
'manage layout builder component attributes',
]);
$this->drupalLogin($this->adminUser);
// Enable layout builder.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
$this->submitForm(
['layout[enabled]' => TRUE],
'Save'
);
$this->drupalPlaceBlock('system_messages_block');
}
/**
* Tests permissions are enforced.
*/
public function testManageComponentAttributesFormPermissions() {
$this->getSession()->resizeWindow(1200, 2000);
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->authUser);
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
$this->resetLayoutBuilderLayout();
$this->assertNotEmpty($page->findAll('xpath', '//*[contains(@class, "layout-builder-block")]//ul[contains(@class, "contextual-links")]', 'Contextual links are rendered.'));
$this->assertEmpty($page->findAll('xpath', '//*[contains(@class, "layout-builder-block")]//ul[contains(@class, "contextual-links")]//a[contains(text(), "Manage attributes")]', 'Manage attributes link is not rendered.'));
$this->drupalLogin($this->adminUser);
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
// Wait for contextual links to load.
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($page->findAll('xpath', '//*[contains(@class, "layout-builder-block")]//ul[contains(@class, "contextual-links")]//a[contains(text(), "Manage attributes")]', 'Manage attributes link is rendered'));
}
/**
* Tests Manage Component Attributes Form.
*/
public function testManageComponentAttributesForm() {
$this->getSession()->resizeWindow(1200, 2000);
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
$this->resetLayoutBuilderLayout();
$this->clickContextualLink('.layout-builder-block', 'Manage attributes');
$assert_session->assertWaitOnAjaxRequest();
// Test validation, one field at a time.
// Block Attributes.
$page->find('xpath', '//details[contains(@id, "edit-block-attributes")]')->click();
$page->fillField('block_attributes[id]', '(block-id-test');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$this->assertSettingsTrayValidationMessage('Element ID must be a valid CSS ID');
$page->fillField('block_attributes[id]', 'block-id-test');
$page->fillField('block_attributes[class]', '*block-class1 block-class2');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$this->assertSettingsTrayValidationMessage('Classes must be valid CSS classes');
$page->fillField('block_attributes[class]', 'block-class1 block-class2');
$page->fillField('block_attributes[style]', 'color blue;');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$this->assertSettingsTrayValidationMessage('Inline styles must be valid CSS');
$page->fillField('block_attributes[style]', 'color: blue;');
$page->fillField('block_attributes[data]', 'data-block-test' . PHP_EOL . 'ata-block-test2|test-value');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$this->assertSettingsTrayValidationMessage('Data attributes must begin with "data-"');
$page->fillField('block_attributes[data]', 'data-block-test' . PHP_EOL . 'data-block-test2|test-value');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
// Block Title Attributes.
$this->clickContextualLink('.layout-builder-block', 'Manage attributes');
$assert_session->assertWaitOnAjaxRequest();
$page->find('xpath', '//details[contains(@id, "edit-block-title-attributes")]')->click();
$page->fillField('block_title_attributes[id]', '(block-title-id-test');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$this->assertSettingsTrayValidationMessage('Element ID must be a valid CSS ID');
$page->fillField('block_title_attributes[id]', 'block-title-id-test');
$page->fillField('block_title_attributes[class]', '*block-title-class1 block-title-class2');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$this->assertSettingsTrayValidationMessage('Classes must be valid CSS classes');
$page->fillField('block_title_attributes[class]', 'block-title-class1 block-title-class2');
$page->fillField('block_title_attributes[style]', 'color white;');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$this->assertSettingsTrayValidationMessage('Inline styles must be valid CSS');
$page->fillField('block_title_attributes[style]', 'color: white;');
$page->fillField('block_title_attributes[data]', 'data-block-title-test' . PHP_EOL . 'ata-block-title-test2|test-value-title');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$this->assertSettingsTrayValidationMessage('Data attributes must begin with "data-"');
$page->fillField('block_title_attributes[data]', 'data-block-title-test' . PHP_EOL . 'data-block-title-test2|test-value-title');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
// Block Content Attributes.
$this->clickContextualLink('.layout-builder-block', 'Manage attributes');
$assert_session->assertWaitOnAjaxRequest();
$page->find('xpath', '//details[contains(@id, "edit-block-content-attributes")]')->click();
$page->fillField('block_content_attributes[id]', '(block-content-id-test');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$this->assertSettingsTrayValidationMessage('Element ID must be a valid CSS ID');
$page->fillField('block_content_attributes[id]', 'block-content-id-test');
$page->fillField('block_content_attributes[class]', '*block-content-class1 block-content-class2');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$this->assertSettingsTrayValidationMessage('Classes must be valid CSS classes');
$page->fillField('block_content_attributes[class]', 'block-content-class1 block-content-class2');
$page->fillField('block_content_attributes[style]', 'color red;');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$this->assertSettingsTrayValidationMessage('Inline styles must be valid CSS');
$page->fillField('block_content_attributes[style]', 'color: red;');
$page->fillField('block_content_attributes[data]', 'data-block-content-test' . PHP_EOL . 'ata-block-content-test2|test-value-content');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$this->assertSettingsTrayValidationMessage('Data attributes must begin with "data-"');
$page->fillField('block_content_attributes[data]', 'data-block-content-test' . PHP_EOL . 'data-block-content-test2|test-value-content');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$page->pressButton('Save layout');
// Verify correct rendering of attributes.
$this->drupalGet('node/add/bundle_with_section_field');
$page->fillField('Title', 'Test Node Title');
$page->pressButton('Save');
$this->drupalGet('node/1');
// Verify Block Attributes.
$element = $page->find('xpath', '//*[@id="block-id-test"]');
$this->assertNotEmpty($element, "Block ID rendered");
$this->assertTrue($element->hasClass('block-class1'), "Block class rendered");
$this->assertTrue($element->hasClass('block-class2'), "Block class rendered");
// Verify existing CSS class added by
// layout_builder_component_attributes_test_preprocess_block() is preserved.
$this->assertTrue($element->hasClass('existing-css-class'), "Existing block class rendered");
$style = $element->getAttribute('style');
$this->assertSame('color: blue;', $style, "Style attribute rendered");
$this->assertTrue($element->hasAttribute('data-block-test'), "Data attribute rendered");
$data1 = $element->getAttribute('data-block-test');
$this->assertEmpty($data1, "Data attribute has no value");
$this->assertTrue($element->hasAttribute('data-block-test2'), "Data attribute rendered");
$data2 = $element->getAttribute('data-block-test2');
$this->assertSame($data2, 'test-value', "Data attribute has expected value");
// Verify Block Title Attributes.
$element = $page->find('xpath', '//*[@id="block-title-id-test"]');
$this->assertNotEmpty($element, "Block ID rendered");
$this->assertTrue($element->hasClass('block-title-class1'), "Block class rendered");
$this->assertTrue($element->hasClass('block-title-class2'), "Block class rendered");
// Verify existing CSS class added by
// layout_builder_component_attributes_test_preprocess_block() is preserved.
$this->assertTrue($element->hasClass('existing-title-css-class'), "Existing block class rendered");
$style = $element->getAttribute('style');
$this->assertSame('color: white;', $style, "Style attribute rendered");
$this->assertTrue($element->hasAttribute('data-block-title-test'), "Data attribute rendered");
$data1 = $element->getAttribute('data-block-title-test');
$this->assertEmpty($data1, "Data attribute has no value");
$this->assertTrue($element->hasAttribute('data-block-title-test2'), "Data attribute rendered");
$data2 = $element->getAttribute('data-block-title-test2');
$this->assertSame($data2, 'test-value-title', "Data attribute has expected value");
// Verify Block Content Attributes.
$element = $page->find('xpath', '//*[@id="block-content-id-test"]');
$this->assertNotEmpty($element, "Block ID rendered");
$this->assertTrue($element->hasClass('block-content-class1'), "Block class rendered");
$this->assertTrue($element->hasClass('block-content-class2'), "Block class rendered");
// Verify existing CSS class added by
// layout_builder_component_attributes_test_preprocess_block() is preserved.
$this->assertTrue($element->hasClass('existing-content-css-class'), "Existing block class rendered");
$style = $element->getAttribute('style');
$this->assertSame('color: red;', $style, "Style attribute rendered");
$this->assertTrue($element->hasAttribute('data-block-content-test'), "Data attribute rendered");
$data1 = $element->getAttribute('data-block-content-test');
$this->assertEmpty($data1, "Data attribute has no value");
$this->assertTrue($element->hasAttribute('data-block-content-test2'), "Data attribute rendered");
$data2 = $element->getAttribute('data-block-content-test2');
$this->assertSame($data2, 'test-value-content', "Data attribute has expected value");
}
/**
* Tests allowed attributes (both form render and page render).
*/
public function testAllowedAttributes() {
$this->getSession()->resizeWindow(1200, 2000);
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Initially, populate all fields. This also verifies they are rendered.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
$this->resetLayoutBuilderLayout();
$this->clickContextualLink('.layout-builder-block', 'Manage attributes');
$assert_session->assertWaitOnAjaxRequest();
$page->find('xpath', '//details[contains(@id, "edit-block-attributes")]')->click();
$page->fillField('block_attributes[id]', 'block-id-test');
$page->fillField('block_attributes[class]', 'block-class-test');
$page->fillField('block_attributes[style]', 'color: blue;');
$page->fillField('block_attributes[data]', 'data-block-test|test-value');
$page->find('xpath', '//details[contains(@id, "edit-block-title-attributes")]')->click();
$page->fillField('block_title_attributes[id]', 'block-title-id-test');
$page->fillField('block_title_attributes[class]', 'block-title-class-test');
$page->fillField('block_title_attributes[style]', 'color: white;');
$page->fillField('block_title_attributes[data]', 'data-block-title-test|test-value-title');
$page->find('xpath', '//details[contains(@id, "edit-block-content-attributes")]')->click();
$page->fillField('block_content_attributes[id]', 'block-content-id-test');
$page->fillField('block_content_attributes[class]', 'block-content-class-test');
$page->fillField('block_content_attributes[style]', 'color: red;');
$page->fillField('block_content_attributes[data]', 'data-block-content-test|test-value-content');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$page->pressButton('Save layout');
// Verify Block Attributes.
$attributes = [
'id' => 'block-id-test',
'class' => 'block-class-test',
'style' => 'color: blue;',
'data-block-test' => 'test-value',
];
$this->verifyAllowedAttributes('block_attributes', $attributes);
// Verify Block Title Attributes.
$attributes = [
'id' => 'block-title-id-test',
'class' => 'block-title-class-test',
'style' => 'color: white;',
'data-block-title-test' => 'test-value-title',
];
$this->verifyAllowedAttributes('block_title_attributes', $attributes);
// Verify Block Content Attributes.
$attributes = [
'id' => 'block-content-id-test',
'class' => 'block-content-class-test',
'style' => 'color: red;',
'data-block-content-test' => 'test-value-content',
];
$this->verifyAllowedAttributes('block_content_attributes', $attributes);
}
/**
* Verifies form rendering and on-page rendering of allowed attributes.
*
* @param string $group
* A group of fields: 'block_attributes', 'block_title_attributes',
* or 'block_content_attributes'.
* @param array $attributes
* An array of attributes with the attribute name as the key and a test
* value as the value. Only one data-* attribute can be passed per group.
*/
private function verifyAllowedAttributes($group, array $attributes) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Create an array to keep track of attributes' statuses during test loops.
// Initially, set all attributes as allowed.
$attribute_fields = [];
foreach ($attributes as $attribute => $test_value) {
// Replace 'data-*' attribute with 'data' to match expected FAPI key.
if (substr($attribute, 0, 5) === "data-") {
$attribute = 'data';
}
$attribute_fields[$attribute] = TRUE;
}
// Load config.
$config = \Drupal::service('config.factory')->getEditable('layout_builder_component_attributes.settings');
// Load contextual menu and observe all fields are rendered.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
$this->clickContextualLink('.layout-builder-block', 'Manage attributes');
$assert_session->assertWaitOnAjaxRequest();
// Loop through fields.
foreach ($attribute_fields as $attribute => $attribute_status) {
$this->assertTrue($page->hasField($group . '[' . $attribute . ']'), "Attribute field " . $attribute . " is rendered for " . $group . " group");
}
// Loop through attributes and disable one attribute per time.
foreach ($attribute_fields as $attribute => $attribute_status) {
$attribute_fields[$attribute] = FALSE;
$config->set('allowed_' . $group, $attribute_fields)->save();
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
$this->clickContextualLink('.layout-builder-block', 'Manage attributes');
$assert_session->assertWaitOnAjaxRequest();
// Verify only fields for allowed attributes are rendered.
foreach ($attribute_fields as $attribute_inner => $attribute_status) {
if ($attribute_fields[$attribute_inner]) {
$this->assertTrue($page->hasField($group . '[' . $attribute_inner . ']'), "Attribute field " . $attribute_inner . " is rendered for " . $group . " group");
}
else {
$this->assertFalse($page->hasField($group . '[' . $attribute_inner . ']'), "Attribute field " . $attribute_inner . " is not rendered for " . $group . " group");
}
}
// Create and load a test node.
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$page->pressButton('Save layout');
$this->drupalGet('node/add/bundle_with_section_field');
$page->fillField('Title', 'Test Node Title');
$page->pressButton('Save');
$this->drupalGet('node/1');
// Load page and verify only allowed attributes are rendered in markup.
foreach ($attributes as $attribute_inner => $test_value) {
// Replace 'data-*' attribute with 'data' to match expected FAPI key.
$attribute_field = (substr($attribute_inner, 0, 5) === "data-") ? 'data' : $attribute_inner;
if ($attribute_fields[$attribute_field]) {
$element = $page->find('xpath', '//*[contains(@' . $attribute_inner . ', "' . $test_value . '")]');
$this->assertNotEmpty($element, "Attribute " . $attribute_inner . " rendered in " . $group . " group");
}
else {
$element = $page->find('xpath', '//*[contains(@' . $attribute_inner . ', "' . $test_value . '")]');
$this->assertEmpty($element, "Attribute " . $attribute_inner . " not rendered in " . $group . " group");
}
}
}
// After last loop, verify details element is no longer rendered.
$element = $page->find('xpath', '//details[contains(@id, "edit-' . $group . '")]');
$this->assertEmpty($element, "Details element not rendered");
}
/**
* Helper method to assert the settings tray is open.
*
* @param string $message
* The expected validation message.
*/
private function assertSettingsTrayValidationMessage($message = '') {
$page = $this->getSession()->getPage();
$element = $page->find('xpath', '//form[contains(@id, "layout-builder-manage-attributes-form")]');
$this->assertStringContainsString($message, $element->getText(), "Validation message found: " . $message);
}
/**
* Helper method to reset a Layout Builder page.
*
* This method removes the default section and blocks before creating a new
* section and adding a single block, which simplifies testing.
*/
private function resetLayoutBuilderLayout() {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$page->clickLink('Remove Section 1');
$assert_session->waitForElementVisible('css', '#drupal-off-canvas input[value="Remove"]');
$page->pressButton('Remove');
$assert_session->waitForElementRemoved('css', '#drupal-off-canvas');
// Assert that there are no sections on the page.
$assert_session->pageTextNotContains('Remove Section 1');
$assert_session->pageTextNotContains('Add block');
// Add back a section and a component.
$page->clickLink('Add section');
$assert_session->waitForElement('css', '#drupal-off-canvas .layout-selection');
$page->clickLink('One column');
$assert_session->waitForElementVisible('css', '#drupal-off-canvas .layout-builder-configure-section input[value="Add section"]');
$page->pressButton('Add section');
$assert_session->waitForElementRemoved('css', '#drupal-off-canvas');
$page->clickLink('Add block');
$assert_session->waitForElement('css', '#drupal-off-canvas .block-categories');
$page->clickLink('Powered by Drupal');
$assert_session->waitForElementVisible('css', '#drupal-off-canvas input[name="settings[label_display]"]');
$page->checkField('Display title');
$page->pressButton('Add block');
$assert_session->assertWaitOnAjaxRequest();
}
/**
* Extends ContextualLinkClickTrait::clickContextualLink.
*
* @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();
// Beginning in Drupal 10, it is necessary to focus the contextual link
// element before executing clickContextualLink().
$page->find('css', "{$selector} .contextual .trigger")->focus();
$this->contextualLinkClickTraitClickContextualLink($selector, $link_locator, $force_visible);
}
}
