first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
<?php
/**
* @file
* Install layout_builder module before testing update paths.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Update core.extension.
$extensions = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.extension')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['layout_builder'] = 0;
$extensions['module']['layout_discovery'] = 0;
$connection->update('config')
->fields(['data' => serialize($extensions)])
->condition('collection', '')
->condition('name', 'core.extension')
->execute();
// Add all layout_builder_removed_post_updates() as existing updates.
require_once __DIR__ . '/../../../../layout_builder/layout_builder.post_update.php';
require_once __DIR__ . '/../../../../layout_discovery/layout_discovery.post_update.php';
$existing_updates = $connection->select('key_value')
->fields('key_value', ['value'])
->condition('collection', 'post_update')
->condition('name', 'existing_updates')
->execute()
->fetchField();
$existing_updates = unserialize($existing_updates);
$existing_updates = array_merge(
$existing_updates,
array_keys(layout_builder_removed_post_updates()),
array_keys(layout_discovery_removed_post_updates())
);
$connection->update('key_value')
->fields(['value' => serialize($existing_updates)])
->condition('collection', 'post_update')
->condition('name', 'existing_updates')
->execute();

View File

@@ -0,0 +1,10 @@
name: 'Layout Builder Decoration test'
type: module
description: 'Support module for testing layout building.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,6 @@
services:
layout_builder_decoration_test.controller.entity_form:
decorates: controller.entity_form
class: Drupal\layout_builder_decoration_test\Controller\LayoutBuilderDecorationTestHtmlEntityFormController
public: false
arguments: ['@layout_builder_decoration_test.controller.entity_form.inner']

View File

@@ -0,0 +1,52 @@
<?php
namespace Drupal\layout_builder_decoration_test\Controller;
use Drupal\Core\Controller\FormController;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Overrides the entity form controller service for layout builder decoration test.
*/
class LayoutBuilderDecorationTestHtmlEntityFormController extends FormController {
/**
* The entity form controller being decorated.
*
* @var \Drupal\Core\Controller\FormController
*/
protected $entityFormController;
/**
* Constructs a LayoutBuilderDecorationTestHtmlEntityFormController object.
*
* @param \Drupal\Core\Controller\FormController $entity_form_controller
* The entity form controller being decorated.
*/
public function __construct(FormController $entity_form_controller) {
$this->entityFormController = $entity_form_controller;
}
/**
* {@inheritdoc}
*/
public function getContentResult(Request $request, RouteMatchInterface $route_match) {
return $this->entityFormController->getContentResult($request, $route_match);
}
/**
* {@inheritdoc}
*/
protected function getFormArgument(RouteMatchInterface $route_match) {
return $this->entityFormController->getFormArgument($route_match);
}
/**
* {@inheritdoc}
*/
protected function getFormObject(RouteMatchInterface $route_match, $form_arg) {
return $this->entityFormController->getFormObject($route_match, $form_arg);
}
}

View File

@@ -0,0 +1,10 @@
name: 'Layout Builder defaults test'
type: module
description: 'Support module for testing layout building defaults.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,12 @@
name: 'Layout Builder element test'
type: module
description: 'Support module for testing the layout builder element.'
package: Testing
# version: VERSION
dependencies:
- drupal:layout_builder
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,6 @@
services:
_defaults:
autoconfigure: true
layout_builder_element_test.prepare_layout:
class: Drupal\layout_builder_element_test\EventSubscriber\TestPrepareLayout
arguments: ['@layout_builder.tempstore_repository', '@messenger']

View File

@@ -0,0 +1,123 @@
<?php
namespace Drupal\layout_builder_element_test\EventSubscriber;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\layout_builder\Event\PrepareLayoutEvent;
use Drupal\layout_builder\LayoutBuilderEvents;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Provides an event subscriber for testing section storage alteration.
*
* @see \Drupal\layout_builder\Event\PrepareLayoutEvent
* @see \Drupal\layout_builder\Element\LayoutBuilder::prepareLayout()
*/
class TestPrepareLayout implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a new TestPrepareLayout.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The tempstore repository.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
// Act before core's layout builder subscriber.
$events[LayoutBuilderEvents::PREPARE_LAYOUT][] = ['onBeforePrepareLayout', 20];
// Act after core's layout builder subscriber.
$events[LayoutBuilderEvents::PREPARE_LAYOUT][] = ['onAfterPrepareLayout', -10];
return $events;
}
/**
* Subscriber to test acting before the LB subscriber.
*
* @param \Drupal\layout_builder\Event\PrepareLayoutEvent $event
* The prepare layout event.
*/
public function onBeforePrepareLayout(PrepareLayoutEvent $event) {
$section_storage = $event->getSectionStorage();
$context = $section_storage->getContextValues();
if (!empty($context['entity'])) {
/** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = $context['entity'];
// Node 1 or 2: Append a block to the layout.
if (in_array($entity->id(), ['1', '2'])) {
$section = new Section('layout_onecol');
$section->appendComponent(new SectionComponent('fake-uuid', 'content', [
'id' => 'static_block',
'label' => 'Test static block title',
'label_display' => 'visible',
'provider' => 'fake_provider',
]));
$section_storage->appendSection($section);
}
// Node 2: Stop event propagation.
if ($entity->id() === '2') {
$event->stopPropagation();
}
}
}
/**
* Subscriber to test acting after the LB subscriber.
*
* @param \Drupal\layout_builder\Event\PrepareLayoutEvent $event
* The prepare layout event.
*/
public function onAfterPrepareLayout(PrepareLayoutEvent $event) {
$section_storage = $event->getSectionStorage();
$context = $section_storage->getContextValues();
if (!empty($context['entity'])) {
/** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = $context['entity'];
// Node 1, 2, or 3: Append a block to the layout.
if (in_array($entity->id(), ['1', '2', '3'])) {
$section = new Section('layout_onecol');
$section->appendComponent(new SectionComponent('fake-uuid', 'content', [
'id' => 'static_block_two',
'label' => 'Test second static block title',
'label_display' => 'visible',
'provider' => 'fake_provider',
]));
$section_storage->appendSection($section);
}
}
}
}

View File

@@ -0,0 +1,10 @@
name: 'Layout Builder Extra Field test'
type: module
description: 'Support module for testing layout building.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,32 @@
<?php
/**
* @file
* Provides hook implementations for Layout Builder tests.
*/
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
/**
* Implements hook_entity_extra_field_info().
*/
function layout_builder_extra_field_test_entity_extra_field_info() {
$extra['node']['bundle_with_section_field']['display']['layout_builder_extra_field_test'] = [
'label' => t('New Extra Field'),
'description' => t('New Extra Field description'),
'weight' => 0,
];
return $extra;
}
/**
* Implements hook_entity_node_view().
*/
function layout_builder_extra_field_test_node_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
if ($display->getComponent('layout_builder_extra_field_test')) {
$build['layout_builder_extra_field_test'] = [
'#markup' => 'A new extra field.',
];
}
}

View File

@@ -0,0 +1,10 @@
name: 'Layout Builder test'
type: module
description: 'Support module for testing layout building.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,5 @@
services:
layout_builder_fieldblock_test.fake_view_mode_context:
class: Drupal\layout_builder_fieldblock_test\ContextProvider\FakeViewModeContext
tags:
- { name: 'context_provider' }

View File

@@ -0,0 +1,30 @@
<?php
namespace Drupal\layout_builder_fieldblock_test\ContextProvider;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
/**
* Provides a global context for view_mode for testing purposes.
*
* @group layout_builder
*/
class FakeViewModeContext implements ContextProviderInterface {
/**
* {@inheritdoc}
*/
public function getRuntimeContexts(array $unqualified_context_ids) {
return ['view_mode' => new Context(new ContextDefinition('string'), 'default')];
}
/**
* {@inheritdoc}
*/
public function getAvailableContexts() {
return $this->getRuntimeContexts([]);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Drupal\layout_builder_fieldblock_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\layout_builder\Plugin\Block\FieldBlock as LayoutBuilderFieldBlock;
use Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver;
/**
* Provides test field block to test with Block UI.
*
* \Drupal\Tests\layout_builder\FunctionalJavascript\FieldBlockTest provides
* test coverage of complex AJAX interactions within certain field blocks.
* layout_builder_plugin_filter_block__block_ui_alter() removes certain blocks
* with 'layout_builder' as the provider. To make these blocks available during
* testing, this plugin uses the same deriver but each derivative will have a
* different provider.
*
* @see \Drupal\Tests\layout_builder\FunctionalJavascript\FieldBlockTest
* @see layout_builder_plugin_filter_block__block_ui_alter()
*/
#[Block(
id: "field_block_test",
deriver: FieldBlockDeriver::class
)]
class FieldBlock extends LayoutBuilderFieldBlock {
}

View File

@@ -0,0 +1,10 @@
name: 'Layout Builder form block test'
type: module
description: 'Support module for testing layout building using blocks with forms.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,116 @@
<?php
namespace Drupal\layout_builder_form_block_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a block containing a Form API form for use in Layout Builder tests.
*/
#[Block(
id: "layout_builder_form_block_test_form_api_form_block",
admin_label: new TranslatableMarkup("Layout Builder form block test form api form block"),
category: new TranslatableMarkup("Layout Builder form block test")
)]
class TestFormApiFormBlock extends BlockBase implements ContainerFactoryPluginInterface, FormInterface {
/**
* The form builder service.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* TestFormApiFormBlock constructor.
*
* @param array $configuration
* The plugin configuration, i.e. an array with configuration values keyed
* by configuration option name. The special key 'context' may be used to
* initialize the defined contexts by setting it to an array of context
* values keyed by context names.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, FormBuilderInterface $form_builder) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->formBuilder = $form_builder;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('form_builder'));
}
/**
* {@inheritdoc}
*/
public function build() {
return $this->formBuilder->getForm($this);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_form_block_test_search_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['keywords'] = [
'#title' => $this->t('Keywords'),
'#type' => 'textfield',
'#attributes' => [
'placeholder' => $this->t('Keywords'),
],
'#required' => TRUE,
'#title_display' => 'invisible',
'#weight' => 1,
];
$form['actions'] = [
'#type' => 'actions',
'submit' => [
'#name' => '',
'#type' => 'submit',
'#value' => $this->t('Search'),
],
'#weight' => 2,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Drupal\layout_builder_form_block_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a block containing inline template with <form> tag.
*
* For use in Layout Builder tests.
*/
#[Block(
id: "layout_builder_form_block_test_inline_template_form_block",
admin_label: new TranslatableMarkup("Layout Builder form block test inline template form block"),
category: new TranslatableMarkup("Layout Builder form block test")
)]
class TestInlineTemplateFormBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
$build['form'] = [
'#type' => 'inline_template',
'#template' => '<form method="POST"><label>{{ "Keywords"|t }}<input name="keyword" type="text" required /></label><input name="submit" type="submit" value="{{ "Submit"|t }}" /></form>',
];
return $build;
}
}

View File

@@ -0,0 +1,10 @@
name: 'Layout Builder test'
type: module
description: 'Support module for testing layout building.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,4 @@
layout_builder_test:
title: 'Test contextual link'
route_name: '<front>'
group: 'layout_builder_test'

View File

@@ -0,0 +1,189 @@
<?php
/**
* @file
* Provides hook implementations for Layout Builder tests.
*/
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
/**
* Implements hook_plugin_filter_TYPE__CONSUMER_alter().
*/
function layout_builder_test_plugin_filter_block__layout_builder_alter(array &$definitions, array $extra) {
// Explicitly remove the "Help" blocks from the list.
unset($definitions['help_block']);
// Explicitly remove the "Sticky at top of lists field_block".
$disallowed_fields = [
'sticky',
];
// Remove "Changed" field if this is the first section.
if ($extra['delta'] === 0) {
$disallowed_fields[] = 'changed';
}
foreach ($definitions as $plugin_id => $definition) {
// Field block IDs are in the form 'field_block:{entity}:{bundle}:{name}',
// for example 'field_block:node:article:revision_timestamp'.
preg_match('/field_block:.*:.*:(.*)/', $plugin_id, $parts);
if (isset($parts[1]) && in_array($parts[1], $disallowed_fields, TRUE)) {
// Unset any field blocks that match our predefined list.
unset($definitions[$plugin_id]);
}
}
}
/**
* Implements hook_entity_extra_field_info().
*/
function layout_builder_test_entity_extra_field_info() {
$extra['node']['bundle_with_section_field']['display']['layout_builder_test'] = [
'label' => t('Extra label'),
'description' => t('Extra description'),
'weight' => 0,
];
$extra['node']['bundle_with_section_field']['display']['layout_builder_test_2'] = [
'label' => t('Extra Field 2'),
'description' => t('Extra Field 2 description'),
'weight' => 0,
'visible' => FALSE,
];
return $extra;
}
/**
* Implements hook_entity_node_view().
*/
function layout_builder_test_node_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
if ($display->getComponent('layout_builder_test')) {
$build['layout_builder_test'] = [
'#markup' => 'Extra, Extra read all about it.',
];
}
if ($display->getComponent('layout_builder_test_2')) {
$build['layout_builder_test_2'] = [
'#markup' => 'Extra Field 2 is hidden by default.',
];
}
}
/**
* Implements hook_form_BASE_FORM_ID_alter() for layout_builder_configure_block.
*/
function layout_builder_test_form_layout_builder_configure_block_alter(&$form, FormStateInterface $form_state, $form_id) {
/** @var \Drupal\layout_builder\Form\ConfigureBlockFormBase $form_object */
$form_object = $form_state->getFormObject();
$form['layout_builder_test']['storage'] = [
'#type' => 'item',
'#title' => 'Layout Builder Storage: ' . $form_object->getSectionStorage()->getStorageId(),
];
$form['layout_builder_test']['section'] = [
'#type' => 'item',
'#title' => 'Layout Builder Section: ' . $form_object->getCurrentSection()->getLayoutId(),
];
$form['layout_builder_test']['component'] = [
'#type' => 'item',
'#title' => 'Layout Builder Component: ' . $form_object->getCurrentComponent()->getPluginId(),
];
}
/**
* Implements hook_form_BASE_FORM_ID_alter() for layout_builder_configure_section.
*/
function layout_builder_test_form_layout_builder_configure_section_alter(&$form, FormStateInterface $form_state, $form_id) {
/** @var \Drupal\layout_builder\Form\ConfigureSectionForm $form_object */
$form_object = $form_state->getFormObject();
$form['layout_builder_test']['storage'] = [
'#type' => 'item',
'#title' => 'Layout Builder Storage: ' . $form_object->getSectionStorage()->getStorageId(),
];
$form['layout_builder_test']['section'] = [
'#type' => 'item',
'#title' => 'Layout Builder Section: ' . $form_object->getCurrentSection()->getLayoutId(),
];
$form['layout_builder_test']['layout'] = [
'#type' => 'item',
'#title' => 'Layout Builder Layout: ' . $form_object->getCurrentLayout()->getPluginId(),
];
}
/**
* Implements hook_entity_form_display_alter().
*/
function layout_builder_entity_form_display_alter(EntityFormDisplayInterface $form_display, array $context) {
if ($context['form_mode'] === 'layout_builder') {
$form_display->setComponent('status', [
'type' => 'boolean_checkbox',
'settings' => [
'display_label' => TRUE,
],
]);
}
}
/**
* Implements hook_preprocess_HOOK() for one-column layout template.
*/
function layout_builder_test_preprocess_layout__onecol(&$vars) {
if (!empty($vars['content']['#entity'])) {
$vars['content']['content'][\Drupal::service('uuid')->generate()] = [
'#type' => 'markup',
'#markup' => sprintf('Yes, I can access the %s', $vars['content']['#entity']->label()),
];
}
}
/**
* Implements hook_preprocess_HOOK() for two-column layout template.
*/
function layout_builder_test_preprocess_layout__twocol_section(&$vars) {
if (!empty($vars['content']['#entity'])) {
$vars['content']['first'][\Drupal::service('uuid')->generate()] = [
'#type' => 'markup',
'#markup' => sprintf('Yes, I can access the entity %s in two column', $vars['content']['#entity']->label()),
];
}
}
/**
* Implements hook_system_breadcrumb_alter().
*/
function layout_builder_test_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context) {
$breadcrumb->addLink(Link::fromTextAndUrl('External link', Url::fromUri('http://www.example.com')));
}
/**
* Implements hook_module_implements_alter().
*/
function layout_builder_test_module_implements_alter(&$implementations, $hook) {
if ($hook === 'system_breadcrumb_alter') {
// Move our hook_system_breadcrumb_alter() implementation to run before
// layout_builder_system_breadcrumb_alter().
$group = $implementations['layout_builder_test'];
$implementations = [
'layout_builder_test' => $group,
] + $implementations;
}
}
/**
* Implements hook_theme().
*/
function layout_builder_test_theme() {
return [
'block__preview_aware_block' => [
'base hook' => 'block',
],
];
}

View File

@@ -0,0 +1,5 @@
services:
layout_builder_test.i_have_runtime_contexts:
class: Drupal\layout_builder_test\ContextProvider\IHaveRuntimeContexts
tags:
- { name: 'context_provider' }

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\layout_builder_test\ContextProvider;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextProviderInterface;
/**
* Defines a class for a fake context provider.
*/
class IHaveRuntimeContexts implements ContextProviderInterface {
/**
* {@inheritdoc}
*/
public function getRuntimeContexts(array $unqualified_context_ids) {
return [
'runtime_contexts' => new Context(new ContextDefinition('string', 'Do you have runtime contexts?'), 'for sure you can'),
];
}
/**
* {@inheritdoc}
*/
public function getAvailableContexts() {
return [
'runtime_contexts' => new Context(new ContextDefinition('string', 'Do you have runtime contexts?')),
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\layout_builder_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a class for a context-aware block.
*/
#[Block(
id: "i_have_runtime_contexts",
admin_label: new TranslatableMarkup("Can I have runtime contexts"),
category: new TranslatableMarkup("Test"),
context_definitions: [
'runtime_contexts' => new ContextDefinition('string', 'Do you have runtime contexts'),
]
)]
class IHaveRuntimeContexts extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
return [
'#markup' => $this->getContextValue('runtime_contexts'),
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Drupal\layout_builder_test\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Defines a class for a context-aware block.
*
* @Block(
* id = "preview_aware_block",
* admin_label = "Preview-aware block",
* category = "Test",
* )
*/
class PreviewAwareBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
$markup = $this->t('This block is being rendered normally.');
if ($this->inPreview) {
$markup = $this->t('This block is being rendered in preview mode.');
}
return [
'#markup' => $markup,
];
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Drupal\layout_builder_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a 'TestAjax' block.
*/
#[Block(
id: "layout_builder_test_ajax",
admin_label: new TranslatableMarkup("TestAjax"),
category: new TranslatableMarkup("Test")
)]
class TestAjaxBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$form['ajax_test'] = [
'#type' => 'radios',
'#options' => [
1 => $this->t('Ajax test option 1'),
2 => $this->t('Ajax test option 2'),
],
'#prefix' => '<div id="test-ajax-wrapper">',
'#suffix' => '</div>',
'#title' => $this->t('Time in this ajax test is @time', [
'@time' => time(),
]),
'#ajax' => [
'wrapper' => 'test-ajax-wrapper',
'callback' => [$this, 'ajaxCallback'],
],
];
return $form;
}
/**
* Ajax callback.
*/
public function ajaxCallback($form, $form_state) {
return $form['settings']['ajax_test'];
}
/**
* {@inheritdoc}
*/
public function build() {
$build['content'] = [
'#markup' => $this->t('Every word is like an unnecessary stain on silence and nothingness.'),
];
return $build;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\layout_builder_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a 'TestAttributes' block.
*/
#[Block(
id: "layout_builder_test_test_attributes",
admin_label: new TranslatableMarkup("Test Attributes"),
category: new TranslatableMarkup("Test")
)]
class TestAttributesBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
return $form;
}
/**
* {@inheritdoc}
*/
public function build() {
$build = [
'#attributes' => [
'class' => ['attribute-test-class'],
'custom-attribute' => 'test',
],
'#markup' => $this->t('Example block providing its own attributes.'),
'#contextual_links' => [
'layout_builder_test' => ['route_parameters' => []],
],
];
return $build;
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\layout_builder_test\Plugin\Layout;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* The Layout Builder Test Plugin.
*/
#[Layout(
id: 'layout_builder_test_plugin',
label: new TranslatableMarkup('Layout Builder Test Plugin'),
regions: [
"main" => [
"label" => new TranslatableMarkup("Main Region"),
],
],
)]
class LayoutBuilderTestPlugin extends LayoutDefault {
/**
* {@inheritdoc}
*/
public function build(array $regions) {
$build = parent::build($regions);
$build['main']['#attributes']['class'][] = 'go-birds';
if ($this->inPreview) {
$build['main']['#attributes']['class'][] = 'go-birds-preview';
}
return $build;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\layout_builder_test\Plugin\Layout;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Layout plugin without a label configuration.
*/
#[Layout(
id: 'layout_without_label',
label: new TranslatableMarkup('Layout Without Label'),
regions: [
"main" => [
"label" => new TranslatableMarkup("Main Region"),
],
],
)]
class LayoutWithoutLabel extends LayoutDefault {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Drupal\layout_builder_test\Plugin\Layout;
use Drupal\Core\Layout\Attribute\Layout;
use Drupal\Core\Layout\LayoutDefault;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* The TestContextAwareLayout Class.
*/
#[Layout(
id: 'layout_builder_test_context_aware',
label: new TranslatableMarkup('Layout Builder Test: Context Aware'),
regions: [
"main" => [
"label" => new TranslatableMarkup("Main Region"),
],
],
context_definitions: [
"user" => new EntityContextDefinition("entity:user"),
],
)]
class TestContextAwareLayout extends LayoutDefault {
/**
* {@inheritdoc}
*/
public function build(array $regions) {
$build = parent::build($regions);
$build['main']['#attributes']['class'][] = 'user--' . $this->getContextValue('user')->getAccountName();
return $build;
}
}

View File

@@ -0,0 +1,211 @@
<?php
namespace Drupal\layout_builder_test\Plugin\SectionStorage;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\ContextAwarePluginTrait;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\layout_builder\Attribute\SectionStorage;
use Drupal\layout_builder\Plugin\SectionStorage\SectionStorageLocalTaskProviderInterface;
use Drupal\layout_builder\Routing\LayoutBuilderRoutesTrait;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionListTrait;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides section storage utilizing simple config.
*/
#[SectionStorage(id: "test_simple_config", context_definitions: [
"config_id" => new ContextDefinition(
data_type: "string",
label: new TranslatableMarkup("Configuration ID"),
),
])]
class SimpleConfigSectionStorage extends PluginBase implements SectionStorageInterface, SectionStorageLocalTaskProviderInterface, ContainerFactoryPluginInterface {
use ContextAwarePluginTrait;
use LayoutBuilderRoutesTrait;
use SectionListTrait;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* An array of sections.
*
* @var \Drupal\layout_builder\Section[]|null
*/
protected $sections;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $config_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('config.factory')
);
}
/**
* {@inheritdoc}
*/
public function getStorageType() {
return $this->getPluginId();
}
/**
* {@inheritdoc}
*/
public function getStorageId() {
return $this->getContextValue('config_id');
}
/**
* {@inheritdoc}
*/
public function label() {
return $this->getStorageId();
}
/**
* Returns the name to be used to store in the config system.
*/
protected function getConfigName() {
return 'layout_builder_test.' . $this->getStorageType() . '.' . $this->getStorageId();
}
/**
* {@inheritdoc}
*/
public function getSections() {
if (is_null($this->sections)) {
$sections = $this->configFactory->get($this->getConfigName())->get('sections') ?: [];
$this->setSections(array_map([Section::class, 'fromArray'], $sections));
}
return $this->sections;
}
/**
* {@inheritdoc}
*/
protected function setSections(array $sections) {
$this->sections = array_values($sections);
return $this;
}
/**
* {@inheritdoc}
*/
public function save() {
$sections = array_map(function (Section $section) {
return $section->toArray();
}, $this->getSections());
$config = $this->configFactory->getEditable($this->getConfigName());
$return = $config->get('sections') ? SAVED_UPDATED : SAVED_NEW;
$config->set('sections', $sections)->save();
return $return;
}
/**
* {@inheritdoc}
*/
public function buildRoutes(RouteCollection $collection) {
$this->buildLayoutRoutes($collection, $this->getPluginDefinition(), 'layout-builder-test-simple-config/{id}');
}
/**
* {@inheritdoc}
*/
public function deriveContextsFromRoute($value, $definition, $name, array $defaults) {
$contexts['config_id'] = new Context(new ContextDefinition('string'), $value ?: $defaults['id']);
return $contexts;
}
/**
* {@inheritdoc}
*/
public function buildLocalTasks($base_plugin_definition) {
$type = $this->getStorageType();
$local_tasks = [];
$local_tasks["layout_builder.$type.view"] = $base_plugin_definition + [
'route_name' => "layout_builder.$type.view",
'title' => $this->t('Layout'),
'base_route' => "layout_builder.$type.view",
];
$local_tasks["layout_builder.$type.view__child"] = $base_plugin_definition + [
'route_name' => "layout_builder.$type.view",
'title' => $this->t('Layout'),
'parent_id' => "layout_builder_ui:layout_builder.$type.view",
];
$local_tasks["layout_builder.$type.discard_changes"] = $base_plugin_definition + [
'route_name' => "layout_builder.$type.discard_changes",
'title' => $this->t('Discard changes'),
'parent_id' => "layout_builder_ui:layout_builder.$type.view",
'weight' => 5,
];
return $local_tasks;
}
/**
* {@inheritdoc}
*/
public function getLayoutBuilderUrl($rel = 'view') {
return Url::fromRoute("layout_builder.{$this->getStorageType()}.$rel", ['id' => $this->getStorageId()]);
}
/**
* {@inheritdoc}
*/
public function getRedirectUrl() {
return $this->getLayoutBuilderUrl();
}
/**
* {@inheritdoc}
*/
public function access($operation, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowed();
return $return_as_object ? $result : $result->isAllowed();
}
/**
* {@inheritdoc}
*/
public function getContextsDuringPreview() {
return $this->getContexts();
}
/**
* {@inheritdoc}
*/
public function isApplicable(RefinableCacheableDependencyInterface $cacheability) {
return TRUE;
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Drupal\layout_builder_test\Plugin\SectionStorage;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\Attribute\SectionStorage;
use Drupal\layout_builder\Plugin\SectionStorage\SectionStorageBase;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides a test section storage that is controlled by state.
*/
#[SectionStorage(id: "layout_builder_test_state")]
class TestStateBasedSectionStorage extends SectionStorageBase {
/**
* {@inheritdoc}
*/
public function getSections() {
// Return a custom section.
$section = new Section('layout_onecol');
$section->appendComponent(new SectionComponent('fake-uuid', 'content', [
'id' => 'system_powered_by_block',
'label' => 'Test block title',
'label_display' => 'visible',
]));
return [$section];
}
/**
* {@inheritdoc}
*/
public function isApplicable(RefinableCacheableDependencyInterface $cacheability) {
$cacheability->mergeCacheMaxAge(0);
return \Drupal::state()->get('layout_builder_test_state', FALSE);
}
/**
* {@inheritdoc}
*/
public function access($operation, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
throw new \RuntimeException(__METHOD__ . " not implemented for " . __CLASS__);
}
/**
* {@inheritdoc}
*/
protected function getSectionList() {
throw new \RuntimeException(__METHOD__ . " not implemented for " . __CLASS__);
}
/**
* {@inheritdoc}
*/
public function getStorageId() {
throw new \RuntimeException(__METHOD__ . " not implemented for " . __CLASS__);
}
/**
* {@inheritdoc}
*/
public function getSectionListFromId($id) {
throw new \RuntimeException(__METHOD__ . " not implemented for " . __CLASS__);
}
/**
* {@inheritdoc}
*/
public function buildRoutes(RouteCollection $collection) {
}
/**
* {@inheritdoc}
*/
public function getRedirectUrl() {
throw new \RuntimeException(__METHOD__ . " not implemented for " . __CLASS__);
}
/**
* {@inheritdoc}
*/
public function getLayoutBuilderUrl($rel = 'view') {
throw new \RuntimeException(__METHOD__ . " not implemented for " . __CLASS__);
}
/**
* {@inheritdoc}
*/
public function extractIdFromRoute($value, $definition, $name, array $defaults) {
throw new \RuntimeException(__METHOD__ . " not implemented for " . __CLASS__);
}
/**
* {@inheritdoc}
*/
public function deriveContextsFromRoute($value, $definition, $name, array $defaults) {
throw new \RuntimeException(__METHOD__ . " not implemented for " . __CLASS__);
}
/**
* {@inheritdoc}
*/
public function label() {
throw new \RuntimeException(__METHOD__ . " not implemented for " . __CLASS__);
}
/**
* {@inheritdoc}
*/
public function save() {
throw new \RuntimeException(__METHOD__ . " not implemented for " . __CLASS__);
}
}

View File

@@ -0,0 +1,5 @@
{% if in_preview %}
The block template is being previewed.
{% endif %}
{% include '@block/block.html.twig' %}

View File

@@ -0,0 +1,10 @@
name: 'Layout Builder Field Block Theme Suggestions Test'
type: module
description: 'Support module for testing.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,30 @@
<?php
/**
* @file
* For testing theme suggestions.
*/
/**
* Implements hook_theme().
*/
function layout_builder_theme_suggestions_test_theme() {
// It is necessary to explicitly register the template via hook_theme()
// because it is added via a module, not a theme.
return [
'field__node__body__bundle_with_section_field__default' => [
'base hook' => 'field',
],
];
}
/**
* Implements hook_preprocess_HOOK() for the list of layouts.
*/
function layout_builder_theme_suggestions_test_preprocess_item_list__layouts(&$variables) {
foreach (array_keys($variables['items']) as $layout_id) {
if (isset($variables['items'][$layout_id]['value']['#title']['icon'])) {
$variables['items'][$layout_id]['value']['#title']['icon'] = ['#markup' => __FUNCTION__];
}
}
}

View File

@@ -0,0 +1 @@
<h1>I am a field template for a specific view mode!</h1>

View File

@@ -0,0 +1,12 @@
name: 'Layout Builder Views Test'
type: module
description: 'Support module for testing.'
package: Testing
# version: VERSION
dependencies:
- drupal:views
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

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

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional\Jsonapi;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\Tests\jsonapi\Functional\EntityViewDisplayTest;
/**
* JSON:API integration test for the "EntityViewDisplay" config entity type.
*
* @group jsonapi
* @group layout_builder
*/
class LayoutBuilderEntityViewDisplayTest extends EntityViewDisplayTest {
/**
* {@inheritdoc}
*/
protected static $modules = ['layout_builder'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function createEntity() {
/** @var \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay $entity */
$entity = parent::createEntity();
$entity
->enableLayoutBuilder()
->setOverridable()
->save();
$this->assertCount(1, $entity->getThirdPartySetting('layout_builder', 'sections'));
return $entity;
}
/**
* {@inheritdoc}
*/
protected function getExpectedDocument() {
$document = parent::getExpectedDocument();
array_unshift($document['data']['attributes']['dependencies']['module'], 'layout_builder');
$document['data']['attributes']['hidden'][OverridesSectionStorage::FIELD_NAME] = TRUE;
$document['data']['attributes']['third_party_settings']['layout_builder'] = [
'enabled' => TRUE,
'allow_custom' => TRUE,
];
return $document;
}
}

View File

@@ -0,0 +1,290 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\BrowserTestBase;
/**
* Tests access to Layout Builder.
*
* @group layout_builder
*/
class LayoutBuilderAccessTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_builder',
'block_test',
'field_ui',
'node',
'user',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Enable Layout Builder for one content type.
$this->createContentType(['type' => 'bundle_with_section_field']);
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
// Enable Layout Builder for user profiles.
$values = [
'targetEntityType' => 'user',
'bundle' => 'user',
'mode' => 'default',
'status' => TRUE,
];
LayoutBuilderEntityViewDisplay::create($values)
->enableLayoutBuilder()
->setOverridable()
->save();
}
/**
* Tests Layout Builder access for an entity type that has bundles.
*
* @dataProvider providerTestAccessWithBundles
*
* @param array $permissions
* An array of permissions to grant to the user.
* @param bool $default_access
* Whether access is expected for the defaults.
* @param bool $non_editable_access
* Whether access is expected for a non-editable override.
* @param bool $editable_access
* Whether access is expected for an editable override.
* @param array $permission_dependencies
* An array of expected permission dependencies.
*/
public function testAccessWithBundles(array $permissions, $default_access, $non_editable_access, $editable_access, array $permission_dependencies): void {
$permissions[] = 'edit own bundle_with_section_field content';
$permissions[] = 'access content';
$user = $this->drupalCreateUser($permissions);
$this->drupalLogin($user);
$editable_node = $this->createNode([
'uid' => $user->id(),
'type' => 'bundle_with_section_field',
'title' => 'The first node title',
'body' => [
[
'value' => 'The first node body',
],
],
]);
$non_editable_node = $this->createNode([
'uid' => 1,
'type' => 'bundle_with_section_field',
'title' => 'The second node title',
'body' => [
[
'value' => 'The second node body',
],
],
]);
$non_viewable_node = $this->createNode([
'uid' => $user->id(),
'status' => 0,
'type' => 'bundle_with_section_field',
'title' => 'Nobody can see this node.',
'body' => [
[
'value' => 'Does it really exist?',
],
],
]);
$this->drupalGet($editable_node->toUrl('edit-form'));
$this->assertExpectedAccess(TRUE);
$this->drupalGet($non_editable_node->toUrl('edit-form'));
$this->assertExpectedAccess(FALSE);
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default/layout');
$this->assertExpectedAccess($default_access);
$this->drupalGet('node/' . $editable_node->id() . '/layout');
$this->assertExpectedAccess($editable_access);
$this->drupalGet('node/' . $non_editable_node->id() . '/layout');
$this->assertExpectedAccess($non_editable_access);
$this->drupalGet($non_viewable_node->toUrl());
$this->assertExpectedAccess(FALSE);
$this->drupalGet('node/' . $non_viewable_node->id() . '/layout');
$this->assertExpectedAccess(FALSE);
if (!empty($permission_dependencies)) {
$permission_definitions = \Drupal::service('user.permissions')->getPermissions();
foreach ($permission_dependencies as $permission => $expected_dependencies) {
$this->assertSame($expected_dependencies, $permission_definitions[$permission]['dependencies']);
}
}
}
/**
* Provides test data for ::testAccessWithBundles().
*/
public static function providerTestAccessWithBundles() {
// Data provider values are:
// - the permissions to grant to the user
// - whether access is expected for the defaults
// - whether access is expected for a non-editable override
// - whether access is expected for an editable override.
$data = [];
$data['configure any layout'] = [
['configure any layout', 'administer node display'],
TRUE,
TRUE,
TRUE,
[],
];
$data['override permissions'] = [
['configure all bundle_with_section_field node layout overrides'],
FALSE,
TRUE,
TRUE,
[
'configure all bundle_with_section_field node layout overrides' => [
'config' => ['core.entity_view_display.node.bundle_with_section_field.default'],
],
],
];
$data['editable override permissions'] = [
['configure editable bundle_with_section_field node layout overrides'],
FALSE,
FALSE,
TRUE,
[
'configure editable bundle_with_section_field node layout overrides' => [
'config' => ['core.entity_view_display.node.bundle_with_section_field.default'],
],
],
];
return $data;
}
/**
* Tests Layout Builder access for an entity type that does not have bundles.
*
* @dataProvider providerTestAccessWithoutBundles
*/
public function testAccessWithoutBundles(array $permissions, $default_access, $non_editable_access, $editable_access, array $permission_dependencies): void {
$permissions[] = 'access user profiles';
$user = $this->drupalCreateUser($permissions);
$this->drupalLogin($user);
$this->drupalGet('admin/config/people/accounts/display/default/layout');
$this->assertExpectedAccess($default_access);
$this->drupalGet($user->toUrl());
$this->assertExpectedAccess(TRUE);
$this->drupalGet($user->toUrl('edit-form'));
$this->assertExpectedAccess(TRUE);
$this->drupalGet('user/' . $user->id() . '/layout');
$this->assertExpectedAccess($editable_access);
$non_editable_user = $this->drupalCreateUser();
$this->drupalGet($non_editable_user->toUrl());
$this->assertExpectedAccess(TRUE);
$this->drupalGet($non_editable_user->toUrl('edit-form'));
$this->assertExpectedAccess(FALSE);
$this->drupalGet('user/' . $non_editable_user->id() . '/layout');
$this->assertExpectedAccess($non_editable_access);
$non_viewable_user = $this->drupalCreateUser(
[],
'bad person',
FALSE,
['status' => 0]
);
$this->drupalGet($non_viewable_user->toUrl());
$this->assertExpectedAccess(FALSE);
$this->drupalGet($non_viewable_user->toUrl('edit-form'));
$this->assertExpectedAccess(FALSE);
$this->drupalGet('user/' . $non_viewable_user->id() . '/layout');
$this->assertExpectedAccess(FALSE);
if (!empty($permission_dependencies)) {
$permission_definitions = \Drupal::service('user.permissions')->getPermissions();
foreach ($permission_dependencies as $permission => $expected_dependencies) {
$this->assertSame($expected_dependencies, $permission_definitions[$permission]['dependencies']);
}
}
}
/**
* Provides test data for ::testAccessWithoutBundles().
*/
public static function providerTestAccessWithoutBundles() {
// Data provider values are:
// - the permissions to grant to the user
// - whether access is expected for the defaults
// - whether access is expected for a non-editable override
// - whether access is expected for an editable override.
$data = [];
$data['configure any layout'] = [
['configure any layout', 'administer user display'],
TRUE,
TRUE,
TRUE,
[],
];
$data['override permissions'] = [
['configure all user user layout overrides'],
FALSE,
TRUE,
TRUE,
[
'configure all user user layout overrides' => [
'config' => ['core.entity_view_display.user.user.default'],
],
],
];
$data['editable override permissions'] = [
['configure editable user user layout overrides'],
FALSE,
FALSE,
TRUE,
[
'configure all user user layout overrides' => [
'config' => ['core.entity_view_display.user.user.default'],
],
],
];
return $data;
}
/**
* Asserts the correct response code is returned based on expected access.
*
* @param bool $expected_access
* The expected access.
*/
private function assertExpectedAccess(bool $expected_access): void {
$expected_status_code = $expected_access ? 200 : 403;
$this->assertSession()->statusCodeEquals($expected_status_code);
}
}

