diff --git a/core/drupalci.yml b/core/drupalci.yml index 2085b9737b..113fe81acd 100644 --- a/core/drupalci.yml +++ b/core/drupalci.yml @@ -3,48 +3,11 @@ # https://www.drupal.org/drupalorg/docs/drupal-ci/customizing-drupalci-testing build: assessment: - validate_codebase: - phplint: - csslint: - halt-on-fail: false - eslint: - # A test must pass eslinting standards check in order to continue processing. - halt-on-fail: false - phpcs: - # phpcs will use core's specified version of Coder. - sniff-all-files: false - halt-on-fail: false testing: - # run_tests task is executed several times in order of performance speeds. - # halt-on-fail can be set on the run_tests tasks in order to fail fast. - # suppress-deprecations is false in order to be alerted to usages of - # deprecated code. - run_tests.phpunit: - types: 'PHPUnit-Unit' - testgroups: '--all' - suppress-deprecations: false - halt-on-fail: false - run_tests.kernel: - types: 'PHPUnit-Kernel' - testgroups: '--all' - suppress-deprecations: false - halt-on-fail: false - run_tests.simpletest: - types: 'Simpletest' - testgroups: '--all' - suppress-deprecations: false - halt-on-fail: false - run_tests.functional: - types: 'PHPUnit-Functional' - testgroups: '--all' - suppress-deprecations: false - halt-on-fail: false run_tests.javascript: - concurrency: 15 + concurrency: 1 types: 'PHPUnit-FunctionalJavascript' - testgroups: '--all' + testgroups: '--class "Drupal\Tests\layout_builder\FunctionalJavascript\LayoutBuilderQuickEditTest"' suppress-deprecations: false halt-on-fail: false - # Run nightwatch testing. - # @see https://www.drupal.org/project/drupal/issues/2869825 - nightwatchjs: + repeat: 50 diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index d746d34325..574e86fd9d 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -26,6 +26,7 @@ use Drupal\Core\Session\AccountInterface; use Drupal\Core\Access\AccessResult; use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; +use Drupal\layout_builder\QuickEditIntegration; /** * Implements hook_help(). @@ -156,6 +157,12 @@ function layout_builder_entity_view_alter(array &$build, EntityInterface $entity if ($display instanceof LayoutBuilderEntityViewDisplay && strpos($route_name, 'layout_builder.') === 0) { unset($build['#contextual_links']); } + + if (\Drupal::moduleHandler()->moduleExists('quickedit')) { + /** @var \Drupal\layout_builder\QuickEditIntegration $quick_edit_integration */ + $quick_edit_integration = \Drupal::classResolver(QuickEditIntegration::class); + $quick_edit_integration->entityViewAlter($build, $entity, $display); + } } /** @@ -334,3 +341,12 @@ function layout_builder_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMa $breadcrumb->addCacheableDependency($cacheability); } } + +/** + * Implements hook_quickedit_render_field(). + */ +function layout_builder_quickedit_render_field(EntityInterface $entity, $field_name, $view_mode_id, $langcode) { + /** @var \Drupal\layout_builder\QuickEditIntegration $quick_edit_integration */ + $quick_edit_integration = \Drupal::classResolver(QuickEditIntegration::class); + return $quick_edit_integration->quickEditRenderField($entity, $field_name, $view_mode_id, $langcode); +} diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php index 8843f8fe57..9bd77f422c 100644 --- a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php @@ -2,6 +2,9 @@ namespace Drupal\layout_builder\Entity; +use Drupal\Component\Plugin\ConfigurableInterface; +use Drupal\Component\Plugin\DerivativeInspectionInterface; +use Drupal\Component\Plugin\PluginBase; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Entity\Entity\EntityViewDisplay as BaseEntityViewDisplay; use Drupal\Core\Entity\EntityStorageInterface; @@ -13,7 +16,9 @@ use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; +use Drupal\layout_builder\LayoutEntityHelperTrait; use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; +use Drupal\layout_builder\QuickEditIntegration; use Drupal\layout_builder\Section; use Drupal\layout_builder\SectionComponent; use Drupal\layout_builder\SectionStorage\SectionStorageTrait; @@ -29,6 +34,7 @@ class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements LayoutEntityDisplayInterface { use SectionStorageTrait; + use LayoutEntityHelperTrait; /** * The entity field manager. @@ -470,4 +476,84 @@ private function sectionStorageManager() { return \Drupal::service('plugin.manager.layout_builder.section_storage'); } + /** + * {@inheritdoc} + */ + public function getComponent($name) { + if ($this->isLayoutBuilderEnabled() && $section_component = $this->getQuickEditSectionComponent() ?: $this->getSectionComponentForFieldName($name)) { + $plugin = $section_component->getPlugin(); + if ($plugin instanceof ConfigurableInterface) { + $configuration = $plugin->getConfiguration(); + if (isset($configuration['formatter'])) { + return $configuration['formatter']; + } + } + } + return parent::getComponent($name); + } + + /** + * Returns the Quick Edit formatter settings. + * + * @return \Drupal\layout_builder\SectionComponent|null + * The section component if it is available. + * + * @see \Drupal\layout_builder\QuickEditIntegration::entityViewAlter() + * @see \Drupal\quickedit\MetadataGenerator::generateFieldMetadata() + */ + private function getQuickEditSectionComponent() { + // To determine the Quick Edit view_mode ID we need an originalMode set. + if ($original_mode = $this->getOriginalMode()) { + $parts = explode('-', $original_mode); + // The Quick Edit view mode ID is created by + // \Drupal\layout_builder\QuickEditIntegration::entityViewAlter() + // concatenating together the information we need to retrieve the Layout + // Builder component. It follows the structure prescribed by the + // documentation of hook_quickedit_render_field(). + if (count($parts) === 6 && $parts[0] === 'layout_builder') { + list(, $delta, $component_uuid, $entity_id) = QuickEditIntegration::deconstructViewModeId($original_mode); + $entity = $this->entityTypeManager()->getStorage($this->getTargetEntityTypeId())->load($entity_id); + $sections = $this->getEntitySections($entity); + if (isset($sections[$delta])) { + $component = $sections[$delta]->getComponent($component_uuid); + $plugin = $component->getPlugin(); + // We only care about FieldBlock because these are only components + // that provide Quick Edit integration: Quick Edit enables in-place + // editing of fields of entities, not of anything else. + if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'field_block') { + return $component; + } + } + } + } + return NULL; + } + + /** + * Gets the component for a given field name if any. + * + * @param string $field_name + * The field name. + * + * @return \Drupal\layout_builder\SectionComponent|null + * The section component if it is available. + */ + private function getSectionComponentForFieldName($field_name) { + // Loop through every component until the first match is found. + foreach ($this->getSections() as $section) { + foreach ($section->getComponents() as $component) { + $plugin = $component->getPlugin(); + if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'field_block') { + // FieldBlock derivative IDs are in the format + // [entity_type]:[bundle]:[field]. + list(, , $field_block_field_name) = explode(PluginBase::DERIVATIVE_SEPARATOR, $plugin->getDerivativeId()); + if ($field_block_field_name === $field_name) { + return $component; + } + } + } + } + return NULL; + } + } diff --git a/core/modules/layout_builder/src/QuickEditIntegration.php b/core/modules/layout_builder/src/QuickEditIntegration.php new file mode 100644 index 0000000000..a87b383a13 --- /dev/null +++ b/core/modules/layout_builder/src/QuickEditIntegration.php @@ -0,0 +1,305 @@ +sectionStorageManager = $section_storage_manager; + $this->currentUser = $current_user; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.layout_builder.section_storage'), + $container->get('current_user'), + $container->get('entity_type.manager') + ); + } + + /** + * Alters the entity view build for Quick Edit compatibility. + * + * When rendering fields outside of normal view modes, Quick Edit requires + * that modules identify themselves with a view mode ID in the format + * [module_name]-[information the module needs to rerender], as prescribed by + * hook_quickedit_render_field(). + * + * @param array $build + * The built entity render array. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display + * The entity view display. + * + * @see hook_quickedit_render_field() + * @see layout_builder_quickedit_render_field() + * + * @internal + */ + public function entityViewAlter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) { + if (!$entity instanceof FieldableEntityInterface || !isset($build['_layout_builder']) || !$this->currentUser->hasPermission('access in-place editing')) { + return; + } + + $cacheable_metadata = CacheableMetadata::createFromRenderArray($build); + $section_list = $this->sectionStorageManager->findByContext( + [ + 'display' => EntityContext::fromEntity($display), + 'entity' => EntityContext::fromEntity($entity), + 'view_mode' => new Context(new ContextDefinition('string'), $display->getMode()), + ], + $cacheable_metadata + ); + $cacheable_metadata->applyTo($build); + + if (empty($section_list)) { + return; + } + + // Create a hash of the sections and use it in the unique Quick Edit view + // mode ID. Any changes to the sections will result in a different hash, + // forcing Quick Edit's JavaScript to recognize any changes and retrieve + // up-to-date metadata. + $sections_hash = hash('sha256', serialize($section_list->getSections())); + + // Track each component by their plugin ID, delta, region, and UUID. + $plugin_ids_to_update = []; + foreach (Element::children($build['_layout_builder']) as $delta) { + $section = $build['_layout_builder'][$delta]; + /** @var \Drupal\Core\Layout\LayoutDefinition $layout */ + $layout = $section['#layout']; + $regions = $layout->getRegionNames(); + + foreach ($regions as $region) { + if (isset($section[$region])) { + foreach ($section[$region] as $uuid => $component) { + if (isset($component['#plugin_id']) && $this->supportQuickEditOnComponent($component, $entity)) { + $plugin_ids_to_update[$component['#plugin_id']][$delta][$region][$uuid] = $uuid; + } + } + } + } + } + + // @todo Remove when https://www.drupal.org/node/3041850 is resolved. + $plugin_ids_to_update = array_filter($plugin_ids_to_update, function ($info) { + // Delta, region, and UUID each count as one. + return count($info, COUNT_RECURSIVE) === 3; + }); + + $plugin_ids_to_update = NestedArray::mergeDeepArray($plugin_ids_to_update, TRUE); + foreach ($plugin_ids_to_update as $delta => $regions) { + foreach ($regions as $region => $uuids) { + foreach ($uuids as $uuid => $component) { + $build['_layout_builder'][$delta][$region][$uuid]['content']['#view_mode'] = static::getViewModeId($entity, $display, $delta, $uuid, $sections_hash); + } + } + } + } + + /** + * Generates a Quick Edit view mode ID. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display + * The entity view display. + * @param int $delta + * The delta. + * @param string $component_uuid + * The component UUID. + * @param string $sections_hash + * The hash of the sections; must change whenever the sections change. + * + * @return string + * The Quick Edit view mode ID. + * + * @see \Drupal\layout_builder\QuickEditIntegration::deconstructViewModeId() + */ + private static function getViewModeId(EntityInterface $entity, EntityViewDisplayInterface $display, $delta, $component_uuid, $sections_hash) { + return implode('-', [ + 'layout_builder', + $display->getMode(), + $delta, + // Replace the dashes in the component UUID because we need to + // use dashes to join the parts. + str_replace('-', '_', $component_uuid), + $entity->id(), + $sections_hash, + ]); + } + + /** + * Deconstructs the Quick Edit view mode ID into its constituent parts. + * + * @param string $quick_edit_view_mode_id + * The Quick Edit view mode ID. + * + * @return array + * An array containing the entity view mode ID, the delta, the component + * UUID, and the entity ID. + * + * @see \Drupal\layout_builder\QuickEditIntegration::getViewModeId() + */ + public static function deconstructViewModeId($quick_edit_view_mode_id) { + list(, $entity_view_mode_id, $delta, $component_uuid, $entity_id) = explode('-', $quick_edit_view_mode_id, 7); + return [ + $entity_view_mode_id, + // @todo Explicitly cast delta to an integer, remove this in + // https://www.drupal.org/project/drupal/issues/2984509. + (int) $delta, + // Replace the underscores with dash to get back the component UUID. + str_replace('_', '-', $component_uuid), + $entity_id, + ]; + } + + /** + * Re-renders a field rendered by Layout Builder, edited with Quick Edit. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity. + * @param string $field_name + * The field name. + * @param string $quick_edit_view_mode_id + * The Quick Edit view mode ID. + * @param string $langcode + * The language code. + * + * @return array + * The re-rendered field. + * + * @internal + */ + public function quickEditRenderField(FieldableEntityInterface $entity, $field_name, $quick_edit_view_mode_id, $langcode) { + list($entity_view_mode, $delta, $component_uuid) = static::deconstructViewModeId($quick_edit_view_mode_id); + + $entity_build = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId())->view($entity, $entity_view_mode, $langcode); + $this->buildEntityView($entity_build); + + if (isset($entity_build['_layout_builder'][$delta])) { + foreach (Element::children($entity_build['_layout_builder'][$delta]) as $region) { + if (isset($entity_build['_layout_builder'][$delta][$region][$component_uuid])) { + return $entity_build['_layout_builder'][$delta][$region][$component_uuid]['content']; + } + } + } + + $this->getLogger('layout_builder')->warning('The field "%field" failed to render.', ['%field' => $field_name]); + return []; + } + + /** + * {@inheritdoc} + * + * @todo Replace this hardcoded processing when + * https://www.drupal.org/project/drupal/issues/3041635 is resolved. + * + * @see \Drupal\Tests\EntityViewTrait::buildEntityView() + */ + private function buildEntityView(array &$elements) { + // If the default values for this element have not been loaded yet, + // populate them. + if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { + $elements += \Drupal::service('element_info')->getInfo($elements['#type']); + } + + // Make any final changes to the element before it is rendered. This means + // that the $element or the children can be altered or corrected before + // the element is rendered into the final text. + if (isset($elements['#pre_render'])) { + foreach ($elements['#pre_render'] as $callable) { + $elements = call_user_func($callable, $elements); + } + } + + // And recurse. + $children = Element::children($elements, TRUE); + foreach ($children as $key) { + $this->buildEntityView($elements[$key]); + } + } + + /** + * Determines whether a component has Quick Edit support. + * + * Only field_block components for display configurable fields should be + * supported. + * + * @param array $component + * The component render array. + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity being displayed. + * + * @return bool + * Whether Quick Edit is supported on the component. + * + * @see \Drupal\layout_builder\Plugin\Block\FieldBlock + */ + private function supportQuickEditOnComponent(array $component, FieldableEntityInterface $entity) { + if (isset($component['content']['#field_name'], $component['#base_plugin_id']) && $component['#base_plugin_id'] === 'field_block') { + return $entity->getFieldDefinition($component['content']['#field_name'])->isDisplayConfigurable('view'); + } + return FALSE; + } + +} diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderQuickEditTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderQuickEditTest.php new file mode 100644 index 0000000000..80de4eb5f5 --- /dev/null +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderQuickEditTest.php @@ -0,0 +1,406 @@ +drupalPlaceBlock('page_title_block'); + + // @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 the Article node type. + $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + $this->article = $this->drupalCreateNode([ + 'type' => 'article', + 'title' => t('My Test Node'), + 'body' => [ + 'value' => 'Hello Layout Builder!', + 'format' => 'plain_text', + ], + ]); + + // Log in as a content author who can use Quick Edit and edit Articles. + $this->contentAuthorUser = $this->drupalCreateUser([ + 'access contextual links', + 'access in-place editing', + 'access content', + 'edit any article content', + ]); + $this->drupalLogin($this->contentAuthorUser); + } + + /** + * Tests that the Layout Builder render pipeline for Quick Edit works. + * + * @covers \Drupal\layout_builder\QuickEditIntegration::quickEditRenderField + */ + public function testLayoutBuilderRenderPipelineForQuickEdit() { + $this->enableLayouts('admin/structure/types/manage/article/display/default'); + $this->usingLayoutBuilder = TRUE; + $this->assertQuickEditInit(); + + // Start in-place editing of the article node. + $this->clickContextualLink('[data-quickedit-entity-id="node/1"]', 'Quick edit'); + $this->assertJsCondition("Drupal.quickedit.collections.entities.get('node/1[0]').get('state') === 'opened'"); + $this->assertQuickEditEntityToolbar((string) $this->article->label(), NULL); + $this->assertEntityInstanceFieldStates('node', 1, 0, [ + 'node/1/title/en/full' => 'candidate', + 'node/1/uid/en/full' => 'candidate', + 'node/1/created/en/full' => 'candidate', + 'node/1/body/en/full' => 'candidate', + ]); + + $assert_session = $this->assertSession(); + + // Click the body field. + $this->click('[data-quickedit-entity-id="node/1"] .field--name-body'); + $this->awaitEntityInstanceFieldState('node', 1, 0, 'body', 'en', 'active'); + $assert_session->waitForElement('css', '.quickedit-toolbar-field div[id*="body"]'); + $this->assertQuickEditEntityToolbar((string) $this->article->label(), 'Body'); + $this->assertEntityInstanceFieldStates('node', 1, 0, [ + 'node/1/title/en/full' => 'candidate', + 'node/1/uid/en/full' => 'candidate', + 'node/1/created/en/full' => 'candidate', + 'node/1/body/en/full' => 'active', + ]); + + // Change the body field. + $page = $this->getSession()->getPage(); + $page->fillField('body[0][value]', 'foobar'); + $this->awaitEntityInstanceFieldState('node', 1, 0, 'body', 'en', 'changed'); + $this->assertEntityInstanceFieldStates('node', 1, 0, [ + 'node/1/title/en/full' => 'candidate', + 'node/1/uid/en/full' => 'candidate', + 'node/1/created/en/full' => 'candidate', + 'node/1/body/en/full' => 'changed', + ]); + + // Save the entity. + $this->saveQuickEdit(); + // Wait for save to complete. + $this->assertJsCondition("Drupal.quickedit.collections.entities.get('node/1[0]').get('state') === 'closed'"); + + // Verify that the render pipeline worked: it should result in updated HTML. + $this->assertSession()->elementContains('css', '.block-layout-builder.block-field-blocknodearticlebody div[data-quickedit-field-id^="node/1/body/en/layout_builder-default-0-"]', 'foobar'); + } + + /** + * Tests that Quick Edit still works even when there are duplicate fields. + * + * @see https://www.drupal.org/project/drupal/issues/3041850 + */ + public function testQuickEditIgnoresDuplicateFields() { + // Place the body field a second time using Layout Builder. + $this->enableLayouts('admin/structure/types/manage/article/display/default'); + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + $this->loginLayoutAdmin(); + $this->drupalGet('admin/structure/types/manage/article/display/default/layout'); + $page->clickLink('Add Block'); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas')); + $assert_session->assertWaitOnAjaxRequest(); + $page->clickLink('Body'); + $assert_session->assertWaitOnAjaxRequest(); + $page->pressButton('Add Block'); + $assert_session->assertWaitOnAjaxRequest(); + $page->pressButton('Save layout'); + $this->assertNotEmpty($assert_session->waitForElement('css', '.messages--status')); + $assert_session->pageTextContains('The layout has been saved.'); + + $this->drupalLogin($this->contentAuthorUser); + $this->drupalGet('node/' . $this->article->id()); + $this->usingLayoutBuilder = TRUE; + + $this->awaitQuickEditForEntity('node', 1); + $this->assertEntityInstanceStates([ + 'node/1[0]' => 'closed', + ]); + $this->assertEntityInstanceFieldStates('node', 1, 0, [ + 'node/1/title/en/full' => 'inactive', + 'node/1/uid/en/full' => 'inactive', + 'node/1/created/en/full' => 'inactive', + ]); + } + + /** + * Tests Quick Edit boots correctly with Layout Builder defaults & overrides. + * + * @param bool $use_revisions + * If revisions are used. + * + * @dataProvider providerEnableDisableLayoutBuilder + */ + public function testEnableDisableLayoutBuilder($use_revisions) { + if (!$use_revisions) { + $content_type = NodeType::load('article'); + $content_type->setNewRevision(FALSE); + $content_type->save(); + } + + // Test article with Layout Builder disabled. + $this->assertQuickEditInit(); + + // Test article with Layout Builder enabled. + $this->enableLayouts('admin/structure/types/manage/article/display/default'); + $this->usingLayoutBuilder = TRUE; + $this->assertQuickEditInit(); + + // Test article with Layout Builder override. + $this->createLayoutOverride(); + $this->assertQuickEditInit(); + + // If we're using revisions, it's not possible to disable Layout Builder + // without deleting the node (unless the revisions containing the override + // would be deleted). + if (!$use_revisions) { + // Test article with Layout Builder when reverted back to defaults. + $this->revertLayoutToDefaults(); + $this->assertQuickEditInit(); + + // Test with Layout Builder disabled after being enabled. + $this->usingLayoutBuilder = FALSE; + $this->disableLayoutBuilder('admin/structure/types/manage/article/display/default'); + $this->assertQuickEditInit(); + } + } + + /** + * DataProvider for testEnableDisableLayoutBuilder(). + */ + public function providerEnableDisableLayoutBuilder() { + return [ + 'use revisions' => [TRUE], + 'do not use revisions' => [FALSE], + ]; + } + + /** + * Enables layouts at an admin path. + * + * @param string $path + * The manage display path. + */ + protected function enableLayouts($path) { + // Save the current user to re-login after Layout Builder changes. + $user = $this->loggedInUser; + $this->loginLayoutAdmin(); + $page = $this->getSession()->getPage(); + $this->drupalGet($path); + $page->checkField('layout[enabled]'); + $page->checkField('layout[allow_custom]'); + $page->pressButton('Save'); + $this->drupalLogin($user); + } + + /** + * {@inheritdoc} + */ + protected function assertEntityInstanceFieldStates($entity_type_id, $entity_id, $entity_instance_id, array $expected_field_states) { + if ($this->usingLayoutBuilder) { + $expected_field_states = $this->replaceLayoutBuilderFieldIdKeys($expected_field_states); + } + parent::assertEntityInstanceFieldStates($entity_type_id, $entity_id, $entity_instance_id, $expected_field_states); + } + + /** + * {@inheritdoc} + */ + protected function assertEntityInstanceFieldMarkup($entity_type_id, $entity_id, $entity_instance_id, array $expected_field_attributes) { + if ($this->usingLayoutBuilder) { + $expected_field_attributes = $this->replaceLayoutBuilderFieldIdKeys($expected_field_attributes); + } + parent::assertEntityInstanceFieldMarkup($entity_type_id, $entity_id, $entity_instance_id, $expected_field_attributes); + } + + /** + * Replaces the keys in the array with field IDs used for Layout Builder. + * + * @param array $array + * The array with field IDs as keys. + * + * @return array + * The array with the keys replaced. + */ + protected function replaceLayoutBuilderFieldIdKeys(array $array) { + $replacement = []; + foreach ($array as $field_key => $value) { + $new_field_key = $this->getQuickEditFieldId($field_key); + $replacement[$new_field_key] = $value; + } + return $replacement; + } + + /** + * Login the Layout admin user for the test. + */ + protected function loginLayoutAdmin() { + // Enable for the Layout Builder. + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + 'access content', + 'administer node display', + 'administer node fields', + 'administer blocks', + ])); + } + + /** + * Creates a layout override. + */ + protected function createLayoutOverride() { + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + + // Save the current user to re-login after Layout Builder changes. + $user = $this->loggedInUser; + $this->loginLayoutAdmin(); + $this->drupalGet('node/' . $this->article->id() . '/layout'); + + $page->pressButton('Save layout'); + $this->assertNotEmpty($assert_session->waitForElement('css', '.messages--status')); + $assert_session->pageTextContains('The layout override has been saved.'); + $this->drupalLogin($user); + } + + /** + * Reverts a layout override. + */ + protected function revertLayoutToDefaults() { + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + // Save the current user to re-login after Layout Builder changes. + $user = $this->loggedInUser; + $this->loginLayoutAdmin(); + $this->drupalGet('node/' . $this->article->id() . '/layout'); + $assert_session->buttonExists('Revert to defaults'); + $page->pressButton('Revert to defaults'); + $page->pressButton('Revert'); + $assert_session->pageTextContains('The layout has been reverted back to defaults.'); + $this->drupalLogin($user); + } + + /** + * Disables Layout Builder. + * + * @param string $path + * The path to the manage display page. + */ + protected function disableLayoutBuilder($path) { + $page = $this->getSession()->getPage(); + // Save the current user to re-login after Layout Builder changes. + $user = $this->loggedInUser; + $this->loginLayoutAdmin(); + $this->drupalGet($path); + $page->uncheckField('layout[allow_custom]'); + $page->uncheckField('layout[enabled]'); + $page->pressButton('Save'); + $page->pressButton('Confirm'); + $this->drupalLogin($user); + } + + /** + * Asserts that Quick Edit is initialized on the node view correctly. + * + * @todo Replace calls to this method with calls to ::doTestArticle() in + * https://www.drupal.org/node/3037436. + */ + private function assertQuickEditInit() { + $node = $this->article; + $this->drupalGet('node/' . $node->id()); + + // Initial state. + $this->awaitQuickEditForEntity('node', 1); + $this->assertEntityInstanceStates([ + 'node/1[0]' => 'closed', + ]); + $this->assertEntityInstanceFieldStates('node', 1, 0, [ + 'node/1/title/en/full' => 'inactive', + 'node/1/uid/en/full' => 'inactive', + 'node/1/created/en/full' => 'inactive', + 'node/1/body/en/full' => 'inactive', + ]); + } + + /** + * Gets the Quick Edit field ID attribute value. + * + * @param string $original_field_id + * The original field ID. + * + * @return string + * The current field ID. + */ + protected function getQuickEditFieldId($original_field_id) { + $page = $this->getSession()->getPage(); + $parts = explode('/', $original_field_id); + // Removes the last part of the field id which will contain the Quick Edit + // view mode ID. When using the Layout Builder the view_mode will contain a + // hash of the layout sections and will be different each time the layout + // changes. + array_pop($parts); + $field_key_without_view_mode = implode('/', $parts); + $element = $page->find('css', "[data-quickedit-field-id^=\"$field_key_without_view_mode\"]"); + $this->assertNotEmpty($element, "Found Quick Edit-enabled field whose data-quickedit-field attribute starts with: $field_key_without_view_mode"); + return $element->getAttribute('data-quickedit-field-id'); + } + +}