View File

@@ -0,0 +1,286 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\node\Entity\Node;
use Drupal\views\Entity\View;
// cspell:ignore blocktest
/**
* Tests the Layout Builder UI with blocks.
*
* @group layout_builder
* @group #slow
*/
class LayoutBuilderBlocksTest extends LayoutBuilderTestBase {
/**
* Tests that block plugins can define custom attributes and contextual links.
*/
public function testPluginsProvidingCustomAttributesAndContextualLinks(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
'administer node display',
]));
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$page->clickLink('Manage layout');
$page->clickLink('Add section');
$page->clickLink('Layout Builder Test Plugin');
$page->pressButton('Add section');
$page->clickLink('Add block');
$page->clickLink('Test Attributes');
$page->pressButton('Add block');
$page->pressButton('Save layout');
$this->drupalGet('node/1');
$assert_session->elementExists('css', '.attribute-test-class');
$assert_session->elementExists('css', '[custom-attribute=test]');
$assert_session->elementExists('css', 'div[data-contextual-id*="layout_builder_test"]');
}
/**
* Tests preview-aware layout & block plugins.
*/
public function testPreviewAwarePlugins(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$page->clickLink('Manage layout');
$page->clickLink('Add section');
$page->clickLink('Layout Builder Test Plugin');
$page->pressButton('Add section');
$page->clickLink('Add block');
$page->clickLink('Preview-aware block');
$page->pressButton('Add block');
$assert_session->elementExists('css', '.go-birds-preview');
$assert_session->pageTextContains('The block template is being previewed.');
$assert_session->pageTextContains('This block is being rendered in preview mode.');
$page->pressButton('Save layout');
$this->drupalGet('node/1');
$assert_session->elementNotExists('css', '.go-birds-preview');
$assert_session->pageTextNotContains('The block template is being previewed.');
$assert_session->pageTextContains('This block is being rendered normally.');
}
/**
* {@inheritdoc}
*/
public function testLayoutBuilderChooseBlocksAlter(): void {
// See layout_builder_test_plugin_filter_block__layout_builder_alter().
$assert_session = $this->assertSession();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer node fields',
]));
// From the manage display page, go to manage the layout.
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$assert_session->linkExists('Manage layout');
$this->clickLink('Manage layout');
// Add a new block.
$this->clickLink('Add block');
// Verify that blocks not modified are present.
$assert_session->linkExists('Powered by Drupal');
$assert_session->linkExists('Default revision');
// Verify that blocks explicitly removed are not present.
$assert_session->linkNotExists('Help');
$assert_session->linkNotExists('Sticky at top of lists');
$assert_session->linkNotExists('Main page content');
$assert_session->linkNotExists('Page title');
$assert_session->linkNotExists('Messages');
$assert_session->linkNotExists('Help');
$assert_session->linkNotExists('Tabs');
$assert_session->linkNotExists('Primary admin actions');
// Verify that Changed block is not present on first section.
$assert_session->linkNotExists('Changed');
// Go back to Manage layout.
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$this->clickLink('Manage layout');
// Add a new section.
$this->clickLink('Add section', 1);
$assert_session->linkExists('Two column');
$this->clickLink('Two column');
$assert_session->buttonExists('Add section');
$this->getSession()->getPage()->pressButton('Add section');
// Add a new block to second section.
$this->clickLink('Add block', 1);
// Verify that Changed block is present on second section.
$assert_session->linkExists('Changed');
}
/**
* Tests that deleting a View block used in Layout Builder works.
*/
public function testDeletedView(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
// Enable overrides.
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
$this->drupalGet('node/1');
$assert_session->linkExists('Layout');
$this->clickLink('Layout');
$this->clickLink('Add block');
$this->clickLink('Test Block View');
$page->pressButton('Add block');
$assert_session->pageTextContains('Test Block View');
$assert_session->elementExists('css', '.block-views-blocktest-block-view-block-1');
$page->pressButton('Save');
$assert_session->pageTextContains('Test Block View');
$assert_session->elementExists('css', '.block-views-blocktest-block-view-block-1');
View::load('test_block_view')->delete();
$this->drupalGet('node/1');
// Node can be loaded after deleting the View.
$assert_session->pageTextContains(Node::load(1)->getTitle());
$assert_session->pageTextNotContains('Test Block View');
}
/**
* Tests the usage of placeholders for empty blocks.
*
* @see \Drupal\Core\Render\PreviewFallbackInterface::getPreviewFallbackString()
* @see \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender()
*/
public function testBlockPlaceholder(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
// Customize the default view mode.
$this->drupalGet("$field_ui_prefix/display/default/layout");
// Add a block whose content is controlled by state and is empty by default.
$this->clickLink('Add block');
$this->clickLink('Test block caching');
$page->fillField('settings[label]', 'The block label');
$page->pressButton('Add block');
$block_content = 'I am content';
$placeholder_content = 'Placeholder for the "The block label" block';
// The block placeholder is displayed and there is no content.
$assert_session->pageTextContains($placeholder_content);
$assert_session->pageTextNotContains($block_content);
// Set block content and reload the page.
\Drupal::state()->set('block_test.content', $block_content);
$this->getSession()->reload();
// The block placeholder is no longer displayed and the content is visible.
$assert_session->pageTextNotContains($placeholder_content);
$assert_session->pageTextContains($block_content);
}
/**
* Tests the ability to use a specified block label for field blocks.
*/
public function testFieldBlockLabel(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
$this->drupalGet("$field_ui_prefix/display/default");
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
// Customize the default view mode.
$this->drupalGet("$field_ui_prefix/display/default/layout");
// Add a body block whose label will be overridden.
$this->clickLink('Add block');
$this->clickLink('Body');
// Enable the Label Display and set the Label to a modified field
// block label.
$modified_field_block_label = 'Modified Field Block Label';
$page->checkField('settings[label_display]');
$page->fillField('settings[label]', $modified_field_block_label);
// Save the block and layout.
$page->pressButton('Add block');
$page->pressButton('Save layout');
// Revisit the default layout view mode page.
$this->drupalGet("$field_ui_prefix/display/default/layout");
// The modified field block label is displayed.
$assert_session->pageTextContains($modified_field_block_label);
}
/**
* Tests the Block UI when Layout Builder is installed.
*/
public function testBlockUiListing(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'administer blocks',
]));
$this->drupalGet('admin/structure/block');
$page->clickLink('Place block');
// Ensure that blocks expected to appear are available.
$assert_session->pageTextContains('Test HTML block');
$assert_session->pageTextContains('Block test');
// Ensure that blocks not expected to appear are not available.
$assert_session->pageTextNotContains('Body');
$assert_session->pageTextNotContains('Content fields');
}
}

View File

@@ -0,0 +1,380 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\File\FileExists;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\file\Entity\File;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\image\Kernel\ImageFieldCreationTrait;
use Drupal\Tests\TestFileCreationTrait;
// cspell:ignore blocknodetest typefield
/**
* Tests rendering default field values in Layout Builder.
*
* @group layout_builder
*/
class LayoutBuilderDefaultValuesTest extends BrowserTestBase {
use ImageFieldCreationTrait;
use TestFileCreationTrait {
getTestFiles as drupalGetTestFiles;
}
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_builder',
'block',
'node',
'image',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType([
'type' => 'test_node_type',
'name' => 'Test Node Type',
]);
$this->addTextFields();
$this->addImageFields();
// Create node 1 with specific values.
$this->createNode([
'type' => 'test_node_type',
'title' => 'Test Node 1 Has Values',
'field_string_no_default' => 'No default, no problem.',
'field_string_with_default' => 'It is ok to be different',
'field_string_with_callback' => 'Not from a callback',
'field_string_late_default' => 'I am way ahead of you.',
'field_image_storage_default' => [
'target_id' => 3,
'alt' => 'My third alt text',
],
'field_image_instance_default' => [
'target_id' => 4,
'alt' => 'My fourth alt text',
],
'field_image_both_defaults' => [
'target_id' => 5,
'alt' => 'My fifth alt text',
],
'field_image_no_default' => [
'target_id' => 6,
'alt' => 'My sixth alt text',
],
]);
// Create node 2 relying on defaults.
$this->createNode([
'type' => 'test_node_type',
'title' => 'Test Node 2 Uses Defaults',
]);
// Add default value to field_string_late_default.
$field = FieldConfig::loadByName('node', 'test_node_type', 'field_string_late_default');
$field->setDefaultValue('Too late!');
$field->save();
}
/**
* Tests display of default field values.
*/
public function testDefaultValues(): void {
// Begin by viewing nodes with Layout Builder disabled.
$this->assertNodeWithValues();
$this->assertNodeWithDefaultValues();
// Enable layout builder.
LayoutBuilderEntityViewDisplay::load('node.test_node_type.default')
->enableLayoutBuilder()
->save();
// Confirm that default values are handled consistently by Layout Builder.
$this->assertNodeWithValues();
$this->assertNodeWithDefaultValues();
}
/**
* Test for expected text on node 1.
*/
protected function assertNodeWithValues() {
$this->drupalGet('node/1');
$assert_session = $this->assertSession();
// String field with no default should render a value.
$assert_session->pageTextContains('field_string_no_default');
$assert_session->pageTextContains('No default, no problem.');
// String field with default should render non-default value.
$assert_session->pageTextContains('field_string_with_default');
$assert_session->pageTextNotContains('This is my default value');
$assert_session->pageTextContains('It is ok to be different');
// String field with callback should render non-default value.
$assert_session->pageTextContains('field_string_with_callback');
$assert_session->pageTextNotContains('This is from my default value callback');
$assert_session->pageTextContains('Not from a callback');
// String field with "late" default should render non-default value.
$assert_session->pageTextContains('field_string_late_default');
$assert_session->pageTextNotContains('Too late!');
$assert_session->pageTextContains('I am way ahead of you');
// Image field with storage default should render non-default value.
$assert_session->pageTextContains('field_image_storage_default');
$assert_session->responseNotContains('My storage default alt text');
$assert_session->responseNotContains('test-file-1');
$assert_session->responseContains('My third alt text');
$assert_session->responseContains('test-file-3');
// Image field with instance default should render non-default value.
$assert_session->pageTextContains('field_image_instance_default');
$assert_session->responseNotContains('My instance default alt text');
$assert_session->responseNotContains('test-file-1');
$assert_session->responseContains('My fourth alt text');
$assert_session->responseContains('test-file-4');
// Image field with both storage and instance defaults should render
// non-default value.
$assert_session->pageTextContains('field_image_both_defaults');
$assert_session->responseNotContains('My storage default alt text');
$assert_session->responseNotContains('My instance default alt text');
$assert_session->responseNotContains('test-file-1');
$assert_session->responseNotContains('test-file-2');
$assert_session->responseContains('My fifth alt text');
$assert_session->responseContains('test-file-5');
// Image field with no default should render a value.
$assert_session->pageTextContains('field_image_no_default');
$assert_session->responseContains('My sixth alt text');
$assert_session->responseContains('test-file-6');
}
/**
* Test for expected text on node 2.
*/
protected function assertNodeWithDefaultValues() {
// Switch theme to starterkit_theme so that layout builder components will
// have block classes.
/** @var \Drupal\Core\Extension\ThemeInstallerInterface $theme_installer */
$theme_installer = $this->container->get('theme_installer');
$theme_installer->install(['starterkit_theme']);
$this->config('system.theme')
->set('default', 'starterkit_theme')
->save();
$this->drupalGet('node/2');
$assert_session = $this->assertSession();
// String field with no default should not render.
$assert_session->pageTextNotContains('field_string_no_default');
// String with default value should render with default value.
$assert_session->pageTextContains('field_string');
$assert_session->pageTextContains('This is my default value');
// String field with callback should render value from callback.
$assert_session->pageTextContains('field_string_with_callback');
$assert_session->pageTextContains('This is from my default value callback');
// String field with "late" default should not render.
$assert_session->pageTextNotContains('field_string_late_default');
$assert_session->pageTextNotContains('Too late!');
// Image field with default should render default value.
$assert_session->pageTextContains('field_image_storage_default');
$assert_session->responseContains('My storage default alt text');
$assert_session->responseContains('test-file-1');
$assert_session->pageTextContains('field_image_instance_default');
$assert_session->responseContains('My instance default alt text');
$assert_session->responseContains('test-file-1');
$assert_session->pageTextContains('field_image_both_defaults');
$assert_session->responseContains('My instance default alt text');
$assert_session->responseContains('test-file-2');
// Image field with no default should not render.
$assert_session->pageTextNotContains('field_image_no_default');
// Confirm that there is no DOM element for the field_image_with_no_default
// field block.
$assert_session->elementNotExists('css', '.block-field-blocknodetest-node-typefield-image-no-default');
}
/**
* Helper function to add string fields.
*/
protected function addTextFields() {
// String field with no default.
$field_storage = FieldStorageConfig::create([
'field_name' => 'field_string_no_default',
'entity_type' => 'node',
'type' => 'string',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'test_node_type',
]);
$field->save();
// String field with default value.
$field_storage = FieldStorageConfig::create([
'field_name' => 'field_string_with_default',
'entity_type' => 'node',
'type' => 'string',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'test_node_type',
]);
$field->setDefaultValue('This is my default value');
$field->save();
// String field with default callback.
$field_storage = FieldStorageConfig::create([
'field_name' => 'field_string_with_callback',
'entity_type' => 'node',
'type' => 'string',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'test_node_type',
]);
$field->setDefaultValueCallback('Drupal\Tests\layout_builder\Functional\LayoutBuilderDefaultValuesTest::defaultValueCallback');
$field->save();
// String field with late default. We will set default later.
$field_storage = FieldStorageConfig::create([
'field_name' => 'field_string_late_default',
'entity_type' => 'node',
'type' => 'string',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'test_node_type',
]);
$field->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getViewDisplay('node', 'test_node_type')
->setComponent('field_string_no_default', [
'type' => 'string',
])
->setComponent('field_string_with_default', [
'type' => 'string',
])
->setComponent('field_string_with_callback', [
'type' => 'string',
])
->setComponent('field_string_late_default', [
'type' => 'string',
])
->save();
}
/**
* Helper function to add image fields.
*/
protected function addImageFields() {
// Create files to use as the default images.
$files = $this->drupalGetTestFiles('image');
$images = [];
for ($i = 1; $i <= 6; $i++) {
$filename = "test-file-$i";
$desired_filepath = 'public://' . $filename;
\Drupal::service('file_system')->copy($files[0]->uri, $desired_filepath, FileExists::Error);
$file = File::create([
'uri' => $desired_filepath,
'filename' => $filename,
'name' => $filename,
]);
$file->save();
$images[] = $file;
}
$field_name = 'field_image_storage_default';
$storage_settings['default_image'] = [
'uuid' => $images[0]->uuid(),
'alt' => 'My storage default alt text',
'title' => '',
'width' => 0,
'height' => 0,
];
$field_settings['default_image'] = [
'uuid' => NULL,
'alt' => '',
'title' => '',
'width' => NULL,
'height' => NULL,
];
$widget_settings = [
'preview_image_style' => 'medium',
];
$this->createImageField($field_name, 'node', 'test_node_type', $storage_settings, $field_settings, $widget_settings);
$field_name = 'field_image_instance_default';
$storage_settings['default_image'] = [
'uuid' => NULL,
'alt' => '',
'title' => '',
'width' => NULL,
'height' => NULL,
];
$field_settings['default_image'] = [
'uuid' => $images[0]->uuid(),
'alt' => 'My instance default alt text',
'title' => '',
'width' => 0,
'height' => 0,
];
$widget_settings = [
'preview_image_style' => 'medium',
];
$this->createImageField($field_name, 'node', 'test_node_type', $storage_settings, $field_settings, $widget_settings);
$field_name = 'field_image_both_defaults';
$storage_settings['default_image'] = [
'uuid' => $images[0]->uuid(),
'alt' => 'My storage default alt text',
'title' => '',
'width' => 0,
'height' => 0,
];
$field_settings['default_image'] = [
'uuid' => $images[1]->uuid(),
'alt' => 'My instance default alt text',
'title' => '',
'width' => 0,
'height' => 0,
];
$widget_settings = [
'preview_image_style' => 'medium',
];
$this->createImageField($field_name, 'node', 'test_node_type', $storage_settings, $field_settings, $widget_settings);
$field_name = 'field_image_no_default';
$storage_settings = [];
$field_settings = [];
$widget_settings = [
'preview_image_style' => 'medium',
];
$this->createImageField($field_name, 'node', 'test_node_type', $storage_settings, $field_settings, $widget_settings);
}
/**
* Sample 'default value' callback.
*/
public static function defaultValueCallback(FieldableEntityInterface $entity, FieldDefinitionInterface $definition) {
return [['value' => 'This is from my default value callback']];
}
}

View File

@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
/**
* Tests cache tags on entity reference field blocks in Layout Builder.
*
* @group layout_builder
*/
class LayoutBuilderFieldBlockEntityReferenceCacheTagsTest extends BrowserTestBase {
use ContentTypeCreationTrait;
use EntityReferenceFieldCreationTrait;
use NodeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'layout_builder',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Enable page caching.
$config = $this->config('system.performance');
$config->set('cache.page.max_age', 3600);
$config->save();
// Create two content types, with one content type containing a field that
// references entities of the second content type.
$this->createContentType([
'type' => 'bundle_with_reference_field',
'name' => 'bundle_with_reference_field',
]);
$this->createContentType([
'type' => 'bundle_referenced',
'name' => 'bundle_referenced',
]);
$this->createEntityReferenceField('node', 'bundle_with_reference_field', 'field_reference', 'Reference field', 'node', 'default', [
'target_bundles' => ['bundle_referenced'],
]);
// Enable layout builder to the content type with the reference field, and
// add the reference field to the layout builder display.
$this->container->get('entity_display.repository')
->getViewDisplay('node', 'bundle_with_reference_field', 'full')
->enableLayoutBuilder()
->setComponent('field_reference', ['type' => 'entity_reference_label'])
->save();
}
/**
* Tests cache tags on field block for entity reference field.
*/
public function testEntityReferenceFieldBlockCaching(): void {
$assert_session = $this->assertSession();
// Create two nodes, one of the referenced content type and one of the
// referencing content type, with the first node being referenced by the
// second. Set the referenced node to be unpublished so anonymous user will
// not have view access.
$referenced_node = $this->createNode([
'type' => 'bundle_referenced',
'title' => 'The referenced node title',
'status' => 0,
]);
$referencing_node = $this->createNode([
'type' => 'bundle_with_reference_field',
'title' => 'The referencing node title',
'field_reference' => ['entity' => $referenced_node],
]);
// When user does not have view access to referenced entities in entity
// reference field blocks, test that the cache tags of the referenced entity
// are still bubbled to page cache.
$referencing_node_url = $referencing_node->toUrl();
$this->verifyPageCacheContainsTags($referencing_node_url, 'MISS');
$this->verifyPageCacheContainsTags($referencing_node_url, 'HIT', $referenced_node->getCacheTags());
// Since the referenced node is inaccessible, it should not appear on the
// referencing node.
$this->drupalGet($referencing_node_url);
$assert_session->linkNotExists('The referenced node title');
// Publish the referenced entity.
$referenced_node->setPublished()
->save();
// Revisit the node with the reference field without clearing cache. Now
// that the referenced node is published, it should appear.
$this->verifyPageCacheContainsTags($referencing_node_url, 'MISS');
$this->verifyPageCacheContainsTags($referencing_node_url, 'HIT', $referenced_node->getCacheTags());
$this->drupalGet($referencing_node_url);
$assert_session->linkExists('The referenced node title');
}
/**
* Verify that when loading a given page, it's a page cache hit or miss.
*
* @param \Drupal\Core\Url $url
* The page for this URL will be loaded.
* @param string $hit_or_miss
* 'HIT' if a page cache hit is expected, 'MISS' otherwise.
* @param array|false $tags
* When expecting a page cache hit, you may optionally specify an array of
* expected cache tags. While FALSE, the cache tags will not be verified.
* This tests whether all expected tags are in the page cache tags, not that
* expected tags and page cache tags are identical.
*/
protected function verifyPageCacheContainsTags(Url $url, $hit_or_miss, $tags = FALSE) {
$this->drupalGet($url);
$this->assertSession()->responseHeaderEquals('X-Drupal-Cache', $hit_or_miss);
if ($hit_or_miss === 'HIT' && is_array($tags)) {
$cache_tags = explode(' ', $this->getSession()->getResponseHeader('X-Drupal-Cache-Tags'));
$tags = array_unique($tags);
$this->assertEmpty(array_diff($tags, $cache_tags), 'Page cache tags contains all expected tags.');
}
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\BrowserTestBase;
/**
* Tests Layout Builder forms.
*
* @group layout_builder
*/
class LayoutBuilderFormModeTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'field',
'entity_test',
'layout_builder',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Set up a field with a validation constraint.
$field_storage = FieldStorageConfig::create([
'field_name' => 'foo',
'entity_type' => 'entity_test',
'type' => 'string',
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
// Expecting required value.
'required' => TRUE,
])->save();
// Enable layout builder custom layouts.
LayoutBuilderEntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
'status' => TRUE,
])
->enable()
->enableLayoutBuilder()
->setOverridable()
->save();
// Add the form mode and show the field with a constraint.
EntityFormMode::create([
'id' => 'entity_test.layout_builder',
'label' => 'Layout Builder',
'targetEntityType' => 'entity_test',
])->save();
EntityFormDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'layout_builder',
'status' => TRUE,
])
->setComponent('foo', [
'type' => 'string_textfield',
])
->save();
$this->drupalLogin($this->drupalCreateUser([
'view test entity',
'configure any layout',
'configure all entity_test entity_test layout overrides',
]));
EntityTest::create()->setName($this->randomMachineName())->save();
}
/**
* Tests that the 'Discard changes' button skips validation and ignores input.
*/
public function testDiscardValidation(): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
// When submitting the form normally, a validation error should be shown.
$this->drupalGet('entity_test/1/layout');
$assert_session->fieldExists('foo[0][value]');
$assert_session->elementAttributeContains('named', ['field', 'foo[0][value]'], 'required', 'required');
$page->pressButton('Save layout');
$assert_session->pageTextContains('foo field is required.');
// When Discarding changes, a validation error will not be shown.
// Reload the form for fresh state.
$this->drupalGet('entity_test/1/layout');
$page->pressButton('Discard changes');
$assert_session->pageTextNotContains('foo field is required.');
$assert_session->addressEquals('entity_test/1/layout/discard-changes');
// Submit the form to ensure no invalid form state retained.
$page->pressButton('Confirm');
$assert_session->pageTextContains('The changes to the layout have been discarded.');
}
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\BrowserTestBase;
/**
* Tests Layout Builder local tasks.
*
* @group layout_builder
*/
class LayoutBuilderLocalTaskTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_builder',
'block',
'node',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('local_tasks_block');
}
/**
* Tests the cacheability of local tasks with Layout Builder module installed.
*/
public function testLocalTaskLayoutBuilderInstalledCacheability(): void {
// Create only one bundle and do not enable layout builder on its display.
$this->drupalCreateContentType([
'type' => 'bundle_no_lb_display',
]);
LayoutBuilderEntityViewDisplay::load('node.bundle_no_lb_display.default')
->disableLayoutBuilder()
->save();
$node = $this->drupalCreateNode([
'type' => 'bundle_no_lb_display',
]);
$assert_session = $this->assertSession();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
]));
$this->drupalGet('node/' . $node->id());
$assert_session->responseHeaderEquals('X-Drupal-Cache-Max-Age', '-1 (Permanent)');
$assert_session->statusCodeEquals(200);
}
/**
* Tests the cacheability of local tasks with multiple content types.
*/
public function testLocalTaskMultipleContentTypesCacheability(): void {
// Test when there are two content types, one with a display having Layout
// Builder enabled with overrides, and another with display not having
// Layout Builder enabled.
$this->drupalCreateContentType([
'type' => 'bundle_no_lb_display',
]);
LayoutBuilderEntityViewDisplay::load('node.bundle_no_lb_display.default')
->disableLayoutBuilder()
->save();
$node_without_lb = $this->drupalCreateNode([
'type' => 'bundle_no_lb_display',
]);
$this->drupalCreateContentType([
'type' => 'bundle_with_overrides',
]);
LayoutBuilderEntityViewDisplay::load('node.bundle_with_overrides.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$node_with_overrides = $this->drupalCreateNode([
'type' => 'bundle_with_overrides',
]);
$assert_session = $this->assertSession();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
]));
$this->drupalGet('node/' . $node_without_lb->id());
$assert_session->responseHeaderEquals('X-Drupal-Cache-Max-Age', '-1 (Permanent)');
$assert_session->statusCodeEquals(200);
$this->drupalGet('node/' . $node_with_overrides->id());
$assert_session->responseHeaderEquals('X-Drupal-Cache-Max-Age', '-1 (Permanent)');
$assert_session->statusCodeEquals(200);
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\block_content\Entity\BlockContent;
use Drupal\block_content\Entity\BlockContentType;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\layout_builder\Traits\EnableLayoutBuilderTrait;
use Drupal\user\Entity\Role;
use Drupal\user\UserInterface;
/**
* Tests overrides editing uses the correct theme.
*
* Block content is used for this test as its canonical & editing routes
* are in the admin section, so we need to test that layout builder editing
* uses the front end theme.
*
* @group layout_builder
*/
class LayoutBuilderOverridesEditingThemeTest extends LayoutBuilderTestBase {
use EnableLayoutBuilderTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'test_theme';
/**
* {@inheritdoc}
*/
protected static $modules = [
'block_content',
];
/**
* Permissions to grant admin user.
*/
protected array $permissions = [
'administer blocks',
'access block library',
'administer block types',
'administer block content',
'administer block_content display',
'configure any layout',
'view the administration theme',
'edit any basic block content',
];
/**
* Admin user.
*/
protected UserInterface $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a basic block content type.
BlockContentType::create([
'id' => 'basic',
'label' => 'Basic',
'revision' => FALSE,
])->save();
$this->adminUser = $this->drupalCreateUser($this->permissions);
}
/**
* Tests editing block content with Layout Builder.
*/
public function testEditing(): void {
// Create a new role for additional permissions needed.
$role = Role::create([
'id' => 'layout_builder_tester',
'label' => 'Layout Builder Tester',
]);
// Set a different theme for the admin pages. So we can assert the theme
// in Layout Builder is not the same as the admin theme.
\Drupal::service('theme_installer')->install(['claro']);
$this->config('system.theme')->set('admin', 'claro')->save();
// Enable layout builder for the block content display.
$display = LayoutBuilderEntityViewDisplay::create([
'targetEntityType' => 'block_content',
'bundle' => 'basic',
'mode' => 'default',
'status' => TRUE,
]);
$display->save();
$this->enableLayoutBuilder($display);
$role->grantPermission('configure all basic block_content layout overrides');
$role->save();
$this->adminUser
->addRole($role->id())
->save();
$this->drupalLogin($this->adminUser);
// Create a block content and test the themes used.
$blockContent = BlockContent::create([
'info' => $this->randomMachineName(),
'type' => 'basic',
'langcode' => 'en',
]);
$blockContent->save();
// Assert the test_theme is being used for overrides.
$this->drupalGet('admin/content/block/' . $blockContent->id() . '/layout');
$this->assertSession()->statusCodeEquals(200);
// Assert the test_theme is being used.
$this->assertSession()->responseContains('test_theme/kitten.css');
// Assert the claro theme is not being used.
$this->assertSession()->elementNotExists('css', '#block-claro-content');
// Assert the default still uses the test_theme.
$this->drupalGet('admin/structure/block-content/manage/basic/display/default/layout');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->responseContains('test_theme/kitten.css');
$this->assertSession()->elementNotExists('css', '#block-claro-content');
}
}

View File

@@ -0,0 +1,270 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\node\Entity\Node;
/**
* Tests the Layout Builder UI.
*
* @group layout_builder
* @group #slow
*/
class LayoutBuilderOverridesTest extends LayoutBuilderTestBase {
/**
* Tests deleting a field in-use by an overridden layout.
*/
public function testDeleteField(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node fields',
]));
// Enable layout builder overrides.
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
// Ensure there is a layout override.
$this->drupalGet('node/1/layout');
$page->pressButton('Save layout');
// Delete one of the fields in use.
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/fields/node.bundle_with_section_field.body/delete');
$page->pressButton('Delete');
// The node should still be accessible.
$this->drupalGet('node/1');
$assert_session->statusCodeEquals(200);
$this->drupalGet('node/1/layout');
$assert_session->statusCodeEquals(200);
}
/**
* Tests Layout Builder overrides without access to edit the default layout.
*/
public function testOverridesWithoutDefaultsAccess(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser(['configure any layout']));
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$this->drupalGet('node/1');
$page->clickLink('Layout');
$assert_session->elementTextContains('css', '.layout-builder__message.layout-builder__message--overrides', 'You are editing the layout for this Bundle with section field content item.');
$assert_session->linkNotExists('Edit the template for all Bundle with section field content items instead.');
}
/**
* Tests Layout Builder overrides without Field UI installed.
*/
public function testOverridesWithoutFieldUi(): void {
$this->container->get('module_installer')->uninstall(['field_ui']);
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
]));
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$this->drupalGet('node/1');
$page->clickLink('Layout');
$assert_session->elementTextContains('css', '.layout-builder__message.layout-builder__message--overrides', 'You are editing the layout for this Bundle with section field content item.');
$assert_session->linkNotExists('Edit the template for all Bundle with section field content items instead.');
}
/**
* Tests functionality of Layout Builder for overrides.
*/
public function testOverrides(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
// From the manage display page, go to manage the layout.
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
// @todo This should not be necessary.
$this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
// Add a block with a custom label.
$this->drupalGet('node/1');
$page->clickLink('Layout');
// The layout form should not contain fields for the title of the node by
// default.
$assert_session->fieldNotExists('title[0][value]');
$assert_session->elementTextContains('css', '.layout-builder__message.layout-builder__message--overrides', 'You are editing the layout for this Bundle with section field content item. Edit the template for all Bundle with section field content items instead.');
$assert_session->linkExists('Edit the template for all Bundle with section field content items instead.');
$page->clickLink('Add block');
$page->clickLink('Powered by Drupal');
$page->fillField('settings[label]', 'This is an override');
$page->checkField('settings[label_display]');
$page->pressButton('Add block');
$page->pressButton('Save layout');
$assert_session->pageTextContains('This is an override');
// Get the UUID of the component.
$components = Node::load(1)->get('layout_builder__layout')->getSection(0)->getComponents();
$uuid = array_key_last($components);
$this->drupalGet('layout_builder/update/block/overrides/node.1/0/content/' . $uuid);
$page->uncheckField('settings[label_display]');
$page->pressButton('Update');
$assert_session->pageTextNotContains('This is an override');
$page->pressButton('Save layout');
$assert_session->pageTextNotContains('This is an override');
}
/**
* Tests a custom alter of the overrides form.
*/
public function testOverridesFormAlter(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer nodes',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
// Enable overrides.
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
$this->drupalGet('node/1');
// The status checkbox should be checked by default.
$page->clickLink('Layout');
$assert_session->checkboxChecked('status[value]');
$page->pressButton('Save layout');
$assert_session->pageTextContains('The layout override has been saved.');
// Unchecking the status checkbox will unpublish the entity.
$page->clickLink('Layout');
$page->uncheckField('status[value]');
$page->pressButton('Save layout');
$assert_session->statusCodeEquals(403);
$assert_session->pageTextContains('The layout override has been saved.');
}
/**
* Tests removing all sections from overrides and defaults.
*/
public function testRemovingAllSections(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
// Enable overrides.
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
// By default, there is one section.
$this->drupalGet('node/1');
$assert_session->elementsCount('css', '.layout', 1);
$assert_session->pageTextContains('The first node body');
$page->clickLink('Layout');
$assert_session->elementsCount('css', '.layout', 1);
$assert_session->elementsCount('css', '.layout-builder__add-block', 1);
$assert_session->elementsCount('css', '.layout-builder__add-section', 2);
// Remove the only section from the override.
$page->clickLink('Remove Section 1');
$page->pressButton('Remove');
$assert_session->elementsCount('css', '.layout', 0);
$assert_session->elementsCount('css', '.layout-builder__add-block', 0);
$assert_session->elementsCount('css', '.layout-builder__add-section', 1);
// The override is still used instead of the default, despite being empty.
$page->pressButton('Save layout');
$assert_session->elementsCount('css', '.layout', 0);
$assert_session->pageTextNotContains('The first node body');
$page->clickLink('Layout');
$assert_session->elementsCount('css', '.layout', 0);
$assert_session->elementsCount('css', '.layout-builder__add-block', 0);
$assert_session->elementsCount('css', '.layout-builder__add-section', 1);
// Add one section to the override.
$page->clickLink('Add section');
$page->clickLink('One column');
$page->pressButton('Add section');
$assert_session->elementsCount('css', '.layout', 1);
$assert_session->elementsCount('css', '.layout-builder__add-block', 1);
$assert_session->elementsCount('css', '.layout-builder__add-section', 2);
$page->pressButton('Save layout');
$assert_session->elementsCount('css', '.layout', 1);
$assert_session->pageTextNotContains('The first node body');
// By default, the default has one section.
$this->drupalGet("$field_ui_prefix/display/default/layout");
$assert_session->elementsCount('css', '.layout', 1);
$assert_session->elementsCount('css', '.layout-builder__add-block', 1);
$assert_session->elementsCount('css', '.layout-builder__add-section', 2);
// Remove the only section from the default.
$page->clickLink('Remove Section 1');
$page->pressButton('Remove');
$assert_session->elementsCount('css', '.layout', 0);
$assert_session->elementsCount('css', '.layout-builder__add-block', 0);
$assert_session->elementsCount('css', '.layout-builder__add-section', 1);
$page->pressButton('Save layout');
$page->clickLink('Manage layout');
$assert_session->elementsCount('css', '.layout', 0);
$assert_session->elementsCount('css', '.layout-builder__add-block', 0);
$assert_session->elementsCount('css', '.layout-builder__add-section', 1);
// The override is still in use.
$this->drupalGet('node/1');
$assert_session->elementsCount('css', '.layout', 1);
$assert_session->pageTextNotContains('The first node body');
$page->clickLink('Layout');
$assert_session->elementsCount('css', '.layout', 1);
$assert_session->elementsCount('css', '.layout-builder__add-block', 1);
$assert_session->elementsCount('css', '.layout-builder__add-section', 2);
// Revert the override.
$page->pressButton('Revert to defaults');
$page->pressButton('Revert');
$assert_session->elementsCount('css', '.layout', 0);
$assert_session->pageTextNotContains('The first node body');
}
}

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the ability to alter a layout builder element while preparing.
*
* @group layout_builder
*/
class LayoutBuilderPrepareLayoutTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'field_ui',
'layout_builder',
'node',
'layout_builder_element_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType(['type' => 'bundle_with_section_field']);
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'The first node title',
'body' => [
[
'value' => 'The first node body',
],
],
]);
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'The second node title',
'body' => [
[
'value' => 'The second node body',
],
],
]);
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'The third node title',
'body' => [
[
'value' => 'The third node body',
],
],
]);
}
/**
* Tests that we can alter a Layout Builder element while preparing.
*
* @see \Drupal\layout_builder_element_test\EventSubscriber\TestPrepareLayout;
*/
public function testAlterPrepareLayout(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'access content',
'administer blocks',
'configure any layout',
'administer node display',
'configure all bundle_with_section_field node layout overrides',
]));
// Add a block to the defaults.
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$page->clickLink('Manage layout');
$page->clickLink('Add block');
$page->clickLink('Powered by Drupal');
$page->fillField('settings[label]', 'Default block title');
$page->checkField('settings[label_display]');
$page->pressButton('Add block');
$page->pressButton('Save layout');
// Check the block is on the node page.
$this->drupalGet('node/1');
$assert_session->pageTextContains('Default block title');
// When we edit the layout, it gets the static blocks.
$this->drupalGet('node/1/layout');
$assert_session->pageTextContains('Test static block title');
$assert_session->pageTextNotContains('Default block title');
$assert_session->pageTextContains('Test second static block title');
// When we edit the second node, only the first event fires.
$this->drupalGet('node/2/layout');
$assert_session->pageTextContains('Test static block title');
$assert_session->pageTextNotContains('Default block title');
$assert_session->pageTextNotContains('Test second static block title');
// When we edit the third node, the default exists PLUS our static block.
$this->drupalGet('node/3/layout');
$assert_session->pageTextNotContains('Test static block title');
$assert_session->pageTextContains('Default block title');
$assert_session->pageTextContains('Test second static block title');
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the UI aspects of section storage.
*
* @group layout_builder
*/
class LayoutBuilderSectionStorageTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_builder',
'field_ui',
'node',
'layout_builder_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType(['type' => 'bundle_with_section_field']);
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'The first node title',
'body' => [
[
'value' => 'The first node body',
],
],
]);
}
/**
* Tests that section loading is delegated to plugins during rendering.
*
* @see \Drupal\layout_builder_test\Plugin\SectionStorage\TestStateBasedSectionStorage
*/
public function testRenderByContextAwarePluginDelegate(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
// No blocks exist on the node by default.
$this->drupalGet('node/1');
$assert_session->pageTextNotContains('Defaults block title');
$assert_session->pageTextNotContains('Test block title');
// Enable Layout Builder.
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
// Add a block to the defaults.
$page->clickLink('Manage layout');
$page->clickLink('Add block');
$page->clickLink('Powered by Drupal');
$page->fillField('settings[label]', 'Defaults block title');
$page->checkField('settings[label_display]');
$page->pressButton('Add block');
$page->pressButton('Save layout');
$this->drupalGet('node/1');
$assert_session->pageTextContains('Defaults block title');
$assert_session->pageTextNotContains('Test block title');
// Enable the test section storage.
$this->container->get('state')->set('layout_builder_test_state', TRUE);
$this->drupalGet('node/1');
$assert_session->pageTextNotContains('Defaults block title');
$assert_session->pageTextContains('Test block title');
// Disabling defaults does not prevent the section storage from running.
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$this->submitForm(['layout[enabled]' => FALSE], 'Save');
$this->assertSession()->pageTextNotContains('Your settings have been saved');
$page->pressButton('Confirm');
$assert_session->pageTextContains('Layout Builder has been disabled');
$this->drupalGet('node/1');
$assert_session->pageTextNotContains('Defaults block title');
$assert_session->pageTextContains('Test block title');
// Disabling the test storage restores the original output.
$this->container->get('state')->set('layout_builder_test_state', FALSE);
$this->drupalGet('node/1');
$assert_session->pageTextNotContains('Defaults block title');
$assert_session->pageTextNotContains('Test block title');
}
}

View File

@@ -0,0 +1,830 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Section;
use Drupal\node\Entity\Node;
use Drupal\Tests\layout_builder\Traits\EnableLayoutBuilderTrait;
/**
* Tests the Layout Builder UI.
*
* @group layout_builder
* @group #slow
*/
class LayoutBuilderTest extends LayoutBuilderTestBase {
use EnableLayoutBuilderTrait;
/**
* Tests the Layout Builder UI for an entity type without a bundle.
*/
public function testNonBundleEntityType(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Log in as a user that can edit layout templates.
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer user display',
]));
$this->drupalGet('admin/config/people/accounts/display/default');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
$page->clickLink('Manage layout');
$assert_session->pageTextContains('You are editing the layout template for all users.');
$this->drupalGet('user');
$page->clickLink('Layout');
$assert_session->pageTextContains('You are editing the layout for this user. Edit the template for all users instead.');
// Log in as a user that cannot edit layout templates.
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
]));
$this->drupalGet('user');
$page->clickLink('Layout');
$assert_session->pageTextContains('You are editing the layout for this user.');
$assert_session->pageTextNotContains('Edit the template for all users instead.');
}
/**
* Tests that the Layout Builder preserves entity values.
*/
public function testPreserverEntityValues(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
// From the manage display page, go to manage the layout.
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
// @todo This should not be necessary.
$this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
$this->drupalGet('node/1');
$assert_session->pageTextContains('The first node body');
// Create a layout override which will store the current node in the
// tempstore.
$page->clickLink('Layout');
$page->clickLink('Add block');
$page->clickLink('Powered by Drupal');
$page->pressButton('Add block');
// Update the node to make a change that is not in the tempstore version.
$node = Node::load(1);
$node->set('body', 'updated body');
$node->save();
$page->clickLink('View');
$assert_session->pageTextNotContains('The first node body');
$assert_session->pageTextContains('updated body');
$page->clickLink('Layout');
$page->pressButton('Save layout');
// Ensure that saving the layout does not revert other field values.
$assert_session->addressEquals('node/1');
$assert_session->pageTextNotContains('The first node body');
$assert_session->pageTextContains('updated body');
}
/**
* {@inheritdoc}
*/
public function testLayoutBuilderUi(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer node fields',
]));
$this->drupalGet('node/1');
$assert_session->elementNotExists('css', '.layout-builder-block');
$assert_session->pageTextContains('The first node body');
$assert_session->pageTextNotContains('Powered by Drupal');
$assert_session->linkNotExists('Layout');
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
// From the manage display page, go to manage the layout.
$this->drupalGet("$field_ui_prefix/display/default");
$assert_session->linkNotExists('Manage layout');
$assert_session->fieldDisabled('layout[allow_custom]');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$assert_session->linkExists('Manage layout');
$this->clickLink('Manage layout');
$assert_session->addressEquals("$field_ui_prefix/display/default/layout");
$assert_session->elementTextContains('css', '.layout-builder__message.layout-builder__message--defaults', 'You are editing the layout template for all Bundle with section field content items.');
// The body field is only present once.
$assert_session->elementsCount('css', '.field--name-body', 1);
// The extra field is only present once.
$assert_session->pageTextContainsOnce('Placeholder for the "Extra label" field');
// Blocks have layout builder specific block class.
$assert_session->elementExists('css', '.layout-builder-block');
// Save the defaults.
$page->pressButton('Save layout');
$assert_session->addressEquals("$field_ui_prefix/display/default");
// Load the default layouts again after saving to confirm fields are only
// added on new layouts.
$this->drupalGet("$field_ui_prefix/display/default");
$assert_session->linkExists('Manage layout');
$this->clickLink('Manage layout');
$assert_session->addressEquals("$field_ui_prefix/display/default/layout");
// The body field is only present once.
$assert_session->elementsCount('css', '.field--name-body', 1);
// The extra field is only present once.
$assert_session->pageTextContainsOnce('Placeholder for the "Extra label" field');
// Add a new block.
$assert_session->linkExists('Add block');
$this->clickLink('Add block');
$assert_session->linkExists('Powered by Drupal');
$this->clickLink('Powered by Drupal');
$page->fillField('settings[label]', 'This is the label');
$page->checkField('settings[label_display]');
$page->pressButton('Add block');
$assert_session->pageTextContains('Powered by Drupal');
$assert_session->pageTextContains('This is the label');
$assert_session->addressEquals("$field_ui_prefix/display/default/layout");
// Save the defaults.
$page->pressButton('Save layout');
$assert_session->pageTextContains('The layout has been saved.');
$assert_session->addressEquals("$field_ui_prefix/display/default");
// The node uses the defaults, no overrides available.
$this->drupalGet('node/1');
$assert_session->pageTextContains('The first node body');
$assert_session->pageTextContains('Powered by Drupal');
$assert_session->pageTextContains('Extra, Extra read all about it.');
$assert_session->pageTextNotContains('Placeholder for the "Extra label" field');
$assert_session->linkNotExists('Layout');
$assert_session->pageTextContains(sprintf('Yes, I can access the %s', Node::load(1)->label()));
// Enable overrides.
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
$this->drupalGet('node/1');
// Remove the section from the defaults.
$assert_session->linkExists('Layout');
$this->clickLink('Layout');
$assert_session->pageTextContains('Placeholder for the "Extra label" field');
$assert_session->linkExists('Remove Section 1');
$this->clickLink('Remove Section 1');
$page->pressButton('Remove');
// Add a new section.
$this->clickLink('Add section');
$this->assertCorrectLayouts();
$assert_session->linkExists('Two column');
$this->clickLink('Two column');
$assert_session->buttonExists('Add section');
$page->pressButton('Add section');
$page->pressButton('Save');
$assert_session->pageTextNotContains('The first node body');
$assert_session->pageTextNotContains('Powered by Drupal');
$assert_session->pageTextNotContains('Extra, Extra read all about it.');
$assert_session->pageTextNotContains('Placeholder for the "Extra label" field');
$assert_session->pageTextContains(sprintf('Yes, I can access the entity %s in two column', Node::load(1)->label()));
// Assert that overrides cannot be turned off while overrides exist.
$this->drupalGet("$field_ui_prefix/display/default");
$assert_session->checkboxChecked('layout[allow_custom]');
$assert_session->fieldDisabled('layout[allow_custom]');
// Alter the defaults.
$this->drupalGet("$field_ui_prefix/display/default/layout");
$assert_session->linkExists('Add block');
$this->clickLink('Add block');
$assert_session->linkExists('Title');
$this->clickLink('Title');
$page->pressButton('Add block');
// The title field is present.
$assert_session->elementExists('css', '.field--name-title');
$page->pressButton('Save layout');
// View the other node, which is still using the defaults.
$this->drupalGet('node/2');
$assert_session->pageTextContains('The second node title');
$assert_session->pageTextContains('The second node body');
$assert_session->pageTextContains('Powered by Drupal');
$assert_session->pageTextContains('Extra, Extra read all about it.');
$assert_session->pageTextNotContains('Placeholder for the "Extra label" field');
$assert_session->pageTextContains(sprintf('Yes, I can access the %s', Node::load(2)->label()));
// The overridden node does not pick up the changes to defaults.
$this->drupalGet('node/1');
$assert_session->elementNotExists('css', '.field--name-title');
$assert_session->pageTextNotContains('The first node body');
$assert_session->pageTextNotContains('Powered by Drupal');
$assert_session->pageTextNotContains('Extra, Extra read all about it.');
$assert_session->pageTextNotContains('Placeholder for the "Extra label" field');
$assert_session->linkExists('Layout');
// Reverting the override returns it to the defaults.
$this->clickLink('Layout');
$assert_session->linkExists('Add block');
$this->clickLink('Add block');
$assert_session->linkExists('ID');
$this->clickLink('ID');
$page->pressButton('Add block');
// The title field is present.
$assert_session->elementExists('css', '.field--name-nid');
$assert_session->pageTextContains('ID');
$assert_session->pageTextContains('1');
$page->pressButton('Revert to defaults');
$page->pressButton('Revert');
$assert_session->addressEquals('node/1');
$assert_session->pageTextContains('The layout has been reverted back to defaults.');
$assert_session->elementExists('css', '.field--name-title');
$assert_session->elementNotExists('css', '.field--name-nid');
$assert_session->pageTextContains('The first node body');
$assert_session->pageTextContains('Powered by Drupal');
$assert_session->pageTextContains('Extra, Extra read all about it.');
$assert_session->pageTextNotContains(sprintf('Yes, I can access the entity %s in two column', Node::load(1)->label()));
$assert_session->pageTextContains(sprintf('Yes, I can access the %s', Node::load(1)->label()));
// Assert that overrides can be turned off now that all overrides are gone.
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[allow_custom]' => FALSE], 'Save');
$this->drupalGet('node/1');
$assert_session->linkNotExists('Layout');
// Add a new field.
$this->fieldUIAddNewField($field_ui_prefix, 'my_text', 'My text field', 'string');
$this->drupalGet("$field_ui_prefix/display/default/layout");
$assert_session->pageTextContains('My text field');
$assert_session->elementExists('css', '.field--name-field-my-text');
// Delete the field.
$this->drupalGet("{$field_ui_prefix}/fields/node.bundle_with_section_field.field_my_text/delete");
$this->submitForm([], 'Delete');
$this->drupalGet("$field_ui_prefix/display/default/layout");
$assert_session->pageTextNotContains('My text field');
$assert_session->elementNotExists('css', '.field--name-field-my-text');
$this->clickLink('Add section');
$this->clickLink('One column');
$page->fillField('layout_settings[label]', 'My Cool Section');
$page->pressButton('Add section');
$expected_labels = [
'My Cool Section',
'Content region in My Cool Section',
'Section 2',
'Content region in Section 2',
];
$labels = [];
foreach ($page->findAll('css', '[role="group"]') as $element) {
$labels[] = $element->getAttribute('aria-label');
}
$this->assertSame($expected_labels, $labels);
}
/**
* Test decorating controller.entity_form while layout_builder is installed.
*/
public function testHtmlEntityFormControllerDecoration(): void {
$assert_session = $this->assertSession();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
// Install module that decorates controller.entity_form.
\Drupal::service('module_installer')->install(['layout_builder_decoration_test']);
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$assert_session->pageTextContains('Manage Display');
}
/**
* Tests that layout builder checks entity view access.
*/
public function testAccess(): void {
$assert_session = $this->assertSession();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
// Allow overrides for the layout.
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
$this->drupalLogin($this->drupalCreateUser(['configure any layout']));
$this->drupalGet('node/1');
$assert_session->pageTextContains('The first node body');
$assert_session->pageTextNotContains('Powered by Drupal');
$node = Node::load(1);
$node->setUnpublished();
$node->save();
$this->drupalGet('node/1');
$assert_session->pageTextNotContains('The first node body');
$assert_session->pageTextContains('Access denied');
$this->drupalGet('node/1/layout');
$assert_session->pageTextNotContains('The first node body');
$assert_session->pageTextContains('Access denied');
}
/**
* Tests that component's dependencies are respected during removal.
*/
public function testPluginDependencies(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->container->get('module_installer')->install(['menu_ui']);
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer menu',
]));
// Create a new menu.
$this->drupalGet('admin/structure/menu/add');
$page->fillField('label', 'My Menu');
$page->fillField('id', 'my-menu');
$page->pressButton('Save');
$this->drupalGet('admin/structure/menu/add');
$page->fillField('label', 'My Menu');
$page->fillField('id', 'my-other-menu');
$page->pressButton('Save');
$page->clickLink('Add link');
$page->fillField('title[0][value]', 'My link');
$page->fillField('link[0][uri]', '/');
$page->pressButton('Save');
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$assert_session->linkExists('Manage layout');
$this->clickLink('Manage layout');
$assert_session->linkExists('Add section');
$this->clickLink('Add section');
$assert_session->linkExists('Layout plugin (with dependencies)');
$this->clickLink('Layout plugin (with dependencies)');
$page->pressButton('Add section');
$assert_session->elementExists('css', '.layout--layout-test-dependencies-plugin');
$assert_session->elementExists('css', '.field--name-body');
$page->pressButton('Save layout');
$this->drupalGet('admin/structure/menu/manage/my-other-menu/delete');
$this->submitForm([], 'Delete');
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default/layout');
$assert_session->elementNotExists('css', '.layout--layout-test-dependencies-plugin');
$assert_session->elementExists('css', '.field--name-body');
// Add a menu block.
$assert_session->linkExists('Add block');
$this->clickLink('Add block');
$assert_session->linkExists('My Menu');
$this->clickLink('My Menu');
$page->pressButton('Add block');
// Add another block alongside the menu.
$assert_session->linkExists('Add block');
$this->clickLink('Add block');
$assert_session->linkExists('Powered by Drupal');
$this->clickLink('Powered by Drupal');
$page->pressButton('Add block');
// Assert that the blocks are visible, and save the layout.
$assert_session->pageTextContains('Powered by Drupal');
$assert_session->pageTextContains('My Menu');
$assert_session->elementExists('css', '.block.menu--my-menu');
$page->pressButton('Save layout');
// Delete the menu.
$this->drupalGet('admin/structure/menu/manage/my-menu/delete');
$this->submitForm([], 'Delete');
// Ensure that the menu block is gone, but that the other block remains.
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default/layout');
$assert_session->pageTextContains('Powered by Drupal');
$assert_session->pageTextNotContains('My Menu');
$assert_session->elementNotExists('css', '.block.menu--my-menu');
}
/**
* Tests preview-aware templates.
*/
public function testPreviewAwareTemplates(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$page->clickLink('Manage layout');
$page->clickLink('Add section');
$page->clickLink('1 column layout');
$page->pressButton('Add section');
$page->clickLink('Add block');
$page->clickLink('Preview-aware block');
$page->pressButton('Add block');
$assert_session->pageTextContains('This is a preview, indeed');
$page->pressButton('Save layout');
$this->drupalGet('node/1');
$assert_session->pageTextNotContains('This is a preview, indeed');
}
/**
* Tests that extra fields work before and after enabling Layout Builder.
*/
public function testExtraFields(): void {
$assert_session = $this->assertSession();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$this->drupalGet('node');
$assert_session->linkExists('Read more');
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
// Extra fields display under "Content fields".
$this->drupalGet("admin/structure/types/manage/bundle_with_section_field/display/default/layout");
$this->clickLink('Add block');
$assert_session->elementTextContains('xpath', '//details/summary[contains(text(),"Content fields")]/parent::details', 'Extra label');
$this->drupalGet('node');
$assert_session->linkExists('Read more');
// Consider an extra field hidden by default. Make sure it's not displayed.
$this->drupalGet('node/1');
$assert_session->pageTextNotContains('Extra Field 2 is hidden by default.');
// View the layout and add the extra field that is not visible by default.
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default/layout');
$assert_session->pageTextNotContains('Extra Field 2');
$page = $this->getSession()->getPage();
$page->clickLink('Add block');
$page->clickLink('Extra Field 2');
$page->pressButton('Add block');
$assert_session->pageTextContains('Extra Field 2');
$page->pressButton('Save layout');
// Confirm that the newly added extra field is visible.
$this->drupalGet('node/1');
$assert_session->pageTextContains('Extra Field 2 is hidden by default.');
}
/**
* Tests loading a pending revision in the Layout Builder UI.
*/
public function testPendingRevision(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
// Enable overrides.
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
$storage = $this->container->get('entity_type.manager')->getStorage('node');
$node = $storage->load(1);
// Create a pending revision.
$pending_revision = $storage->createRevision($node, FALSE);
$pending_revision->set('title', 'The pending title of the first node');
$pending_revision->save();
// The original node title is available when viewing the node, but the
// pending title is visible within the Layout Builder UI.
$this->drupalGet('node/1');
$assert_session->pageTextContains('The first node title');
$page->clickLink('Layout');
$assert_session->pageTextNotContains('The first node title');
$assert_session->pageTextContains('The pending title of the first node');
}
/**
* Tests that hook_form_alter() has access to the Layout Builder info.
*/
public function testFormAlter(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer node fields',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
$display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
$this->enableLayoutBuilder($display);
$this->drupalGet("$field_ui_prefix/display/default");
$page->clickLink('Manage layout');
$page->clickLink('Add block');
$page->clickLink('Powered by Drupal');
$assert_session->pageTextContains('Layout Builder Storage: node.bundle_with_section_field.default');
$assert_session->pageTextContains('Layout Builder Section: layout_onecol');
$assert_session->pageTextContains('Layout Builder Component: system_powered_by_block');
$this->drupalGet("$field_ui_prefix/display/default");
$page->clickLink('Manage layout');
$page->clickLink('Add section');
$page->clickLink('One column');
$assert_session->pageTextContains('Layout Builder Storage: node.bundle_with_section_field.default');
$assert_session->pageTextContains('Layout Builder Section: layout_onecol');
$assert_session->pageTextContains('Layout Builder Layout: layout_onecol');
}
/**
* Tests the functionality of custom section labels.
*/
public function testSectionLabels(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
$this->enableLayoutBuilder($display);
$this->drupalGet('node/1/layout');
$page->clickLink('Add section');
$page->clickLink('One column');
$page->fillField('layout_settings[label]', 'My Cool Section');
$page->pressButton('Add section');
$assert_session->pageTextContains('My Cool Section');
$page->pressButton('Save layout');
$assert_session->pageTextNotContains('My Cool Section');
}
/**
* Tests that layouts can be context-aware.
*/
public function testContextAwareLayouts(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$account = $this->drupalCreateUser([
'configure any layout',
'administer node display',
]);
$this->drupalLogin($account);
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$page->clickLink('Manage layout');
$page->clickLink('Add section');
$page->clickLink('Layout Builder Test: Context Aware');
$page->pressButton('Add section');
// See \Drupal\layout_builder_test\Plugin\Layout\TestContextAwareLayout::build().
$assert_session->elementExists('css', '.user--' . $account->getAccountName());
$page->clickLink('Configure Section 1');
$page->fillField('layout_settings[label]', 'My section');
$page->pressButton('Update');
$assert_session->linkExists('Configure My section');
$page->clickLink('Add block');
$page->clickLink('Powered by Drupal');
$page->pressButton('Add block');
$page->pressButton('Save layout');
$this->drupalGet('node/1');
// See \Drupal\layout_builder_test\Plugin\Layout\TestContextAwareLayout::build().
$assert_session->elementExists('css', '.user--' . $account->getAccountName());
}
/**
* Tests that sections can provide custom attributes.
*/
public function testCustomSectionAttributes(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$page->clickLink('Manage layout');
$page->clickLink('Add section');
$page->clickLink('Layout Builder Test Plugin');
$page->pressButton('Add section');
// See \Drupal\layout_builder_test\Plugin\Layout\LayoutBuilderTestPlugin::build().
$assert_session->elementExists('css', '.go-birds');
}
/**
* Tests the expected breadcrumbs of the Layout Builder UI.
*/
public function testBreadcrumb(): void {
$page = $this->getSession()->getPage();
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer content types',
'access administration pages',
]));
// From the manage display page, go to manage the layout.
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
$page->clickLink('Manage layout');
$breadcrumb_titles = [];
foreach ($page->findAll('css', '.breadcrumb a') as $link) {
$breadcrumb_titles[$link->getText()] = $link->getAttribute('href');
}
$base_path = base_path();
$expected = [
'Home' => $base_path,
'Administration' => $base_path . 'admin',
'Structure' => $base_path . 'admin/structure',
'Content types' => $base_path . 'admin/structure/types',
'Bundle with section field' => $base_path . 'admin/structure/types/manage/bundle_with_section_field',
'Manage display' => $base_path . 'admin/structure/types/manage/bundle_with_section_field/display/default',
'External link' => 'http://www.example.com',
];
$this->assertSame($expected, $breadcrumb_titles);
}
/**
* Tests a config-based implementation of Layout Builder.
*
* @see \Drupal\layout_builder_test\Plugin\SectionStorage\SimpleConfigSectionStorage
*/
public function testSimpleConfigBasedLayout(): void {
$assert_session = $this->assertSession();
$this->drupalLogin($this->createUser(['configure any layout']));
// Prepare an object with a pre-existing section.
$this->container->get('config.factory')->getEditable('layout_builder_test.test_simple_config.existing')
->set('sections', [(new Section('layout_twocol'))->toArray()])
// `layout_builder_test.test_simple_config.existing.sections.0.layout_settings.label`
// contains a translatable label, so a `langcode` is required.
// @see \Drupal\Core\Config\Plugin\Validation\Constraint\LangcodeRequiredIfTranslatableValuesConstraint
->set('langcode', 'en')
->save();
// The pre-existing section is found.
$this->drupalGet('layout-builder-test-simple-config/existing');
$assert_session->elementsCount('css', '.layout', 1);
$assert_session->elementsCount('css', '.layout--twocol', 1);
// No layout is selected for a new object.
$this->drupalGet('layout-builder-test-simple-config/new');
$assert_session->elementNotExists('css', '.layout');
}
/**
* Tests removing section without layout label configuration.
*/
public function testRemovingSectionWithoutLayoutLabel(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
// Enable overrides.
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
$this->drupalGet("$field_ui_prefix/display/default");
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
$this->drupalGet("$field_ui_prefix/display/default/layout");
$page->clickLink('Add section');
$assert_session->linkExists('Layout Without Label');
$page->clickLink('Layout Without Label');
$page->pressButton('Add section');
$assert_session->elementsCount('css', '.layout', 2);
$assert_session->linkExists('Remove Section 1');
$this->clickLink('Remove Section 1');
$page->pressButton('Remove');
$assert_session->statusCodeEquals(200);
$assert_session->elementsCount('css', '.layout', 1);
}
/**
* Asserts that the correct layouts are available.
*
* @internal
*/
protected function assertCorrectLayouts(): void {
$assert_session = $this->assertSession();
// Ensure the layouts provided by layout_builder are available.
$expected_layouts_hrefs = [
'layout_builder/configure/section/overrides/node.1/0/layout_onecol',
'layout_builder/configure/section/overrides/node.1/0/layout_twocol_section',
'layout_builder/configure/section/overrides/node.1/0/layout_threecol_section',
'layout_builder/configure/section/overrides/node.1/0/layout_fourcol_section',
];
foreach ($expected_layouts_hrefs as $expected_layouts_href) {
$assert_session->linkByHrefExists($expected_layouts_href);
}
// Ensure the layout_discovery module's layouts were removed.
$unexpected_layouts = [
'twocol',
'twocol_bricks',
'threecol_25_50_25',
'threecol_33_34_33',
];
foreach ($unexpected_layouts as $unexpected_layout) {
$assert_session->linkByHrefNotExists("layout_builder/add/section/overrides/node.1/0/$unexpected_layout");
$assert_session->linkByHrefNotExists("layout_builder/configure/section/overrides/node.1/0/$unexpected_layout");
}
}
/**
* Tests the Layout Builder UI with a context defined at runtime.
*/
public function testLayoutBuilderContexts(): void {
$node_url = 'node/1';
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
$this->drupalGet("$field_ui_prefix/display/default");
$this->submitForm([
'layout[enabled]' => TRUE,
], 'Save');
$this->drupalGet("$field_ui_prefix/display/default");
$this->submitForm([
'layout[allow_custom]' => TRUE,
], 'Save');
$this->drupalGet($node_url);
$assert_session->linkExists('Layout');
$this->clickLink('Layout');
$assert_session->linkExists('Add section');
// Add the testing block.
$page->clickLink('Add block');
$this->clickLink('Can I have runtime contexts');
$page->pressButton('Add block');
// Ensure the runtime context value is rendered before saving.
$assert_session->pageTextContains('for sure you can');
// Save the layout, and test that the value is rendered after save.
$page->pressButton('Save layout');
$assert_session->addressEquals($node_url);
$assert_session->pageTextContains('for sure you can');
$assert_session->elementExists('css', '.layout');
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
/**
* Tests the Layout Builder UI.
*/
class LayoutBuilderTestBase extends BrowserTestBase {
use FieldUiTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'field_ui',
'views',
'layout_builder',
'layout_builder_views_test',
'layout_test',
'block',
'block_test',
'contextual',
'node',
'layout_builder_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('local_tasks_block');
// Create two nodes.
$this->createContentType([
'type' => 'bundle_with_section_field',
'name' => 'Bundle with section field',
]);
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'The first node title',
'body' => [
[
'value' => 'The first node body',
],
],
]);
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'The second node title',
'body' => [
[
'value' => 'The second node body',
],
],
]);
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\BrowserTestBase;
/**
* Tests template suggestions.
*
* @group layout_builder
*/
class LayoutBuilderThemeSuggestionsTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_builder',
'node',
'layout_builder_theme_suggestions_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType([
'type' => 'bundle_with_section_field',
'name' => 'Bundle with section field',
]);
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'A node title',
'body' => [
[
'value' => 'This is content that the template should not render',
],
],
]);
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
]));
}
/**
* Tests alterations of the layout list via preprocess functions.
*/
public function testLayoutListSuggestion(): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$this->drupalGet('node/1/layout');
$page->clickLink('Add section');
$assert_session->pageTextContains('layout_builder_theme_suggestions_test_preprocess_item_list__layouts');
}
/**
* Tests that of view mode specific field templates are suggested.
*/
public function testFieldBlockViewModeTemplates(): void {
$assert_session = $this->assertSession();
$this->drupalGet('node/1');
// Confirm that content is displayed by layout builder.
$assert_session->elementExists('css', '.block-layout-builder');
// Text that only appears in the view mode specific template.
$assert_session->pageTextContains('I am a field template for a specific view mode!');
// The content of the body field should not be visible because it is
// displayed via a template that does not render it.
$assert_session->pageTextNotContains('This is content that the template should not render');
}
}

View File

@@ -0,0 +1,216 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\Tests\content_translation\Functional\ContentTranslationTestBase;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Url;
/**
* Tests that the Layout Builder works with translated content.
*
* @group layout_builder
*/
class LayoutBuilderTranslationTest extends ContentTranslationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_translation',
'contextual',
'entity_test',
'layout_builder',
'block',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The entity used for testing.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->doSetup();
$this->setUpViewDisplay();
$this->setUpEntities();
}
/**
* Tests that layout overrides work when created after a translation.
*/
public function testTranslationBeforeLayoutOverride(): void {
$assert_session = $this->assertSession();
$this->addEntityTranslation();
$entity_url = $this->entity->toUrl()->toString();
$language = \Drupal::languageManager()->getLanguage($this->langcodes[2]);
$translated_entity_url = $this->entity->toUrl('canonical', ['language' => $language])->toString();
$translated_layout_url = $translated_entity_url . '/layout';
$this->drupalGet($entity_url);
$assert_session->pageTextNotContains('The translated field value');
$assert_session->pageTextContains('The untranslated field value');
$assert_session->linkExists('Layout');
$this->drupalGet($translated_entity_url);
$assert_session->pageTextNotContains('The untranslated field value');
$assert_session->pageTextContains('The translated field value');
$assert_session->linkNotExists('Layout');
$this->drupalGet($translated_layout_url);
$assert_session->pageTextContains('Access denied');
$this->addLayoutOverride();
$this->drupalGet($entity_url);
$assert_session->pageTextNotContains('The translated field value');
$assert_session->pageTextContains('The untranslated field value');
$assert_session->pageTextContains('Powered by Drupal');
// Ensure that the layout change propagates to the translated entity.
$this->drupalGet($translated_entity_url);
$assert_session->pageTextNotContains('The untranslated field value');
$assert_session->pageTextContains('The translated field value');
$assert_session->pageTextContains('Powered by Drupal');
}
/**
* Tests that layout overrides work when created before a translation.
*/
public function testLayoutOverrideBeforeTranslation(): void {
$assert_session = $this->assertSession();
$entity_url = $this->entity->toUrl()->toString();
$language = \Drupal::languageManager()->getLanguage($this->langcodes[2]);
$this->addLayoutOverride();
$this->drupalGet($entity_url);
$assert_session->pageTextNotContains('The translated field value');
$assert_session->pageTextContains('The untranslated field value');
$assert_session->pageTextContains('Powered by Drupal');
$assert_session->linkExists('Layout');
$this->addEntityTranslation();
$translated_entity_url = $this->entity->toUrl('canonical', ['language' => $language])->toString();
$translated_layout_url = $translated_entity_url . '/layout';
$this->drupalGet($entity_url);
$assert_session->pageTextNotContains('The translated field value');
$assert_session->pageTextContains('The untranslated field value');
$assert_session->pageTextContains('Powered by Drupal');
$assert_session->linkExists('Layout');
$this->drupalGet($translated_entity_url);
$assert_session->pageTextNotContains('The untranslated field value');
$assert_session->pageTextContains('The translated field value');
$assert_session->pageTextContains('Powered by Drupal');
$assert_session->linkNotExists('Layout');
$this->drupalGet($translated_layout_url);
$assert_session->pageTextContains('Access denied');
}
/**
* {@inheritdoc}
*/
protected function getTranslatorPermissions() {
$permissions = parent::getTranslatorPermissions();
$permissions[] = 'view test entity translations';
$permissions[] = 'view test entity';
$permissions[] = 'configure any layout';
return $permissions;
}
/**
* Setup translated entity with layouts.
*/
protected function setUpEntities() {
$this->drupalLogin($this->administrator);
// @todo The Layout Builder UI relies on local tasks; fix in
// https://www.drupal.org/project/drupal/issues/2917777.
$this->drupalPlaceBlock('local_tasks_block');
// Create a test entity.
$id = $this->createEntity([
$this->fieldName => [['value' => 'The untranslated field value']],
'name' => 'Test entity',
], $this->langcodes[0]);
$storage = $this->container->get('entity_type.manager')
->getStorage($this->entityTypeId);
$storage->resetCache([$id]);
$this->entity = $storage->load($id);
}
/**
* Set up the View Display.
*/
protected function setUpViewDisplay() {
EntityViewDisplay::create([
'targetEntityType' => $this->entityTypeId,
'bundle' => $this->bundle,
'mode' => 'default',
'status' => TRUE,
])
->setComponent($this->fieldName, ['type' => 'string'])
->enableLayoutBuilder()
->setOverridable()
->save();
}
/**
* Adds an entity translation.
*/
protected function addEntityTranslation() {
$user = $this->loggedInUser;
$this->drupalLogin($this->translator);
$add_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_add", [
$this->entityTypeId => $this->entity->id(),
'source' => $this->langcodes[0],
'target' => $this->langcodes[2],
]);
$this->drupalGet($add_translation_url);
$this->submitForm(["{$this->fieldName}[0][value]" => 'The translated field value'], 'Save');
$this->drupalLogin($user);
}
/**
* Adds a layout override.
*/
protected function addLayoutOverride() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$entity_url = $this->entity->toUrl()->toString();
$layout_url = $entity_url . '/layout';
$this->drupalGet($layout_url);
$assert_session->pageTextNotContains('The translated field value');
$assert_session->pageTextContains('The untranslated field value');
// Adjust the layout.
$assert_session->linkExists('Add block');
$this->clickLink('Add block');
$assert_session->linkExists('Powered by Drupal');
$this->clickLink('Powered by Drupal');
$page->pressButton('Add block');
$assert_session->pageTextContains('Powered by Drupal');
$assert_session->buttonExists('Save layout');
$page->pressButton('Save layout');
}
}

View File

@@ -0,0 +1,280 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\layout_builder\Traits\EnableLayoutBuilderTrait;
/**
* Tests the Layout Builder UI with view modes.
*
* @group layout_builder
* @group #slow
*/
class LayoutBuilderViewModeTest extends LayoutBuilderTestBase {
use EnableLayoutBuilderTrait;
/**
* Tests that a non-default view mode works as expected.
*/
public function testNonDefaultViewMode(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
// Allow overrides for the layout.
$display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
$this->enableLayoutBuilder($display);
$this->drupalGet("$field_ui_prefix/display/default");
$this->clickLink('Manage layout');
// Confirm the body field only is shown once.
$assert_session->elementsCount('css', '.field--name-body', 1);
$page->pressButton('Discard changes');
$page->pressButton('Confirm');
$this->clickLink('Teaser');
// Enabling Layout Builder for the default mode does not affect the teaser.
$assert_session->addressEquals("$field_ui_prefix/display/teaser");
$assert_session->elementNotExists('css', '#layout-builder__layout');
$assert_session->checkboxNotChecked('layout[enabled]');
$this->enableLayoutBuilderFromUi('bundle_with_section_field', 'teaser', FALSE);
$assert_session->linkExists('Manage layout');
$page->clickLink('Manage layout');
// Confirm the body field only is shown once.
$assert_session->elementsCount('css', '.field--name-body', 1);
// Enable a disabled view mode.
$page->pressButton('Discard changes');
$page->pressButton('Confirm');
$assert_session->addressEquals("$field_ui_prefix/display/teaser");
$page->clickLink('Default');
$assert_session->addressEquals("$field_ui_prefix/display");
$assert_session->linkNotExists('Full content');
$page->checkField('display_modes_custom[full]');
$page->pressButton('Save');
// Enable Layout Builder for the full view mode.
$display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.full');
$this->enableLayoutBuilder($display);
$this->drupalGet("$field_ui_prefix/display/full");
$assert_session->linkExists('Manage layout');
$page->clickLink('Manage layout');
// Confirm the body field only is shown once.
$assert_session->elementsCount('css', '.field--name-body', 1);
}
/**
* Tests the interaction between full and default view modes.
*
* @see \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage::getDefaultSectionStorage()
*/
public function testLayoutBuilderUiFullViewMode(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer node fields',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
// For the purposes of this test, turn the full view mode on and off to
// prevent copying from the customized default view mode.
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['display_modes_custom[full]' => TRUE], 'Save');
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['display_modes_custom[full]' => FALSE], 'Save');
// Allow overrides for the layout.
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
// Customize the default view mode.
$this->drupalGet("$field_ui_prefix/display/default/layout");
$this->clickLink('Add block');
$this->clickLink('Powered by Drupal');
$page->fillField('settings[label]', 'This is the default view mode');
$page->checkField('settings[label_display]');
$page->pressButton('Add block');
$assert_session->pageTextContains('This is the default view mode');
$page->pressButton('Save layout');
// The default view mode is used for both the node display and layout UI.
$this->drupalGet('node/1');
$assert_session->pageTextContains('This is the default view mode');
$assert_session->pageTextNotContains('This is the full view mode');
$this->drupalGet('node/1/layout');
$assert_session->pageTextContains('This is the default view mode');
$assert_session->pageTextNotContains('This is the full view mode');
$page->pressButton('Discard changes');
$page->pressButton('Confirm');
// Enable the full view mode and customize it.
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['display_modes_custom[full]' => TRUE], 'Save');
$this->drupalGet("{$field_ui_prefix}/display/full");
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$this->drupalGet("{$field_ui_prefix}/display/full");
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
$this->drupalGet("$field_ui_prefix/display/full/layout");
$this->clickLink('Add block');
$this->clickLink('Powered by Drupal');
$page->fillField('settings[label]', 'This is the full view mode');
$page->checkField('settings[label_display]');
$page->pressButton('Add block');
$assert_session->pageTextContains('This is the full view mode');
$page->pressButton('Save layout');
// The full view mode is now used for both the node display and layout UI.
$this->drupalGet('node/1');
$assert_session->pageTextContains('This is the full view mode');
$assert_session->pageTextNotContains('This is the default view mode');
$this->drupalGet('node/1/layout');
$assert_session->pageTextContains('This is the full view mode');
$assert_session->pageTextNotContains('This is the default view mode');
$page->pressButton('Discard changes');
$page->pressButton('Confirm');
// Disable the full view mode, the default should be used again.
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['display_modes_custom[full]' => FALSE], 'Save');
$this->drupalGet('node/1');
$assert_session->pageTextContains('This is the default view mode');
$assert_session->pageTextNotContains('This is the full view mode');
$this->drupalGet('node/1/layout');
$assert_session->pageTextContains('This is the default view mode');
$assert_session->pageTextNotContains('This is the full view mode');
$page->pressButton('Discard changes');
$page->pressButton('Confirm');
// Re-enabling the full view mode restores the layout changes.
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['display_modes_custom[full]' => TRUE], 'Save');
$this->drupalGet('node/1');
$assert_session->pageTextContains('This is the full view mode');
$assert_session->pageTextNotContains('This is the default view mode');
$this->drupalGet('node/1/layout');
$assert_session->pageTextContains('This is the full view mode');
$assert_session->pageTextNotContains('This is the default view mode');
// Create an override of the full view mode.
$this->clickLink('Add block');
$this->clickLink('Powered by Drupal');
$page->fillField('settings[label]', 'This is an override of the full view mode');
$page->checkField('settings[label_display]');
$page->pressButton('Add block');
$assert_session->pageTextContains('This is an override of the full view mode');
$page->pressButton('Save layout');
$this->drupalGet('node/1');
$assert_session->pageTextContains('This is the full view mode');
$assert_session->pageTextContains('This is an override of the full view mode');
$assert_session->pageTextNotContains('This is the default view mode');
$this->drupalGet('node/1/layout');
$assert_session->pageTextContains('This is the full view mode');
$assert_session->pageTextContains('This is an override of the full view mode');
$assert_session->pageTextNotContains('This is the default view mode');
$page->pressButton('Discard changes');
$page->pressButton('Confirm');
// The override does not affect the full view mode.
$this->drupalGet("$field_ui_prefix/display/full/layout");
$assert_session->pageTextContains('This is the full view mode');
$assert_session->pageTextNotContains('This is an override of the full view mode');
$assert_session->pageTextNotContains('This is the default view mode');
// Reverting the override restores back to the full view mode.
$this->drupalGet('node/1/layout');
$page->pressButton('Revert to default');
$page->pressButton('Revert');
$assert_session->pageTextContains('This is the full view mode');
$assert_session->pageTextNotContains('This is an override of the full view mode');
$assert_session->pageTextNotContains('This is the default view mode');
$this->drupalGet('node/1/layout');
$assert_session->pageTextContains('This is the full view mode');
$assert_session->pageTextNotContains('This is an override of the full view mode');
$assert_session->pageTextNotContains('This is the default view mode');
// Recreate an override of the full view mode.
$this->clickLink('Add block');
$this->clickLink('Powered by Drupal');
$page->fillField('settings[label]', 'This is an override of the full view mode');
$page->checkField('settings[label_display]');
$page->pressButton('Add block');
$assert_session->pageTextContains('This is an override of the full view mode');
$page->pressButton('Save layout');
$assert_session->pageTextContains('This is the full view mode');
$assert_session->pageTextContains('This is an override of the full view mode');
$assert_session->pageTextNotContains('This is the default view mode');
$this->drupalGet('node/1/layout');
$assert_session->pageTextContains('This is the full view mode');
$assert_session->pageTextContains('This is an override of the full view mode');
$assert_session->pageTextNotContains('This is the default view mode');
$page->pressButton('Discard changes');
$page->pressButton('Confirm');
// Disable the full view mode.
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['display_modes_custom[full]' => FALSE], 'Save');
// The override of the full view mode is still available.
$this->drupalGet('node/1');
$assert_session->pageTextContains('This is the full view mode');
$assert_session->pageTextContains('This is an override of the full view mode');
$assert_session->pageTextNotContains('This is the default view mode');
// Reverting the override restores back to the default view mode.
$this->drupalGet('node/1/layout');
$page->pressButton('Revert to default');
$page->pressButton('Revert');
$assert_session->pageTextContains('This is the default view mode');
$assert_session->pageTextNotContains('This is the full view mode');
$this->drupalGet('node/1/layout');
$assert_session->pageTextContains('This is the default view mode');
$assert_session->pageTextNotContains('This is the full view mode');
$page->pressButton('Discard changes');
$page->pressButton('Confirm');
}
/**
* Ensures that one bundle doesn't interfere with another bundle.
*/
public function testFullViewModeMultipleBundles(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
// Create one bundle with the full view mode enabled.
$this->createContentType(['type' => 'full_bundle']);
$this->drupalGet('admin/structure/types/manage/full_bundle/display/default');
$page->checkField('display_modes_custom[full]');
$page->pressButton('Save');
// Create another bundle without the full view mode enabled.
$this->createContentType(['type' => 'default_bundle']);
$display = LayoutBuilderEntityViewDisplay::load('node.default_bundle.default');
$this->enableLayoutBuilder($display);
$this->drupalGet('admin/structure/types/manage/default_bundle/display/default');
$assert_session->checkboxChecked('layout[allow_custom]');
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\layout_builder\Traits\EnableLayoutBuilderTrait;
/**
* Tests functionality of the entity view display with regard to Layout Builder.
*
* @group layout_builder
*/
class LayoutDisplayTest extends BrowserTestBase {
use EnableLayoutBuilderTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['field_ui', 'layout_builder', 'block', 'node'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType([
'type' => 'bundle_with_section_field',
]);
$this->createNode(['type' => 'bundle_with_section_field']);
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer display modes',
], 'foobar'));
}
/**
* Tests the interaction between multiple view modes.
*/
public function testMultipleViewModes(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field/display';
// Enable Layout Builder for the default view modes, and overrides.
$display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
$this->enableLayoutBuilder($display);
$this->drupalGet('node/1');
$assert_session->pageTextNotContains('Powered by Drupal');
$this->drupalGet('node/1/layout');
$assert_session->linkExists('Add block');
$this->clickLink('Add block');
$assert_session->linkExists('Powered by Drupal');
$this->clickLink('Powered by Drupal');
$page->pressButton('Add block');
$page->pressButton('Save');
$assert_session->pageTextContains('Powered by Drupal');
// Add a new view mode.
$this->drupalGet('admin/structure/display-modes/view/add/node');
$page->fillField('label', 'New');
$page->fillField('id', 'new');
$page->pressButton('Save');
// Enable the new view mode.
$this->drupalGet("$field_ui_prefix/default");
$page->checkField('display_modes_custom[new]');
$page->pressButton('Save');
// Enable and disable Layout Builder for the new view mode.
$this->enableLayoutBuilderFromUi('bundle_with_section_field', 'new', FALSE);
$this->disableLayoutBuilderFromUi('bundle_with_section_field', 'new');
// The node using the default view mode still contains its overrides.
$this->drupalGet('node/1');
$assert_session->pageTextContains('Powered by Drupal');
}
}

View File

@@ -0,0 +1,344 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the rendering of a layout section field.
*
* @group layout_builder
*/
class LayoutSectionTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'field_ui',
'layout_builder',
'node',
'block_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType([
'type' => 'bundle_without_section_field',
]);
$this->createContentType([
'type' => 'bundle_with_section_field',
]);
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer node fields',
'administer content types',
], 'foobar'));
}
/**
* Provides test data for ::testLayoutSectionFormatter().
*/
public static function providerTestLayoutSectionFormatter() {
$data = [];
$data['block_with_global_context'] = [
[
[
'section' => new Section('layout_onecol', [], [
'baz' => new SectionComponent('baz', 'content', [
'id' => 'test_context_aware',
'context_mapping' => [
'user' => '@user.current_user_context:current_user',
],
]),
]),
],
],
[
'.layout--onecol',
'#test_context_aware--username',
],
[
'foobar',
],
'user',
'user:2',
'UNCACHEABLE',
];
$data['block_with_entity_context'] = [
[
[
'section' => new Section('layout_onecol', [], [
'baz' => new SectionComponent('baz', 'content', [
'id' => 'field_block:node:bundle_with_section_field:body',
'context_mapping' => [
'entity' => 'layout_builder.entity',
],
]),
]),
],
],
[
'.layout--onecol',
'.field--name-body',
],
[
'Body',
'The node body',
],
'',
'',
'MISS',
];
$data['single_section_single_block'] = [
[
[
'section' => new Section('layout_onecol', [], [
'baz' => new SectionComponent('baz', 'content', [
'id' => 'system_powered_by_block',
]),
]),
],
],
'.layout--onecol',
'Powered by',
'',
'',
'MISS',
];
$data['multiple_sections'] = [
[
[
'section' => new Section('layout_onecol', [], [
'baz' => new SectionComponent('baz', 'content', [
'id' => 'system_powered_by_block',
]),
]),
],
[
'section' => new Section('layout_twocol', [], [
'foo' => new SectionComponent('foo', 'first', [
'id' => 'test_block_instantiation',
'display_message' => 'foo text',
]),
'bar' => new SectionComponent('bar', 'second', [
'id' => 'test_block_instantiation',
'display_message' => 'bar text',
]),
]),
],
],
[
'.layout--onecol',
'.layout--twocol',
],
[
'Powered by',
'foo text',
'bar text',
],
'user.permissions',
'',
'MISS',
];
return $data;
}
/**
* Tests layout_section formatter output.
*
* @dataProvider providerTestLayoutSectionFormatter
*/
public function testLayoutSectionFormatter($layout_data, $expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache): void {
$node = $this->createSectionNode($layout_data);
$canonical_url = $node->toUrl('canonical');
$this->drupalGet($canonical_url);
$this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache);
$this->drupalGet($canonical_url->toString() . '/layout');
$this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, 'UNCACHEABLE');
}
/**
* Tests the access checking of the section formatter.
*/
public function testLayoutSectionFormatterAccess(): void {
$node = $this->createSectionNode([
[
'section' => new Section('layout_onecol', [], [
'baz' => new SectionComponent('baz', 'content', [
'id' => 'test_access',
]),
]),
],
]);
// Restrict access to the block.
$this->container->get('state')->set('test_block_access', FALSE);
$this->drupalGet($node->toUrl('canonical'));
$this->assertLayoutSection('.layout--onecol', NULL, '', '', 'UNCACHEABLE');
// Ensure the block was not rendered.
$this->assertSession()->pageTextNotContains('Hello test world');
// Grant access to the block, and ensure it was rendered.
$this->container->get('state')->set('test_block_access', TRUE);
$this->drupalGet($node->toUrl('canonical'));
$this->assertLayoutSection('.layout--onecol', 'Hello test world', '', '', 'UNCACHEABLE');
}
/**
* Ensures that the entity title is displayed.
*/
public function testLayoutPageTitle(): void {
$this->drupalPlaceBlock('page_title_block');
$node = $this->createSectionNode([]);
$this->drupalGet($node->toUrl('canonical')->toString() . '/layout');
$this->assertSession()->titleEquals('Edit layout for The node title | Drupal');
$this->assertEquals('Edit layout for The node title', $this->cssSelect('h1.page-title')[0]->getText());
}
/**
* Tests that no Layout link shows without a section field.
*/
public function testLayoutUrlNoSectionField(): void {
$node = $this->createNode([
'type' => 'bundle_without_section_field',
'title' => 'The node title',
'body' => [
[
'value' => 'The node body',
],
],
]);
$node->save();
$this->drupalGet($node->toUrl('canonical')->toString() . '/layout');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests that deleting a field removes it from the layout.
*/
public function testLayoutDeletingField(): void {
$assert_session = $this->assertSession();
$this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display/default/layout');
$assert_session->statusCodeEquals(200);
$assert_session->elementExists('css', '.field--name-body');
// Delete the field from both bundles.
$this->drupalGet('/admin/structure/types/manage/bundle_without_section_field/fields/node.bundle_without_section_field.body/delete');
$this->submitForm([], 'Delete');
$this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display/default/layout');
$assert_session->statusCodeEquals(200);
$assert_session->elementExists('css', '.field--name-body');
$this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/fields/node.bundle_with_section_field.body/delete');
$this->submitForm([], 'Delete');
$this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display/default/layout');
$assert_session->statusCodeEquals(200);
$assert_session->elementNotExists('css', '.field--name-body');
}
/**
* Tests that deleting a bundle removes the layout.
*/
public function testLayoutDeletingBundle(): void {
$assert_session = $this->assertSession();
$display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
$this->assertInstanceOf(LayoutBuilderEntityViewDisplay::class, $display);
$this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/delete');
$this->submitForm([], 'Delete');
$assert_session->statusCodeEquals(200);
$display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
$this->assertNull($display);
}
/**
* Asserts the output of a layout section.
*
* @param string|array $expected_selector
* A selector or list of CSS selectors to find.
* @param string|array $expected_content
* A string or list of strings to find.
* @param string $expected_cache_contexts
* A string of cache contexts to be found in the header.
* @param string $expected_cache_tags
* A string of cache tags to be found in the header.
* @param string $expected_dynamic_cache
* The expected dynamic cache header. Either 'HIT', 'MISS' or 'UNCACHEABLE'.
*
* @internal
*/
protected function assertLayoutSection($expected_selector, $expected_content, string $expected_cache_contexts = '', string $expected_cache_tags = '', string $expected_dynamic_cache = 'MISS'): void {
$assert_session = $this->assertSession();
// Find the given selector.
foreach ((array) $expected_selector as $selector) {
$element = $this->cssSelect($selector);
$this->assertNotEmpty($element);
}
// Find the given content.
foreach ((array) $expected_content as $content) {
$assert_session->pageTextContains($content);
}
if ($expected_cache_contexts) {
$assert_session->responseHeaderContains('X-Drupal-Cache-Contexts', $expected_cache_contexts);
}
if ($expected_cache_tags) {
$assert_session->responseHeaderContains('X-Drupal-Cache-Tags', $expected_cache_tags);
}
$assert_session->responseHeaderEquals('X-Drupal-Dynamic-Cache', $expected_dynamic_cache);
}
/**
* Creates a node with a section field.
*
* @param array $section_values
* An array of values for a section field.
*
* @return \Drupal\node\NodeInterface
* The node object.
*/
protected function createSectionNode(array $section_values) {
return $this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'The node title',
'body' => [
[
'value' => 'The node body',
],
],
OverridesSectionStorage::FIELD_NAME => $section_values,
]);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group layout_builder
* @group rest
*/
class LayoutBuilderEntityViewDisplayJsonAnonTest extends LayoutBuilderEntityViewDisplayResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group layout_builder
* @group rest
*/
class LayoutBuilderEntityViewDisplayJsonBasicAuthTest extends LayoutBuilderEntityViewDisplayResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group layout_builder
* @group rest
*/
class LayoutBuilderEntityViewDisplayJsonCookieTest extends LayoutBuilderEntityViewDisplayResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional\Rest;
use Drupal\FunctionalTests\Rest\EntityViewDisplayResourceTestBase;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
/**
* Provides a base class for testing LayoutBuilderEntityViewDisplay resources.
*/
abstract class LayoutBuilderEntityViewDisplayResourceTestBase extends EntityViewDisplayResourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['layout_builder'];
/**
* {@inheritdoc}
*/
protected function createEntity() {
/** @var \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay $entity */
$entity = parent::createEntity();
$entity
->enableLayoutBuilder()
->setOverridable()
->save();
$this->assertCount(1, $entity->getThirdPartySetting('layout_builder', 'sections'));
return $entity;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
$expected = parent::getExpectedNormalizedEntity();
array_unshift($expected['dependencies']['module'], 'layout_builder');
$expected['hidden'][OverridesSectionStorage::FIELD_NAME] = TRUE;
$expected['third_party_settings']['layout_builder'] = [
'enabled' => TRUE,
'allow_custom' => TRUE,
];
return $expected;
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group layout_builder
* @group rest
*/
class LayoutBuilderEntityViewDisplayXmlAnonTest extends LayoutBuilderEntityViewDisplayResourceTestBase {
use AnonResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group layout_builder
* @group rest
*/
class LayoutBuilderEntityViewDisplayXmlBasicAuthTest extends LayoutBuilderEntityViewDisplayResourceTestBase {
use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group layout_builder
* @group rest
*/
class LayoutBuilderEntityViewDisplayXmlCookieTest extends LayoutBuilderEntityViewDisplayResourceTestBase {
use CookieResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional\Rest;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Url;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\ResourceTestBase;
use GuzzleHttp\RequestOptions;
use Psr\Http\Message\ResponseInterface;
/**
* Base class for Layout Builder REST tests.
*/
abstract class LayoutRestTestBase extends ResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'layout_builder',
'serialization',
'basic_auth',
];
/**
* A test node.
*
* @var \Drupal\node\NodeInterface
*/
protected $node;
/**
* The node storage.
*
* @var \Drupal\node\NodeStorageInterface
*/
protected $nodeStorage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$assert_session = $this->assertSession();
$this->createContentType(['type' => 'bundle_with_section_field']);
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'bypass node access',
'create bundle_with_section_field content',
'edit any bundle_with_section_field content',
]));
$page = $this->getSession()->getPage();
// Create a node.
$this->node = $this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'A node at rest will stay at rest.',
]);
$this->drupalGet('node/' . $this->node->id() . '/layout');
$page->clickLink('Add block');
$page->clickLink('Powered by Drupal');
$page->fillField('settings[label]', 'This is an override');
$page->checkField('settings[label_display]');
$page->pressButton('Add block');
$page->pressButton('Save layout');
$assert_session->pageTextContains('This is an override');
$this->nodeStorage = $this->container->get('entity_type.manager')->getStorage('node');
$this->node = $this->nodeStorage->load($this->node->id());
$this->drupalLogout();
$this->setUpAuthorization('ALL');
$this->provisionResource([static::$format], ['basic_auth']);
}
/**
* {@inheritdoc}
*/
protected function request($method, Url $url, array $request_options = []) {
$request_options[RequestOptions::HEADERS] = [
'Content-Type' => static::$mimeType,
];
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions($method));
$request_options[RequestOptions::QUERY] = ['_format' => static::$format];
return parent::request($method, $url, $request_options);
}
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$permissions = array_keys($this->container->get('user.permissions')->getPermissions());
// Give the test user all permissions on the site. There should be no
// permission that gives the user access to layout sections over REST.
$this->account = $this->drupalCreateUser($permissions);
}
/**
* {@inheritdoc}
*/
protected function assertResponseWhenMissingAuthentication($method, ResponseInterface $response) {}
/**
* {@inheritdoc}
*/
protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) {}
/**
* {@inheritdoc}
*/
protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options) {}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessCacheability() {
return (new CacheableMetadata());
}
/**
* Gets the decoded contents.
*
* @param \Psr\Http\Message\ResponseInterface $response
* The response.
*
* @return array
* The decoded contents.
*/
protected function getDecodedContents(ResponseInterface $response) {
return $this->serializer->decode((string) $response->getBody(), static::$format);
}
}

View File

@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Functional\Rest;
use Drupal\Core\Url;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\node\Entity\Node;
use GuzzleHttp\RequestOptions;
/**
* Tests that override layout sections are not exposed via the REST API.
*
* @group layout_builder
* @group rest
*/
class OverrideSectionsTest extends LayoutRestTestBase {
/**
* {@inheritdoc}
*/
protected static $resourceConfigId = 'entity.node';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// @todo Figure why field definitions have to cleared in
// https://www.drupal.org/project/drupal/issues/2985882.
$this->container->get('entity_field.manager')->clearCachedFieldDefinitions();
}
/**
* Tests that the layout override field is not normalized.
*/
public function testOverrideField(): void {
$this->assertCount(1, $this->node->get(OverridesSectionStorage::FIELD_NAME));
// Make a GET request and ensure override field is not included.
$response = $this->request(
'GET',
Url::fromRoute('rest.entity.node.GET', ['node' => $this->node->id()])
);
$this->assertResourceResponse(
200,
FALSE,
$response,
[
'config:filter.format.plain_text',
'config:rest.resource.entity.node',
'http_response',
'node:1',
],
[
'languages:language_interface',
'theme',
'url.site',
'user.permissions',
],
FALSE,
'MISS'
);
$get_data = $this->getDecodedContents($response);
$this->assertSame('A node at rest will stay at rest.', $get_data['title'][0]['value']);
$this->assertArrayNotHasKey('layout_builder__layout', $get_data);
// Make a POST request without the override field.
$new_node = [
'type' => [
[
'target_id' => 'bundle_with_section_field',
],
],
'title' => [
[
'value' => 'On with the rest of the test.',
],
],
];
$response = $this->request(
'POST',
Url::fromRoute(
'rest.entity.node.POST'),
[
RequestOptions::BODY => $this->serializer->encode($new_node, static::$format),
]
);
$this->assertResourceResponse(201, FALSE, $response);
$posted_node = $this->nodeStorage->load(2);
$this->assertEquals('On with the rest of the test.', $posted_node->getTitle());
// Make a POST request with override field.
$new_node['layout_builder__layout'] = [];
$post_contents = $this->serializer->encode($new_node, static::$format);
$response = $this->request(
'POST',
Url::fromRoute(
'rest.entity.node.POST'),
[
RequestOptions::BODY => $post_contents,
]
);
$this->assertResourceErrorResponse(403, 'Access denied on creating field \'layout_builder__layout\'.', $response);
// Make a PATCH request without the override field.
$patch_data = [
'title' => [
[
'value' => 'New and improved title',
],
],
'type' => [
[
'target_id' => 'bundle_with_section_field',
],
],
];
$response = $this->request(
'PATCH',
Url::fromRoute(
'rest.entity.node.PATCH',
['node' => 1]
),
[
RequestOptions::BODY => $this->serializer->encode($patch_data, static::$format),
]
);
$this->assertResourceResponse(200, FALSE, $response);
$this->nodeStorage->resetCache([1]);
$this->node = $this->nodeStorage->load(1);
$this->assertEquals('New and improved title', $this->node->getTitle());
// Make a PATCH request with the override field.
$patch_data['title'][0]['value'] = 'This title will not save.';
$patch_data['layout_builder__layout'] = [];
$response = $this->request(
'PATCH',
Url::fromRoute(
'rest.entity.node.PATCH',
['node' => 1]
),
[
RequestOptions::BODY => $this->serializer->encode($patch_data, static::$format),
]
);
$this->assertResourceErrorResponse(403, 'Access denied on updating field \'layout_builder__layout\'.', $response);
// Ensure the title has not changed.
$this->assertEquals('New and improved title', Node::load(1)->getTitle());
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\system\Traits\OffCanvasTestTrait;
/**
* Ajax blocks tests.
*
* @group layout_builder
*/
class AjaxBlockTest extends WebDriverTestBase {
use OffCanvasTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'node',
'datetime',
'layout_builder',
'user',
'layout_builder_test',
'off_canvas_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// @todo The Layout Builder UI relies on local tasks; fix in
// https://www.drupal.org/project/drupal/issues/2917777.
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
]));
$this->createContentType(['type' => 'bundle_with_section_field']);
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
}
/**
* Tests configuring a field block for a user field.
*/
public function testAddAjaxBlock(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Start by creating a node.
$this->createNode([
'type' => 'bundle_with_section_field',
'body' => [
[
'value' => 'The node body',
],
],
]);
$this->drupalGet('node/1');
$assert_session->pageTextContains('The node body');
$assert_session->pageTextNotContains('Every word is like an unnecessary stain on silence and nothingness.');
// From the manage display page, go to manage the layout.
$this->clickLink('Layout');
// The body field is present.
$assert_session->elementExists('css', '.field--name-body');
// Add a new block.
$assert_session->linkExists('Add block');
$this->clickLink('Add block');
$this->waitForOffCanvasArea();
$assert_session->assertWaitOnAjaxRequest();
$assert_session->linkExists('TestAjax');
$this->clickLink('TestAjax');
$this->waitForOffCanvasArea();
$assert_session->assertWaitOnAjaxRequest();
// Find the radio buttons.
$name = 'settings[ajax_test]';
/** @var \Behat\Mink\Element\NodeElement[] $radios */
$radios = $this->assertSession()->fieldExists($name);
// Click them both a couple of times.
foreach ([1, 2] as $rounds) {
foreach ($radios as $radio) {
$radio->click();
$assert_session->assertWaitOnAjaxRequest();
}
}
// Then add the block.
$assert_session->waitForElementVisible('named', ['button', 'Add block'])->press();
$assert_session->assertWaitOnAjaxRequest();
$assert_session->waitForElementVisible('css', '.block-layout-builder-test-ajax');
$block_elements = $this->cssSelect('.block-layout-builder-test-ajax');
// Should be exactly one of these in there.
$this->assertCount(1, $block_elements);
$assert_session->pageTextContains('Every word is like an unnecessary stain on silence and nothingness.');
}
}

View File

@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Behat\Mink\Element\NodeElement;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
/**
* Tests the JavaScript functionality of the block add filter.
*
* @group layout_builder
* @group legacy
*/
class BlockFilterTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'node',
'datetime',
'layout_builder',
'layout_builder_expose_all_field_blocks',
'user',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
]));
$this->createContentType(['type' => 'bundle_with_section_field']);
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$this->createNode(['type' => 'bundle_with_section_field']);
}
/**
* Tests block filter.
*/
public function testBlockFilter(): void {
$assert_session = $this->assertSession();
$session = $this->getSession();
$page = $session->getPage();
// Open the block listing.
$this->drupalGet('node/1/layout');
$assert_session->linkExists('Add block');
$this->clickLink('Add block');
$assert_session->assertWaitOnAjaxRequest();
// Get all blocks, for assertions later.
$blocks = $page->findAll('css', '.js-layout-builder-categories li');
$categories = $page->findAll('css', '.js-layout-builder-category');
$filter = $assert_session->elementExists('css', '.js-layout-builder-filter');
// Set announce to ensure it is not cleared.
$init_message = 'init message';
$session->evaluateScript("Drupal.announce('$init_message')");
// Test block filter does not take effect for 1 character.
$filter->setValue('a');
$this->assertAnnounceContains($init_message);
$visible_rows = $this->filterVisibleElements($blocks);
$this->assertSameSize($blocks, $visible_rows);
// Get the Content Fields category, which will be closed before filtering.
$contentFieldsCategory = $page->find('named', ['content', 'Content fields']);
// Link that belongs to the Content Fields category, to verify collapse.
$promoteToFrontPageLink = $page->find('named', ['content', 'Promoted to front page']);
// Test that front page link is visible until Content Fields collapsed.
$this->assertTrue($promoteToFrontPageLink->isVisible());
$contentFieldsCategory->click();
$this->assertFalse($promoteToFrontPageLink->isVisible());
// Test block filter reduces the number of visible rows.
$filter->setValue('ad');
$fewer_blocks_message = ' blocks are available in the modified list';
$this->assertAnnounceContains($fewer_blocks_message);
$visible_rows = $this->filterVisibleElements($blocks);
$this->assertCount(3, $visible_rows);
$visible_categories = $this->filterVisibleElements($categories);
$this->assertCount(3, $visible_categories);
// Test Drupal.announce() message when multiple matches are present.
$expected_message = count($visible_rows) . $fewer_blocks_message;
$this->assertAnnounceContains($expected_message);
// Test 3 letter search.
$filter->setValue('adm');
$visible_rows = $this->filterVisibleElements($blocks);
$this->assertCount(2, $visible_rows);
$visible_categories = $this->filterVisibleElements($categories);
$this->assertCount(2, $visible_categories);
// Retest that blocks appear when reducing letters.
$filter->setValue('ad');
$visible_rows = $this->filterVisibleElements($blocks);
$this->assertCount(3, $visible_rows);
$visible_categories = $this->filterVisibleElements($categories);
$this->assertCount(3, $visible_categories);
// Test blocks reappear after being filtered by repeating search for "a"
$filter->setValue('a');
$this->assertAnnounceContains('All available blocks are listed.');
// Test Drupal.announce() message when only one match is present.
$filter->setValue('Powered by');
$this->assertAnnounceContains(' block is available');
$visible_rows = $this->filterVisibleElements($blocks);
$this->assertCount(1, $visible_rows);
$visible_categories = $this->filterVisibleElements($categories);
$this->assertCount(1, $visible_categories);
$this->assertAnnounceContains('1 block is available in the modified list.');
// Test Drupal.announce() message when no matches are present.
$filter->setValue('Pan-Galactic Gargle Blaster');
$visible_rows = $this->filterVisibleElements($blocks);
$this->assertCount(0, $visible_rows);
$visible_categories = $this->filterVisibleElements($categories);
$this->assertCount(0, $visible_categories);
$announce_element = $page->find('css', '#drupal-live-announce');
$page->waitFor(2, function () use ($announce_element) {
return str_starts_with($announce_element->getText(), '0 blocks are available');
});
// Test Drupal.announce() message when all blocks are listed.
$filter->setValue('');
$this->assertAnnounceContains('All available blocks are listed.');
// Confirm the Content Fields category remains collapsed after filtering.
$this->assertFalse($promoteToFrontPageLink->isVisible());
}
/**
* Removes any non-visible elements from the passed array.
*
* @param \Behat\Mink\Element\NodeElement[] $elements
* An array of node elements.
*
* @return \Behat\Mink\Element\NodeElement[]
* An array of visible node elements.
*/
protected function filterVisibleElements(array $elements) {
return array_filter($elements, function (NodeElement $element) {
return $element->isVisible();
});
}
/**
* Checks for inclusion of text in #drupal-live-announce.
*
* @param string $expected_message
* The text expected to be present in #drupal-live-announce.
*
* @internal
*/
protected function assertAnnounceContains(string $expected_message): void {
$assert_session = $this->assertSession();
$this->assertNotEmpty($assert_session->waitForElement('css', "#drupal-live-announce:contains('$expected_message')"));
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
/**
* Tests that messages appear in the off-canvas dialog with configuring blocks.
*
* @group layout_builder
*/
class BlockFormMessagesTest extends WebDriverTestBase {
use ContextualLinkClickTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_builder',
'block',
'node',
'contextual',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType(['type' => 'bundle_with_section_field']);
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$this->createNode(['type' => 'bundle_with_section_field']);
}
/**
* Tests that validation messages are shown on the block form.
*/
public function testValidationMessage(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
]));
$this->drupalGet('node/1/layout');
$page->findLink('Add block')->click();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas .block-categories'));
$page->findLink('Powered by Drupal')->click();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas [name="settings[label]"]'));
$page->findField('Title')->setValue('');
$page->findButton('Add block')->click();
$this->assertMessagesDisplayed();
$page->findField('Title')->setValue('New title');
$page->pressButton('Add block');
$block_css_locator = '#layout-builder .block-system-powered-by-block';
$this->assertNotEmpty($assert_session->waitForElementVisible('css', $block_css_locator));
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$assert_session->assertWaitOnAjaxRequest();
$this->drupalGet($this->getUrl());
$page->findButton('Save layout')->click();
$this->assertNotEmpty($assert_session->waitForElement('css', 'div:contains("The layout override has been saved")'));
// Ensure that message are displayed when configuring an existing block.
$this->drupalGet('node/1/layout');
$this->clickContextualLink($block_css_locator, 'Configure', TRUE);
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas [name="settings[label]"]'));
$page->findField('Title')->setValue('');
$page->findButton('Update')->click();
$this->assertMessagesDisplayed();
}
/**
* Asserts that the validation messages are shown correctly.
*
* @internal
*/
protected function assertMessagesDisplayed(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$messages_locator = '#drupal-off-canvas .messages--error';
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForElement('css', $messages_locator));
$assert_session->elementTextContains('css', $messages_locator, 'Title field is required.');
/** @var \Behat\Mink\Element\NodeElement[] $top_form_elements */
$top_form_elements = $page->findAll('css', '#drupal-off-canvas form > *');
// Ensure the messages are the first top level element of the form.
$this->assertStringContainsStringIgnoringCase('Title field is required.', $top_form_elements[0]->getText());
$this->assertGreaterThan(4, count($top_form_elements));
}
}

View File

@@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
use Drupal\Tests\system\Traits\OffCanvasTestTrait;
// cspell:ignore testbody
/**
* Tests toggling of content preview.
*
* @group layout_builder
*/
class ContentPreviewToggleTest extends WebDriverTestBase {
use ContextualLinkClickTrait;
use LayoutBuilderSortTrait;
use OffCanvasTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_builder',
'block',
'node',
'contextual',
'off_canvas_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType(['type' => 'bundle_for_this_particular_test']);
LayoutBuilderEntityViewDisplay::load('node.bundle_for_this_particular_test.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'access contextual links',
]));
}
/**
* Tests the content preview toggle.
*/
public function testContentPreviewToggle(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$links_field_placeholder_label = '"Links" field';
$body_field_placeholder_label = '"Body" field';
$content_preview_body_text = 'I should only be visible if content preview is enabled.';
$this->createNode([
'type' => 'bundle_for_this_particular_test',
'body' => [
[
'value' => $content_preview_body_text,
],
],
]);
// Open single item layout page.
$this->drupalGet('node/1/layout');
// Placeholder label should not be visible, preview content should be.
$assert_session->elementNotExists('css', '.layout-builder-block__content-preview-placeholder-label');
$assert_session->pageTextContains($content_preview_body_text);
// Disable content preview, confirm presence of placeholder labels.
$this->assertTrue($page->hasCheckedField('layout-builder-content-preview'));
$page->uncheckField('layout-builder-content-preview');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.layout-builder-block__content-preview-placeholder-label'));
// Confirm that block content is not on page.
$assert_session->pageTextNotContains($content_preview_body_text);
$this->assertContextualLinks();
// Check that content preview is still disabled on page reload.
$this->getSession()->reload();
$this->assertNotEmpty($assert_session->waitForElement('css', '.layout-builder-block__content-preview-placeholder-label'));
$assert_session->pageTextNotContains($content_preview_body_text);
$this->assertContextualLinks();
// Confirm repositioning blocks works with content preview disabled.
$this->assertOrderInPage([$links_field_placeholder_label, $body_field_placeholder_label]);
$region_content = '.layout__region--content';
$links_block = "[data-layout-content-preview-placeholder-label='$links_field_placeholder_label']";
$body_block = "[data-layout-content-preview-placeholder-label='$body_field_placeholder_label']";
$assert_session->elementExists('css', $links_block . " div");
$assert_session->elementExists('css', $body_block . " div");
$this->sortableAfter($links_block, $body_block, $region_content);
$assert_session->assertWaitOnAjaxRequest();
// Check that the drag-triggered rebuild did not trigger content preview.
$assert_session->pageTextNotContains($content_preview_body_text);
// Check that drag successfully repositioned blocks.
$this->assertOrderInPage([$body_field_placeholder_label, $links_field_placeholder_label]);
// Check if block position maintained after enabling content preview.
$this->assertTrue($page->hasUncheckedField('layout-builder-content-preview'));
$page->checkField('layout-builder-content-preview');
$this->assertNotEmpty($assert_session->waitForText($content_preview_body_text));
$assert_session->pageTextContains($content_preview_body_text);
$this->assertNotEmpty($assert_session->waitForText('Placeholder for the "Links" field'));
$this->assertOrderInPage([$content_preview_body_text, 'Placeholder for the "Links" field']);
}
/**
* Checks if contextual links are working properly.
*
* @internal
*/
protected function assertContextualLinks(): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$this->clickContextualLink('.block-field-blocknodebundle-for-this-particular-testbody', 'Configure');
$this->waitForOffCanvasArea();
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertNotEmpty($this->assertSession()->waitForButton('Close'));
$page->pressButton('Close');
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
}
/**
* Asserts that blocks in a given order in the page.
*
* @param string[] $items
* An ordered list of strings that should appear in the blocks.
*
* @internal
*/
protected function assertOrderInPage(array $items): void {
$session = $this->getSession();
$page = $session->getPage();
$blocks = $page->findAll('css', '[data-layout-content-preview-placeholder-label]');
// Filter will only return value if block contains expected text.
$blocks_with_expected_text = array_filter($blocks, function ($block, $key) use ($items) {
$block_text = $block->getText();
return str_contains($block_text, $items[$key]);
}, ARRAY_FILTER_USE_BOTH);
$this->assertSameSize($items, $blocks_with_expected_text);
}
}

View File

@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
// cspell:ignore blocktest
/**
* Test contextual links compatibility with the Layout Builder.
*
* @group layout_builder
*/
class ContextualLinksTest extends WebDriverTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'views',
'views_ui',
'layout_builder',
'layout_builder_views_test',
'layout_test',
'block',
'node',
'contextual',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$user = $this->drupalCreateUser([
'configure any layout',
'access contextual links',
'administer nodes',
'bypass node access',
'administer views',
'administer blocks',
]);
$user->save();
$this->drupalLogin($user);
$this->createContentType(['type' => 'bundle_with_section_field']);
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$this->createNode([
'type' => 'bundle_with_section_field',
'body' => [
[
'value' => 'The node body',
],
],
]);
}
/**
* Tests that the contextual links inside Layout Builder are removed.
*/
public function testContextualLinks(): void {
$page = $this->getSession()->getPage();
$this->drupalGet('node/1/layout');
// Add a block that includes an entity contextual link.
$this->addBlock('Test Block View: Teaser block');
// Add a block that includes a views contextual link.
$this->addBlock('Recent content');
// Ensure the contextual links are correct before the layout is saved.
$this->assertCorrectContextualLinksInUi();
// Ensure the contextual links are correct when the Layout Builder is loaded
// after being saved.
$page->hasButton('Save layout');
$page->pressButton('Save layout');
$this->drupalGet('node/1/layout');
$this->assertCorrectContextualLinksInUi();
$this->drupalGet('node/1');
$this->assertCorrectContextualLinksInNode();
}
/**
* Tests that contextual links outside the layout are removed.
*/
public function testContextualLinksOutsideLayout(): void {
$assert_session = $this->assertSession();
$this->drupalPlaceBlock('system_powered_by_block', ['id' => 'global_block']);
$this->drupalGet('node/1');
// Ensure global blocks contextual link is present when not on
// Layout Builder.
$assert_session->elementsCount('css', '[data-contextual-id*=\'block:block=global_block:\']', 1);
$this->drupalGet('node/1/layout');
$this->addBlock('Test Block View: Teaser block');
// Ensure that only the layout specific contextual links are present.
$this->assertCorrectContextualLinks();
$page = $this->getSession()->getPage();
$page->hasButton('Save layout');
$page->pressButton('Save layout');
$this->drupalGet('node/1/layout');
// Ensure the contextual links are correct when the Layout Builder is loaded
// after being saved.
$this->assertCorrectContextualLinks();
}
/**
* Adds block to the layout via Layout Builder's UI.
*
* @param string $block_name
* The block name as it appears in the Add block form.
*/
protected function addBlock($block_name) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$assert_session->linkExists('Add block');
$page->clickLink('Add block');
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', "#drupal-off-canvas a:contains('$block_name')"));
$page->clickLink($block_name);
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '[data-drupal-selector=\'edit-actions-submit\']'));
$page->pressButton('Add block');
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$assert_session->assertWaitOnAjaxRequest();
}
/**
* Asserts the contextual links are correct in Layout Builder UI.
*
* @internal
*/
protected function assertCorrectContextualLinksInUi(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-views-blocktest-block-view-block-2'));
$layout_builder_specific_contextual_links = $page->findAll('css', '[data-contextual-id*=\'layout_builder_block:\']');
$this->assertNotEmpty($layout_builder_specific_contextual_links);
// Confirms Layout Builder contextual links are the only contextual links
// inside the Layout Builder UI.
$this->assertSameSize($layout_builder_specific_contextual_links, $page->findAll('css', '#layout-builder [data-contextual-id]'));
}
/**
* Asserts the contextual links are correct on the canonical entity route.
*
* @internal
*/
protected function assertCorrectContextualLinksInNode(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '[data-contextual-id]'));
// Ensure that no Layout Builder contextual links are visible on node view.
$this->assertEmpty($page->findAll('css', '[data-contextual-id*=\'layout_builder_block:\']'));
// Ensure that the contextual links that are hidden in Layout Builder UI
// are visible on node view.
$this->assertNotEmpty($page->findAll('css', '.layout-content [data-contextual-id]'));
}
/**
* Assert the contextual links are correct.
*
* @internal
*/
protected function assertCorrectContextualLinks() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-views-blocktest-block-view-block-2'));
$assert_session->assertNoElementAfterWait('css', '[data-contextual-id*=\'node:\']');
// Ensure that the Layout Builder's own contextual links are not removed.
$this->assertCount(3, $page->findAll('css', '[data-contextual-id*=\'layout_builder_block:\']'));
// Ensure that the global block's contextual links are removed.
$assert_session->elementNotExists('css', '[data-contextual-id*=\'block:block=global_block:\']');
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
// cspell:ignore datefield
/**
* @coversDefaultClass \Drupal\layout_builder\Plugin\Block\FieldBlock
*
* @group field
* @group legacy
*/
class FieldBlockTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'datetime',
'layout_builder',
'user',
// See \Drupal\layout_builder_fieldblock_test\Plugin\Block\FieldBlock.
'layout_builder_fieldblock_test',
'layout_builder_expose_all_field_blocks',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$field_storage = FieldStorageConfig::create([
'field_name' => 'field_date',
'entity_type' => 'user',
'type' => 'datetime',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'user',
'label' => 'Date field',
]);
$field->save();
$user = $this->drupalCreateUser([
'administer blocks',
'access administration pages',
]);
$user->field_date = '1978-11-19T05:00:00';
$user->save();
$this->drupalLogin($user);
}
/**
* Tests configuring a field block for a user field.
*/
public function testUserFieldBlock(): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
// Assert that the field value is not displayed.
$this->drupalGet('admin');
$assert_session->pageTextNotContains('Sunday, November 19, 1978 - 16:00');
$this->drupalGet('admin/structure/block');
$this->clickLink('Place block');
$assert_session->assertWaitOnAjaxRequest();
// Ensure that focus is on the first focusable element on modal.
$this->assertJsCondition('document.activeElement === document.getElementsByClassName("block-filter-text")[0]');
// Ensure that fields without any formatters are not available.
$assert_session->pageTextNotContains('Password');
// Ensure that non-display-configurable fields are not available.
$assert_session->pageTextNotContains('Initial email');
$assert_session->pageTextContains('Date field');
$block_url = 'admin/structure/block/add/field_block_test%3Auser%3Auser%3Afield_date/starterkit_theme';
$assert_session->linkByHrefExists($block_url);
$this->drupalGet($block_url);
$page->fillField('region', 'content');
// Assert the default formatter configuration.
$assert_session->fieldValueEquals('settings[formatter][type]', 'datetime_default');
$assert_session->fieldValueEquals('settings[formatter][settings][format_type]', 'medium');
// Change the formatter.
$page->selectFieldOption('settings[formatter][type]', 'datetime_time_ago');
$assert_session->assertWaitOnAjaxRequest();
// Changing the formatter removes the old settings and introduces new ones.
$assert_session->fieldNotExists('settings[formatter][settings][format_type]');
$assert_session->fieldExists('settings[formatter][settings][granularity]');
$page->pressButton('Save block');
$this->assertTrue($assert_session->waitForText('The block configuration has been saved.'));
// Configure the block and change the formatter again.
$this->clickLink('Configure');
$page->selectFieldOption('settings[formatter][type]', 'datetime_default');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->fieldValueEquals('settings[formatter][settings][format_type]', 'medium');
$page->selectFieldOption('settings[formatter][settings][format_type]', 'long');
$page->pressButton('Save block');
$this->assertTrue($assert_session->waitForText('The block configuration has been saved.'));
// Assert that the field value is updated.
$this->clickLink('Configure');
$assert_session->fieldValueEquals('settings[formatter][settings][format_type]', 'long');
// Assert that the field block is configured as expected.
$expected = [
'label' => 'above',
'type' => 'datetime_default',
'settings' => [
'format_type' => 'long',
'timezone_override' => '',
],
'third_party_settings' => [],
];
$config = $this->container->get('config.factory')->get('block.block.starterkit_theme_datefield');
$this->assertEquals($expected, $config->get('settings.formatter'));
$this->assertEquals(['field.field.user.user.field_date'], $config->get('dependencies.config'));
// Assert that the block is displaying the user field.
$this->drupalGet('admin');
$assert_session->pageTextContains('Sunday, November 19, 1978 - 16:00');
}
/**
* Tests configuring a field block that uses #states.
*/
public function testStatesFieldBlock(): void {
$page = $this->getSession()->getPage();
$timestamp_field_storage = FieldStorageConfig::create([
'field_name' => 'field_timestamp',
'entity_type' => 'user',
'type' => 'timestamp',
]);
$timestamp_field_storage->save();
$timestamp_field = FieldConfig::create([
'field_storage' => $timestamp_field_storage,
'bundle' => 'user',
'label' => 'Timestamp',
]);
$timestamp_field->save();
$this->drupalGet('admin/structure/block/add/field_block_test%3Auser%3Auser%3Afield_timestamp/starterkit_theme');
$this->assertFalse($page->findField('settings[formatter][settings][custom_date_format]')->isVisible(), 'Custom date format is not visible');
$page->selectFieldOption('settings[formatter][settings][date_format]', 'custom');
$this->assertTrue($page->findField('settings[formatter][settings][custom_date_format]')->isVisible(), 'Custom date format is visible');
}
}

View File

@@ -0,0 +1,293 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\file\Entity\File;
use Drupal\file\FileInterface;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\file\Functional\FileFieldCreationTrait;
use Drupal\Tests\TestFileCreationTrait;
/**
* Test access to private files in block fields on the Layout Builder.
*
* @group layout_builder
*/
class InlineBlockPrivateFilesTest extends InlineBlockTestBase {
use FileFieldCreationTrait;
use TestFileCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'file',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Update the test node type to not create new revisions by default. This
// allows testing for cases when a new revision is made and when it isn't.
$node_type = NodeType::load('bundle_with_section_field');
$node_type->setNewRevision(FALSE);
$node_type->save();
$field_settings = [
'file_extensions' => 'txt',
'uri_scheme' => 'private',
];
$this->createFileField('field_file', 'block_content', 'basic', $field_settings);
$this->fileSystem = $this->container->get('file_system');
}
/**
* Tests access to private files added to inline blocks in the layout builder.
*/
public function testPrivateFiles(): void {
$assert_session = $this->assertSession();
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
// Log in as user you can only configure layouts and access content.
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
'access content',
'create and edit custom blocks',
]));
$this->drupalGet('node/1/layout');
// @todo Occasionally SQLite has database locks here. Waiting seems to
// resolve it. https://www.drupal.org/project/drupal/issues/3055983
$assert_session->assertWaitOnAjaxRequest();
$file = $this->createPrivateFile('drupal.txt');
$file_real_path = $this->fileSystem->realpath($file->getFileUri());
$this->assertFileExists($file_real_path);
$this->addInlineFileBlockToLayout('The file', $file);
$this->assertSaveLayout();
$this->drupalGet('node/1');
$private_href1 = $this->getFileHrefAccessibleOnNode($file);
// Remove the inline block with the private file.
$this->drupalGet('node/1/layout');
$this->removeInlineBlockFromLayout();
$this->assertSaveLayout();
$this->drupalGet('node/1');
$assert_session->pageTextNotContains($file->label());
// Try to access file directly after it has been removed. Since a new
// revision was not created for the node the inline block is not in the
// layout of a previous revision of the node.
$this->drupalGet($private_href1);
$assert_session->pageTextContains('You are not authorized to access this page');
$assert_session->pageTextNotContains($this->getFileSecret($file));
$this->assertFileExists($file_real_path);
$file2 = $this->createPrivateFile('2ndFile.txt');
$this->drupalGet('node/1/layout');
$this->addInlineFileBlockToLayout('Number2', $file2);
$this->assertSaveLayout();
$this->drupalGet('node/1');
$private_href2 = $this->getFileHrefAccessibleOnNode($file2);
$this->createNewNodeRevision(1);
$file3 = $this->createPrivateFile('3rdFile.txt');
$this->drupalGet('node/1/layout');
$this->replaceFileInBlock($file3);
$this->assertSaveLayout();
$this->drupalGet('node/1');
$private_href3 = $this->getFileHrefAccessibleOnNode($file3);
// $file2 is on a previous revision of the block which is on a previous
// revision of the node. The user does not have access to view the previous
// revision of the node.
$this->drupalGet($private_href2);
$assert_session->pageTextContains('You are not authorized to access this page');
$node = Node::load(1);
$node->setUnpublished();
$node->save();
$this->drupalGet('node/1');
$assert_session->pageTextContains('You are not authorized to access this page');
$this->drupalGet($private_href3);
$assert_session->pageTextNotContains($this->getFileSecret($file3));
$assert_session->pageTextContains('You are not authorized to access this page');
$this->drupalGet('node/2/layout');
$file4 = $this->createPrivateFile('drupal_4.txt');
$this->addInlineFileBlockToLayout('The file', $file4);
$this->assertSaveLayout();
$this->drupalGet('node/2');
$private_href4 = $this->getFileHrefAccessibleOnNode($file4);
$this->createNewNodeRevision(2);
// Remove the inline block with the private file.
// The inline block will still be attached to the previous revision of the
// node.
$this->drupalGet('node/2/layout');
$this->removeInlineBlockFromLayout();
$this->assertSaveLayout();
// Ensure that since the user cannot view the previous revision of the node
// they can not view the file which is only used on that revision.
$this->drupalGet($private_href4);
$assert_session->pageTextContains('You are not authorized to access this page');
}
/**
* Replaces the file in the block with another one.
*
* @param \Drupal\file\FileInterface $file
* The file entity.
*/
protected function replaceFileInBlock(FileInterface $file) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->clickContextualLink(static::INLINE_BLOCK_LOCATOR, 'Configure');
$assert_session->waitForElement('css', "#drupal-off-canvas input[value='Remove']");
$assert_session->assertWaitOnAjaxRequest();
$page->find('css', '#drupal-off-canvas')->pressButton('Remove');
$this->attachFileToBlockForm($file);
$page->pressButton('Update');
$this->assertDialogClosedAndTextVisible($file->label(), static::INLINE_BLOCK_LOCATOR);
}
/**
* Adds an entity block with a file.
*
* @param string $title
* The title field value.
* @param \Drupal\file\Entity\File $file
* The file entity.
*/
protected function addInlineFileBlockToLayout($title, File $file) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$page->clickLink('Add block');
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForLink('Create content block'));
$this->clickLink('Create content block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->fieldValueEquals('Title', '');
$page->findField('Title')->setValue($title);
$this->attachFileToBlockForm($file);
$page->pressButton('Add block');
$this->assertDialogClosedAndTextVisible($file->label(), static::INLINE_BLOCK_LOCATOR);
}
/**
* Creates a private file.
*
* @param string $file_name
* The file name.
*
* @return \Drupal\Core\Entity\EntityInterface|\Drupal\file\Entity\File
* The file entity.
*/
protected function createPrivateFile($file_name) {
// Create a new file entity.
$file = File::create([
'uid' => 1,
'filename' => $file_name,
'uri' => "private://$file_name",
'filemime' => 'text/plain',
]);
$file->setPermanent();
file_put_contents($file->getFileUri(), $this->getFileSecret($file));
$file->save();
return $file;
}
/**
* Returns the href of a file, asserting it is accessible on the page.
*
* @param \Drupal\file\FileInterface $file
* The file entity.
*
* @return string
* The file href.
*/
protected function getFileHrefAccessibleOnNode(FileInterface $file): string {
$page = $this->getSession()->getPage();
$this->assertSession()->linkExists($file->label());
$private_href = $page->findLink($file->label())->getAttribute('href');
$page->clickLink($file->label());
$this->assertSession()->pageTextContains($this->getFileSecret($file));
// Access file directly.
$this->drupalGet($private_href);
$this->assertSession()->pageTextContains($this->getFileSecret($file));
return $private_href;
}
/**
* Gets the text secret for a file.
*
* @param \Drupal\file\FileInterface $file
* The file entity.
*
* @return string
* The text secret.
*/
protected function getFileSecret(FileInterface $file) {
return "The secret in {$file->label()}";
}
/**
* Attaches a file to the block edit form.
*
* @param \Drupal\file\FileInterface $file
* The file to be attached.
*/
protected function attachFileToBlockForm(FileInterface $file) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->assertSession()->waitForElementVisible('named', ['field', 'files[settings_block_form_field_file_0]']);
$page->attachFileToField("files[settings_block_form_field_file_0]", $this->fileSystem->realpath($file->getFileUri()));
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForLink($file->label()));
}
/**
* Create a new revision of the node.
*
* @param int $node_id
* The node id.
*/
protected function createNewNodeRevision($node_id) {
$node = Node::load($node_id);
$node->setTitle('Update node');
$node->setNewRevision();
$node->save();
}
}

View File

@@ -0,0 +1,714 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\node\Entity\Node;
/**
* Tests that the inline block feature works correctly.
*
* @group layout_builder
* @group #slow
*/
class InlineBlockTest extends InlineBlockTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* {@inheritdoc}
*/
protected static $modules = [
'field_ui',
];
/**
* Tests adding and editing of inline blocks.
*/
public function testInlineBlocks(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
'administer node display',
'administer node fields',
'create and edit custom blocks',
]));
// Enable layout builder.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$this->clickLink('Manage layout');
$assert_session->addressEquals(static::FIELD_UI_PREFIX . '/display/default/layout');
// Add a basic block with the body field set.
$this->addInlineBlockToLayout('Block title', 'The DEFAULT block body');
$this->assertSaveLayout();
$this->drupalGet('node/1');
$assert_session->pageTextContains('The DEFAULT block body');
$this->drupalGet('node/2');
$assert_session->pageTextContains('The DEFAULT block body');
// Enable overrides.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
$this->drupalGet('node/1/layout');
// Confirm the block can be edited.
$this->drupalGet('node/1/layout');
$this->configureInlineBlock('The DEFAULT block body', 'The NEW block body!');
$this->assertSaveLayout();
$this->drupalGet('node/1');
$assert_session->pageTextContains('The NEW block body');
$assert_session->pageTextNotContains('The DEFAULT block body');
$this->drupalGet('node/2');
// Node 2 should use default layout.
$assert_session->pageTextContains('The DEFAULT block body');
$assert_session->pageTextNotContains('The NEW block body');
// Add a basic block with the body field set.
$this->drupalGet('node/1/layout');
$this->addInlineBlockToLayout('2nd Block title', 'The 2nd block body');
$this->assertSaveLayout();
$this->drupalGet('node/1');
$assert_session->pageTextContains('The NEW block body!');
$assert_session->pageTextContains('The 2nd block body');
$this->drupalGet('node/2');
// Node 2 should use default layout.
$assert_session->pageTextContains('The DEFAULT block body');
$assert_session->pageTextNotContains('The NEW block body');
$assert_session->pageTextNotContains('The 2nd block body');
// Confirm the block can be edited.
$this->drupalGet('node/1/layout');
/** @var \Behat\Mink\Element\NodeElement $inline_block_2 */
$inline_block_2 = $page->findAll('css', static::INLINE_BLOCK_LOCATOR)[1];
$uuid = $inline_block_2->getAttribute('data-layout-block-uuid');
$block_css_locator = static::INLINE_BLOCK_LOCATOR . "[data-layout-block-uuid=\"$uuid\"]";
$this->configureInlineBlock('The 2nd block body', 'The 2nd NEW block body!', $block_css_locator);
$this->assertSaveLayout();
$this->drupalGet('node/1');
$assert_session->pageTextContains('The NEW block body!');
$assert_session->pageTextContains('The 2nd NEW block body!');
$this->drupalGet('node/2');
// Node 2 should use default layout.
$assert_session->pageTextContains('The DEFAULT block body');
$assert_session->pageTextNotContains('The NEW block body!');
$assert_session->pageTextNotContains('The 2nd NEW block body!');
// The default layout entity block should be changed.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
$assert_session->pageTextContains('The DEFAULT block body');
// Confirm default layout still only has 1 entity block.
$assert_session->elementsCount('css', static::INLINE_BLOCK_LOCATOR, 1);
}
/**
* Tests adding a new entity block and then not saving the layout.
*
* @dataProvider layoutNoSaveProvider
*/
public function testNoLayoutSave($operation, $no_save_button_text, $confirm_button_text): void {
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
'administer node display',
'create and edit custom blocks',
]));
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->assertEmpty($this->blockStorage->loadMultiple(), 'No entity blocks exist');
// Enable layout builder and overrides.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
$this->submitForm([
'layout[enabled]' => TRUE,
'layout[allow_custom]' => TRUE,
], 'Save');
$this->drupalGet('node/1/layout');
$this->addInlineBlockToLayout('Block title', 'The block body');
$page->pressButton($no_save_button_text);
if ($confirm_button_text) {
$page->pressButton($confirm_button_text);
}
$this->drupalGet('node/1');
$this->assertEmpty($this->blockStorage->loadMultiple(), 'No entity blocks were created when layout changes are discarded.');
$assert_session->pageTextNotContains('The block body');
$this->drupalGet('node/1/layout');
$this->addInlineBlockToLayout('Block title', 'The block body');
$this->assertSaveLayout();
$this->drupalGet('node/1');
$assert_session->pageTextContains('The block body');
$blocks = $this->blockStorage->loadMultiple();
$this->assertCount(1, $blocks);
/** @var \Drupal\Core\Entity\ContentEntityBase $block */
$block = array_pop($blocks);
$revision_id = $block->getRevisionId();
// Confirm the block can be edited.
$this->drupalGet('node/1/layout');
$this->configureInlineBlock('The block body', 'The block updated body');
$page->pressButton($no_save_button_text);
if ($confirm_button_text) {
$page->pressButton($confirm_button_text);
}
$this->drupalGet('node/1');
$blocks = $this->blockStorage->loadMultiple();
// When reverting or discarding the update block should not be on the page.
$assert_session->pageTextNotContains('The block updated body');
if ($operation === 'discard_changes') {
// When discarding the original block body should appear.
$assert_session->pageTextContains('The block body');
$this->assertCount(1, $blocks);
$block = array_pop($blocks);
$this->assertEquals($block->getRevisionId(), $revision_id);
$this->assertEquals('The block body', $block->get('body')->getValue()[0]['value']);
}
else {
// The block should not be visible.
// Blocks are currently only deleted when the parent entity is deleted.
$assert_session->pageTextNotContains('The block body');
}
}
/**
* Provides test data for ::testNoLayoutSave().
*/
public static function layoutNoSaveProvider() {
return [
'discard_changes' => [
'discard_changes',
'Discard changes',
'Confirm',
],
'revert' => [
'revert',
'Revert to defaults',
'Revert',
],
];
}
/**
* Tests entity blocks revisioning.
*/
public function testInlineBlocksRevisioning(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
'administer node display',
'administer node fields',
'administer nodes',
'bypass node access',
'create and edit custom blocks',
]));
// Enable layout builder and overrides.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
$this->submitForm(['layout[enabled]' => TRUE, 'layout[allow_custom]' => TRUE], 'Save');
$this->drupalGet('node/1/layout');
// Add an inline block.
$this->addInlineBlockToLayout('Block title', 'The DEFAULT block body');
$this->assertSaveLayout();
$this->drupalGet('node/1');
$assert_session->pageTextContains('The DEFAULT block body');
/** @var \Drupal\node\NodeStorageInterface $node_storage */
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$original_revision_id = $node_storage->getLatestRevisionId(1);
// Create a new revision.
$this->drupalGet('node/1/edit');
$page->findField('title[0][value]')->setValue('Node updated');
$page->pressButton('Save');
$this->drupalGet('node/1');
$assert_session->pageTextContains('The DEFAULT block body');
$assert_session->linkExists('Revisions');
// Update the block.
$this->drupalGet('node/1/layout');
$this->configureInlineBlock('The DEFAULT block body', 'The NEW block body');
$this->assertSaveLayout();
$this->drupalGet('node/1');
$assert_session->pageTextContains('The NEW block body');
$assert_session->pageTextNotContains('The DEFAULT block body');
$revision_url = "node/1/revisions/$original_revision_id";
// Ensure viewing the previous revision shows the previous block revision.
$this->drupalGet("$revision_url/view");
$assert_session->pageTextContains('The DEFAULT block body');
$assert_session->pageTextNotContains('The NEW block body');
// Revert to first revision.
$revision_url = "$revision_url/revert";
$this->drupalGet($revision_url);
$page->pressButton('Revert');
$this->drupalGet('node/1');
$assert_session->pageTextContains('The DEFAULT block body');
$assert_session->pageTextNotContains('The NEW block body');
}
/**
* Tests entity blocks revisioning.
*/
public function testInlineBlocksRevisioningIntegrity(): void {
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
'administer node display',
'view all revisions',
'access content',
'create and edit custom blocks',
]));
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
$this->submitForm(['layout[enabled]' => TRUE, 'layout[allow_custom]' => TRUE], 'Save');
$block_1_locator = static::INLINE_BLOCK_LOCATOR;
$block_2_locator = sprintf('%s + %s', static::INLINE_BLOCK_LOCATOR, static::INLINE_BLOCK_LOCATOR);
// Add two blocks to the page and assert the content in each.
$this->drupalGet('node/1/layout');
$this->addInlineBlockToLayout('Block 1', 'Block 1 original');
$this->addInlineBlockToLayout('Block 2', 'Block 2 original');
$this->assertSaveLayout();
$this->assertNodeRevisionContent(3, ['Block 1 original', 'Block 2 original']);
$this->assertBlockRevisionCountByTitle('Block 1', 1);
$this->assertBlockRevisionCountByTitle('Block 2', 1);
// Update the contents of one of the blocks and assert the updated content
// appears on the next revision.
$this->drupalGet('node/1/layout');
$this->configureInlineBlock('Block 2 original', 'Block 2 updated', $block_2_locator);
$this->assertSaveLayout();
$this->assertNodeRevisionContent(4, ['Block 1 original', 'Block 2 updated']);
$this->assertBlockRevisionCountByTitle('Block 1', 1);
$this->assertBlockRevisionCountByTitle('Block 2', 2);
// Update block 1 without creating a new revision of the parent.
$this->drupalGet('node/1/layout');
$this->configureInlineBlock('Block 1 original', 'Block 1 updated', $block_1_locator);
$this->getSession()->getPage()->uncheckField('revision');
$this->getSession()->getPage()->pressButton('Save layout');
$this->assertNotEmpty($this->assertSession()->waitForElement('css', '.messages--status'));
$this->assertNodeRevisionContent(4, ['Block 1 updated', 'Block 2 updated']);
$this->assertBlockRevisionCountByTitle('Block 1', 2);
$this->assertBlockRevisionCountByTitle('Block 2', 2);
// Reassert all of the parent revisions contain the correct block content
// and the integrity of the revisions was preserved.
$this->assertNodeRevisionContent(3, ['Block 1 original', 'Block 2 original']);
}
/**
* Assert the contents of a node revision.
*
* @param int $revision_id
* The revision ID to assert.
* @param array $content
* The content items to assert on the page.
*
* @internal
*/
protected function assertNodeRevisionContent(int $revision_id, array $content): void {
$this->drupalGet("node/1/revisions/$revision_id/view");
foreach ($content as $content_item) {
$this->assertSession()->pageTextContains($content_item);
}
}
/**
* Assert the number of block content revisions by the block title.
*
* @param string $block_title
* The block title.
* @param int $expected_revision_count
* The revision count.
*
* @internal
*/
protected function assertBlockRevisionCountByTitle(string $block_title, int $expected_revision_count): void {
$actual_revision_count = $this->blockStorage->getQuery()
->accessCheck(FALSE)
->condition('info', $block_title)
->allRevisions()
->count()
->execute();
$this->assertEquals($actual_revision_count, $expected_revision_count);
}
/**
* Tests that entity blocks deleted correctly.
*/
public function testDeletion(): void {
/** @var \Drupal\Core\Cron $cron */
$cron = \Drupal::service('cron');
/** @var \Drupal\layout_builder\InlineBlockUsageInterface $usage */
$usage = \Drupal::service('inline_block.usage');
$this->drupalLogin($this->drupalCreateUser([
'administer content types',
'access contextual links',
'configure any layout',
'administer node display',
'administer node fields',
'administer nodes',
'bypass node access',
'create and edit custom blocks',
]));
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Enable layout builder.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
// Add a block to default layout.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
$this->clickLink('Manage layout');
$assert_session->addressEquals(static::FIELD_UI_PREFIX . '/display/default/layout');
$this->addInlineBlockToLayout('Block title', 'The DEFAULT block body');
$this->assertSaveLayout();
$this->assertCount(1, $this->blockStorage->loadMultiple());
$default_block_id = $this->getLatestBlockEntityId();
// Ensure the block shows up on node pages.
$this->drupalGet('node/1');
$assert_session->pageTextContains('The DEFAULT block body');
$this->drupalGet('node/2');
$assert_session->pageTextContains('The DEFAULT block body');
// Enable overrides.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
// Ensure we have 2 copies of the block in node overrides.
$this->drupalGet('node/1/layout');
$this->assertSaveLayout();
$node_1_block_id = $this->getLatestBlockEntityId();
$this->drupalGet('node/2/layout');
$this->assertSaveLayout();
$node_2_block_id = $this->getLatestBlockEntityId();
$this->assertCount(3, $this->blockStorage->loadMultiple());
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
$this->clickLink('Manage layout');
$assert_session->addressEquals(static::FIELD_UI_PREFIX . '/display/default/layout');
$this->assertNotEmpty($this->blockStorage->load($default_block_id));
$this->assertNotEmpty($usage->getUsage($default_block_id));
// Remove block from default.
$this->removeInlineBlockFromLayout();
$this->assertSaveLayout();
// Ensure the block in the default was deleted.
$this->blockStorage->resetCache([$default_block_id]);
$this->assertEmpty($this->blockStorage->load($default_block_id));
// Ensure other blocks still exist.
$this->assertCount(2, $this->blockStorage->loadMultiple());
$this->assertEmpty($usage->getUsage($default_block_id));
$this->drupalGet('node/1/layout');
$assert_session->pageTextContains('The DEFAULT block body');
$this->removeInlineBlockFromLayout();
$this->assertSaveLayout();
$cron->run();
// Ensure entity block is not deleted because it is needed in revision.
$this->assertNotEmpty($this->blockStorage->load($node_1_block_id));
$this->assertCount(2, $this->blockStorage->loadMultiple());
$this->assertNotEmpty($usage->getUsage($node_1_block_id));
// Ensure entity block is deleted when node is deleted.
$this->drupalGet('node/1/delete');
$page->pressButton('Delete');
$this->assertEmpty(Node::load(1));
$cron->run();
$this->assertEmpty($this->blockStorage->load($node_1_block_id));
$this->assertEmpty($usage->getUsage($node_1_block_id));
$this->assertCount(1, $this->blockStorage->loadMultiple());
// Add another block to the default.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
$this->clickLink('Manage layout');
$assert_session->addressEquals(static::FIELD_UI_PREFIX . '/display/default/layout');
$this->addInlineBlockToLayout('Title 2', 'Body 2');
$this->assertSaveLayout();
$cron->run();
$default_block2_id = $this->getLatestBlockEntityId();
$this->assertCount(2, $this->blockStorage->loadMultiple());
// Delete the other node so bundle can be deleted.
$this->assertNotEmpty($usage->getUsage($node_2_block_id));
$this->drupalGet('node/2/delete');
$page->pressButton('Delete');
$this->assertEmpty(Node::load(2));
$cron->run();
// Ensure entity block was deleted.
$this->assertEmpty($this->blockStorage->load($node_2_block_id));
$this->assertEmpty($usage->getUsage($node_2_block_id));
$this->assertCount(1, $this->blockStorage->loadMultiple());
// Delete the bundle which has the default layout.
$this->assertNotEmpty($usage->getUsage($default_block2_id));
$this->drupalGet(static::FIELD_UI_PREFIX . '/delete');
$page->pressButton('Delete');
$cron->run();
// Ensure the entity block in default is deleted when bundle is deleted.
$this->assertEmpty($this->blockStorage->load($default_block2_id));
$this->assertEmpty($usage->getUsage($default_block2_id));
$this->assertCount(0, $this->blockStorage->loadMultiple());
}
/**
* Tests access to the block edit form of inline blocks.
*
* This module does not provide links to these forms but in case the paths are
* accessed directly they should accessible by users with the
* 'configure any layout' permission.
*
* @see layout_builder_block_content_access()
*/
public function testAccess(): void {
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
'administer node display',
'administer node fields',
'create and edit custom blocks',
]));
$assert_session = $this->assertSession();
// Enable layout builder and overrides.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
$this->submitForm(['layout[enabled]' => TRUE, 'layout[allow_custom]' => TRUE], 'Save');
// Ensure we have 2 copies of the block in node overrides.
$this->drupalGet('node/1/layout');
$this->addInlineBlockToLayout('Block title', 'Block body');
$this->assertSaveLayout();
$node_1_block_id = $this->getLatestBlockEntityId();
$this->drupalGet("block/$node_1_block_id");
$assert_session->pageTextNotContains('You are not authorized to access this page');
$this->drupalLogout();
$this->drupalLogin($this->drupalCreateUser([
'administer nodes',
]));
$this->drupalGet("block/$node_1_block_id");
$assert_session->pageTextContains('You are not authorized to access this page');
$this->drupalLogin($this->drupalCreateUser([
'create and edit custom blocks',
]));
$this->drupalGet("block/$node_1_block_id");
$assert_session->pageTextNotContains('You are not authorized to access this page');
}
/**
* Tests the workflow for adding an inline block depending on number of types.
*
* @throws \Behat\Mink\Exception\ElementNotFoundException
* @throws \Behat\Mink\Exception\ExpectationException
*/
public function testAddWorkFlow(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$type_storage = $this->container->get('entity_type.manager')->getStorage('block_content_type');
foreach ($type_storage->loadByProperties() as $type) {
$type->delete();
}
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
'administer node display',
'administer node fields',
'create and edit custom blocks',
]));
// Enable layout builder and overrides.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
$this->submitForm(['layout[enabled]' => TRUE, 'layout[allow_custom]' => TRUE], 'Save');
$layout_default_path = 'admin/structure/types/manage/bundle_with_section_field/display/default/layout';
$this->drupalGet($layout_default_path);
// Add a basic block with the body field set.
$page->clickLink('Add block');
$assert_session->assertWaitOnAjaxRequest();
// Confirm that with no block content types the link does not appear.
$assert_session->linkNotExists('Create content block');
$this->createBlockContentType('basic', 'Basic block');
$this->drupalGet($layout_default_path);
// Add a basic block with the body field set.
$page->clickLink('Add block');
$assert_session->assertWaitOnAjaxRequest();
// Confirm with only 1 type the "Create content block" link goes directly t
// block add form.
$assert_session->linkNotExists('Basic block');
$this->clickLink('Create content block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->fieldExists('Title');
$this->createBlockContentType('advanced', 'Advanced block');
$this->drupalGet($layout_default_path);
// Add a basic block with the body field set.
$page->clickLink('Add block');
// Confirm that, when more than 1 type exists, "Create content block" shows a
// list of block types.
$assert_session->assertWaitOnAjaxRequest();
$assert_session->linkNotExists('Basic block');
$assert_session->linkNotExists('Advanced block');
$this->clickLink('Create content block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->fieldNotExists('Title');
$assert_session->linkExists('Basic block');
$assert_session->linkExists('Advanced block');
$this->clickLink('Advanced block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->fieldExists('Title');
}
/**
* Tests the 'create and edit content blocks' permission to add a new block.
*/
public function testAddInlineBlocksPermission(): void {
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$assert = function ($permissions, $expected) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser($permissions));
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
$page->clickLink('Add block');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas .block-categories'));
if ($expected) {
$assert_session->linkExists('Create content block');
}
else {
$assert_session->linkNotExists('Create content block');
}
};
$permissions = [
'configure any layout',
'administer node display',
];
$assert($permissions, FALSE);
$permissions[] = 'create and edit custom blocks';
$assert($permissions, TRUE);
}
/**
* Tests 'create and edit custom blocks' permission to edit an existing block.
*/
public function testEditInlineBlocksPermission(): void {
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
'administer node display',
'create and edit custom blocks',
]));
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
$this->addInlineBlockToLayout('The block label', 'The body value');
$assert = function ($permissions, $expected) {
$assert_session = $this->assertSession();
$this->drupalLogin($this->drupalCreateUser($permissions));
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
$this->clickContextualLink(static::INLINE_BLOCK_LOCATOR, 'Configure');
$assert_session->assertWaitOnAjaxRequest();
if ($expected) {
$assert_session->fieldExists('settings[block_form][body][0][value]');
}
else {
$assert_session->fieldNotExists('settings[block_form][body][0][value]');
}
};
$permissions = [
'access contextual links',
'configure any layout',
'administer node display',
];
$assert($permissions, FALSE);
$permissions[] = 'create and edit custom blocks';
$assert($permissions, TRUE);
}
/**
* Test editing inline blocks when the parent has been reverted.
*/
public function testInlineBlockParentRevert(): void {
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
'administer node display',
'administer node fields',
'administer nodes',
'bypass node access',
'create and edit custom blocks',
]));
$display = \Drupal::service('entity_display.repository')->getViewDisplay('node', 'bundle_with_section_field');
$display->enableLayoutBuilder()->setOverridable()->save();
$test_node = $this->createNode([
'title' => 'test node',
'type' => 'bundle_with_section_field',
]);
$this->drupalGet("node/{$test_node->id()}/layout");
$this->addInlineBlockToLayout('Example block', 'original content');
$this->assertSaveLayout();
$original_content_revision_id = Node::load($test_node->id())->getLoadedRevisionId();
$this->drupalGet("node/{$test_node->id()}/layout");
$this->configureInlineBlock('original content', 'updated content');
$this->assertSaveLayout();
$this->drupalGet("node/{$test_node->id()}/revisions/$original_content_revision_id/revert");
$this->submitForm([], 'Revert');
$this->drupalGet("node/{$test_node->id()}/layout");
$this->configureInlineBlock('original content', 'second updated content');
$this->assertSaveLayout();
$this->drupalGet($test_node->toUrl());
$this->assertSession()->pageTextContains('second updated content');
}
}

View File

@@ -0,0 +1,224 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\block_content\Entity\BlockContentType;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
/**
* Base class for testing inline blocks.
*/
abstract class InlineBlockTestBase extends WebDriverTestBase {
use ContextualLinkClickTrait;
/**
* Locator for inline blocks.
*/
const INLINE_BLOCK_LOCATOR = '.block-inline-blockbasic';
/**
* Path prefix for the field UI for the test bundle.
*/
const FIELD_UI_PREFIX = 'admin/structure/types/manage/bundle_with_section_field';
/**
* {@inheritdoc}
*/
protected static $modules = [
'block_content',
'layout_builder',
'block',
'node',
'contextual',
];
/**
* The block storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $blockStorage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('local_tasks_block');
$this->createContentType(['type' => 'bundle_with_section_field', 'new_revision' => TRUE]);
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'The node title',
'body' => [
[
'value' => 'The node body',
],
],
]);
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'The node2 title',
'body' => [
[
'value' => 'The node2 body',
],
],
]);
$this->createBlockContentType('basic', 'Basic block');
$this->blockStorage = $this->container->get('entity_type.manager')->getStorage('block_content');
}
/**
* Saves a layout and asserts the message is correct.
*/
protected function assertSaveLayout() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Reload the page to prevent random failures.
$this->drupalGet($this->getUrl());
$page->pressButton('Save layout');
$this->assertNotEmpty($assert_session->waitForElement('css', '.messages--status'));
if (stristr($this->getUrl(), 'admin/structure') === FALSE) {
$assert_session->pageTextContains('The layout override has been saved.');
}
else {
$assert_session->pageTextContains('The layout has been saved.');
}
}
/**
* Gets the latest block entity id.
*/
protected function getLatestBlockEntityId() {
$block_ids = \Drupal::entityQuery('block_content')
->accessCheck(FALSE)
->sort('id', 'DESC')
->range(0, 1)
->execute();
$block_id = array_pop($block_ids);
$this->assertNotEmpty($this->blockStorage->load($block_id));
return $block_id;
}
/**
* Removes an entity block from the layout but does not save the layout.
*/
protected function removeInlineBlockFromLayout($selector = NULL) {
$selector = $selector ?? static::INLINE_BLOCK_LOCATOR;
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$block_text = $page->find('css', $selector)->getText();
$this->assertNotEmpty($block_text);
$assert_session->pageTextContains($block_text);
$this->clickContextualLink($selector, 'Remove block');
$assert_session->waitForElement('css', "#drupal-off-canvas input[value='Remove']");
$assert_session->assertWaitOnAjaxRequest();
// Output the new HTML.
$this->htmlOutput($page->getHtml());
$page->find('css', '#drupal-off-canvas')->pressButton('Remove');
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$assert_session->assertNoElementAfterWait('css', $selector);
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextNotContains($block_text);
}
/**
* Adds an entity block to the layout.
*
* @param string $title
* The title field value.
* @param string $body
* The body field value.
*/
protected function addInlineBlockToLayout($title, $body) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$page->clickLink('Add block');
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForLink('Create content block'));
$this->clickLink('Create content block');
$assert_session->assertWaitOnAjaxRequest();
$textarea = $assert_session->waitForElement('css', '[name="settings[block_form][body][0][value]"]');
$this->assertNotEmpty($textarea);
$assert_session->fieldValueEquals('Title', '');
$page->findField('Title')->setValue($title);
$textarea->setValue($body);
$page->pressButton('Add block');
$this->assertDialogClosedAndTextVisible($body, static::INLINE_BLOCK_LOCATOR);
}
/**
* Configures an inline block in the Layout Builder.
*
* @param string $old_body
* The old body field value.
* @param string $new_body
* The new body field value.
* @param string $block_css_locator
* The CSS locator to use to select the contextual link.
*/
protected function configureInlineBlock($old_body, $new_body, $block_css_locator = NULL) {
$block_css_locator = $block_css_locator ?: static::INLINE_BLOCK_LOCATOR;
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->clickContextualLink($block_css_locator, 'Configure');
$textarea = $assert_session->waitForElementVisible('css', '[name="settings[block_form][body][0][value]"]');
$this->assertNotEmpty($textarea);
$this->assertSame($old_body, $textarea->getValue());
$textarea->setValue($new_body);
$page->pressButton('Update');
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$assert_session->assertWaitOnAjaxRequest();
$this->assertDialogClosedAndTextVisible($new_body);
}
/**
* Asserts that the dialog closes and the new text appears on the main canvas.
*
* @param string $text
* The text.
* @param string|null $css_locator
* The css locator to use inside the main canvas if any.
*/
protected function assertDialogClosedAndTextVisible($text, $css_locator = NULL) {
$assert_session = $this->assertSession();
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$assert_session->elementNotExists('css', '#drupal-off-canvas');
if ($css_locator) {
$this->assertNotEmpty($assert_session->waitForElementVisible('css', ".dialog-off-canvas-main-canvas $css_locator:contains('$text')"));
}
else {
$this->assertNotEmpty($assert_session->waitForElementVisible('css', ".dialog-off-canvas-main-canvas:contains('$text')"));
}
}
/**
* Creates a block content type.
*
* @param string $id
* The block type id.
* @param string $label
* The block type label.
*/
protected function createBlockContentType($id, $label) {
$bundle = BlockContentType::create([
'id' => $id,
'label' => $label,
'revision' => 1,
]);
$bundle->save();
block_content_add_body_field($bundle->id());
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
/**
* Field blocks tests for the override layout.
*
* @group layout_builder
*/
class ItemLayoutFieldBlockTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'layout_builder',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
]));
// We need more then one content type for this test.
$this->createContentType(['type' => 'bundle_with_layout_overrides']);
LayoutBuilderEntityViewDisplay::load('node.bundle_with_layout_overrides.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$this->createContentType(['type' => 'filler_bundle']);
}
/**
* Tests configuring a field block for a user field.
*/
public function testAddAjaxBlock(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Start by creating a node of type with layout overrides.
$node = $this->createNode([
'type' => 'bundle_with_layout_overrides',
'body' => [
[
'value' => 'The node body',
],
],
]);
$node->save();
// Open single item layout page.
$this->drupalGet('node/1/layout');
// Add a new block.
$this->clickLink('Add block');
$assert_session->assertWaitOnAjaxRequest();
// Validate that only field blocks for layout bundles are present.
$valid_links = $page->findAll('css', 'a[href$="field_block%3Anode%3Abundle_with_layout_overrides%3Abody"]');
$this->assertCount(1, $valid_links);
$invalid_links = $page->findAll('css', 'a[href$="field_block%3Anode%3Afiller_bundle%3Abody"]');
$this->assertCount(0, $invalid_links);
}
}

View File

@@ -0,0 +1,334 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Behat\Mink\Element\NodeElement;
use Drupal\block_content\Entity\BlockContent;
use Drupal\block_content\Entity\BlockContentType;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\FunctionalJavascriptTests\JSWebAssert;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
use Drupal\Tests\system\Traits\OffCanvasTestTrait;
// cspell:ignore fieldbody
/**
* Tests the Layout Builder disables interactions of rendered blocks.
*
* @group layout_builder
*/
class LayoutBuilderDisableInteractionsTest extends WebDriverTestBase {
use ContextualLinkClickTrait;
use OffCanvasTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'block_content',
'field_ui',
'filter',
'filter_test',
'layout_builder',
'node',
'search',
'contextual',
'off_canvas_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType(['type' => 'bundle_with_section_field']);
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'The first node title',
'body' => [
[
'value' => 'Node body',
],
],
]);
$bundle = BlockContentType::create([
'id' => 'basic',
'label' => 'Basic block',
'revision' => 1,
]);
$bundle->save();
block_content_add_body_field($bundle->id());
BlockContent::create([
'type' => 'basic',
'info' => 'Block with link',
'body' => [
// Create a link that should be disabled in Layout Builder preview.
'value' => '<a id="link-that-should-be-disabled" href="/search/node">Take me away</a>',
'format' => 'full_html',
],
])->save();
BlockContent::create([
'type' => 'basic',
'info' => 'Block with iframe',
'body' => [
// Add iframe that should be non-interactive in Layout Builder preview.
'value' => '<iframe id="iframe-that-should-be-disabled" width="1" height="1" src="https://www.youtube.com/embed/gODZzSOelss" frameborder="0"></iframe>',
'format' => 'full_html',
],
])->save();
}
/**
* Tests that forms and links are disabled in the Layout Builder preview.
*/
public function testFormsLinksDisabled(): void {
// Resize window due to bug in Chromedriver when clicking on overlays over
// iFrames.
// @see https://bugs.chromium.org/p/chromedriver/issues/detail?id=2758
$this->getSession()->resizeWindow(1200, 1200);
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer node fields',
'search content',
'access contextual links',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
$this->drupalGet("{$field_ui_prefix}/display");
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$assert_session->linkExists('Manage layout');
$this->clickLink('Manage layout');
// Add a block with a form, another with a link, and one with an iframe.
$this->addBlock('Search form', '#layout-builder .search-block-form');
$this->addBlock('Block with link', '#link-that-should-be-disabled');
$this->addBlock('Block with iframe', '#iframe-that-should-be-disabled');
// Ensure the links and forms are disabled using the defaults before the
// layout is saved.
$this->assertLinksFormIframeNotInteractive();
$page->pressButton('Save layout');
$this->clickLink('Manage layout');
// Ensure the links and forms are disabled using the defaults.
$this->assertLinksFormIframeNotInteractive();
// Ensure contextual links were not disabled.
$this->assertContextualLinksClickable();
$this->drupalGet("{$field_ui_prefix}/display/default");
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
$this->drupalGet('node/1/layout');
// Ensure the links and forms are also disabled in using the override.
$this->assertLinksFormIframeNotInteractive();
// Ensure contextual links were not disabled.
$this->assertContextualLinksClickable();
}
/**
* Adds a block in the Layout Builder.
*
* @param string $block_link_text
* The link text to add the block.
* @param string $rendered_locator
* The CSS locator to confirm the block was rendered.
*/
protected function addBlock($block_link_text, $rendered_locator) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Add a new block.
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#layout-builder a:contains(\'Add block\')'));
$this->clickLink('Add block');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->linkExists($block_link_text);
$this->clickLink($block_link_text);
// Wait for off-canvas dialog to reopen with block form.
$this->assertNotEmpty($assert_session->waitForElementVisible('css', ".layout-builder-add-block"));
$assert_session->assertWaitOnAjaxRequest();
$page->pressButton('Add block');
// Wait for block form to be rendered in the Layout Builder.
$this->assertNotEmpty($assert_session->waitForElement('css', $rendered_locator));
}
/**
* Checks if element is not clickable.
*
* @param \Behat\Mink\Element\NodeElement $element
* Element being checked for.
*
* @internal
*/
protected function assertElementNotClickable(NodeElement $element): void {
try {
$element->click();
$tag_name = $element->getTagName();
$this->fail(new FormattableMarkup("@tag_name was clickable when it shouldn't have been", ['@tag_name' => $tag_name]));
}
catch (\Exception $e) {
$this->assertTrue(JSWebAssert::isExceptionNotClickable($e));
}
}
/**
* Asserts that forms, links, and iframes in preview are non-interactive.
*
* @internal
*/
protected function assertLinksFormIframeNotInteractive(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->assertNotEmpty($assert_session->waitForElement('css', '.block-search'));
$searchButton = $assert_session->buttonExists('Search');
$this->assertElementNotClickable($searchButton);
$assert_session->linkExists('Take me away');
$this->assertElementNotClickable($page->findLink('Take me away'));
$iframe = $assert_session->elementExists('css', '#iframe-that-should-be-disabled');
$this->assertElementNotClickable($iframe);
}
/**
* Confirms that Layout Builder contextual links remain active.
*
* @internal
*/
protected function assertContextualLinksClickable(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalGet($this->getUrl());
$this->clickContextualLink('.block-field-blocknodebundle-with-section-fieldbody [data-contextual-id^="layout_builder_block"]', 'Configure');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.ui-dialog-titlebar [title="Close"]'));
// We explicitly wait for the off-canvas area to be fully resized before
// trying to press the Close button, instead of waiting for the Close button
// itself to become visible. This is to prevent a regularly occurring random
// test failure.
$this->waitForOffCanvasArea();
$page->pressButton('Close');
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
// Run the steps a second time after closing dialog, which reverses the
// order that behaviors.layoutBuilderDisableInteractiveElements and
// contextual link initialization occurs.
$this->clickContextualLink('.block-field-blocknodebundle-with-section-fieldbody [data-contextual-id^="layout_builder_block"]', 'Configure');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
$page->pressButton('Close');
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$this->assertContextualLinkRetainsMouseup();
}
/**
* Makes sure contextual links respond to mouseup event.
*
* Disabling interactive elements includes preventing defaults on the mouseup
* event for links. However, this should not happen with contextual links.
* This is confirmed by clicking a contextual link then moving the mouse
* pointer. If mouseup is working properly, the draggable element will not
* be moved by the pointer moving.
*
* @internal
*/
protected function assertContextualLinkRetainsMouseup(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$body_field_selector = '.block-field-blocknodebundle-with-section-fieldbody';
$body_block = $page->find('css', $body_field_selector);
$this->assertNotEmpty($body_block);
// Get the current Y position of the body block.
$body_block_top_position = $this->getElementVerticalPosition($body_field_selector, 'top');
$body_block_contextual_link_button = $body_block->find('css', '.trigger');
$this->assertNotEmpty($body_block_contextual_link_button);
// If the body block contextual link is hidden, make it visible.
if ($body_block_contextual_link_button->hasClass('visually-hidden')) {
$this->toggleContextualTriggerVisibility($body_field_selector);
}
// For the purposes of this test, the contextual link must be accessed with
// discrete steps instead of using ContextualLinkClickTrait.
$body_block->pressButton('Open configuration options');
$body_block->clickLink('Configure');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
$assert_session->assertWaitOnAjaxRequest();
// After the contextual link opens the dialog, move the mouse pointer
// elsewhere on the page. If mouse up were not working correctly this would
// actually drag the body field too.
$this->movePointerTo('#iframe-that-should-be-disabled');
$new_body_block_bottom_position = $this->getElementVerticalPosition($body_field_selector, 'bottom');
$iframe_top_position = $this->getElementVerticalPosition('#iframe-that-should-be-disabled', 'top');
$minimum_distance_mouse_moved = $iframe_top_position - $new_body_block_bottom_position;
$this->assertGreaterThan(200, $minimum_distance_mouse_moved, 'The mouse moved at least 200 pixels');
// If mouseup is working properly, the body block should be nearly in same
// position as it was when $body_block_y_position was declared. It will have
// moved slightly because the current block being configured will have a
// border that was not present when the dialog was not open.
$new_body_block_top_position = $this->getElementVerticalPosition($body_field_selector, 'top');
$distance_body_block_moved = abs($body_block_top_position - $new_body_block_top_position);
// Confirm that body moved only slightly compared to the distance the mouse
// moved and therefore was not dragged when the mouse moved.
$this->assertGreaterThan($distance_body_block_moved * 20, $minimum_distance_mouse_moved);
}
/**
* Gets the element position.
*
* @param string $css_selector
* The CSS selector of the element.
* @param string $position_type
* The position type to get, either 'top' or 'bottom'.
*
* @return int
* The element position.
*/
protected function getElementVerticalPosition($css_selector, $position_type) {
$this->assertContains($position_type, ['top', 'bottom'], 'Expected position type.');
return (int) $this->getSession()->evaluateScript("document.querySelector('$css_selector').getBoundingClientRect().$position_type + window.pageYOffset");
}
/**
* Moves mouse pointer to location of $selector.
*
* @param string $selector
* CSS selector.
*/
protected function movePointerTo($selector) {
$driver_session = $this->getSession()->getDriver()->getWebDriverSession();
$element = $driver_session->element('css selector', $selector);
$driver_session->moveto(['element' => $element->getID()]);
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests placing blocks containing forms in theLayout Builder UI.
*
* @group layout_builder
*/
class LayoutBuilderNestedFormUiTest extends WebDriverTestBase {
/**
* The form block labels used as text for links to add blocks.
*/
const FORM_BLOCK_LABELS = [
'Layout Builder form block test form api form block',
'Layout Builder form block test inline template form block',
'Test Block View: Exposed form block',
];
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'field_ui',
'node',
'layout_builder',
'layout_builder_form_block_test',
'views',
'layout_builder_views_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('local_tasks_block');
// Create a separate node to add a form block to, respectively.
// - Block with form api form will be added to first node layout.
// - Block with inline template with <form> tag added to second node layout.
// - Views block exposed form added to third node layout.
$this->createContentType([
'type' => 'bundle_with_section_field',
'name' => 'Bundle with section field',
]);
for ($i = 1; $i <= count(static::FORM_BLOCK_LABELS); $i++) {
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => "Node $i title",
]);
}
}
/**
* Tests blocks containing forms can be successfully saved editing defaults.
*/
public function testAddingFormBlocksToDefaults(): void {
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
// From the manage display page, enable Layout Builder.
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
$this->drupalGet("$field_ui_prefix/display/default");
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
// Save the entity view display so that it can be reverted to later.
/** @var \Drupal\Core\Config\StorageInterface $active_config_storage */
$active_config_storage = $this->container->get('config.storage');
$original_display_config_data = $active_config_storage->read('core.entity_view_display.node.bundle_with_section_field.default');
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_view_display_storage */
$entity_view_display_storage = $this->container->get('entity_type.manager')->getStorage('entity_view_display');
$entity_view_display = $entity_view_display_storage->load('node.bundle_with_section_field.default');
$expected_save_message = 'The layout has been saved.';
foreach (static::FORM_BLOCK_LABELS as $label) {
$this->addFormBlock($label, "$field_ui_prefix/display/default", $expected_save_message);
// Revert the entity view display back to remove the previously added form
// block.
$entity_view_display = $entity_view_display_storage
->updateFromStorageRecord($entity_view_display, $original_display_config_data);
$entity_view_display->save();
}
}
/**
* Tests blocks containing forms can be successfully saved editing overrides.
*/
public function testAddingFormBlocksToOverrides(): void {
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
// From the manage display page, enable Layout Builder.
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
$this->drupalGet("$field_ui_prefix/display/default");
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$this->submitForm(['layout[allow_custom]' => TRUE], 'Save');
$expected_save_message = 'The layout override has been saved.';
$nid = 1;
foreach (static::FORM_BLOCK_LABELS as $label) {
$this->addFormBlock($label, "node/$nid", $expected_save_message);
$nid++;
}
}
/**
* Adds a form block specified by label layout and checks it can be saved.
*
* Need to test saving and resaving, because nested forms can cause issues
* on the second save.
*
* @param string $label
* The form block label that will be used to identify link to add block.
* @param string $path
* Root path of the entity (i.e. node/{NID) or the entity view display path.
* @param string $expected_save_message
* The message that should be displayed after successful layout save.
*/
protected function addFormBlock($label, $path, $expected_save_message) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Go to edit the layout.
$this->drupalGet($path . '/layout');
// Add the form block.
$assert_session->linkExists('Add block');
$this->clickLink('Add block');
$assert_session->waitForElementVisible('named', ['link', $label]);
$assert_session->linkExists($label);
$this->clickLink($label);
$assert_session->waitForElementVisible('named', ['button', 'Add block']);
$page->pressButton('Add block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextContains($label);
$assert_session->addressEquals($path . '/layout');
// Save the defaults.
$page->pressButton('Save layout');
$assert_session->pageTextContains($expected_save_message);
$assert_session->addressEquals($path);
// Go back to edit layout and try to re-save.
$this->drupalGet($path . '/layout');
$page->pressButton('Save layout');
$assert_session->pageTextContains($expected_save_message);
$assert_session->addressEquals($path);
}
}

View File

@@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\layout_builder\Traits\EnableLayoutBuilderTrait;
/**
* Tests the ability for opting in and out of Layout Builder.
*
* @group layout_builder
*/
class LayoutBuilderOptInTest extends WebDriverTestBase {
use EnableLayoutBuilderTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'field_ui',
'block',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create one content type before installing Layout Builder and one after.
$this->createContentType(['type' => 'before']);
$this->container->get('module_installer')->install(['layout_builder']);
$this->rebuildAll();
$this->createContentType(['type' => 'after']);
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
}
/**
* Tests the interaction between the two layout checkboxes.
*/
public function testCheckboxLogic(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalGet('admin/structure/types/manage/before/display/default');
// Both fields are unchecked and allow_custom is disabled and hidden.
$assert_session->checkboxNotChecked('layout[enabled]');
$assert_session->checkboxNotChecked('layout[allow_custom]');
$assert_session->fieldDisabled('layout[allow_custom]');
$this->assertFalse($page->findField('layout[allow_custom]')->isVisible());
// Checking is_enable will show allow_custom.
$page->checkField('layout[enabled]');
$assert_session->checkboxNotChecked('layout[allow_custom]');
$this->assertTrue($page->findField('layout[allow_custom]')->isVisible());
$page->pressButton('Save');
$assert_session->checkboxChecked('layout[enabled]');
$assert_session->checkboxNotChecked('layout[allow_custom]');
// Check and submit allow_custom.
$page->checkField('layout[allow_custom]');
$page->pressButton('Save');
$assert_session->checkboxChecked('layout[enabled]');
$assert_session->checkboxChecked('layout[allow_custom]');
// Reset the checkboxes.
$this->disableLayoutBuilderFromUi('before', 'default');
$assert_session->checkboxNotChecked('layout[enabled]');
$assert_session->checkboxNotChecked('layout[allow_custom]');
// Check both at the same time.
$page->checkField('layout[enabled]');
$page->checkField('layout[allow_custom]');
$page->pressButton('Save');
$assert_session->checkboxChecked('layout[enabled]');
$assert_session->checkboxChecked('layout[allow_custom]');
}
/**
* Tests the expected default values for enabling Layout Builder.
*/
public function testDefaultValues(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Both the content type created before and after Layout Builder was
// installed is still using the Field UI.
$this->drupalGet('admin/structure/types/manage/before/display/default');
$assert_session->checkboxNotChecked('layout[enabled]');
$field_ui_prefix = 'admin/structure/types/manage/after/display/default';
$this->drupalGet($field_ui_prefix);
$assert_session->checkboxNotChecked('layout[enabled]');
$page->checkField('layout[enabled]');
$page->pressButton('Save');
$layout_builder_ui = $this->getPathForFieldBlock('node', 'after', 'default', 'body');
$assert_session->linkExists('Manage layout');
$this->clickLink('Manage layout');
// Ensure the body appears once and only once.
$assert_session->elementsCount('css', '.field--name-body', 1);
// Change the body formatter to Trimmed.
$this->drupalGet($layout_builder_ui);
$assert_session->fieldValueEquals('settings[formatter][type]', 'text_default');
$page->selectFieldOption('settings[formatter][type]', 'text_trimmed');
$assert_session->assertWaitOnAjaxRequest();
$page->pressButton('Update');
$page->pressButton('Save layout');
$this->drupalGet($layout_builder_ui);
$assert_session->fieldValueEquals('settings[formatter][type]', 'text_trimmed');
// Disable Layout Builder.
$this->drupalGet($field_ui_prefix);
$this->submitForm(['layout[enabled]' => FALSE], 'Save');
$page->pressButton('Confirm');
// The Layout Builder UI is no longer accessible.
$this->drupalGet($layout_builder_ui);
$assert_session->pageTextContains('You are not authorized to access this page.');
// The original body formatter is reflected in Field UI.
$this->drupalGet($field_ui_prefix);
$assert_session->fieldValueEquals('fields[body][type]', 'text_default');
// Change the body formatter to Summary.
$page->selectFieldOption('fields[body][type]', 'text_summary_or_trimmed');
$assert_session->assertWaitOnAjaxRequest();
$page->pressButton('Save');
$assert_session->fieldValueEquals('fields[body][type]', 'text_summary_or_trimmed');
// Reactivate Layout Builder.
$this->drupalGet($field_ui_prefix);
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$assert_session->linkExists('Manage layout');
$this->clickLink('Manage layout');
// Ensure the body appears once and only once.
$assert_session->elementsCount('css', '.field--name-body', 1);
// The changed body formatter is reflected in Layout Builder UI.
$this->drupalGet($this->getPathForFieldBlock('node', 'after', 'default', 'body'));
$assert_session->fieldValueEquals('settings[formatter][type]', 'text_summary_or_trimmed');
}
/**
* Returns the path to update a field block in the UI.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The bundle.
* @param string $view_mode
* The view mode.
* @param string $field_name
* The field name.
*
* @return string
* The path.
*/
protected function getPathForFieldBlock($entity_type_id, $bundle, $view_mode, $field_name) {
$delta = 0;
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface $display */
$display = $this->container->get('entity_type.manager')->getStorage('entity_view_display')->load("$entity_type_id.$bundle.$view_mode");
$body_component = NULL;
foreach ($display->getSection($delta)->getComponents() as $component) {
if ($component->getPluginId() === "field_block:$entity_type_id:$bundle:$field_name") {
$body_component = $component;
}
}
$this->assertNotNull($body_component);
return 'layout_builder/update/block/defaults/node.after.default/0/content/' . $body_component->getUuid();
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\SortableTestTrait;
/**
* LayoutBuilderSortTrait, provides callback for simulated layout change.
*/
trait LayoutBuilderSortTrait {
use SortableTestTrait;
/**
* {@inheritdoc}
*/
protected function sortableUpdate($item, $from, $to = NULL) {
// If container does not change, $from and $to are equal.
$to = $to ?: $from;
$script = <<<JS
(function (src, from, to) {
var sourceElement = document.querySelector(src);
var fromElement = document.querySelector(from);
var toElement = document.querySelector(to);
Drupal.layoutBuilderBlockUpdate(sourceElement, fromElement, toElement)
})('{$item}', '{$from}', '{$to}')
JS;
$options = [
'script' => $script,
'args' => [],
];
$this->getSession()->getDriver()->getWebDriverSession()->execute($options);
}
}

View File

@@ -0,0 +1,520 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\block_content\Entity\BlockContent;
use Drupal\block_content\Entity\BlockContentType;
use Drupal\Core\Url;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
use Drupal\Tests\system\Traits\OffCanvasTestTrait;
/**
* Tests the Layout Builder UI.
*
* @group layout_builder
*/
class LayoutBuilderTest extends WebDriverTestBase {
use ContextualLinkClickTrait;
use LayoutBuilderSortTrait;
use OffCanvasTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'block_content',
'field_ui',
'layout_builder',
'layout_test',
'node',
'off_canvas_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* The node to customize with Layout Builder.
*
* @var \Drupal\node\NodeInterface
*/
protected $node;
/**
* A string used to mark the current page.
*
* @var string
*
* @todo Remove in https://www.drupal.org/project/drupal/issues/2909782.
*/
private $pageReloadMarker;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('local_tasks_block');
$bundle = BlockContentType::create([
'id' => 'basic',
'label' => 'Basic',
]);
$bundle->save();
block_content_add_body_field($bundle->id());
BlockContent::create([
'info' => 'My content block',
'type' => 'basic',
'body' => [
[
'value' => 'This is the block content',
'format' => filter_default_format(),
],
],
])->save();
$this->createContentType(['type' => 'bundle_with_section_field']);
$this->node = $this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'The node title',
'body' => [
[
'value' => 'The node body',
],
],
]);
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
'administer node display',
], 'foobar'));
}
/**
* Tests the Layout Builder UI.
*/
public function testLayoutBuilderUi(): void {
$layout_url = 'node/1/layout';
$node_url = 'node/1';
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Ensure the block is not displayed initially.
$this->drupalGet($node_url);
$assert_session->pageTextContains('The node body');
$assert_session->pageTextNotContains('Powered by Drupal');
$assert_session->linkNotExists('Layout');
$this->enableLayoutsForBundle('admin/structure/types/manage/bundle_with_section_field/display', TRUE);
// The existing content is still shown until overridden.
$this->drupalGet($node_url);
$assert_session->pageTextContains('The node body');
// Enter the layout editing mode.
$assert_session->linkExists('Layout');
$this->clickLink('Layout');
$this->markCurrentPage();
$assert_session->pageTextContains('The node body');
$assert_session->linkExists('Add section');
// Add a new block.
$this->openAddBlockForm('Powered by Drupal');
$page->fillField('settings[label]', 'This is the label');
$page->checkField('settings[label_display]');
// Save the new block, and ensure it is displayed on the page.
$page->pressButton('Add block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$assert_session->addressEquals($layout_url);
$assert_session->pageTextContains('Powered by Drupal');
$assert_session->pageTextContains('This is the label');
$this->assertPageNotReloaded();
// Until the layout is saved, the new block is not visible on the node page.
$this->drupalGet($node_url);
$assert_session->pageTextNotContains('Powered by Drupal');
// When returning to the layout edit mode, the new block is visible.
$this->drupalGet($layout_url);
$assert_session->pageTextContains('Powered by Drupal');
// Save the layout, and the new block is visible.
$page->pressButton('Save layout');
$assert_session->addressEquals($node_url);
$assert_session->pageTextContains('Powered by Drupal');
$assert_session->pageTextContains('This is the label');
$assert_session->elementExists('css', '.layout');
$this->drupalGet($layout_url);
$this->markCurrentPage();
$assert_session->linkExists('Add section');
$this->clickLink('Add section');
$this->assertNotEmpty($assert_session->waitForElementVisible('named', ['link', 'Two column']));
$this->clickLink('Two column');
$assert_session->waitForElementVisible('named', ['button', 'Add section']);
$page->pressButton('Add section');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->assertNoElementAfterWait('css', '.layout__region--second .block-system-powered-by-block');
$assert_session->elementTextNotContains('css', '.layout__region--second', 'Powered by Drupal');
// Drag the block to a region in different section.
$this->sortableTo('.block-system-powered-by-block', '.layout__region--content', '.layout__region--second');
$assert_session->assertWaitOnAjaxRequest();
// Ensure the drag succeeded.
$assert_session->elementExists('css', '.layout__region--second .block-system-powered-by-block');
$assert_session->elementTextContains('css', '.layout__region--second', 'Powered by Drupal');
$this->assertPageNotReloaded();
// Ensure the dragged block is still in the correct position after reload.
$this->drupalGet($layout_url);
$assert_session->elementExists('css', '.layout__region--second .block-system-powered-by-block');
$assert_session->elementTextContains('css', '.layout__region--second', 'Powered by Drupal');
// Ensure the dragged block is still in the correct position after save.
$page->pressButton('Save layout');
$assert_session->elementExists('css', '.layout__region--second .block-system-powered-by-block');
$assert_session->elementTextContains('css', '.layout__region--second', 'Powered by Drupal');
// Reconfigure a block and ensure that the layout content is updated.
$this->drupalGet($layout_url);
$this->markCurrentPage();
$this->clickContextualLink('.block-system-powered-by-block', 'Configure');
$this->assertOffCanvasFormAfterWait('layout_builder_update_block');
$page->fillField('settings[label]', 'This is the new label');
$page->pressButton('Update');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$assert_session->addressEquals($layout_url);
$assert_session->pageTextContains('Powered by Drupal');
$assert_session->pageTextContains('This is the new label');
$assert_session->pageTextNotContains('This is the label');
// Remove a block.
$this->clickContextualLink('.block-system-powered-by-block', 'Remove block');
$this->assertOffCanvasFormAfterWait('layout_builder_remove_block');
$assert_session->pageTextContains('Are you sure you want to remove the This is the new label block?');
$assert_session->pageTextContains('This action cannot be undone.');
$page->pressButton('Remove');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$assert_session->pageTextNotContains('Powered by Drupal');
$assert_session->linkExists('Add block');
$assert_session->addressEquals($layout_url);
$this->assertPageNotReloaded();
$page->pressButton('Save layout');
$assert_session->elementExists('css', '.layout');
// Test deriver-based blocks.
$this->drupalGet($layout_url);
$this->markCurrentPage();
$this->openAddBlockForm('My content block');
$page->pressButton('Add block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextContains('This is the block content');
// Remove both sections.
$assert_session->linkExists('Remove Section 1');
$this->clickLink('Remove Section 1');
$this->assertOffCanvasFormAfterWait('layout_builder_remove_section');
$assert_session->pageTextContains('Are you sure you want to remove section 1?');
$assert_session->pageTextContains('This action cannot be undone.');
$page->pressButton('Remove');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->linkExists('Remove Section 1');
$this->clickLink('Remove Section 1');
$this->assertOffCanvasFormAfterWait('layout_builder_remove_section');
$page->pressButton('Remove');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextNotContains('This is the block content');
$assert_session->linkNotExists('Add block');
$this->assertPageNotReloaded();
$page->pressButton('Save layout');
// Removing all sections results in no layout being used.
$assert_session->addressEquals($node_url);
$assert_session->elementNotExists('css', '.layout');
$assert_session->pageTextNotContains('The node body');
}
/**
* Tests configurable layouts.
*/
public function testConfigurableLayoutSections(): void {
$layout_url = 'node/1/layout';
\Drupal::entityTypeManager()
->getStorage('entity_view_display')
->create([
'targetEntityType' => 'node',
'bundle' => 'bundle_with_section_field',
'mode' => 'full',
])
->enable()
->setThirdPartySetting('layout_builder', 'enabled', TRUE)
->setThirdPartySetting('layout_builder', 'allow_custom', TRUE)
->save();
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalGet($layout_url);
$this->markCurrentPage();
$assert_session->linkExists('Add section');
$this->clickLink('Add section');
$assert_session->assertWaitOnAjaxRequest();
$this->waitForOffCanvasArea();
$assert_session->linkExists('One column');
$this->clickLink('One column');
$assert_session->assertWaitOnAjaxRequest();
$this->waitForOffCanvasArea();
// Add another section.
$assert_session->linkExists('Add section');
$this->clickLink('Add section');
$this->waitForOffCanvasArea();
$assert_session->waitForElementVisible('named', ['link', 'Layout plugin (with settings)']);
$assert_session->linkExists('Layout plugin (with settings)');
$this->clickLink('Layout plugin (with settings)');
$this->assertOffCanvasFormAfterWait('layout_builder_configure_section');
$assert_session->fieldExists('layout_settings[setting_1]');
$page->pressButton('Add section');
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$assert_session->pageTextContains('Default');
$assert_session->linkExists('Add block');
// Ensure validation error is displayed for ConfigureSectionForm.
$assert_session->linkExists('Add section');
$this->clickLink('Add section');
$this->waitForOffCanvasArea();
$assert_session->waitForElementVisible('named', ['link', 'Layout plugin (with settings)']);
$this->clickLink('Layout plugin (with settings)');
$this->assertOffCanvasFormAfterWait('layout_builder_configure_section');
$page->fillField('layout_settings[setting_1]', 'Test Validation Error Message');
$page->pressButton('Add section');
$assert_session->waitForElement('css', '.messages--error');
$assert_session->pageTextContains('Validation Error Message');
$page->fillField('layout_settings[setting_1]', 'Setting 1 Value');
$page->pressButton('Add section');
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$assert_session->pageTextContains('Setting 1 Value');
// Configure the existing section.
$assert_session->linkExists('Configure Section 1');
$this->clickLink('Configure Section 1');
$this->assertOffCanvasFormAfterWait('layout_builder_configure_section');
$page->fillField('layout_settings[setting_1]', 'Test setting value');
$page->pressButton('Update');
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$assert_session->pageTextContains('Test setting value');
$this->assertPageNotReloaded();
}
/**
* Tests bypassing the off-canvas dialog.
*/
public function testLayoutNoDialog(): void {
$layout_url = 'node/1/layout';
\Drupal::entityTypeManager()
->getStorage('entity_view_display')
->create([
'targetEntityType' => 'node',
'bundle' => 'bundle_with_section_field',
'mode' => 'full',
])
->enable()
->setThirdPartySetting('layout_builder', 'enabled', TRUE)
->setThirdPartySetting('layout_builder', 'allow_custom', TRUE)
->save();
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Set up a layout with one section.
$this->drupalGet(Url::fromRoute('layout_builder.choose_section', [
'section_storage_type' => 'overrides',
'section_storage' => 'node.1',
'delta' => 0,
]));
$assert_session->linkExists('One column');
$this->clickLink('One column');
$page->pressButton('Add section');
// Add a block.
$this->drupalGet(Url::fromRoute('layout_builder.add_block', [
'section_storage_type' => 'overrides',
'section_storage' => 'node.1',
'delta' => 0,
'region' => 'content',
'plugin_id' => 'system_powered_by_block',
]));
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$page->fillField('settings[label]', 'The block label');
$page->fillField('settings[label_display]', TRUE);
$page->pressButton('Add block');
$assert_session->addressEquals($layout_url);
$assert_session->pageTextContains('Powered by Drupal');
$assert_session->pageTextContains('The block label');
// Remove the section.
$this->drupalGet(Url::fromRoute('layout_builder.remove_section', [
'section_storage_type' => 'overrides',
'section_storage' => 'node.1',
'delta' => 0,
]));
$page->pressButton('Remove');
$assert_session->addressEquals($layout_url);
$assert_session->pageTextNotContains('Powered by Drupal');
$assert_session->pageTextNotContains('The block label');
$assert_session->linkNotExists('Add block');
}
/**
* {@inheritdoc}
*
* @todo Remove this in https://www.drupal.org/project/drupal/issues/2918718.
*/
protected function clickContextualLink($selector, $link_locator, $force_visible = TRUE) {
/** @var \Drupal\FunctionalJavascriptTests\JSWebAssert $assert_session */
$assert_session = $this->assertSession();
/** @var \Behat\Mink\Element\DocumentElement $page */
$page = $this->getSession()->getPage();
$page->waitFor(10, function () use ($page, $selector) {
return $page->find('css', "$selector .contextual-links");
});
if (count($page->findAll('css', "$selector .contextual-links")) > 1) {
throw new \Exception('More than one contextual links found by selector');
}
if ($force_visible && $page->find('css', "$selector .contextual .trigger.visually-hidden")) {
$this->toggleContextualTriggerVisibility($selector);
}
$link = $assert_session->elementExists('css', $selector)->findLink($link_locator);
$this->assertNotEmpty($link);
if (!$link->isVisible()) {
$button = $assert_session->waitForElementVisible('css', "$selector .contextual button");
$this->assertNotEmpty($button);
$button->press();
$link = $page->waitFor(10, function () use ($link) {
return $link->isVisible() ? $link : FALSE;
});
}
$link->click();
if ($force_visible) {
$this->toggleContextualTriggerVisibility($selector);
}
}
/**
* Enable layouts.
*
* @param string $path
* The path for the manage display page.
* @param bool $allow_custom
* Whether to allow custom layouts.
*/
private function enableLayoutsForBundle($path, $allow_custom = FALSE) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalGet($path);
$page->checkField('layout[enabled]');
if ($allow_custom) {
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'input[name="layout[allow_custom]"]'));
$page->checkField('layout[allow_custom]');
}
$page->pressButton('Save');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#edit-manage-layout'));
$assert_session->linkExists('Manage layout');
}
/**
* Opens the add block form in the off-canvas dialog.
*
* @param string $block_title
* The block title which will be the link text.
*/
private function openAddBlockForm($block_title) {
$assert_session = $this->assertSession();
$assert_session->linkExists('Add block');
$this->clickLink('Add block');
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForElementVisible('named', ['link', $block_title]));
$this->clickLink($block_title);
$this->assertOffCanvasFormAfterWait('layout_builder_add_block');
}
/**
* Waits for the specified form and returns it when available and visible.
*
* @param string $expected_form_id
* The expected form ID.
*/
private function assertOffCanvasFormAfterWait(string $expected_form_id): void {
$this->assertSession()->assertWaitOnAjaxRequest();
$this->waitForOffCanvasArea();
$off_canvas = $this->assertSession()->elementExists('css', '#drupal-off-canvas');
$this->assertNotNull($off_canvas);
$form_id_element = $off_canvas->find('hidden_field_selector', ['hidden_field', 'form_id']);
// Ensure the form ID has the correct value and that the form is visible.
$this->assertNotEmpty($form_id_element);
$this->assertSame($expected_form_id, $form_id_element->getValue());
$this->assertTrue($form_id_element->getParent()->isVisible());
}
/**
* Marks the page to assist determining if the page has been reloaded.
*
* @todo Remove in https://www.drupal.org/project/drupal/issues/2909782.
*/
private function markCurrentPage() {
$this->pageReloadMarker = $this->randomMachineName();
$this->getSession()->executeScript('document.body.appendChild(document.createTextNode("' . $this->pageReloadMarker . '"));');
}
/**
* Asserts that the page has not been reloaded.
*
* @todo Remove in https://www.drupal.org/project/drupal/issues/2909782.
*/
private function assertPageNotReloaded(): void {
$this->assertSession()->pageTextContains($this->pageReloadMarker);
}
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Test Layout Builder integration with Toolbar.
*
* @group layout_builder
*/
class LayoutBuilderToolbarTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'node',
'field_ui',
'layout_builder',
'node',
'toolbar',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('local_tasks_block');
// Create a content type.
$this->createContentType([
'type' => 'bundle_with_section_field',
'name' => 'Bundle with section field',
]);
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => 'The first node title',
'body' => [
[
'value' => 'The first node body',
],
],
]);
}
/**
* Tests the 'Back to site' link behaves with manage layout as admin page.
*/
public function testBackToSiteLink(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'access administration pages',
'administer node display',
'administer node fields',
'access toolbar',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
// From the manage display page, go to manage the layout.
$this->drupalGet("$field_ui_prefix/display/default");
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
$assert_session->linkExists('Manage layout');
$this->clickLink('Manage layout');
// Save the defaults.
$page->pressButton('Save layout');
$assert_session->addressEquals("$field_ui_prefix/display/default");
// As the Layout Builder UI is typically displayed using the frontend theme,
// it is not marked as an administrative page at the route level even though
// it performs an administrative task, therefore, we need to verify that it
// behaves as such, redirecting out of the admin section.
// Clicking "Back to site" navigates to the homepage.
$this->drupalGet("$field_ui_prefix/display/default/layout");
$this->clickLink('Back to site');
$assert_session->addressEquals("/user/2");
$this->drupalGet("$field_ui_prefix/display/default/layout/discard-changes");
$page->pressButton('Confirm');
$this->clickLink('Back to site');
$assert_session->addressEquals("/user/2");
$this->drupalGet("$field_ui_prefix/display/default/layout/disable");
$page->pressButton('Confirm');
$this->clickLink('Back to site');
$assert_session->addressEquals("/user/2");
}
}

View File

@@ -0,0 +1,309 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\block_content\Entity\BlockContentType;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
// cspell:ignore fieldbody
/**
* Tests the Layout Builder UI.
*
* @group layout_builder
*/
class LayoutBuilderUiTest extends WebDriverTestBase {
use ContextualLinkClickTrait;
/**
* 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 = [
'layout_builder',
'block',
'field_ui',
'node',
'block_content',
'contextual',
'views',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType(['type' => 'bundle_with_section_field']);
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'create and edit custom blocks',
'administer node display',
'administer node fields',
'access contextual links',
]));
// Enable layout builder.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default');
$this->submitForm(['layout[enabled]' => TRUE], 'Save');
}
/**
* Tests that after removing sections reloading the page does not re-add them.
*/
public function testReloadWithNoSections(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Remove all of the sections from the page.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
$page->clickLink('Remove Section 1');
$assert_session->assertWaitOnAjaxRequest();
$page->pressButton('Remove');
$assert_session->assertWaitOnAjaxRequest();
// Assert that there are no sections on the page.
$assert_session->pageTextNotContains('Remove Section 1');
$assert_session->pageTextNotContains('Add block');
// Reload the page.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
// Assert that there are no sections on the page.
$assert_session->pageTextNotContains('Remove Section 1');
$assert_session->pageTextNotContains('Add block');
}
/**
* Tests the message indicating unsaved changes.
*/
public function testUnsavedChangesMessage(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->assertModifiedLayout(static::FIELD_UI_PREFIX . '/display/default/layout');
// Discard then cancel.
$page->pressButton('Discard changes');
$page->clickLink('Cancel');
$assert_session->addressEquals(static::FIELD_UI_PREFIX . '/display/default/layout');
$assert_session->pageTextContainsOnce('You have unsaved changes.');
// Discard then confirm.
$page->pressButton('Discard changes');
$page->pressButton('Confirm');
$assert_session->addressEquals(static::FIELD_UI_PREFIX . '/display/default');
$assert_session->pageTextNotContains('You have unsaved changes.');
// Make and then save changes.
$this->assertModifiedLayout(static::FIELD_UI_PREFIX . '/display/default/layout');
$page->pressButton('Save layout');
$assert_session->pageTextNotContains('You have unsaved changes.');
}
/**
* Asserts that modifying a layout works as expected.
*
* @param string $path
* The path to a Layout Builder UI page.
*
* @internal
*/
protected function assertModifiedLayout(string $path): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalGet($path);
$page->clickLink('Add section');
$assert_session->waitForElementVisible('named', ['link', 'One column']);
$assert_session->pageTextNotContains('You have unsaved changes.');
$page->clickLink('One column');
$assert_session->waitForElementVisible('named', ['button', 'Add section']);
$page->pressButton('Add section');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextContainsOnce('You have unsaved changes.');
// Reload the page.
$this->drupalGet($path);
$assert_session->pageTextContainsOnce('You have unsaved changes.');
}
/**
* Tests that elements that open the dialog are properly highlighted.
*/
public function testAddHighlights(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$bundle = BlockContentType::create([
'id' => 'basic',
'label' => 'Basic block',
'revision' => 1,
]);
$bundle->save();
block_content_add_body_field($bundle->id());
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
$assert_session->elementsCount('css', '.layout-builder__add-section', 2);
$assert_session->elementNotExists('css', '.is-layout-builder-highlighted');
$page->clickLink('Add section');
$this->assertNotEmpty($assert_session->waitForElement('css', '#drupal-off-canvas .item-list'));
$assert_session->assertWaitOnAjaxRequest();
// Highlight is present with AddSectionController.
$this->assertHighlightedElement('[data-layout-builder-highlight-id="section-0"]');
$page->clickLink('Two column');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas input[type="submit"][value="Add section"]'));
$assert_session->assertWaitOnAjaxRequest();
// The highlight is present with ConfigureSectionForm.
$this->assertHighlightedElement('[data-layout-builder-highlight-id="section-0"]');
// Submit the form to add the section and then confirm that no element is
// highlighted anymore.
$page->pressButton("Add section");
$assert_session->assertWaitOnAjaxRequest();
$this->assertHighlightNotExists();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '[data-layout-delta="1"]'));
$assert_session->elementsCount('css', '.layout-builder__add-block', 3);
// Add a content block.
$page->clickLink('Add block');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'a:contains("Create content block")'));
$assert_session->assertWaitOnAjaxRequest();
// Highlight is present with ChooseBlockController::build().
$this->assertHighlightedElement('[data-layout-builder-highlight-id="block-0-first"]');
$page->clickLink('Create content block');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas input[value="Add block"]'));
$assert_session->assertWaitOnAjaxRequest();
// Highlight is present with ChooseBlockController::inlineBlockList().
$this->assertHighlightedElement('[data-layout-builder-highlight-id="block-0-first"]');
$page->pressButton('Close');
$this->assertHighlightNotExists();
// The highlight should persist with all block config dialogs.
$page->clickLink('Add block');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'a:contains("Recent content")'));
$assert_session->assertWaitOnAjaxRequest();
$this->assertHighlightedElement('[data-layout-builder-highlight-id="block-0-first"]');
$page->clickLink('Recent content');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas input[value="Add block"]'));
// The highlight is present with ConfigureBlockFormBase::doBuildForm().
$this->assertHighlightedElement('[data-layout-builder-highlight-id="block-0-first"]');
$page->pressButton('Close');
$this->assertHighlightNotExists();
// The highlight is present when the "Configure section" dialog is open.
$page->clickLink('Configure Section 1');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
$this->assertHighlightedElement('[data-layout-builder-highlight-id="section-update-0"]');
$page->pressButton('Close');
$this->assertHighlightNotExists();
// The highlight is present when the "Remove Section" dialog is open.
$page->clickLink('Remove Section 1');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
$assert_session->assertWaitOnAjaxRequest();
$this->assertHighlightedElement('[data-layout-builder-highlight-id="section-update-0"]');
$page->pressButton('Close');
$this->assertHighlightNotExists();
// A block is highlighted when its "Configure" contextual link is clicked.
$this->clickContextualLink('.block-field-blocknodebundle-with-section-fieldbody', 'Configure');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
$assert_session->assertWaitOnAjaxRequest();
$this->assertHighlightedElement('.block-field-blocknodebundle-with-section-fieldbody');
// Make sure the highlight remains when contextual links are revealed with
// the mouse.
$this->toggleContextualTriggerVisibility('.block-field-blocknodebundle-with-section-fieldbody');
$active_section = $page->find('css', '.block-field-blocknodebundle-with-section-fieldbody');
$active_section->pressButton('Open configuration options');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-field-blocknodebundle-with-section-fieldbody .contextual.open'));
$page->pressButton('Close');
$this->assertHighlightNotExists();
// @todo Remove the reload once https://www.drupal.org/node/2918718 is
// completed.
$this->getSession()->reload();
// Block is highlighted when its "Remove block" contextual link is clicked.
$this->clickContextualLink('.block-field-blocknodebundle-with-section-fieldbody', 'Remove block');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
$assert_session->assertWaitOnAjaxRequest();
$this->assertHighlightedElement('.block-field-blocknodebundle-with-section-fieldbody');
$page->pressButton('Close');
$this->assertHighlightNotExists();
}
/**
* Tests removing newly added extra field.
*/
public function testNewExtraField(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// At this point layout builder has been enabled for the test content type.
// Install a test module that creates a new extra field then clear cache.
\Drupal::service('module_installer')->install(['layout_builder_extra_field_test']);
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
// View the layout and try to remove the new extra field.
$this->drupalGet(static::FIELD_UI_PREFIX . '/display/default/layout');
$assert_session->pageTextContains('New Extra Field');
$this->clickContextualLink('.block-extra-field-blocknodebundle-with-section-fieldlayout-builder-extra-field-test', 'Remove block');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas'));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextContains('Are you sure you want to remove');
$page->pressButton('Remove');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextNotContains('New Extra Field');
}
/**
* Confirms the presence of the 'is-layout-builder-highlighted' class.
*
* @param string $selector
* The highlighted element must also match this selector.
*/
private function assertHighlightedElement(string $selector): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// There is only one highlighted element.
$assert_session->elementsCount('css', '.is-layout-builder-highlighted', 1);
// The selector is also the highlighted element.
$this->assertTrue($page->find('css', $selector)->hasClass('is-layout-builder-highlighted'));
}
/**
* Waits for the dialog to close and confirms no highlights are present.
*/
private function assertHighlightNotExists(): void {
$this->markTestSkipped("Skipped temporarily for random fails.");
$assert_session = $this->assertSession();
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$assert_session->assertNoElementAfterWait('css', '.is-layout-builder-highlighted');
}
}

View File

@@ -0,0 +1,299 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
// cspell:ignore fieldbody fieldlinks
/**
* Tests moving blocks via the form.
*
* @group layout_builder
*/
class MoveBlockFormTest extends WebDriverTestBase {
use ContextualLinkClickTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_builder',
'block',
'node',
'contextual',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
// @todo The Layout Builder UI relies on local tasks; fix in
// https://www.drupal.org/project/drupal/issues/2917777.
$this->drupalPlaceBlock('local_tasks_block');
$this->createContentType(['type' => 'bundle_with_section_field']);
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$this->createNode([
'type' => 'bundle_with_section_field',
]);
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'access contextual links',
]));
$this->drupalGet('node/1/layout');
$expected_block_order = [
'.block-extra-field-blocknodebundle-with-section-fieldlinks',
'.block-field-blocknodebundle-with-section-fieldbody',
];
$this->markTestSkipped("Skipped temporarily for random fails.");
$this->assertRegionBlocksOrder(0, 'content', $expected_block_order);
// Add a top section using the Two column layout.
$page->clickLink('Add section');
$assert_session->waitForElementVisible('css', '#drupal-off-canvas');
$assert_session->assertWaitOnAjaxRequest();
$page->clickLink('Two column');
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'input[value="Add section"]'));
$page->pressButton('Add section');
$this->assertRegionBlocksOrder(1, 'content', $expected_block_order);
// Add a 'Powered by Drupal' block in the 'first' region of the new section.
$first_region_block_locator = '[data-layout-delta="0"].layout--twocol-section [data-region="first"] [data-layout-block-uuid]';
$assert_session->elementNotExists('css', $first_region_block_locator);
$assert_session->elementExists('css', '[data-layout-delta="0"].layout--twocol-section [data-region="first"] .layout-builder__add-block')->click();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas a:contains("Powered by Drupal")'));
$assert_session->assertWaitOnAjaxRequest();
$page->clickLink('Powered by Drupal');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'input[value="Add block"]'));
$assert_session->assertWaitOnAjaxRequest();
$page->pressButton('Add block');
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', $first_region_block_locator));
// Ensure the request has completed before the test starts.
$assert_session->assertWaitOnAjaxRequest();
}
/**
* Tests moving a block.
*/
public function testMoveBlock(): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
// Reorder body field in current region.
$this->openBodyMoveForm(1, 'content', ['Links', 'Body (current)']);
$this->moveBlockWithKeyboard('up', 'Body (current)', ['Body (current)*', 'Links']);
$page->pressButton('Move');
$expected_block_order = [
'.block-field-blocknodebundle-with-section-fieldbody',
'.block-extra-field-blocknodebundle-with-section-fieldlinks',
];
$this->assertRegionBlocksOrder(1, 'content', $expected_block_order);
$page->pressButton('Save layout');
$page->clickLink('Layout');
$this->assertRegionBlocksOrder(1, 'content', $expected_block_order);
// Move the body block into the first region above existing block.
$this->openBodyMoveForm(1, 'content', ['Body (current)', 'Links']);
$page->selectFieldOption('Region', '0:first');
$this->markTestSkipped("Skipped temporarily for random fails.");
$this->assertBlockTable(['Powered by Drupal', 'Body (current)']);
$this->moveBlockWithKeyboard('up', 'Body', ['Body (current)*', 'Powered by Drupal']);
$page->pressButton('Move');
$expected_block_order = [
'.block-field-blocknodebundle-with-section-fieldbody',
'.block-system-powered-by-block',
];
$this->assertRegionBlocksOrder(0, 'first', $expected_block_order);
// Ensure the body block is no longer in the content region.
$this->assertRegionBlocksOrder(1, 'content', ['.block-extra-field-blocknodebundle-with-section-fieldlinks']);
$page->pressButton('Save layout');
$page->clickLink('Layout');
$this->assertRegionBlocksOrder(0, 'first', $expected_block_order);
// Move into the second region that has no existing blocks.
$this->openBodyMoveForm(0, 'first', ['Body (current)', 'Powered by Drupal']);
$page->selectFieldOption('Region', '0:second');
$this->assertBlockTable(['Body (current)']);
$page->pressButton('Move');
$this->assertRegionBlocksOrder(0, 'second', ['.block-field-blocknodebundle-with-section-fieldbody']);
// The weight element uses -10 to 10 by default, which can cause bugs.
// Add 25 'Powered by Drupal' blocks to a new section.
$page->clickLink('Add section');
$assert_session->waitForElementVisible('css', '#drupal-off-canvas');
$assert_session->assertWaitOnAjaxRequest();
$page->clickLink('One column');
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'input[value="Add section"]'));
$page->pressButton('Add section');
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$large_block_number = 25;
for ($i = 0; $i < $large_block_number; $i++) {
$assert_session->elementExists('css', '[data-layout-delta="0"].layout--onecol [data-region="content"] .layout-builder__add-block')->click();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas a:contains("Powered by Drupal")'));
$assert_session->assertWaitOnAjaxRequest();
$page->clickLink('Powered by Drupal');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'input[value="Add block"]'));
$assert_session->assertWaitOnAjaxRequest();
$page->pressButton('Add block');
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
}
$first_region_block_locator = '[data-layout-delta="0"].layout--onecol [data-region="content"] [data-layout-block-uuid]';
$assert_session->elementsCount('css', $first_region_block_locator, $large_block_number);
// Move the Body block to the end of the new section.
$this->openBodyMoveForm(1, 'second', ['Body (current)']);
$page->selectFieldOption('Region', '0:content');
$expected_block_table = array_fill(0, $large_block_number, 'Powered by Drupal');
$expected_block_table[] = 'Body (current)';
$this->assertBlockTable($expected_block_table);
$expected_block_table = array_fill(0, $large_block_number - 1, 'Powered by Drupal');
$expected_block_table[] = 'Body (current)*';
$expected_block_table[] = 'Powered by Drupal';
$this->moveBlockWithKeyboard('up', 'Body', $expected_block_table);
$page->pressButton('Move');
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
// Get all blocks currently in the region.
$blocks = $page->findAll('css', $first_region_block_locator);
// The second to last $block should be the body.
$this->assertTrue($blocks[count($blocks) - 2]->hasClass('block-field-blocknodebundle-with-section-fieldbody'));
}
/**
* Asserts the correct block labels appear in the draggable tables.
*
* @param string[] $expected_block_labels
* The expected block labels.
*
* @internal
*/
protected function assertBlockTable(array $expected_block_labels): void {
$page = $this->getSession()->getPage();
$this->assertSession()->assertWaitOnAjaxRequest();
$block_tds = $page->findAll('css', '.layout-builder-components-table__block-label');
$this->assertSameSize($block_tds, $expected_block_labels);
/** @var \Behat\Mink\Element\NodeElement $block_td */
foreach ($block_tds as $block_td) {
$this->assertSame(array_shift($expected_block_labels), trim($block_td->getText()));
}
}
/**
* Moves a block in the draggable table.
*
* @param string $direction
* The direction to move the block in the table.
* @param string $block_label
* The block label.
* @param array $updated_blocks
* The updated blocks order.
*/
protected function moveBlockWithKeyboard($direction, $block_label, array $updated_blocks) {
$keys = [
'up' => 38,
'down' => 40,
];
$key = $keys[$direction];
$handle = $this->findRowHandle($block_label);
$handle->keyDown($key);
$handle->keyUp($key);
$handle->blur();
$this->assertBlockTable($updated_blocks);
}
/**
* Finds the row handle for a block in the draggable table.
*
* @param string $block_label
* The block label.
*
* @return \Behat\Mink\Element\NodeElement
* The row handle element.
*/
protected function findRowHandle($block_label) {
$assert_session = $this->assertSession();
return $assert_session->elementExists('css', "[data-drupal-selector=\"edit-components\"] td:contains(\"$block_label\") a.tabledrag-handle");
}
/**
* Asserts that blocks are in the correct order for a region.
*
* @param int $section_delta
* The section delta.
* @param string $region
* The region.
* @param array $expected_block_selectors
* The block selectors.
*
* @internal
*/
protected function assertRegionBlocksOrder(int $section_delta, string $region, array $expected_block_selectors): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$assert_session->assertWaitOnAjaxRequest();
$assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas');
$region_selector = "[data-layout-delta=\"$section_delta\"] [data-region=\"$region\"]";
// Get all blocks currently in the region.
$blocks = $page->findAll('css', "$region_selector [data-layout-block-uuid]");
$this->assertSameSize($expected_block_selectors, $blocks);
/** @var \Behat\Mink\Element\NodeElement $block */
foreach ($blocks as $block) {
$block_selector = array_shift($expected_block_selectors);
$assert_session->elementsCount('css', "$region_selector $block_selector", 1);
$expected_block = $page->find('css', "$region_selector $block_selector");
$this->assertSame($expected_block->getAttribute('data-layout-block-uuid'), $block->getAttribute('data-layout-block-uuid'));
}
}
/**
* Open block for the body field.
*
* @param int $delta
* The section delta where the field should be.
* @param string $region
* The region where the field should be.
* @param array $initial_blocks
* The initial blocks that should be shown in the draggable table.
*/
protected function openBodyMoveForm($delta, $region, array $initial_blocks) {
$assert_session = $this->assertSession();
$body_field_locator = "[data-layout-delta=\"$delta\"] [data-region=\"$region\"] .block-field-blocknodebundle-with-section-fieldbody";
$this->clickContextualLink($body_field_locator, 'Move');
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForElementVisible('named', ['select', 'Region']));
$assert_session->fieldValueEquals('Region', "$delta:$region");
$this->assertBlockTable($initial_blocks);
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
/**
* Test the multi-width layout plugins.
*
* @group layout_builder
*/
class TestMultiWidthLayoutsTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_builder',
'block',
'node',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType(['type' => 'bundle_with_section_field']);
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
->enableLayoutBuilder()
->setOverridable()
->save();
$this->createNode([
'type' => 'bundle_with_section_field',
]);
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
]));
}
/**
* Tests changing the columns widths of a multi-width section.
*/
public function testWidthChange(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalGet('node/1/layout');
$width_options = [
[
'label' => 'Two column',
'default_width' => '50-50',
'additional_widths' => [
'33-67',
'67-33',
'25-75',
'75-25',
],
'class' => 'layout--twocol-section--',
],
[
'label' => 'Three column',
'default_width' => '33-34-33',
'additional_widths' => [
'25-50-25',
'25-25-50',
'50-25-25',
],
'class' => 'layout--threecol-section--',
],
];
foreach ($width_options as $width_option) {
$width = $width_option['default_width'];
$assert_session->linkExists('Add section');
$page->clickLink('Add section');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', "#drupal-off-canvas a:contains(\"{$width_option['label']}\")"));
$page->clickLink($width_option['label']);
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas input[type="submit"][value="Add section"]'));
$page->pressButton("Add section");
$this->assertWidthClassApplied($width_option['class'] . $width);
foreach ($width_option['additional_widths'] as $width) {
$width_class = $width_option['class'] . $width;
$assert_session->linkExists('Configure Section 1');
$page->clickLink('Configure Section 1');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas input[type="submit"][value="Update"]'));
$page->findField('layout_settings[column_widths]')->setValue($width);
$page->pressButton("Update");
$this->assertWidthClassApplied($width_class);
}
$assert_session->linkExists('Remove Section 1');
$this->clickLink('Remove Section 1');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas input[type="submit"][value="Remove"]'));
$page->pressButton('Remove');
$assert_session->assertNoElementAfterWait('css', ".$width_class");
}
}
/**
* Asserts the width class is applied to the first section.
*
* @param string $width_class
* The width class.
*
* @internal
*/
protected function assertWidthClassApplied(string $width_class): void {
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', ".{$width_class}[data-layout-delta=\"0\"]"));
}
}

View File

@@ -0,0 +1,228 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\KernelTests\KernelTestBase;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Drupal\layout_builder\SectionStorage\SectionStorageDefinition;
/**
* @coversDefaultClass \Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage
*
* @group layout_builder
* @group #slow
*/
class DefaultsSectionStorageTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_discovery',
'layout_builder',
'layout_builder_defaults_test',
'entity_test',
'field',
'system',
'user',
];
/**
* The plugin.
*
* @var \Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage
*/
protected $plugin;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
entity_test_create_bundle('bundle_with_extra_fields');
$this->installEntitySchema('entity_test');
$this->installEntitySchema('user');
$this->installConfig(['layout_builder_defaults_test']);
$definition = (new SectionStorageDefinition())
->addContextDefinition('display', EntityContextDefinition::fromEntityTypeId('entity_view_display'))
->addContextDefinition('view_mode', new ContextDefinition('string'));
$this->plugin = DefaultsSectionStorage::create($this->container, [], 'defaults', $definition);
}
/**
* Tests installing defaults via config install.
*/
public function testConfigInstall(): void {
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface $display */
$display = LayoutBuilderEntityViewDisplay::load('entity_test.bundle_with_extra_fields.default');
$section = $display->getSection(0);
$this->assertInstanceOf(Section::class, $section);
$this->assertEquals('layout_twocol_section', $section->getLayoutId());
$this->assertEquals([
'column_widths' => '50-50',
'label' => '',
], $section->getLayoutSettings());
}
/**
* @covers ::access
* @dataProvider providerTestAccess
*
* @param bool $expected
* The expected outcome of ::access().
* @param string $operation
* The operation to pass to ::access().
* @param bool $is_enabled
* Whether Layout Builder is enabled for this display.
* @param array $section_data
* Data to store as the sections value for Layout Builder.
*/
public function testAccess($expected, $operation, $is_enabled, array $section_data): void {
$display = LayoutBuilderEntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
'status' => TRUE,
]);
if ($is_enabled) {
$display->enableLayoutBuilder();
}
$display
->setThirdPartySetting('layout_builder', 'sections', $section_data)
->save();
$this->plugin->setContext('display', EntityContext::fromEntity($display));
$result = $this->plugin->access($operation);
$this->assertSame($expected, $result);
}
/**
* Provides test data for ::testAccess().
*/
public static function providerTestAccess() {
$section_data = [
new Section(
'layout_onecol',
[],
[
'10000000-0000-1000-a000-000000000000' => new SectionComponent('10000000-0000-1000-a000-000000000000', 'content', ['id' => 'foo'], ['harold' => 'maude']),
],
['layout_builder_defaults_test' => ['which_party' => 'third']]
),
];
// Data provider values are:
// - the expected outcome of the call to ::access()
// - the operation
// - whether Layout Builder has been enabled for this display
// - whether this display has any section data.
$data = [];
$data['view, disabled, no data'] = [FALSE, 'view', FALSE, []];
$data['view, enabled, no data'] = [TRUE, 'view', TRUE, []];
$data['view, disabled, data'] = [FALSE, 'view', FALSE, $section_data];
$data['view, enabled, data'] = [TRUE, 'view', TRUE, $section_data];
return $data;
}
/**
* @covers ::getContexts
*/
public function testGetContexts(): void {
$display = LayoutBuilderEntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
'status' => TRUE,
]);
$display->save();
$context = EntityContext::fromEntity($display);
$this->plugin->setContext('display', $context);
$result = $this->plugin->getContexts();
$this->assertSame(['view_mode', 'display'], array_keys($result));
$this->assertSame($context, $result['display']);
}
/**
* @covers ::getContextsDuringPreview
*/
public function testGetContextsDuringPreview(): void {
$display = LayoutBuilderEntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
'status' => TRUE,
]);
$display->save();
$context = EntityContext::fromEntity($display);
$this->plugin->setContext('display', $context);
$result = $this->plugin->getContextsDuringPreview();
$this->assertSame(['view_mode', 'display', 'layout_builder.entity'], array_keys($result));
$this->assertSame($context, $result['display']);
$this->assertInstanceOf(EntityContext::class, $result['layout_builder.entity']);
$result_value = $result['layout_builder.entity']->getContextValue();
$this->assertInstanceOf(EntityTest::class, $result_value);
$this->assertSame('entity_test', $result_value->bundle());
$this->assertInstanceOf(Context::class, $result['view_mode']);
$result_value = $result['view_mode']->getContextValue();
$this->assertSame('default', $result_value);
}
/**
* @covers ::getTempstoreKey
*/
public function testGetTempstoreKey(): void {
$display = LayoutBuilderEntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
'status' => TRUE,
]);
$display->save();
$context = EntityContext::fromEntity($display);
$this->plugin->setContext('display', $context);
$result = $this->plugin->getTempstoreKey();
$this->assertSame('entity_test.entity_test.default', $result);
}
/**
* Tests loading given a display.
*/
public function testLoadFromDisplay(): void {
$display = LayoutBuilderEntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
'status' => TRUE,
]);
$display->save();
$contexts = [
'display' => EntityContext::fromEntity($display),
];
$section_storage_manager = $this->container->get('plugin.manager.layout_builder.section_storage');
$section_storage = $section_storage_manager->load('defaults', $contexts);
$this->assertInstanceOf(DefaultsSectionStorage::class, $section_storage);
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\Core\Routing\RouteObjectInterface;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\KernelTests\KernelTestBase;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Routing\Route;
/**
* @covers layout_builder_entity_view_alter
*
* @group layout_builder
*/
class EntityViewAlterTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_discovery',
'layout_builder',
'layout_builder_defaults_test',
'entity_test',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
entity_test_create_bundle('bundle_with_extra_fields');
$this->installEntitySchema('entity_test');
$this->installConfig(['layout_builder_defaults_test']);
}
/**
* Tests that contextual links are removed when rendering Layout Builder.
*/
public function testContextualLinksRemoved(): void {
$display = LayoutBuilderEntityViewDisplay::load('entity_test.bundle_with_extra_fields.default');
$entity = EntityTest::create();
$build = [
'#contextual_links' => ['entity.node.canonical'],
];
// Create a fake request that starts with layout_builder.
$request = Request::create('<front>');
$request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'layout_builder.test');
$request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, new Route('/'));
$request->setSession(new Session(new MockArraySessionStorage()));
\Drupal::requestStack()->push($request);
// Assert the contextual links are removed.
layout_builder_entity_view_alter($build, $entity, $display);
$this->assertArrayNotHasKey('#contextual_links', $build);
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
/**
* Tests field block plugin derivatives.
*
* @group layout_builder
* @group legacy
*/
class FieldBlockDeriverTest extends EntityKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_builder',
'layout_discovery',
];
/**
* Tests that field block derivers respect expose_all_field_blocks config.
*
* When expose_all_field_blocks is disabled (the default setting), only
* bundles that have layout builder enabled will expose their fields as
* field blocks.
*/
public function testFieldBlockDerivers(): void {
$plugins = $this->getBlockPluginIds();
// Setting is disabled and entity_test bundles do not have layout builder
// configured.
$this->assertNotContains('field_block:user:user:name', $plugins);
$this->assertNotContains('extra_field_block:user:user:member_for', $plugins);
$this->assertNotContains('field_block:entity_test:entity_test:id', $plugins);
// Enabling layout builder for entity_test adds field blocks for entity_test
// bundles, but not for the user entity type.
$display = LayoutBuilderEntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
'status' => TRUE,
'third_party_settings' => [
'layout_builder' => [
'enabled' => TRUE,
],
],
]);
$display->save();
$plugins = $this->getBlockPluginIds();
$this->assertContains('field_block:entity_test:entity_test:id', $plugins);
$this->assertNotContains('field_block:user:user:name', $plugins);
$this->assertNotContains('extra_field_block:user:user:member_for', $plugins);
// Exposing all field blocks adds them for the user entity type.
\Drupal::service('module_installer')->install(['layout_builder_expose_all_field_blocks']);
$plugins = $this->getBlockPluginIds();
$this->assertContains('field_block:user:user:name', $plugins);
$this->assertContains('extra_field_block:user:user:member_for', $plugins);
}
/**
* Get an uncached list of block plugin IDs.
*
* @return array
* A list of block plugin IDs.
*/
private function getBlockPluginIds(): array {
return \array_keys(\Drupal::service('plugin.manager.block')->getDefinitions());
}
}

View File

@@ -0,0 +1,332 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterPluginManager;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Form\EnforcedResponseException;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\Session\AccountInterface;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\layout_builder\Plugin\Block\FieldBlock;
use Prophecy\Argument;
use Prophecy\Promise\PromiseInterface;
use Prophecy\Promise\ReturnPromise;
use Prophecy\Promise\ThrowPromise;
use Prophecy\Prophecy\ProphecyInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;
/**
* @coversDefaultClass \Drupal\layout_builder\Plugin\Block\FieldBlock
* @group Field
*/
class FieldBlockTest extends EntityKernelTestBase {
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class);
$this->logger = $this->prophesize(LoggerInterface::class);
}
/**
* Tests entity access.
*
* @covers ::blockAccess
* @dataProvider providerTestBlockAccessNotAllowed
*/
public function testBlockAccessEntityNotAllowed($expected, $entity_access): void {
$entity = $this->prophesize(FieldableEntityInterface::class);
$block = $this->getTestBlock($entity);
$account = $this->prophesize(AccountInterface::class);
$entity->access('view', $account->reveal(), TRUE)->willReturn($entity_access);
$entity->hasField()->shouldNotBeCalled();
$access = $block->access($account->reveal(), TRUE);
$this->assertSame($expected, $access->isAllowed());
}
/**
* Provides test data for ::testBlockAccessEntityNotAllowed().
*/
public static function providerTestBlockAccessNotAllowed() {
$data = [];
$data['entity_forbidden'] = [
FALSE,
AccessResult::forbidden(),
];
$data['entity_neutral'] = [
FALSE,
AccessResult::neutral(),
];
return $data;
}
/**
* Tests unfieldable entity.
*
* @covers ::blockAccess
*/
public function testBlockAccessEntityAllowedNotFieldable(): void {
$entity = $this->prophesize(EntityInterface::class);
$block = $this->getTestBlock($entity);
$account = $this->prophesize(AccountInterface::class);
$entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed());
$access = $block->access($account->reveal(), TRUE);
$this->assertFalse($access->isAllowed());
}
/**
* Tests fieldable entity without a particular field.
*
* @covers ::blockAccess
*/
public function testBlockAccessEntityAllowedNoField(): void {
$entity = $this->prophesize(FieldableEntityInterface::class);
$block = $this->getTestBlock($entity);
$account = $this->prophesize(AccountInterface::class);
$entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed());
$entity->hasField('the_field_name')->willReturn(FALSE);
$entity->get('the_field_name')->shouldNotBeCalled();
$access = $block->access($account->reveal(), TRUE);
$this->assertFalse($access->isAllowed());
}
/**
* Tests field access.
*
* @covers ::blockAccess
* @dataProvider providerTestBlockAccessNotAllowed
*/
public function testBlockAccessEntityAllowedFieldNotAllowed($expected, $field_access): void {
$entity = $this->prophesize(FieldableEntityInterface::class);
$block = $this->getTestBlock($entity);
$account = $this->prophesize(AccountInterface::class);
$entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed());
$entity->hasField('the_field_name')->willReturn(TRUE);
$field = $this->prophesize(FieldItemListInterface::class);
$entity->get('the_field_name')->willReturn($field->reveal());
$field->access('view', $account->reveal(), TRUE)->willReturn($field_access);
$field->isEmpty()->shouldNotBeCalled();
$access = $block->access($account->reveal(), TRUE);
$this->assertSame($expected, $access->isAllowed());
}
/**
* Tests populated vs empty build.
*
* @covers ::blockAccess
* @covers ::build
* @dataProvider providerTestBlockAccessEntityAllowedFieldHasValue
*/
public function testBlockAccessEntityAllowedFieldHasValue($expected, $is_empty, $default_value): void {
$entity = $this->prophesize(FieldableEntityInterface::class);
$block = $this->getTestBlock($entity);
$account = $this->prophesize(AccountInterface::class);
$entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed());
$entity->hasField('the_field_name')->willReturn(TRUE);
$field = $this->prophesize(FieldItemListInterface::class);
$field_definition = $this->prophesize(FieldDefinitionInterface::class);
$field->getFieldDefinition()->willReturn($field_definition->reveal());
$field_definition->getDefaultValue($entity->reveal())->willReturn($default_value);
$field_definition->getType()->willReturn('not_an_image');
$entity->get('the_field_name')->willReturn($field->reveal());
$field->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed());
$field->isEmpty()->willReturn($is_empty)->shouldBeCalled();
$access = $block->access($account->reveal(), TRUE);
$this->assertSame($expected, $access->isAllowed());
}
/**
* Provides test data for ::testBlockAccessEntityAllowedFieldHasValue().
*/
public static function providerTestBlockAccessEntityAllowedFieldHasValue() {
$data = [];
$data['empty'] = [
FALSE,
TRUE,
FALSE,
];
$data['populated'] = [
TRUE,
FALSE,
FALSE,
];
$data['empty, with default'] = [
TRUE,
TRUE,
TRUE,
];
return $data;
}
/**
* Instantiates a block for testing.
*
* @param \Prophecy\Prophecy\ProphecyInterface $entity_prophecy
* An entity prophecy for use as an entity context value.
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
*
* @return \Drupal\layout_builder\Plugin\Block\FieldBlock
* The block to test.
*/
protected function getTestBlock(ProphecyInterface $entity_prophecy, array $configuration = [], array $plugin_definition = []) {
$entity_prophecy->getCacheContexts()->willReturn([]);
$entity_prophecy->getCacheTags()->willReturn([]);
$entity_prophecy->getCacheMaxAge()->willReturn(0);
$plugin_definition += [
'provider' => 'test',
'default_formatter' => '',
'category' => 'Test',
'admin_label' => 'Test Block',
'bundles' => ['entity_test'],
'context_definitions' => [
'entity' => EntityContextDefinition::fromEntityTypeId('entity_test')->setLabel('Test'),
'view_mode' => new ContextDefinition('string'),
],
];
$formatter_manager = $this->prophesize(FormatterPluginManager::class);
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
$block = new FieldBlock(
$configuration,
'field_block:entity_test:entity_test:the_field_name',
$plugin_definition,
$this->entityFieldManager->reveal(),
$formatter_manager->reveal(),
$module_handler->reveal(),
$this->logger->reveal()
);
$block->setContextValue('entity', $entity_prophecy->reveal());
$block->setContextValue('view_mode', 'default');
return $block;
}
/**
* @covers ::build
* @dataProvider providerTestBuild
*/
public function testBuild(PromiseInterface $promise, $expected_markup, $log_message = '', $log_arguments = []): void {
$entity = $this->prophesize(FieldableEntityInterface::class);
$field = $this->prophesize(FieldItemListInterface::class);
$entity->get('the_field_name')->willReturn($field->reveal());
$field->view(Argument::type('array'))->will($promise);
$field_definition = $this->prophesize(FieldDefinitionInterface::class);
$field_definition->getLabel()->willReturn('The Field Label');
$this->entityFieldManager->getFieldDefinitions('entity_test', 'entity_test')->willReturn(['the_field_name' => $field_definition]);
if ($log_message) {
$this->logger->warning($log_message, $log_arguments)->shouldBeCalled();
}
else {
$this->logger->warning(Argument::cetera())->shouldNotBeCalled();
}
$block = $this->getTestBlock($entity);
$expected = [
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => 0,
],
];
if ($expected_markup) {
$expected[0]['content']['#markup'] = $expected_markup;
}
$actual = $block->build();
$this->assertEquals($expected, $actual);
}
/**
* Provides test data for ::testBuild().
*/
public static function providerTestBuild() {
$data = [];
$data['array'] = [
new ReturnPromise([['content' => ['#markup' => 'The field value']]]),
'The field value',
];
$data['empty array'] = [
new ReturnPromise([[]]),
'',
];
return $data;
}
/**
* @covers ::build
*/
public function testBuildException(): void {
// In PHP 7.4 ReflectionClass cannot be serialized so this cannot be part of
// providerTestBuild().
$promise = new ThrowPromise(new \Exception('The exception message'));
$this->testBuild(
$promise,
'',
'The field "%field" failed to render with the error of "%error".',
['%field' => 'the_field_name', '%error' => 'The exception message']
);
}
/**
* Tests a field block that throws a form exception.
*
* @todo Remove in https://www.drupal.org/project/drupal/issues/2367555.
*/
public function testBuildWithFormException(): void {
$field = $this->prophesize(FieldItemListInterface::class);
$field->view(Argument::type('array'))->willThrow(new EnforcedResponseException(new Response()));
$entity = $this->prophesize(FieldableEntityInterface::class);
$entity->get('the_field_name')->willReturn($field->reveal());
$block = $this->getTestBlock($entity);
$this->expectException(EnforcedResponseException::class);
$block->build();
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\Core\Database\Connection;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\KernelTests\KernelTestBase;
use Drupal\layout_builder\InlineBlockUsageInterface;
/**
* Class for testing the InlineBlockUsage service.
*
* @coversDefaultClass \Drupal\layout_builder\InlineBlockUsage
*
* @group layout_builder
*/
class InlineBlockUsageTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_discovery',
'layout_builder',
'entity_test',
'user',
];
/**
* The database connection.
*/
protected Connection $database;
/**
* The inline block usage service.
*/
protected InlineBlockUsageInterface $inlineBlockUsage;
/**
* The entity for testing.
*/
protected EntityTest $entity;
protected function setUp(): void {
parent::setUp();
$this->database = $this->container->get('database');
$this->inlineBlockUsage = $this->container->get('inline_block.usage');
$this->installSchema('layout_builder', ['inline_block_usage']);
entity_test_create_bundle('bundle_with_extra_fields');
$this->installEntitySchema('entity_test');
$this->entity = EntityTest::create();
$this->entity->save();
}
/**
* Covers ::addUsage.
*/
public function testAddUsage(): void {
$this->inlineBlockUsage->addUsage('1', $this->entity);
$results = $this->database->select('inline_block_usage')
->fields('inline_block_usage')
->condition('block_content_id', 1)
->condition('layout_entity_id', $this->entity->id())
->condition('layout_entity_type', $this->entity->getEntityTypeId())
->execute()
->fetchAll();
$this->assertCount(1, $results);
}
/**
* Covers ::getUnused.
*/
public function testGetUnused(): void {
// Add a valid usage.
$this->inlineBlockUsage->addUsage('1', $this->entity);
$this->assertEmpty($this->inlineBlockUsage->getUnused());
// Add an invalid usage.
$this->database->merge('inline_block_usage')
->keys([
'block_content_id' => 2,
'layout_entity_id' => NULL,
'layout_entity_type' => NULL,
])->execute();
$this->assertCount(1, $this->inlineBlockUsage->getUnused());
}
/**
* Covers ::removeByLayoutEntity.
*/
public function testRemoveByLayoutEntity(): void {
$this->inlineBlockUsage->addUsage('1', $this->entity);
$this->inlineBlockUsage->removeByLayoutEntity($this->entity);
$results = $this->database->select('inline_block_usage')
->fields('inline_block_usage')
->condition('block_content_id', '1')
->isNull('layout_entity_id')
->isNull('layout_entity_type')
->execute()
->fetchAll();
$this->assertCount(1, $results);
}
/**
* Covers ::deleteUsage.
*/
public function testDeleteUsage(): void {
$this->inlineBlockUsage->addUsage('1', $this->entity);
$this->inlineBlockUsage->deleteUsage(['1']);
$results = $this->database->select('inline_block_usage')
->fields('inline_block_usage')
->condition('block_content_id', 1)
->condition('layout_entity_id', $this->entity->id())
->condition('layout_entity_type', $this->entity->getEntityTypeId())
->execute()
->fetchAll();
$this->assertEmpty($results);
}
/**
* Covers ::getUsage.
*/
public function testGetUsage(): void {
$this->inlineBlockUsage->addUsage('1', $this->entity);
$result = $this->inlineBlockUsage->getUsage('1');
$this->assertEquals($this->entity->id(), $result->layout_entity_id);
$this->assertEquals($this->entity->getEntityTypeId(), $result->layout_entity_type);
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Routing\NullRouteMatch;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
/**
* Tests layout_builder_system_breadcrumb_alter().
*
* @group layout_builder
*/
class LayoutBuilderBreadcrumbAlterTest extends EntityKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_builder',
'layout_discovery',
];
/**
* Check that there are no errors when alter called with null route match.
*/
public function testBreadcrumbAlterNullRouteMatch(): void {
$breadcrumb = new Breadcrumb();
$route_match = new NullRouteMatch();
layout_builder_system_breadcrumb_alter($breadcrumb, $route_match, []);
}
}

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
/**
* Tests Layout Builder's compatibility with existing systems.
*/
abstract class LayoutBuilderCompatibilityTestBase extends EntityKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_discovery',
];
/**
* The entity view display.
*
* @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface
*/
protected $display;
/**
* The entity being rendered.
*
* @var \Drupal\Core\Entity\FieldableEntityInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('entity_test_base_field_display');
$this->installConfig(['filter']);
// Set up a non-admin user that is allowed to view test entities.
\Drupal::currentUser()->setAccount($this->createUser(['view test entity'], NULL, FALSE, ['uid' => 2]));
\Drupal::service('theme_installer')->install(['starterkit_theme']);
$this->config('system.theme')->set('default', 'starterkit_theme')->save();
$field_storage = FieldStorageConfig::create([
'entity_type' => 'entity_test_base_field_display',
'field_name' => 'test_field_display_configurable',
'type' => 'boolean',
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test_base_field_display',
'label' => 'FieldConfig with configurable display',
])->save();
$this->display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test_base_field_display',
'bundle' => 'entity_test_base_field_display',
'mode' => 'default',
'status' => TRUE,
]);
$this->display
->setComponent('test_field_display_configurable', ['weight' => 5])
->save();
// Create an entity with fields that are configurable and non-configurable.
$entity_storage = $this->container->get('entity_type.manager')->getStorage('entity_test_base_field_display');
// @todo Remove langcode workarounds after resolving
// https://www.drupal.org/node/2915034.
$this->entity = $entity_storage->createWithSampleValues('entity_test_base_field_display', [
'langcode' => 'en',
'langcode_default' => TRUE,
]);
$this->entity->save();
}
/**
* Installs the Layout Builder.
*
* Also configures and reloads the entity display.
*/
protected function installLayoutBuilder() {
$this->container->get('module_installer')->install(['layout_builder']);
$this->refreshServices();
$this->display = $this->reloadEntity($this->display);
$this->display->enableLayoutBuilder()->save();
$this->entity = $this->reloadEntity($this->entity);
}
/**
* Enables overrides for the display and reloads the entity.
*/
protected function enableOverrides() {
$this->display->setOverridable()->save();
$this->entity = $this->reloadEntity($this->entity);
}
/**
* Asserts that the rendered entity has the correct fields.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to render.
* @param array $attributes
* An array of field attributes to assert.
*/
protected function assertFieldAttributes(EntityInterface $entity, array $attributes) {
$view_builder = $this->container->get('entity_type.manager')->getViewBuilder($entity->getEntityTypeId());
$build = $view_builder->view($entity);
$this->render($build);
$actual = array_map(function (\SimpleXMLElement $element) {
return (string) $element->attributes();
}, $this->cssSelect('.field'));
$this->assertSame($attributes, $actual);
}
}

View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\Core\Config\Schema\SchemaIncompleteException;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
/**
* @coversDefaultClass \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay
*
* @group layout_builder
* @group #slow
*/
class LayoutBuilderEntityViewDisplayTest extends SectionListTestBase {
/**
* {@inheritdoc}
*/
protected function getSectionList(array $section_data) {
$display = LayoutBuilderEntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
'status' => TRUE,
'third_party_settings' => [
'layout_builder' => [
'enabled' => TRUE,
'sections' => $section_data,
],
],
]);
$display->save();
return $display;
}
/**
* Tests that configuration schema enforces valid values.
*/
public function testInvalidConfiguration(): void {
$this->expectException(SchemaIncompleteException::class);
$this->sectionList->getSection(0)->getComponent('10000000-0000-1000-a000-000000000000')->setConfiguration(['id' => 'foo', 'bar' => 'baz']);
$this->sectionList->save();
}
/**
* @dataProvider providerTestIsLayoutBuilderEnabled
*/
public function testIsLayoutBuilderEnabled($expected, $view_mode, $enabled): void {
$display = LayoutBuilderEntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => $view_mode,
'status' => TRUE,
'third_party_settings' => [
'layout_builder' => [
'enabled' => $enabled,
],
],
]);
$result = $display->isLayoutBuilderEnabled();
$this->assertSame($expected, $result);
}
/**
* Provides test data for ::testIsLayoutBuilderEnabled().
*/
public static function providerTestIsLayoutBuilderEnabled() {
$data = [];
$data['default enabled'] = [TRUE, 'default', TRUE];
$data['default disabled'] = [FALSE, 'default', FALSE];
$data['full enabled'] = [TRUE, 'full', TRUE];
$data['full disabled'] = [FALSE, 'full', FALSE];
$data['_custom enabled'] = [FALSE, '_custom', TRUE];
$data['_custom disabled'] = [FALSE, '_custom', FALSE];
return $data;
}
/**
* Tests that setting overridable enables Layout Builder only when TRUE.
*/
public function testSetOverridable(): void {
// Disable Layout Builder.
$this->sectionList->disableLayoutBuilder();
// Set Overridable to TRUE and ensure Layout Builder is enabled.
$this->sectionList->setOverridable();
$this->assertTrue($this->sectionList->isLayoutBuilderEnabled());
// Ensure Layout Builder is still enabled after setting Overridable to FALSE.
$this->sectionList->setOverridable(FALSE);
$this->assertTrue($this->sectionList->isLayoutBuilderEnabled());
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\entity_test\Entity\EntityTestBundle;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Tests validation of Layout Builder's entity_view_display entities.
*
* @group layout_builder
* @group #slow
*/
class LayoutBuilderEntityViewDisplayValidationTest extends ConfigEntityValidationTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_test',
'field',
'layout_builder',
'node',
'text',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig('node');
$this->createContentType(['type' => 'one']);
$this->createContentType(['type' => 'two']);
EntityTestBundle::create(['id' => 'one'])->save();
EntityTestBundle::create(['id' => 'two'])->save();
EntityViewMode::create([
'id' => 'node.layout',
'label' => 'Layout',
'targetEntityType' => 'node',
])->save();
$this->entity = $this->container->get(EntityDisplayRepositoryInterface::class)
->getViewDisplay('node', 'one', 'layout');
$this->assertInstanceOf(LayoutBuilderEntityViewDisplay::class, $this->entity);
$this->entity->save();
}
/**
* {@inheritdoc}
*/
public function testLabelValidation(): void {
// @todo Remove this override in https://www.drupal.org/i/2939931. The label of Layout Builder's EntityViewDisplay override is computed dynamically, that issue will change this.
// @see \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::label()
$this->markTestSkipped();
}
/**
* {@inheritdoc}
*/
public function testImmutableProperties(array $valid_values = []): void {
parent::testImmutableProperties([
'targetEntityType' => 'entity_test_with_bundle',
'bundle' => 'two',
]);
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
/**
* Ensures that Layout Builder and Field Layout are compatible with each other.
*
* @group layout_builder
*/
class LayoutBuilderFieldLayoutCompatibilityTest extends LayoutBuilderCompatibilityTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'field_layout',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->display
->setLayoutId('layout_twocol')
->save();
}
/**
* Tests the compatibility of Layout Builder and Field Layout.
*/
public function testCompatibility(): void {
// Ensure that the configurable field is shown in the correct region and
// that the non-configurable field is shown outside the layout.
$expected_fields = [
'field field--name-name field--type-string field--label-hidden field__item',
'field field--name-test-field-display-configurable field--type-boolean field--label-above',
'clearfix text-formatted field field--name-test-display-configurable field--type-text field--label-above',
'clearfix text-formatted field field--name-test-display-non-configurable field--type-text field--label-above',
'clearfix text-formatted field field--name-test-display-multiple field--type-text field--label-above',
];
$this->assertFieldAttributes($this->entity, $expected_fields);
$this->assertNotEmpty($this->cssSelect('.layout__region--first .field--name-test-display-configurable'));
$this->assertNotEmpty($this->cssSelect('.layout__region--first .field--name-test-field-display-configurable'));
$this->assertNotEmpty($this->cssSelect('.field--name-test-display-non-configurable'));
$this->assertEmpty($this->cssSelect('.layout__region .field--name-test-display-non-configurable'));
$this->installLayoutBuilder();
// Without using Layout Builder for an override, the result has not changed.
$this->assertFieldAttributes($this->entity, $expected_fields);
// Add a layout override.
$this->enableOverrides();
/** @var \Drupal\layout_builder\SectionStorageInterface $field_list */
$field_list = $this->entity->get(OverridesSectionStorage::FIELD_NAME);
$field_list->appendSection(new Section('layout_onecol'));
$this->entity->save();
// The rendered entity has now changed. The non-configurable field is shown
// outside the layout, the configurable field is not shown at all, and the
// layout itself is rendered (but empty).
$new_expected_fields = [
'field field--name-name field--type-string field--label-hidden field__item',
'clearfix text-formatted field field--name-test-display-non-configurable field--type-text field--label-above',
'clearfix text-formatted field field--name-test-display-multiple field--type-text field--label-above',
];
$this->assertFieldAttributes($this->entity, $new_expected_fields);
$this->assertNotEmpty($this->cssSelect('.layout--onecol'));
// Removing the layout restores the original rendering of the entity.
$field_list->removeAllSections();
$this->entity->save();
$this->assertFieldAttributes($this->entity, $expected_fields);
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
/**
* Ensures that Layout Builder and core EntityViewDisplays are compatible.
*
* @group layout_builder
*/
class LayoutBuilderInstallTest extends LayoutBuilderCompatibilityTestBase {
/**
* Tests the compatibility of Layout Builder with existing entity displays.
*/
public function testCompatibility(): void {
// Ensure that the fields are shown.
$expected_fields = [
'field field--name-name field--type-string field--label-hidden field__item',
'field field--name-test-field-display-configurable field--type-boolean field--label-above',
'clearfix text-formatted field field--name-test-display-configurable field--type-text field--label-above',
'clearfix text-formatted field field--name-test-display-non-configurable field--type-text field--label-above',
'clearfix text-formatted field field--name-test-display-multiple field--type-text field--label-above',
];
$this->assertFieldAttributes($this->entity, $expected_fields);
$this->installLayoutBuilder();
// Without using Layout Builder for an override, the result has not changed.
$this->assertFieldAttributes($this->entity, $expected_fields);
// Add a layout override.
$this->enableOverrides();
$this->entity = $this->reloadEntity($this->entity);
$this->entity->get(OverridesSectionStorage::FIELD_NAME)->appendSection(new Section('layout_onecol'));
$this->entity->save();
// The rendered entity has now changed. The non-configurable field is shown
// outside the layout, the configurable field is not shown at all, and the
// layout itself is rendered (but empty).
$new_expected_fields = [
'field field--name-name field--type-string field--label-hidden field__item',
'clearfix text-formatted field field--name-test-display-non-configurable field--type-text field--label-above',
'clearfix text-formatted field field--name-test-display-multiple field--type-text field--label-above',
];
$this->assertFieldAttributes($this->entity, $new_expected_fields);
$this->assertNotEmpty($this->cssSelect('.layout--onecol'));
// Removing the layout restores the original rendering of the entity.
$this->entity->get(OverridesSectionStorage::FIELD_NAME)->removeAllSections();
$this->entity->save();
$this->assertFieldAttributes($this->entity, $expected_fields);
// Test that adding a new field after Layout Builder has been installed will
// add the new field to the default region of the first section.
$field_storage = FieldStorageConfig::create([
'entity_type' => 'entity_test_base_field_display',
'field_name' => 'test_field_display_post_install',
'type' => 'text',
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test_base_field_display',
'label' => 'FieldConfig with configurable display',
])->save();
$this->entity = $this->reloadEntity($this->entity);
$this->entity->test_field_display_post_install = 'Test string';
$this->entity->save();
$this->display = $this->reloadEntity($this->display);
$this->display
->setComponent('test_field_display_post_install', ['weight' => 50])
->save();
$new_expected_fields = [
'field field--name-name field--type-string field--label-hidden field__item',
'field field--name-test-field-display-configurable field--type-boolean field--label-above',
'clearfix text-formatted field field--name-test-display-configurable field--type-text field--label-above',
'clearfix text-formatted field field--name-test-field-display-post-install field--type-text field--label-above',
'clearfix text-formatted field field--name-test-display-non-configurable field--type-text field--label-above',
'clearfix text-formatted field field--name-test-display-multiple field--type-text field--label-above',
];
$this->assertFieldAttributes($this->entity, $new_expected_fields);
$this->assertNotEmpty($this->cssSelect('.layout--onecol'));
$this->assertText('Test string');
}
}

View File

@@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\KernelTests\KernelTestBase;
use Drupal\layout_builder\DefaultsSectionStorageInterface;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\LayoutEntityHelperTrait;
use Drupal\layout_builder\OverridesSectionStorageInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Prophecy\Argument;
/**
* @coversDefaultClass \Drupal\layout_builder\LayoutEntityHelperTrait
*
* @group layout_builder
* @group #slow
*/
class LayoutEntityHelperTraitTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_builder',
'entity_test',
'system',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('entity_test');
}
/**
* Data provider for testGetSectionStorageForEntity().
*/
public static function providerTestGetSectionStorageForEntity() {
$data = [];
$data['entity_view_display'] = [
'entity_view_display',
[
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
'status' => TRUE,
'third_party_settings' => [
'layout_builder' => [
'enabled' => TRUE,
],
],
],
['display', 'view_mode'],
];
$data['fieldable entity'] = [
'entity_test',
[],
['entity', 'display', 'view_mode'],
];
return $data;
}
/**
* @covers ::getSectionStorageForEntity
*
* @dataProvider providerTestGetSectionStorageForEntity
*/
public function testGetSectionStorageForEntity($entity_type_id, $values, $expected_context_keys): void {
$section_storage_manager = $this->prophesize(SectionStorageManagerInterface::class);
$section_storage_manager->load('')->willReturn(NULL);
$section_storage_manager->findByContext(Argument::cetera())->will(function ($arguments) {
return $arguments[0];
});
$this->container->set('plugin.manager.layout_builder.section_storage', $section_storage_manager->reveal());
$entity = $this->container->get('entity_type.manager')->getStorage($entity_type_id)->create($values);
$entity->save();
$class = new TestLayoutEntityHelperTrait();
$result = $class->getSectionStorageForEntity($entity);
$this->assertEquals($expected_context_keys, array_keys($result));
if ($entity instanceof EntityViewDisplayInterface) {
$this->assertEquals(EntityContext::fromEntity($entity), $result['display']);
}
elseif ($entity instanceof FieldableEntityInterface) {
$this->assertEquals(EntityContext::fromEntity($entity), $result['entity']);
$this->assertInstanceOf(Context::class, $result['view_mode']);
$this->assertEquals('full', $result['view_mode']->getContextData()->getValue());
$expected_display = EntityViewDisplay::collectRenderDisplay($entity, 'full');
$this->assertInstanceOf(EntityContext::class, $result['display']);
/** @var \Drupal\Core\Plugin\Context\EntityContext $display_entity_context */
$display_entity_context = $result['display'];
/** @var \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay $display_entity */
$display_entity = $display_entity_context->getContextData()->getValue();
$this->assertInstanceOf(LayoutBuilderEntityViewDisplay::class, $display_entity);
$this->assertEquals('full', $display_entity->getMode());
$this->assertEquals($expected_display->getEntityTypeId(), $display_entity->getEntityTypeId());
$this->assertEquals($expected_display->getComponents(), $display_entity->getComponents());
$this->assertEquals($expected_display->getThirdPartySettings('layout_builder'), $display_entity->getThirdPartySettings('layout_builder'));
}
else {
throw new \UnexpectedValueException("Unexpected entity type.");
}
}
/**
* Data provider for testOriginalEntityUsesDefaultStorage().
*/
public static function providerTestOriginalEntityUsesDefaultStorage() {
return [
'original uses default' => [
[
'updated' => 'override',
'original' => 'default',
],
FALSE,
TRUE,
TRUE,
],
'original uses override' => [
[
'updated' => 'override',
'original' => 'override',
],
FALSE,
TRUE,
FALSE,
],
'no original use override' => [
[
'updated' => 'override',
],
FALSE,
FALSE,
FALSE,
],
'no original uses default' => [
[
'updated' => 'default',
],
FALSE,
FALSE,
FALSE,
],
'is new use override' => [
[
'updated' => 'override',
],
TRUE,
FALSE,
FALSE,
],
'is new use default' => [
[
'updated' => 'default',
],
TRUE,
FALSE,
FALSE,
],
];
}
/**
* @covers ::originalEntityUsesDefaultStorage
*
* @dataProvider providerTestOriginalEntityUsesDefaultStorage
*/
public function testOriginalEntityUsesDefaultStorage($entity_storages, $is_new, $has_original, $expected): void {
$this->assertFalse($is_new && $has_original);
$entity = EntityTest::create(['name' => 'updated']);
if (!$is_new) {
$entity->save();
if ($has_original) {
$original_entity = EntityTest::create(['name' => 'original']);
$entity->original = $original_entity;
}
}
$section_storage_manager = $this->prophesize(SectionStorageManagerInterface::class);
$section_storage_manager->load('')->willReturn(NULL);
$storages = [
'default' => $this->prophesize(DefaultsSectionStorageInterface::class)->reveal(),
'override' => $this->prophesize(OverridesSectionStorageInterface::class)->reveal(),
];
$section_storage_manager->findByContext(Argument::cetera())->will(function ($arguments) use ($storages, $entity_storages) {
$contexts = $arguments[0];
if (isset($contexts['entity'])) {
/** @var \Drupal\entity_test\Entity\EntityTest $entity */
$entity = $contexts['entity']->getContextData()->getValue();
return $storages[$entity_storages[$entity->getName()]];
}
});
$this->container->set('plugin.manager.layout_builder.section_storage', $section_storage_manager->reveal());
$class = new TestLayoutEntityHelperTrait();
$this->assertSame($expected, $class->originalEntityUsesDefaultStorage($entity));
}
/**
* @covers ::getEntitySections
*/
public function testGetEntitySections(): void {
$entity = EntityTest::create(['name' => 'updated']);
$section_storage_manager = $this->prophesize(SectionStorageManagerInterface::class);
$section_storage_manager->load('')->willReturn(NULL);
$section_storage = $this->prophesize(SectionStorageInterface::class);
$sections = [
new Section('layout_onecol'),
];
$this->assertCount(1, $sections);
$section_storage->getSections()->willReturn($sections);
$section_storage->count()->willReturn(1);
$section_storage_manager->findByContext(Argument::cetera())->willReturn($section_storage->reveal());
$this->container->set('plugin.manager.layout_builder.section_storage', $section_storage_manager->reveal());
$class = new TestLayoutEntityHelperTrait();
// Ensure that if the entity has a section storage the sections will be
// returned.
$this->assertSame($sections, $class->getEntitySections($entity));
$section_storage_manager->findByContext(Argument::cetera())->willReturn(NULL);
$this->container->set('plugin.manager.layout_builder.section_storage', $section_storage_manager->reveal());
// Ensure that if the entity has no section storage an empty array will be
// returned.
$this->assertSame([], $class->getEntitySections($entity));
}
}
/**
* Test class using the trait.
*/
class TestLayoutEntityHelperTrait {
use LayoutEntityHelperTrait {
getSectionStorageForEntity as public;
originalEntityUsesDefaultStorage as public;
getEntitySections as public;
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\entity_test\Entity\EntityTestBaseFieldDisplay;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
/**
* Tests the field type for Layout Sections.
*
* @coversDefaultClass \Drupal\layout_builder\Field\LayoutSectionItemList
*
* @group layout_builder
* @group #slow
*/
class LayoutSectionItemListTest extends SectionListTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'field',
'text',
];
/**
* {@inheritdoc}
*/
protected function getSectionList(array $section_data) {
$this->installEntitySchema('entity_test_base_field_display');
LayoutBuilderEntityViewDisplay::create([
'targetEntityType' => 'entity_test_base_field_display',
'bundle' => 'entity_test_base_field_display',
'mode' => 'default',
'status' => TRUE,
])
->enableLayoutBuilder()
->setOverridable()
->save();
array_map(function ($row) {
return ['section' => $row];
}, $section_data);
$entity = EntityTestBaseFieldDisplay::create([
'name' => 'The test entity',
OverridesSectionStorage::FIELD_NAME => $section_data,
]);
$entity->save();
return $entity->get(OverridesSectionStorage::FIELD_NAME);
}
/**
* @covers ::equals
*/
public function testEquals(): void {
$this->sectionList->getSection(0)->setLayoutSettings(['foo' => 1]);
$second_section_storage = clone $this->sectionList;
$this->assertTrue($this->sectionList->equals($second_section_storage));
$second_section_storage->getSection(0)->setLayoutSettings(['foo' => '1']);
$this->assertFalse($this->sectionList->equals($second_section_storage));
}
/**
* @covers ::equals
*/
public function testEqualsNonSection(): void {
$list = $this->prophesize(FieldItemListInterface::class);
$this->assertFalse($this->sectionList->equals($list->reveal()));
}
}

View File

@@ -0,0 +1,293 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\layout_builder\DefaultsSectionStorageInterface;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* @coversDefaultClass \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage
*
* @group layout_builder
* @group #slow
*/
class OverridesSectionStorageTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_discovery',
'layout_builder',
'entity_test',
'field',
'system',
'user',
'language',
];
/**
* The plugin.
*
* @var \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage
*/
protected $plugin;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpCurrentUser();
$this->installEntitySchema('entity_test');
$definition = $this->container->get('plugin.manager.layout_builder.section_storage')->getDefinition('overrides');
$this->plugin = OverridesSectionStorage::create($this->container, [], 'overrides', $definition);
}
/**
* @covers ::access
* @dataProvider providerTestAccess
*
* @param bool $expected
* The expected outcome of ::access().
* @param bool $is_enabled
* Whether Layout Builder is enabled for this display.
* @param array $section_data
* Data to store as the sections value for Layout Builder.
* @param string[] $permissions
* An array of permissions to grant to the user.
*/
public function testAccess($expected, $is_enabled, array $section_data, array $permissions): void {
$display = LayoutBuilderEntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
'status' => TRUE,
]);
if ($is_enabled) {
$display->enableLayoutBuilder();
}
$display
->setOverridable()
->save();
$entity = EntityTest::create([OverridesSectionStorage::FIELD_NAME => $section_data]);
$entity->save();
$account = $this->setUpCurrentUser([], $permissions);
$this->plugin->setContext('entity', EntityContext::fromEntity($entity));
$this->plugin->setContext('view_mode', new Context(new ContextDefinition('string'), 'default'));
// Check access with both the global current user as well as passing one in.
$result = $this->plugin->access('view');
$this->assertSame($expected, $result);
$result = $this->plugin->access('view', $account);
$this->assertSame($expected, $result);
// Create a translation.
ConfigurableLanguage::createFromLangcode('es')->save();
$entity = EntityTest::load($entity->id());
$translation = $entity->addTranslation('es');
$translation->save();
$this->plugin->setContext('entity', EntityContext::fromEntity($translation));
// Perform the same checks again but with a non default translation which
// should always deny access.
$result = $this->plugin->access('view');
$this->assertFalse($result);
$result = $this->plugin->access('view', $account);
$this->assertFalse($result);
}
/**
* Provides test data for ::testAccess().
*/
public static function providerTestAccess() {
$section_data = [
new Section('layout_onecol', [], [
'10000000-0000-1000-a000-000000000000' => new SectionComponent('10000000-0000-1000-a000-000000000000', 'content', ['id' => 'foo']),
]),
];
// Data provider values are:
// - the expected outcome of the call to ::access()
// - whether Layout Builder has been enabled for this display
// - any section data
// - any permissions to grant to the user.
$data = [];
$data['disabled, no data, no permissions'] = [
FALSE, FALSE, [], [],
];
$data['disabled, data, no permissions'] = [
FALSE, FALSE, $section_data, [],
];
$data['enabled, no data, no permissions'] = [
FALSE, TRUE, [], [],
];
$data['enabled, data, no permissions'] = [
FALSE, TRUE, $section_data, [],
];
$data['enabled, no data, configure any layout'] = [
TRUE, TRUE, [], ['configure any layout'],
];
$data['enabled, data, configure any layout'] = [
TRUE, TRUE, $section_data, ['configure any layout'],
];
$data['enabled, no data, bundle overrides'] = [
TRUE, TRUE, [], ['configure all entity_test entity_test layout overrides'],
];
$data['enabled, data, bundle overrides'] = [
TRUE, TRUE, $section_data, ['configure all entity_test entity_test layout overrides'],
];
$data['enabled, no data, bundle edit overrides, no edit access'] = [
FALSE, TRUE, [], ['configure editable entity_test entity_test layout overrides'],
];
$data['enabled, data, bundle edit overrides, no edit access'] = [
FALSE, TRUE, $section_data, ['configure editable entity_test entity_test layout overrides'],
];
$data['enabled, no data, bundle edit overrides, edit access'] = [
TRUE, TRUE, [], ['configure editable entity_test entity_test layout overrides', 'administer entity_test content'],
];
$data['enabled, data, bundle edit overrides, edit access'] = [
TRUE, TRUE, $section_data, ['configure editable entity_test entity_test layout overrides', 'administer entity_test content'],
];
return $data;
}
/**
* @covers ::getContexts
*/
public function testGetContexts(): void {
$entity = EntityTest::create();
$entity->save();
$context = EntityContext::fromEntity($entity);
$this->plugin->setContext('entity', $context);
$expected = [
'entity',
'view_mode',
];
$result = $this->plugin->getContexts();
$this->assertEquals($expected, array_keys($result));
$this->assertSame($context, $result['entity']);
}
/**
* @covers ::getContextsDuringPreview
*/
public function testGetContextsDuringPreview(): void {
$entity = EntityTest::create();
$entity->save();
$context = EntityContext::fromEntity($entity);
$this->plugin->setContext('entity', $context);
$expected = [
'view_mode',
'layout_builder.entity',
];
$result = $this->plugin->getContextsDuringPreview();
$this->assertEquals($expected, array_keys($result));
$this->assertSame($context, $result['layout_builder.entity']);
}
/**
* @covers ::getDefaultSectionStorage
*/
public function testGetDefaultSectionStorage(): void {
$entity = EntityTest::create();
$entity->save();
$this->plugin->setContext('entity', EntityContext::fromEntity($entity));
$this->plugin->setContext('view_mode', new Context(ContextDefinition::create('string'), 'default'));
$this->assertInstanceOf(DefaultsSectionStorageInterface::class, $this->plugin->getDefaultSectionStorage());
}
/**
* @covers ::getTempstoreKey
*/
public function testGetTempstoreKey(): void {
$entity = EntityTest::create();
$entity->save();
$this->plugin->setContext('entity', EntityContext::fromEntity($entity));
$this->plugin->setContext('view_mode', new Context(new ContextDefinition('string'), 'default'));
$result = $this->plugin->getTempstoreKey();
$this->assertSame('entity_test.1.default.en', $result);
}
/**
* @covers ::deriveContextsFromRoute
*/
public function testDeriveContextsFromRoute(): void {
$display = LayoutBuilderEntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
'status' => TRUE,
]);
$display
->enableLayoutBuilder()
->setOverridable()
->save();
$entity = EntityTest::create();
$entity->save();
$entity = EntityTest::load($entity->id());
$result = $this->plugin->deriveContextsFromRoute('entity_test.1', [], '', []);
$this->assertSame(['entity', 'view_mode'], array_keys($result));
$this->assertSame($entity, $result['entity']->getContextValue());
$this->assertSame('default', $result['view_mode']->getContextValue());
}
/**
* @covers ::isOverridden
*/
public function testIsOverridden(): void {
$display = LayoutBuilderEntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
'status' => TRUE,
]);
$display
->enableLayoutBuilder()
->setOverridable()
->save();
$entity = EntityTest::create();
$entity->set(OverridesSectionStorage::FIELD_NAME, [new Section('layout_onecol')]);
$entity->save();
$entity = EntityTest::load($entity->id());
$context = EntityContext::fromEntity($entity);
$this->plugin->setContext('entity', $context);
$this->assertTrue($this->plugin->isOverridden());
$this->plugin->removeSection(0);
$this->assertTrue($this->plugin->isOverridden());
$this->plugin->removeAllSections(TRUE);
$this->assertTrue($this->plugin->isOverridden());
$this->plugin->removeAllSections();
$this->assertFalse($this->plugin->isOverridden());
}
}

View File

@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
/**
* Provides a base class for testing implementations of a section list.
*
* @coversDefaultClass \Drupal\layout_builder\Plugin\SectionStorage\SectionStorageBase
*/
abstract class SectionListTestBase extends EntityKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_builder',
'layout_discovery',
'layout_test',
];
/**
* The section list implementation.
*
* @var \Drupal\layout_builder\SectionListInterface
*/
protected $sectionList;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$section_data = [
new Section('layout_test_plugin', [], [
'10000000-0000-1000-a000-000000000000' => new SectionComponent('10000000-0000-1000-a000-000000000000', 'content', ['id' => 'foo']),
]),
new Section('layout_test_plugin', ['setting_1' => 'bar'], [
'20000000-0000-1000-a000-000000000000' => new SectionComponent('20000000-0000-1000-a000-000000000000', 'content', ['id' => 'foo']),
]),
];
$this->sectionList = $this->getSectionList($section_data);
}
/**
* Sets up the section list.
*
* @param array $section_data
* An array of section data.
*
* @return \Drupal\layout_builder\SectionListInterface
* The section list.
*/
abstract protected function getSectionList(array $section_data);
/**
* Tests ::getSections().
*/
public function testGetSections(): void {
$expected = [
new Section('layout_test_plugin', ['setting_1' => 'Default'], [
'10000000-0000-1000-a000-000000000000' => new SectionComponent('10000000-0000-1000-a000-000000000000', 'content', ['id' => 'foo']),
]),
new Section('layout_test_plugin', ['setting_1' => 'bar'], [
'20000000-0000-1000-a000-000000000000' => new SectionComponent('20000000-0000-1000-a000-000000000000', 'content', ['id' => 'foo']),
]),
];
$this->assertSections($expected);
}
/**
* @covers ::getSection
*/
public function testGetSection(): void {
$this->assertInstanceOf(Section::class, $this->sectionList->getSection(0));
}
/**
* @covers ::getSection
*/
public function testGetSectionInvalidDelta(): void {
$this->expectException(\OutOfBoundsException::class);
$this->expectExceptionMessage('Invalid delta "2"');
$this->sectionList->getSection(2);
}
/**
* @covers ::insertSection
*/
public function testInsertSection(): void {
$expected = [
new Section('layout_test_plugin', ['setting_1' => 'Default'], [
'10000000-0000-1000-a000-000000000000' => new SectionComponent('10000000-0000-1000-a000-000000000000', 'content', ['id' => 'foo']),
]),
new Section('layout_onecol'),
new Section('layout_test_plugin', ['setting_1' => 'bar'], [
'20000000-0000-1000-a000-000000000000' => new SectionComponent('20000000-0000-1000-a000-000000000000', 'content', ['id' => 'foo']),
]),
];
$this->sectionList->insertSection(1, new Section('layout_onecol'));
$this->assertSections($expected);
}
/**
* @covers ::appendSection
*/
public function testAppendSection(): void {
$expected = [
new Section('layout_test_plugin', ['setting_1' => 'Default'], [
'10000000-0000-1000-a000-000000000000' => new SectionComponent('10000000-0000-1000-a000-000000000000', 'content', ['id' => 'foo']),
]),
new Section('layout_test_plugin', ['setting_1' => 'bar'], [
'20000000-0000-1000-a000-000000000000' => new SectionComponent('20000000-0000-1000-a000-000000000000', 'content', ['id' => 'foo']),
]),
new Section('layout_onecol'),
];
$this->sectionList->appendSection(new Section('layout_onecol'));
$this->assertSections($expected);
}
/**
* @covers ::removeAllSections
*
* @dataProvider providerTestRemoveAllSections
*/
public function testRemoveAllSections($set_blank, $expected): void {
if ($set_blank === NULL) {
$this->sectionList->removeAllSections();
}
else {
$this->sectionList->removeAllSections($set_blank);
}
$this->assertSections($expected);
}
/**
* Provides test data for ::testRemoveAllSections().
*/
public static function providerTestRemoveAllSections() {
$data = [];
$data[] = [NULL, []];
$data[] = [FALSE, []];
$data[] = [TRUE, [new Section('layout_builder_blank')]];
return $data;
}
/**
* @covers ::removeSection
*/
public function testRemoveSection(): void {
$expected = [
new Section('layout_test_plugin', ['setting_1' => 'bar'], [
'20000000-0000-1000-a000-000000000000' => new SectionComponent('20000000-0000-1000-a000-000000000000', 'content', ['id' => 'foo']),
]),
];
$this->sectionList->removeSection(0);
$this->assertSections($expected);
}
/**
* @covers ::removeSection
*/
public function testRemoveMultipleSections(): void {
$expected = [
new Section('layout_builder_blank'),
];
$this->sectionList->removeSection(0);
$this->sectionList->removeSection(0);
$this->assertSections($expected);
}
/**
* Tests __clone().
*/
public function testClone(): void {
$this->assertSame(['setting_1' => 'Default'], $this->sectionList->getSection(0)->getLayoutSettings());
$new_section_storage = clone $this->sectionList;
$new_section_storage->getSection(0)->setLayoutSettings(['asdf' => 'qwer']);
$this->assertSame(['setting_1' => 'Default'], $this->sectionList->getSection(0)->getLayoutSettings());
}
/**
* Asserts that the field list has the expected sections.
*
* @param \Drupal\layout_builder\Section[] $expected
* The expected sections.
*/
protected function assertSections(array $expected) {
$result = $this->sectionList->getSections();
$this->assertEquals($expected, $result);
$this->assertSame(array_keys($expected), array_keys($result));
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionListInterface;
use Drupal\layout_builder\SectionListTrait;
/**
* @coversDefaultClass \Drupal\layout_builder\SectionListTrait
*
* @group layout_builder
* @group #slow
*/
class SectionListTraitTest extends SectionListTestBase {
/**
* {@inheritdoc}
*/
protected function getSectionList(array $section_data) {
return new TestSectionList($section_data);
}
/**
* @covers ::addBlankSection
*/
public function testAddBlankSection(): void {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('A blank section must only be added to an empty list');
$this->sectionList->addBlankSection();
}
}
class TestSectionList implements SectionListInterface {
use SectionListTrait {
addBlankSection as public;
}
/**
* An array of sections.
*
* @var \Drupal\layout_builder\Section[]
*/
protected $sections;
/**
* TestSectionList constructor.
*/
public function __construct(array $sections) {
// Loop through each section and reconstruct it to ensure that all default
// values are present.
foreach ($sections as $section) {
$this->sections[] = Section::fromArray($section->toArray());
}
}
/**
* {@inheritdoc}
*/
protected function setSections(array $sections) {
$this->sections = array_values($sections);
return $sections;
}
/**
* {@inheritdoc}
*/
public function getSections() {
return $this->sections;
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionStorage\SectionStorageDefinition;
use Drupal\layout_builder_test\Plugin\SectionStorage\SimpleConfigSectionStorage;
/**
* Tests the test implementation of section storage.
*
* @coversDefaultClass \Drupal\layout_builder_test\Plugin\SectionStorage\SimpleConfigSectionStorage
*
* @group layout_builder
* @group #slow
*/
class SimpleConfigSectionListTest extends SectionListTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'layout_builder_test',
];
/**
* {@inheritdoc}
*/
protected function getSectionList(array $section_data) {
$config = $this->container->get('config.factory')->getEditable('layout_builder_test.test_simple_config.foobar');
$section_data = array_map(function (Section $section) {
return $section->toArray();
}, $section_data);
$config->set('sections', $section_data)->save();
$definition = new SectionStorageDefinition(['id' => 'test_simple_config']);
$plugin = SimpleConfigSectionStorage::create($this->container, [], 'test_simple_config', $definition);
$plugin->setContext('config_id', new Context(new ContextDefinition('string'), 'foobar'));
return $plugin;
}
}

Some files were not shown because too many files have changed in this diff Show More