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,10 @@
name: 'Field UI test'
type: module
description: 'Support module for Field UI tests.'
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,68 @@
<?php
/**
* @file
* Field UI test module.
*/
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Field\FieldConfigInterface;
/**
* Implements hook_ENTITY_TYPE_access().
*/
function field_ui_test_field_config_access(FieldConfigInterface $field) {
return AccessResult::forbiddenIf($field->getName() == 'highlander');
}
/**
* Implements hook_form_FORM_BASE_ID_alter().
*/
function field_ui_test_form_entity_view_display_edit_form_alter(&$form, FormStateInterface $form_state) {
$table = &$form['fields'];
foreach (Element::children($table) as $name) {
$table[$name]['parent_wrapper']['parent']['#options'] = ['indent' => 'Indent'];
$table[$name]['parent_wrapper']['parent']['#default_value'] = 'indent';
}
$table['indent'] = [
'#attributes' => ['class' => ['draggable', 'field-group'], 'id' => 'indent-id'],
'#row_type' => 'group',
'#region_callback' => 'field_ui_test_region_callback',
'#js_settings' => ['rowHandler' => 'group'],
'human_name' => [
'#markup' => 'Indent',
'#prefix' => '<span class="group-label">',
'#suffix' => '</span>',
],
'weight' => [
'#type' => 'textfield',
'#default_value' => 0,
'#size' => 3,
'#attributes' => ['class' => ['field-weight']],
],
'parent_wrapper' => [
'parent' => [
'#type' => 'select',
'#options' => ['indent' => 'Indent'],
'#empty_value' => '',
'#default_value' => '',
'#attributes' => ['class' => ['field-parent']],
'#parents' => ['fields', 'indent', 'parent'],
],
'hidden_name' => [
'#type' => 'hidden',
'#default_value' => 'indent',
'#attributes' => ['class' => ['field-name']],
],
],
];
}
function field_ui_test_region_callback($row) {
return 'content';
}

View File

@@ -0,0 +1,10 @@
name: 'Field UI test deprecated'
type: module
description: 'Support module for testing deprecated Field UI functionality.'
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,29 @@
<?php
/**
* @file
* Field UI test module.
*/
use Drupal\Core\Form\FormStateInterface;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\field_ui\Form\FieldStorageConfigEditForm;
/**
* Implements hook_form_FORM_ID_alter() for field_storage_config_edit_form.
*/
function field_ui_test_deprecated_form_field_storage_config_edit_form_alter(&$form, FormStateInterface $form_state) {
if (!($form_state->getFormObject() instanceof FieldStorageConfigEditForm)) {
throw new \LogicException('field_storage_config_edit_form() expects to get access to the field storage config entity edit form.');
}
if (!($form_state->getFormObject()->getEntity() instanceof FieldStorageConfigInterface)) {
throw new \LogicException('field_storage_config_edit_form() expects to get access to the field storage config entity.');
}
if (!isset($form['cardinality_container']['cardinality'])) {
throw new \LogicException('field_storage_config_edit_form() expects to that the cardinality container with the cardinality form element exists.');
}
$form['cardinality_container']['hello'] = [
'#markup' => 'Greetings from the field_storage_config_edit_form() alter.',
];
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the UI for configuring entity displays.
*
* @group field_ui
*/
class EntityDisplayFormBaseTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['field_ui', 'entity_test', 'field_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
foreach (entity_test_entity_types() as $entity_type) {
// Auto-create fields for testing.
FieldStorageConfig::create([
'entity_type' => $entity_type,
'field_name' => 'field_test_no_plugin',
'type' => 'field_test',
'cardinality' => 1,
])->save();
FieldConfig::create([
'entity_type' => $entity_type,
'field_name' => 'field_test_no_plugin',
'bundle' => $entity_type,
'label' => 'Test field with no plugin',
'translatable' => FALSE,
])->save();
\Drupal::service('entity_display.repository')
->getFormDisplay($entity_type, $entity_type)
->setComponent('field_test_no_plugin', [
'type' => 'test_field_widget',
])
->save();
}
$this->drupalLogin($this->drupalCreateUser([
'administer entity_test form display',
]));
}
/**
* Ensures the entity is not affected when there are no applicable formatters.
*/
public function testNoApplicableFormatters(): void {
$storage = $this->container->get('entity_type.manager')->getStorage('entity_form_display');
$id = 'entity_test.entity_test.default';
$entity_before = $storage->load($id);
$this->drupalGet('entity_test/structure/entity_test/form-display');
$entity_after = $storage->load($id);
$this->assertSame($entity_before->toArray(), $entity_after->toArray());
}
}

View File

@@ -0,0 +1,221 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\Core\Url;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the entity display modes UI.
*
* @group field_ui
*/
class EntityDisplayModeTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var string[]
*/
protected static $modules = ['block', 'entity_test', 'field_ui', 'node'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a node type.
$this->drupalCreateContentType([
'type' => 'article',
'name' => 'Article',
]);
$this->drupalPlaceBlock('local_actions_block');
$this->drupalPlaceBlock('page_title_block');
}
/**
* Tests the EntityViewMode user interface.
*/
public function testEntityViewModeUI(): void {
// Test the listing page.
$this->drupalGet('admin/structure/display-modes/view');
$this->assertSession()->statusCodeEquals(403);
$this->drupalLogin($this->drupalCreateUser(['administer display modes']));
$this->drupalGet('admin/structure/display-modes/view');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Add view mode');
$this->assertSession()->linkByHrefExists('admin/structure/display-modes/view/add');
$this->assertSession()->linkByHrefExists('admin/structure/display-modes/view/add/entity_test');
$this->drupalGet('admin/structure/display-modes/view/add/entity_test_mulrev');
$this->assertSession()->statusCodeEquals(404);
$this->drupalGet('admin/structure/display-modes/view/add');
$this->assertSession()->linkNotExists('Test entity - revisions and data table', 'An entity type with no view builder cannot have view modes.');
// Test adding a view mode including dots in machine_name.
$this->clickLink('Test entity');
// Check if 'Name' field is required.
$this->assertTrue($this->getSession()->getPage()->findField('label')->hasClass('required'));
$edit = [
'id' => $this->randomMachineName() . '.' . $this->randomMachineName(),
'label' => $this->randomString(),
];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and underscores.');
// Test adding a view mode.
$edit = [
'id' => $this->randomMachineName(),
'label' => $this->randomString(),
];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Saved the {$edit['label']} view mode.");
// Test editing the view mode.
$this->drupalGet('admin/structure/display-modes/view/manage/entity_test.' . $edit['id']);
// Test that the link templates added by field_ui_entity_type_build() are
// generating valid routes.
$view_mode = EntityViewMode::load('entity_test.' . $edit['id']);
$this->assertEquals(Url::fromRoute('entity.entity_view_mode.collection')->toString(), $view_mode->toUrl('collection')->toString());
$this->assertEquals(Url::fromRoute('entity.entity_view_mode.add_form', ['entity_type_id' => $view_mode->getTargetType()])->toString(), $view_mode->toUrl('add-form')->toString());
$this->assertEquals(Url::fromRoute('entity.entity_view_mode.edit_form', ['entity_view_mode' => $view_mode->id()])->toString(), $view_mode->toUrl('edit-form')->toString());
$this->assertEquals(Url::fromRoute('entity.entity_view_mode.delete_form', ['entity_view_mode' => $view_mode->id()])->toString(), $view_mode->toUrl('delete-form')->toString());
// Test deleting the view mode.
$this->clickLink('Delete');
$this->assertSession()->pageTextContains("Are you sure you want to delete the view mode {$edit['label']}?");
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("The view mode {$edit['label']} has been deleted.");
}
/**
* Tests the EntityFormMode user interface.
*/
public function testEntityFormModeUI(): void {
// Test the listing page.
$this->drupalGet('admin/structure/display-modes/form');
$this->assertSession()->statusCodeEquals(403);
$this->drupalLogin($this->drupalCreateUser(['administer display modes']));
$this->drupalGet('admin/structure/display-modes/form');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('Add form mode');
$this->assertSession()->linkByHrefExists('admin/structure/display-modes/form/add');
$this->drupalGet('admin/structure/display-modes/form/add/entity_test_no_label');
$this->assertSession()->statusCodeEquals(404);
$this->drupalGet('admin/structure/display-modes/form/add');
$this->assertSession()->linkNotExists('Entity Test without label', 'An entity type with no form cannot have form modes.');
// Test adding a view mode including dots in machine_name.
$this->clickLink('Test entity');
// Check if 'Name' field is required.
$this->assertTrue($this->getSession()->getPage()->findField('label')->hasClass('required'));
$edit = [
'id' => $this->randomMachineName() . '.' . $this->randomMachineName(),
'label' => $this->randomString(),
];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and underscores.');
// Test adding a form mode.
$edit = [
'id' => $this->randomMachineName(),
'label' => $this->randomString(),
];
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Saved the {$edit['label']} form mode.");
// Test editing the form mode.
$this->drupalGet('admin/structure/display-modes/form/manage/entity_test.' . $edit['id']);
// Test that the link templates added by field_ui_entity_type_build() are
// generating valid routes.
$form_mode = EntityFormMode::load('entity_test.' . $edit['id']);
$this->assertEquals(Url::fromRoute('entity.entity_form_mode.collection')->toString(), $form_mode->toUrl('collection')->toString());
$this->assertEquals(Url::fromRoute('entity.entity_form_mode.add_form', ['entity_type_id' => $form_mode->getTargetType()])->toString(), $form_mode->toUrl('add-form')->toString());
$this->assertEquals(Url::fromRoute('entity.entity_form_mode.edit_form', ['entity_form_mode' => $form_mode->id()])->toString(), $form_mode->toUrl('edit-form')->toString());
$this->assertEquals(Url::fromRoute('entity.entity_form_mode.delete_form', ['entity_form_mode' => $form_mode->id()])->toString(), $form_mode->toUrl('delete-form')->toString());
// Test deleting the form mode.
$this->clickLink('Delete');
$this->assertSession()->pageTextContains("Are you sure you want to delete the form mode {$edit['label']}?");
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("The form mode {$edit['label']} has been deleted.");
}
/**
* Tests if view modes appear in alphabetical order by visible name.
*
* The machine name should not be used for sorting.
*
* @see https://www.drupal.org/node/2858569
*/
public function testAlphabeticalDisplaySettings(): void {
$this->drupalLogin($this->drupalCreateUser([
'access administration pages',
'administer content types',
'administer display modes',
'administer nodes',
'administer node fields',
'administer node display',
'administer node form display',
'view the administration theme',
]));
$this->drupalGet('admin/structure/types/manage/article/display');
// Verify that the order of view modes is alphabetical by visible label.
// Since the default view modes all have machine names which coincide with
// the English labels, they should appear in alphabetical order, by default
// if viewing the site in English and if no changes have been made. We will
// verify this first.
$page_text = $this->getTextContent();
$start = strpos($page_text, 'view modes');
$pos = $start;
$list = ['Full content', 'RSS', 'Search index', 'Search result', 'Teaser'];
// Verify that the order of the view modes is correct on the page.
foreach ($list as $name) {
$new_pos = strpos($page_text, $name, $start);
$this->assertGreaterThan($pos, $new_pos);
$pos = $new_pos;
}
// Now that we have verified the original display order, we can change the
// label for one of the view modes. If we rename "Teaser" to "Breezier", it
// should appear as the first of the listed view modes:
// Set new values and enable test plugins.
$edit = [
'label' => 'Breezier',
];
$this->drupalGet('admin/structure/display-modes/view/manage/node.teaser');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('Saved the Breezier view mode.');
// Re-open the display settings for the article content type and verify
// that changing "Teaser" to "Breezier" makes it appear before "Full
// content".
$this->drupalGet('admin/structure/types/manage/article/display');
$page_text = $this->getTextContent();
$start = strpos($page_text, 'view modes');
$pos = $start;
$list = ['Breezier', 'Full content'];
// Verify that the order of the view modes is correct on the page.
foreach ($list as $name) {
$new_pos = strpos($page_text, $name, $start);
$this->assertGreaterThan($pos, $new_pos);
$pos = $new_pos;
}
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the UI for entity displays.
*
* @group field_ui
*/
class EntityDisplayTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['field_ui', 'entity_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser([
'administer entity_test display',
]));
}
/**
* Tests the use of regions for entity view displays.
*/
public function testEntityView(): void {
$this->drupalGet('entity_test/structure/entity_test/display');
$this->assertSession()->elementExists('css', '.region-content-message.region-empty');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
$this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Your settings have been saved.');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests field UI integration with field type categories for loading libraries.
*
* @group field_ui
*/
class FieldTypeCategoriesIntegrationTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'file',
'field_ui',
'options',
'comment',
'link',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a test user.
$admin_user = $this->drupalCreateUser(['administer node fields']);
$this->drupalLogin($admin_user);
}
/**
* Tests if the libraries are loaded on FieldStorageAddForm.
*/
public function testLibrariesLoaded(): void {
$this->drupalGet('admin/structure/types/manage/' . $this->drupalCreateContentType()->id() . '/fields/add-field');
$settings = $this->getDrupalSettings();
$css_libraries = [
'file/drupal.file-icon',
'text/drupal.text-icon',
'options/drupal.options-icon',
'comment/drupal.comment-icon',
'link/drupal.link-icon',
];
$libraries = explode(',', $settings['ajaxPageState']['libraries']);
foreach ($css_libraries as $css_library) {
$this->assertContains($css_library, $libraries);
}
}
}

View File

@@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
use Drupal\views\Entity\View;
use Drupal\views\Tests\ViewTestData;
/**
* Tests deletion of a field and their dependencies in the UI.
*
* @group field_ui
*/
class FieldUIDeleteTest extends BrowserTestBase {
use FieldUiTestTrait;
/**
* Modules to install.
*
* @var array
*/
protected static $modules = [
'node',
'field_ui',
'field_test',
'block',
'field_test_views',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Test views to enable.
*
* @var string[]
*/
public static $testViews = ['test_view_field_delete'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalPlaceBlock('page_title_block');
$this->drupalPlaceBlock('local_actions_block');
// Create a test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer content types',
'administer node fields',
'administer node form display',
'administer node display',
'administer users',
'administer account settings',
'administer user display',
'bypass node access',
]);
$this->drupalLogin($admin_user);
}
/**
* Tests that deletion removes field storages and fields as expected.
*/
public function testDeleteField(): void {
$field_label = $this->randomMachineName();
$field_name_input = 'test';
$field_name = 'field_test';
// Create an additional node type.
$type_name1 = $this->randomMachineName(8) . '_test';
$type1 = $this->drupalCreateContentType(['name' => $type_name1, 'type' => $type_name1]);
$type_name1 = $type1->id();
// Create a new field.
$bundle_path1 = 'admin/structure/types/manage/' . $type_name1;
$this->fieldUIAddNewField($bundle_path1, $field_name_input, $field_label);
// Create an additional node type.
$type_name2 = $this->randomMachineName(8) . '_test';
$type2 = $this->drupalCreateContentType(['name' => $type_name2, 'type' => $type_name2]);
$type_name2 = $type2->id();
// Add a field to the second node type.
$bundle_path2 = 'admin/structure/types/manage/' . $type_name2;
$this->fieldUIAddExistingField($bundle_path2, $field_name, $field_label);
\Drupal::service('module_installer')->install(['views']);
ViewTestData::createTestViews(static::class, ['field_test_views']);
$view = View::load('test_view_field_delete');
$this->assertNotNull($view);
$this->assertTrue($view->status());
// Test that the View depends on the field.
$dependencies = $view->getDependencies() + ['config' => []];
$this->assertContains("field.storage.node.$field_name", $dependencies['config']);
// Check the config dependencies of the first field, the field storage must
// not be shown as being deleted yet.
$this->drupalGet("$bundle_path1/fields/node.$type_name1.$field_name/delete");
$this->assertSession()->pageTextNotContains('The listed configuration will be deleted.');
$this->assertSession()->elementNotExists('xpath', '//ul[@data-drupal-selector="edit-view"]');
$this->assertSession()->pageTextNotContains('test_view_field_delete');
// Delete the first field.
$this->fieldUIDeleteField($bundle_path1, "node.$type_name1.$field_name", $field_label, $type_name1, 'content type');
// Check that the field was deleted.
$this->assertNull(FieldConfig::loadByName('node', $type_name1, $field_name), 'Field was deleted.');
// Check that the field storage was not deleted.
$this->assertNotNull(FieldStorageConfig::loadByName('node', $field_name), 'Field storage was not deleted.');
// Check the config dependencies of the first field.
$this->drupalGet("$bundle_path2/fields/node.$type_name2.$field_name/delete");
$this->assertSession()->pageTextContains('The listed configuration will be updated.');
$this->assertSession()->elementTextEquals('xpath', '//ul[@data-drupal-selector="edit-view"]', 'test_view_field_delete');
// Test that nothing is scheduled for deletion.
$this->assertSession()->elementNotExists('css', '#edit-entity-deletes');
// Delete the second field.
$this->fieldUIDeleteField($bundle_path2, "node.$type_name2.$field_name", $field_label, $type_name2, 'content type');
// Check that the field was deleted.
$this->assertNull(FieldConfig::loadByName('node', $type_name2, $field_name), 'Field was deleted.');
// Check that the field storage was deleted too.
$this->assertNull(FieldStorageConfig::loadByName('node', $field_name), 'Field storage was deleted.');
// Test that the View isn't deleted and has been disabled.
$view = View::load('test_view_field_delete');
$this->assertNotNull($view);
$this->assertFalse($view->status());
// Test that the View no longer depends on the deleted field.
$dependencies = $view->getDependencies() + ['config' => []];
$this->assertNotContains("field.storage.node.$field_name", $dependencies['config']);
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests indentation on Field UI.
*
* @group field_ui
*/
class FieldUIIndentationTest extends BrowserTestBase {
/**
* Modules to install.
*
* @var array
*/
protected static $modules = ['node', 'field_ui', 'field_ui_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer content types',
'administer node display',
]);
$this->drupalLogin($admin_user);
// Create Basic page node type.
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
}
public function testIndentation(): void {
$this->drupalGet('admin/structure/types/manage/page/display');
$this->assertSession()->responseContains('js-indentation indentation');
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the functionality of the Field UI route subscriber.
*
* @group field_ui
*/
class FieldUIRouteTest extends BrowserTestBase {
/**
* Modules to install.
*
* @var string[]
*/
protected static $modules = ['block', 'entity_test', 'field_ui'];
/**
* {@inheritdoc}
*
* @todo Remove and fix test to not rely on super user.
* @see https://www.drupal.org/project/drupal/issues/3437620
*/
protected bool $usesSuperUserAccessPolicy = TRUE;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->rootUser);
$this->drupalPlaceBlock('local_tasks_block');
}
/**
* Ensures that entity types with bundles do not break following entity types.
*/
public function testFieldUIRoutes(): void {
$this->drupalGet('entity_test_no_id/structure/entity_test/fields');
$this->assertSession()->pageTextContains('No fields are present yet.');
$this->drupalGet('admin/config/people/accounts/fields');
$this->assertSession()->titleEquals('Manage fields | Drupal');
$this->assertLocalTasks();
// Test manage display tabs and titles.
$this->drupalGet('admin/config/people/accounts/display/compact');
$this->assertSession()->statusCodeEquals(403);
$this->drupalGet('admin/config/people/accounts/display');
$this->assertSession()->titleEquals('Manage display | Drupal');
$this->assertLocalTasks();
$edit = ['display_modes_custom[compact]' => TRUE];
$this->submitForm($edit, 'Save');
$this->drupalGet('admin/config/people/accounts/display/compact');
$this->assertSession()->titleEquals('Manage display | Drupal');
$this->assertLocalTasks();
// Test manage form display tabs and titles.
$this->drupalGet('admin/config/people/accounts/form-display/register');
$this->assertSession()->statusCodeEquals(403);
$this->drupalGet('admin/config/people/accounts/form-display');
$this->assertSession()->titleEquals('Manage form display | Drupal');
$this->assertLocalTasks();
$edit = ['display_modes_custom[register]' => TRUE];
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet('admin/config/people/accounts/form-display/register');
$this->assertSession()->titleEquals('Manage form display | Drupal');
$this->assertLocalTasks();
// Test that default secondary tab is in first position.
$this->assertSession()->elementsCount('xpath', "//ul/li[1]/a[contains(text(), 'Default')]", 1);
// Create new view mode and verify it's available on the Manage Display
// screen after enabling it.
EntityViewMode::create([
'id' => 'user.test',
'label' => 'Test',
'targetEntityType' => 'user',
])->save();
$this->container->get('router.builder')->rebuildIfNeeded();
$edit = ['display_modes_custom[test]' => TRUE];
$this->drupalGet('admin/config/people/accounts/display');
$this->submitForm($edit, 'Save');
$this->assertSession()->linkExists('Test');
// Create new form mode and verify it's available on the Manage Form
// Display screen after enabling it.
EntityFormMode::create([
'id' => 'user.test',
'label' => 'Test',
'targetEntityType' => 'user',
])->save();
$this->container->get('router.builder')->rebuildIfNeeded();
$edit = ['display_modes_custom[test]' => TRUE];
$this->drupalGet('admin/config/people/accounts/form-display');
$this->submitForm($edit, 'Save');
$this->assertSession()->linkExists('Test');
}
/**
* Asserts that local tasks exists.
*
* @internal
*/
public function assertLocalTasks(): void {
$this->assertSession()->linkExists('Settings');
$this->assertSession()->linkExists('Manage fields');
$this->assertSession()->linkExists('Manage display');
$this->assertSession()->linkExists('Manage form display');
}
/**
* Asserts that admin routes are correctly marked as such.
*/
public function testAdminRoute(): void {
$route = \Drupal::service('router.route_provider')->getRouteByName('entity.entity_test.field_ui_fields');
$is_admin = \Drupal::service('router.admin_context')->isAdminRoute($route);
$this->assertTrue($is_admin, 'Admin route correctly marked for "Manage fields" page.');
}
}

View File

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

View File

@@ -0,0 +1,441 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Behat\Mink\Exception\ExpectationException;
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\Core\Url;
use Behat\Mink\Element\NodeElement;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\node\Entity\NodeType;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
/**
* Tests the Field UI "Manage display" and "Manage form display" screens.
*
* @group field_ui
* @group #slow
*/
class ManageDisplayTest extends BrowserTestBase {
use FieldUiTestTrait;
/**
* Modules to install.
*
* @var array
*/
protected static $modules = [
'node',
'field_ui',
'taxonomy',
'search',
'field_test',
'field_third_party_test',
'block',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* @var string
*/
private string $type;
/**
* @var string
*/
private string $vocabulary;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->drupalPlaceBlock('local_tasks_block');
// Create a test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer content types',
'administer display modes',
'administer node fields',
'administer node form display',
'administer node display',
'administer taxonomy',
'administer taxonomy_term fields',
'administer taxonomy_term display',
'administer users',
'administer account settings',
'administer user display',
'bypass node access',
]);
$this->drupalLogin($admin_user);
// Create content type, with underscores.
$type_name = $this->randomMachineName(8) . '_test';
$type = $this->drupalCreateContentType(['name' => $type_name, 'type' => $type_name]);
$this->type = $type->id();
// Create a default vocabulary.
$vocabulary = Vocabulary::create([
'name' => $this->randomMachineName(),
'description' => $this->randomMachineName(),
'vid' => $this->randomMachineName(),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'help' => '',
'nodes' => ['article' => 'article'],
'weight' => mt_rand(0, 10),
]);
$vocabulary->save();
$this->vocabulary = $vocabulary->id();
}
/**
* Tests switching view modes to use custom or 'default' settings'.
*/
public function testViewModeCustom(): void {
// Create a field, and a node with some data for the field.
$this->fieldUIAddNewField('admin/structure/types/manage/' . $this->type, 'test', 'Test field');
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
// For this test, use a formatter setting value that is an integer unlikely
// to appear in a rendered node other than as part of the field being tested
// (for example, unlikely to be part of the "Submitted by ... on ..." line).
$value = '12345';
$settings = [
'type' => $this->type,
'field_test' => [['value' => $value]],
];
$node = $this->drupalCreateNode($settings);
// Gather expected output values with the various formatters.
$formatter_plugin_manager = \Drupal::service('plugin.manager.field.formatter');
$field_test_default_settings = $formatter_plugin_manager->getDefaultSettings('field_test_default');
$field_test_with_prepare_view_settings = $formatter_plugin_manager->getDefaultSettings('field_test_with_prepare_view');
$output = [
'field_test_default' => $field_test_default_settings['test_formatter_setting'] . '|' . $value,
'field_test_with_prepare_view' => $field_test_with_prepare_view_settings['test_formatter_setting_additional'] . '|' . $value . '|' . ($value + 1),
];
// Check that the field is displayed with the default formatter in 'rss'
// mode (uses 'default'), and hidden in 'teaser' mode (uses custom settings).
$this->assertNodeViewText($node, 'rss', $output['field_test_default'], "The field is displayed as expected in view modes that use 'default' settings.");
$this->assertNodeViewNoText($node, 'teaser', $value, "The field is hidden in view modes that use custom settings.");
// Change formatter for 'default' mode, check that the field is displayed
// accordingly in 'rss' mode.
$edit = [
'fields[field_test][type]' => 'field_test_with_prepare_view',
'fields[field_test][region]' => 'content',
];
$this->drupalGet('admin/structure/types/manage/' . $this->type . '/display');
$this->submitForm($edit, 'Save');
$this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected in view modes that use 'default' settings.");
// Specialize the 'rss' mode, check that the field is displayed the same.
$edit = [
"display_modes_custom[rss]" => TRUE,
];
$this->drupalGet('admin/structure/types/manage/' . $this->type . '/display');
$this->submitForm($edit, 'Save');
$this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected in newly specialized 'rss' mode.");
// Set the field to 'hidden' in the view mode, check that the field is
// hidden.
$edit = [
'fields[field_test][region]' => 'hidden',
];
$this->drupalGet('admin/structure/types/manage/' . $this->type . '/display/rss');
$this->submitForm($edit, 'Save');
$this->assertNodeViewNoText($node, 'rss', $value, "The field is hidden in 'rss' mode.");
// Set the view mode back to 'default', check that the field is displayed
// accordingly.
$edit = [
"display_modes_custom[rss]" => FALSE,
];
$this->drupalGet('admin/structure/types/manage/' . $this->type . '/display');
$this->submitForm($edit, 'Save');
$this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected when 'rss' mode is set back to 'default' settings.");
// Specialize the view mode again.
$edit = [
"display_modes_custom[rss]" => TRUE,
];
$this->drupalGet('admin/structure/types/manage/' . $this->type . '/display');
$this->submitForm($edit, 'Save');
// Check that the previous settings for the view mode have been kept.
$this->assertNodeViewNoText($node, 'rss', $value, "The previous settings are kept when 'rss' mode is specialized again.");
}
/**
* Tests the local tasks are displayed correctly for view modes.
*/
public function testViewModeLocalTasks(): void {
$manage_display = 'admin/structure/types/manage/' . $this->type . '/display';
$this->drupalGet($manage_display);
$this->assertSession()->linkNotExists('Full content');
$this->assertSession()->linkExists('Teaser');
$this->drupalGet($manage_display . '/teaser');
$this->assertSession()->linkNotExists('Full content');
$this->assertSession()->linkExists('Default');
}
/**
* Tests that fields with no explicit display settings do not break.
*/
public function testNonInitializedFields(): void {
// Create a test field.
$this->fieldUIAddNewField('admin/structure/types/manage/' . $this->type, 'test', 'Test');
// Check that the field appears as 'hidden' on the 'Manage display' page
// for the 'teaser' mode.
$this->drupalGet('admin/structure/types/manage/' . $this->type . '/display/teaser');
$this->assertSession()->fieldValueEquals('fields[field_test][region]', 'hidden');
}
/**
* Tests hiding the view modes fieldset when there's only one available.
*/
public function testSingleViewMode(): void {
$this->drupalGet('admin/structure/taxonomy/manage/' . $this->vocabulary . '/display');
$this->assertSession()->pageTextNotContains('Use custom display settings for the following view modes');
// This may not trigger a notice when 'view_modes_custom' isn't available.
$this->drupalGet('admin/structure/taxonomy/manage/' . $this->vocabulary . '/overview/display');
$this->submitForm([], 'Save');
}
/**
* Tests that a message is shown when there are no fields.
*/
public function testNoFieldsDisplayOverview(): void {
// Create a fresh content type without any fields.
NodeType::create([
'type' => 'no_fields',
'name' => 'No fields',
])->save();
$this->drupalGet('admin/structure/types/manage/no_fields/display');
$this->assertSession()->pageTextContains("There are no fields yet added. You can add new fields on the Manage fields page.");
$this->assertSession()->linkByHrefExists(Url::fromRoute('entity.node.field_ui_fields', ['node_type' => 'no_fields'])->toString());
}
/**
* Tests if display mode local tasks appear in alphabetical order by label.
*/
public function testViewModeLocalTasksOrder(): void {
$manage_display = 'admin/structure/types/manage/' . $this->type . '/display';
// Specify the 'rss' mode, check that the field is displayed the same.
$edit = [
'display_modes_custom[rss]' => TRUE,
'display_modes_custom[teaser]' => TRUE,
];
$this->drupalGet($manage_display);
$this->submitForm($edit, 'Save');
$this->assertOrderInPage(['RSS', 'Teaser']);
$edit = [
'label' => 'Breezier',
];
$this->drupalGet('admin/structure/display-modes/view/manage/node.teaser');
$this->submitForm($edit, 'Save');
$this->assertOrderInPage(['Breezier', 'RSS']);
}
/**
* Tests if form mode local tasks appear in alphabetical order by label.
*/
public function testFormModeLocalTasksOrder(): void {
EntityFormMode::create([
'id' => 'node.big',
'label' => 'Big Form',
'targetEntityType' => 'node',
])->save();
EntityFormMode::create([
'id' => 'node.little',
'label' => 'Little Form',
'targetEntityType' => 'node',
])->save();
$manage_form = 'admin/structure/types/manage/' . $this->type . '/form-display';
$this->drupalGet($manage_form);
$this->assertOrderInPage(['Big Form', 'Little Form']);
$edit = [
'label' => 'Ultimate Form',
];
$this->drupalGet('admin/structure/display-modes/form/manage/node.big');
$this->submitForm($edit, 'Save');
$this->drupalGet($manage_form);
$this->assertOrderInPage(['Little Form', 'Ultimate Form']);
}
/**
* Asserts that a string is found in the rendered node in a view mode.
*
* @param \Drupal\Core\Entity\EntityInterface $node
* The node.
* @param string $view_mode
* The view mode in which the node should be displayed.
* @param string $text
* Plain text to look for.
* @param string $message
* Message to display.
*
* @internal
*/
public function assertNodeViewText(EntityInterface $node, string $view_mode, string $text, string $message): void {
$this->assertNodeViewTextHelper($node, $view_mode, $text, $message, FALSE);
}
/**
* Asserts that a string is not found in the rendered node in a view mode.
*
* @param \Drupal\Core\Entity\EntityInterface $node
* The node.
* @param string $view_mode
* The view mode in which the node should be displayed.
* @param string $text
* Plain text to look for.
* @param string $message
* Message to display.
*
* @internal
*/
public function assertNodeViewNoText(EntityInterface $node, string $view_mode, string $text, string $message): void {
$this->assertNodeViewTextHelper($node, $view_mode, $text, $message, TRUE);
}
/**
* Asserts that a string is (not) found in the rendered node in a view mode.
*
* This helper function is used by assertNodeViewText() and
* assertNodeViewNoText().
*
* @param \Drupal\Core\Entity\EntityInterface $node
* The node.
* @param string $view_mode
* The view mode in which the node should be displayed.
* @param string $text
* Plain text to look for.
* @param string $message
* Message to display.
* @param bool $not_exists
* TRUE if this text should not exist, FALSE if it should.
*
* @internal
*/
public function assertNodeViewTextHelper(EntityInterface $node, string $view_mode, string $text, string $message, bool $not_exists): void {
// Make sure caches on the tester side are refreshed after changes
// submitted on the tested side.
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
// Render a cloned node, so that we do not alter the original.
$clone = clone $node;
$element = \Drupal::entityTypeManager()
->getViewBuilder('node')
->view($clone, $view_mode);
$output = (string) \Drupal::service('renderer')->renderRoot($element);
if ($not_exists) {
$this->assertStringNotContainsString((string) $text, $output, $message);
}
else {
$this->assertStringContainsString((string) $text, $output, $message);
}
}
/**
* Checks if a select element contains the specified options.
*
* @param string $name
* The field name.
* @param array $expected_options
* An array of expected options.
*
* @internal
*/
protected function assertFieldSelectOptions(string $name, array $expected_options): void {
$xpath = $this->assertSession()->buildXPathQuery('//select[@name=:name]', [':name' => $name]);
$fields = $this->xpath($xpath);
if ($fields) {
$field = $fields[0];
$options = $this->getAllOptionsList($field);
sort($options);
sort($expected_options);
$this->assertSame($expected_options, $options);
}
else {
$this->fail('Unable to find field ' . $name);
}
}
/**
* Extracts all options from a select element.
*
* @param \Behat\Mink\Element\NodeElement $element
* The select element field information.
*
* @return array
* An array of option values as strings.
*/
protected function getAllOptionsList(NodeElement $element) {
$options = [];
// Add all options items.
foreach ($element->option as $option) {
$options[] = $option->getValue();
}
// Loops trough all the option groups
foreach ($element->optgroup as $optgroup) {
$options = array_merge($this->getAllOptionsList($optgroup), $options);
}
return $options;
}
/**
* Asserts that several pieces of markup are in a given order in the page.
*
* @param string[] $items
* An ordered list of strings.
*
* @throws \Behat\Mink\Exception\ExpectationException
* When any of the given string is not found.
*
* @internal
*
* @todo Remove this once https://www.drupal.org/node/2817657 is committed.
*/
protected function assertOrderInPage(array $items): void {
$session = $this->getSession();
$text = $session->getPage()->getHtml();
$strings = [];
foreach ($items as $item) {
if (($pos = strpos($text, $item)) === FALSE) {
throw new ExpectationException("Cannot find '$item' in the page", $session->getDriver());
}
$strings[$pos] = $item;
}
ksort($strings);
$ordered = implode(', ', array_map(function ($item) {
return "'$item'";
}, $items));
$this->assertSame($items, array_values($strings), "Found strings, ordered as: $ordered.");
}
}

View File

@@ -0,0 +1,440 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Behat\Mink\Exception\ElementNotFoundException;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests the Field UI "Manage fields" screen.
*
* @group field_ui
* @group #slow
*/
class ManageFieldsFunctionalTest extends ManageFieldsFunctionalTestBase {
/**
* Tests that default value is correctly validated and saved.
*/
public function testDefaultValue(): void {
// Create a test field storage and field.
$field_name = 'test';
FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'node',
'type' => 'test_field',
])->save();
$field = FieldConfig::create([
'field_name' => $field_name,
'entity_type' => 'node',
'bundle' => $this->contentType,
]);
$field->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getFormDisplay('node', $this->contentType)
->setComponent($field_name)
->save();
$admin_path = 'admin/structure/types/manage/' . $this->contentType . '/fields/' . $field->id();
$element_id = "edit-default-value-input-$field_name-0-value";
$element_name = "default_value_input[{$field_name}][0][value]";
$this->drupalGet($admin_path);
$this->assertSession()->fieldValueEquals($element_id, '');
// Check that invalid default values are rejected.
$edit = [$element_name => '-1', 'set_default_value' => '1'];
$this->drupalGet($admin_path);
$this->submitForm($edit, 'Save settings');
$this->assertSession()->pageTextContains("$field_name does not accept the value -1");
// Check that the default value is saved.
$edit = [$element_name => '1', 'set_default_value' => '1'];
$this->drupalGet($admin_path);
$this->submitForm($edit, 'Save settings');
$this->assertSession()->pageTextContains("Saved $field_name configuration");
$field = FieldConfig::loadByName('node', $this->contentType, $field_name);
$this->assertEquals([['value' => 1]], $field->getDefaultValueLiteral(), 'The default value was correctly saved.');
// Check that the default value shows up in the form.
$this->drupalGet($admin_path);
$this->assertSession()->fieldValueEquals($element_id, '1');
// Check that the default value is left empty when "Set default value"
// checkbox is not checked.
$edit = [$element_name => '1', 'set_default_value' => '0'];
$this->drupalGet($admin_path);
$this->submitForm($edit, 'Save settings');
$this->assertSession()->pageTextContains("Saved $field_name configuration");
$field = FieldConfig::loadByName('node', $this->contentType, $field_name);
$this->assertEquals([], $field->getDefaultValueLiteral(), 'The default value was removed.');
// Check that the default value can be emptied.
$this->drupalGet($admin_path);
$edit = [$element_name => ''];
$this->submitForm($edit, 'Save settings');
$this->assertSession()->pageTextContains("Saved $field_name configuration");
$field = FieldConfig::loadByName('node', $this->contentType, $field_name);
$this->assertEquals([], $field->getDefaultValueLiteral(), 'The default value was correctly saved.');
// Check that the default value can be empty when the field is marked as
// required and can store unlimited values.
$field_storage = FieldStorageConfig::loadByName('node', $field_name);
$field_storage->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$field_storage->save();
$this->drupalGet($admin_path);
$edit = [
'required' => 1,
];
$this->submitForm($edit, 'Save settings');
$this->drupalGet($admin_path);
$this->submitForm([], 'Save settings');
$this->assertSession()->pageTextContains("Saved $field_name configuration");
$field = FieldConfig::loadByName('node', $this->contentType, $field_name);
$this->assertEquals([], $field->getDefaultValueLiteral(), 'The default value was correctly saved.');
// Check that the default widget is used when the field is hidden.
$display_repository->getFormDisplay($field->getTargetEntityTypeId(), $field->getTargetBundle())
->removeComponent($field_name)
->save();
$this->drupalGet($admin_path);
$this->assertSession()->fieldValueEquals($element_id, '');
}
/**
* Tests that Field UI respects disallowed field names.
*/
public function testDisallowedFieldNames(): void {
// Reset the field prefix so we can test properly.
$this->config('field_ui.settings')->set('field_prefix', '')->save();
$label = 'Disallowed field';
$edit1 = [
'new_storage_type' => 'test_field',
];
$edit2 = [
'label' => $label,
];
// Try with an entity key.
$edit2['field_name'] = 'title';
$bundle_path = 'admin/structure/types/manage/' . $this->contentType;
$this->drupalGet("{$bundle_path}/fields/add-field");
$this->submitForm($edit1, 'Continue');
$this->submitForm($edit2, 'Continue');
$this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
// Try with a base field.
$edit2['field_name'] = 'sticky';
$bundle_path = 'admin/structure/types/manage/' . $this->contentType;
$this->drupalGet("{$bundle_path}/fields/add-field");
$this->submitForm($edit1, 'Continue');
$this->submitForm($edit2, 'Continue');
$this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
}
/**
* Tests that Field UI respects locked fields.
*/
public function testLockedField(): void {
// Create a locked field and attach it to a bundle. We need to do this
// programmatically as there's no way to create a locked field through UI.
$field_name = $this->randomMachineName(8);
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'node',
'type' => 'test_field',
'cardinality' => 1,
'locked' => TRUE,
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => $this->contentType,
])->save();
\Drupal::service('entity_display.repository')
->getFormDisplay('node', $this->contentType)
->setComponent($field_name, [
'type' => 'test_field_widget',
])
->save();
// Check that the links for edit and delete are not present.
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields');
$locked = $this->xpath('//tr[@id=:field_name]/td[4]', [':field_name' => $field_name]);
$this->assertSame('Locked', $locked[0]->getHtml(), 'Field is marked as Locked in the UI');
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/node.' . $this->contentType . '.' . $field_name . '/delete');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests that Field UI respects the 'no_ui' flag in the field type definition.
*/
public function testHiddenFields(): void {
// Check that the field type is not available in the 'add new field' row.
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/add-field');
$this->assertSession()->elementNotExists('css', "[name='new_storage_type'][value='hidden_test_field']");
$this->assertSession()->elementExists('css', "[name='new_storage_type'][value='shape']");
// Create a field storage and a field programmatically.
$field_name = 'hidden_test_field';
FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'node',
'type' => $field_name,
])->save();
$field = [
'field_name' => $field_name,
'bundle' => $this->contentType,
'entity_type' => 'node',
'label' => 'Hidden field',
];
FieldConfig::create($field)->save();
\Drupal::service('entity_display.repository')
->getFormDisplay('node', $this->contentType)
->setComponent($field_name)
->save();
$this->assertInstanceOf(FieldConfig::class, FieldConfig::load('node.' . $this->contentType . '.' . $field_name));
// Check that the newly added field appears on the 'Manage Fields'
// screen.
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields');
$this->assertSession()->elementTextContains('xpath', '//table[@id="field-overview"]//tr[@id="hidden-test-field"]//td[1]', $field['label']);
// Check that the field does not appear in the 're-use existing field' row
// on other bundles.
$this->drupalGet('admin/structure/types/manage/page/fields/reuse');
$this->assertSession()->elementNotExists('css', ".js-reuse-table [data-field-id='{$field_name}']");
$this->assertSession()->elementExists('css', '.js-reuse-table [data-field-id="field_tags"]');
// Check that non-configurable fields are not available.
$field_types = \Drupal::service('plugin.manager.field.field_type')->getDefinitions();
$this->drupalGet('admin/structure/types/manage/page/fields/add-field');
foreach ($field_types as $field_type => $definition) {
if (empty($definition['no_ui'])) {
try {
$this->assertSession()
->elementExists('css', "[name='new_storage_type'][value='$field_type']");
}
catch (ElementNotFoundException) {
if ($group = $this->getFieldFromGroup($field_type)) {
$this->assertSession()
->elementExists('css', "[name='new_storage_type'][value='$group']");
$this->submitForm(['new_storage_type' => $group], 'Continue');
$this->assertSession()
->elementExists('css', "[name='group_field_options_wrapper'][value='$field_type']");
$this->submitForm([], 'Back');
}
}
}
else {
$this->assertSession()->elementNotExists('css', "[name='new_storage_type'][value='$field_type']");
}
}
}
/**
* Tests that a duplicate field name is caught by validation.
*/
public function testDuplicateFieldName(): void {
// field_tags already exists, so we're expecting an error when trying to
// create a new field with the same name.
$url = 'admin/structure/types/manage/' . $this->contentType . '/fields/add-field';
$this->drupalGet($url);
$edit = [
'new_storage_type' => 'boolean',
];
$this->submitForm($edit, 'Continue');
$edit = [
'label' => $this->randomMachineName(),
'field_name' => 'tags',
];
$this->submitForm($edit, 'Continue');
$this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
$this->assertSession()->addressEquals($url);
}
/**
* Tests that external URLs in the 'destinations' query parameter are blocked.
*/
public function testExternalDestinations(): void {
$options = [
'query' => ['destinations' => ['http://example.com']],
];
$this->drupalGet('admin/structure/types/manage/article/fields/node.article.body', $options);
$this->submitForm([], 'Save settings');
// The external redirect should not fire.
$this->assertSession()->addressEquals('admin/structure/types/manage/article/fields/node.article.body?destinations%5B0%5D=http%3A//example.com');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->responseContains('Attempt to update field <em class="placeholder">Body</em> failed: <em class="placeholder">The internal path component &#039;http://example.com&#039; is external. You are not allowed to specify an external URL together with internal:/.</em>.');
}
/**
* Tests that deletion removes field storages and fields as expected for a term.
*/
public function testDeleteTaxonomyField(): void {
// Create a new field.
$bundle_path = 'admin/structure/taxonomy/manage/tags/overview';
$this->fieldUIAddNewField($bundle_path, $this->fieldNameInput, $this->fieldLabel);
// Delete the field.
$this->fieldUIDeleteField($bundle_path, "taxonomy_term.tags.$this->fieldName", $this->fieldLabel, 'Tags', 'taxonomy vocabulary');
// Check that the field was deleted.
$this->assertNull(FieldConfig::loadByName('taxonomy_term', 'tags', $this->fieldName), 'Field was deleted.');
// Check that the field storage was deleted too.
$this->assertNull(FieldStorageConfig::loadByName('taxonomy_term', $this->fieldName), 'Field storage was deleted.');
}
/**
* Tests that help descriptions render valid HTML.
*/
public function testHelpDescriptions(): void {
// Create an image field.
FieldStorageConfig::create([
'field_name' => 'field_image',
'entity_type' => 'node',
'type' => 'image',
])->save();
FieldConfig::create([
'field_name' => 'field_image',
'entity_type' => 'node',
'label' => 'Image',
'bundle' => 'article',
])->save();
\Drupal::service('entity_display.repository')
->getFormDisplay('node', 'article')
->setComponent('field_image')
->save();
$edit = [
'description' => '<strong>Test with an upload field.',
];
$this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_image');
$this->submitForm($edit, 'Save settings');
// Check that hook_field_widget_single_element_form_alter() does believe
// this is the default value form.
$this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_tags');
$this->assertSession()->pageTextContains('From hook_field_widget_single_element_form_alter(): Default form is true.');
$edit = [
'description' => '<em>Test with a non upload field.',
];
$this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_tags');
$this->submitForm($edit, 'Save settings');
$this->drupalGet('node/add/article');
$this->assertSession()->responseContains('<strong>Test with an upload field.</strong>');
$this->assertSession()->responseContains('<em>Test with a non upload field.</em>');
}
/**
* Tests the "preconfigured field" functionality.
*
* @see \Drupal\Core\Field\PreconfiguredFieldUiOptionsInterface
*/
public function testPreconfiguredFields(): void {
$this->drupalGet('admin/structure/types/manage/article/fields/add-field');
// Check that the preconfigured field option exist alongside the regular
// field type option.
$this->assertSession()->elementExists('css', "[name='new_storage_type'][value='field_ui:test_field_with_preconfigured_options:custom_options']");
$this->assertSession()->elementExists('css', "[name='new_storage_type'][value='test_field_with_preconfigured_options']");
// Add a field with every possible preconfigured value.
$this->fieldUIAddNewField(NULL, 'test_custom_options', 'Test label', 'field_ui:test_field_with_preconfigured_options:custom_options');
$field_storage = FieldStorageConfig::loadByName('node', 'field_test_custom_options');
$this->assertEquals(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, $field_storage->getCardinality());
$this->assertEquals('preconfigured_storage_setting', $field_storage->getSetting('test_field_storage_setting'));
$field = FieldConfig::loadByName('node', 'article', 'field_test_custom_options');
$this->assertTrue($field->isRequired());
$this->assertEquals('preconfigured_field_setting', $field->getSetting('test_field_setting'));
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$form_display = $display_repository->getFormDisplay('node', 'article');
$this->assertEquals('test_field_widget_multiple', $form_display->getComponent('field_test_custom_options')['type']);
$view_display = $display_repository->getViewDisplay('node', 'article');
$this->assertEquals('field_test_multiple', $view_display->getComponent('field_test_custom_options')['type']);
$this->assertEquals('altered dummy test string', $view_display->getComponent('field_test_custom_options')['settings']['test_formatter_setting_multiple']);
}
/**
* Tests the access to non-existent field URLs.
*/
public function testNonExistentFieldUrls(): void {
$field_id = 'node.foo.bar';
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id);
$this->assertSession()->statusCodeEquals(404);
}
/**
* Tests that the 'field_prefix' setting works on Field UI.
*/
public function testFieldPrefix(): void {
// Change default field prefix.
$field_prefix = $this->randomMachineName(10);
$this->config('field_ui.settings')->set('field_prefix', $field_prefix)->save();
// Create a field input and label exceeding the new maxlength, which is 22.
$field_exceed_max_length_label = $this->randomString(23);
$field_exceed_max_length_input = $this->randomMachineName(23);
// Try to create the field.
$edit1 = [
'new_storage_type' => 'test_field',
];
$edit2 = [
'label' => $field_exceed_max_length_label,
'field_name' => $field_exceed_max_length_input,
];
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/add-field');
$this->submitForm($edit1, 'Continue');
$this->submitForm($edit2, 'Continue');
$this->assertSession()->pageTextContains('Machine-readable name cannot be longer than 22 characters but is currently 23 characters long.');
// Create a valid field.
$this->fieldUIAddNewField('admin/structure/types/manage/' . $this->contentType, $this->fieldNameInput, $this->fieldLabel);
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/node.' . $this->contentType . '.' . $field_prefix . $this->fieldNameInput);
$this->assertSession()->pageTextContains($this->fieldLabel . ' settings for ' . $this->contentType);
}
/**
* Test translation defaults.
*/
public function testTranslationDefaults(): void {
$this->fieldUIAddNewField('admin/structure/types/manage/' . $this->contentType, $this->fieldNameInput, $this->fieldLabel);
$field_storage = FieldStorageConfig::loadByName('node', 'field_' . $this->fieldNameInput);
$this->assertTrue($field_storage->isTranslatable(), 'Field storage translatable.');
$field = FieldConfig::loadByName('node', $this->contentType, 'field_' . $this->fieldNameInput);
$this->assertFalse($field->isTranslatable(), 'Field instance should not be translatable by default.');
// Add a new field based on an existing field.
$this->drupalCreateContentType(['type' => 'additional', 'name' => 'Additional type']);
$this->fieldUIAddExistingField("admin/structure/types/manage/additional", $this->fieldName, 'Additional type');
$field_storage = FieldStorageConfig::loadByName('node', 'field_' . $this->fieldNameInput);
$this->assertTrue($field_storage->isTranslatable(), 'Field storage translatable.');
$field = FieldConfig::loadByName('node', 'additional', 'field_' . $this->fieldNameInput);
$this->assertFalse($field->isTranslatable(), 'Field instance should not be translatable by default.');
}
}

View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Core\Language\LanguageInterface;
use Drupal\node\Entity\NodeType;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
/**
* Tests the Field UI "Manage fields" screen.
*/
class ManageFieldsFunctionalTestBase extends BrowserTestBase {
use FieldUiTestTrait;
use EntityReferenceFieldCreationTrait;
/**
* Modules to install.
*
* @var array
*/
protected static $modules = [
'node',
'field_ui',
'field_test',
'taxonomy',
'image',
'block',
'node_access_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The ID of the custom content type created for testing.
*
* @var string
*/
protected $contentType;
/**
* The label for a random field to be created for testing.
*
* @var string
*/
protected $fieldLabel;
/**
* The input name of a random field to be created for testing.
*
* @var string
*/
protected $fieldNameInput;
/**
* The name of a random field to be created for testing.
*
* @var string
*/
protected $fieldName;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->drupalPlaceBlock('local_actions_block');
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalPlaceBlock('page_title_block');
// Create a test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer content types',
'bypass node access',
'administer node fields',
'administer node form display',
'administer node display',
'administer taxonomy',
'administer taxonomy_term fields',
'administer taxonomy_term display',
'administer users',
'administer account settings',
'administer user display',
]);
$this->drupalLogin($admin_user);
// Create content type, with underscores.
$type_name = $this->randomMachineName(8) . '_test';
$type = $this->drupalCreateContentType(['name' => $type_name, 'type' => $type_name]);
$this->contentType = $type->id();
// Create random field name with markup to test escaping.
$this->fieldLabel = '<em>' . $this->randomMachineName(8) . '</em>';
$this->fieldNameInput = $this->randomMachineName(8);
$this->fieldName = 'field_' . $this->fieldNameInput;
// Create Basic page and Article node types.
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
// Create a vocabulary named "Tags".
$vocabulary = Vocabulary::create([
'name' => 'Tags',
'vid' => 'tags',
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
$vocabulary->save();
// Create a vocabulary named "Kittens".
Vocabulary::create([
'name' => 'Kittens',
'vid' => 'kittens',
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
])->save();
$handler_settings = [
'target_bundles' => [
$vocabulary->id() => $vocabulary->id(),
],
];
$this->createEntityReferenceField('node', 'article', 'field_' . $vocabulary->id(), 'Tags', 'taxonomy_term', 'default', $handler_settings);
\Drupal::service('entity_display.repository')
->getFormDisplay('node', 'article')
->setComponent('field_' . $vocabulary->id())
->save();
// Setup node access testing.
node_access_rebuild();
node_access_test_add_field(NodeType::load('article'));
\Drupal::state()->set('node_access_test.private', TRUE);
}
}

View File

@@ -0,0 +1,333 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests the Field UI "Manage fields" screen.
*
* @group field_ui
* @group #slow
*/
class ManageFieldsLifecycleTest extends ManageFieldsFunctionalTestBase {
/**
* Runs the field CRUD tests.
*
* In order to act on the same fields, and not create the fields over and over
* again the following tests create, update and delete the same fields.
*/
public function testCRUDFields(): void {
$this->manageFieldsPage();
$this->createField();
$this->updateField();
$this->addExistingField();
$this->cardinalitySettings();
$this->fieldListAdminPage();
$this->deleteField();
$this->addPersistentFieldStorage();
}
/**
* Tests the manage fields page.
*
* @param string $type
* (optional) The name of a content type.
*/
protected function manageFieldsPage($type = '') {
$type = empty($type) ? $this->contentType : $type;
$this->drupalGet('admin/structure/types/manage/' . $type . '/fields');
// Check all table columns.
$table_headers = ['Label', 'Machine name', 'Field type', 'Operations'];
foreach ($table_headers as $table_header) {
// We check that the label appear in the table headings.
$this->assertSession()->responseContains($table_header . '</th>');
}
// Test the "Create a new field" action link.
$this->assertSession()->linkExists('Create a new field');
// Assert entity operations for all fields.
$number_of_links = 2;
$number_of_links_found = 0;
$operation_links = $this->xpath('//ul[@class = "dropbutton"]/li/a');
$url = base_path() . "admin/structure/types/manage/$type/fields/node.$type.body";
foreach ($operation_links as $link) {
switch ($link->getAttribute('title')) {
case 'Edit field settings.':
$this->assertSame($url, $link->getAttribute('href'));
$number_of_links_found++;
break;
case 'Delete field.':
$this->assertSame("$url/delete", $link->getAttribute('href'));
$number_of_links_found++;
break;
}
}
$this->assertEquals($number_of_links, $number_of_links_found);
}
/**
* Tests adding a new field.
*
* @todo Assert properties can be set in the form and read back in
* $field_storage and $fields.
*/
protected function createField() {
// Create a test field.
$this->fieldUIAddNewField('admin/structure/types/manage/' . $this->contentType, $this->fieldNameInput, $this->fieldLabel);
}
/**
* Tests editing an existing field.
*/
protected function updateField() {
$field_id = 'node.' . $this->contentType . '.' . $this->fieldName;
// Go to the field edit page.
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id);
$this->assertSession()->assertEscaped($this->fieldLabel);
// Populate the field settings with new settings.
$string = 'updated dummy test string';
$edit = [
'settings[test_field_setting]' => $string,
'field_storage[subform][settings][test_field_storage_setting]' => $string,
];
$this->assertSession()->pageTextContains('Default value');
$this->submitForm($edit, 'Save settings');
// Assert the field settings are correct.
$this->assertFieldSettings($this->contentType, $this->fieldName, $string);
// Assert redirection back to the "manage fields" page.
$this->assertSession()->addressEquals('admin/structure/types/manage/' . $this->contentType . '/fields');
}
/**
* Tests adding an existing field in another content type.
*/
protected function addExistingField() {
// Check "Re-use existing field" appears.
$this->drupalGet('admin/structure/types/manage/page/fields');
$this->assertSession()->pageTextContains('Re-use an existing field');
$this->clickLink('Re-use an existing field');
// Check that fields of other entity types (here, the 'comment_body' field)
// do not show up in the "Re-use existing field" list.
$this->assertSession()->elementNotExists('css', '.js-reuse-table [data-field-id="comment_body"]');
// Validate the FALSE assertion above by also testing a valid one.
$this->assertSession()->elementExists('css', ".js-reuse-table [data-field-id='{$this->fieldName}']");
$new_label = $this->fieldLabel . '_2';
// Add a new field based on an existing field.
$this->fieldUIAddExistingField("admin/structure/types/manage/page", $this->fieldName, $new_label);
}
/**
* Tests the cardinality settings of a field.
*
* We do not test if the number can be submitted with anything else than a
* numeric value. That is tested already in FormTest::testNumber().
*/
protected function cardinalitySettings() {
$field_edit_path = 'admin/structure/types/manage/article/fields/node.article.body';
// Assert the cardinality other field cannot be empty when cardinality is
// set to 'number'.
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => '',
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->assertSession()->pageTextContains('Number of values is required.');
// Submit a custom number.
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 6,
];
$this->submitForm($edit, 'Update settings');
$this->submitForm([], 'Save settings');
$this->drupalGet($field_edit_path);
$this->assertSession()->fieldValueEquals('field_storage[subform][cardinality]', 'number');
$this->assertSession()->fieldValueEquals('field_storage[subform][cardinality_number]', 6);
// Add two entries in the body.
$edit = ['title[0][value]' => 'Cardinality', 'body[0][value]' => 'Body 1', 'body[1][value]' => 'Body 2'];
$this->drupalGet('node/add/article');
$this->submitForm($edit, 'Save');
// Assert that you can't set the cardinality to a lower number than the
// highest delta of this field.
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 1,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->assertSession()->pageTextContains("There is 1 entity with 2 or more values in this field");
// Create a second entity with three values.
$edit = ['title[0][value]' => 'Cardinality 3', 'body[0][value]' => 'Body 1', 'body[1][value]' => 'Body 2', 'body[2][value]' => 'Body 3'];
$this->drupalGet('node/add/article');
$this->submitForm($edit, 'Save');
// Set to unlimited.
$edit = [
'field_storage[subform][cardinality]' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->submitForm([], 'Save settings');
$this->drupalGet($field_edit_path);
$this->assertSession()->fieldValueEquals('field_storage[subform][cardinality]', FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$this->assertSession()->fieldValueEquals('field_storage[subform][cardinality_number]', 1);
// Assert that you can't set the cardinality to a lower number then the
// highest delta of this field but can set it to the same.
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 1,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->submitForm([], 'Save settings');
$this->assertSession()->pageTextContains("There are 2 entities with 2 or more values in this field");
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 2,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->assertSession()->pageTextContains("There is 1 entity with 3 or more values in this field");
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 3,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
// Test the cardinality validation is not access sensitive.
// Remove the cardinality limit 4 so we can add a node the user doesn't have access to.
$edit = [
'field_storage[subform][cardinality]' => (string) FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$node = $this->drupalCreateNode([
'private' => TRUE,
'uid' => 0,
'type' => 'article',
]);
$node->body->appendItem('body 1');
$node->body->appendItem('body 2');
$node->body->appendItem('body 3');
$node->body->appendItem('body 4');
$node->save();
// Assert that you can't set the cardinality to a lower number then the
// highest delta of this field (including inaccessible entities) but can
// set it to the same.
$this->drupalGet($field_edit_path);
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 2,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->assertSession()->pageTextContains("There are 2 entities with 3 or more values in this field");
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 3,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->assertSession()->pageTextContains("There is 1 entity with 4 or more values in this field");
$edit = [
'field_storage[subform][cardinality]' => 'number',
'field_storage[subform][cardinality_number]' => 4,
];
$this->drupalGet($field_edit_path);
$this->submitForm($edit, 'Update settings');
$this->submitForm([], 'Save settings');
}
/**
* Tests deleting a field from the field edit form.
*/
protected function deleteField() {
// Delete the field.
$field_id = 'node.' . $this->contentType . '.' . $this->fieldName;
$this->drupalGet('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field_id);
$this->clickLink('Delete');
$this->assertSession()->statusCodeEquals(200);
}
/**
* Tests that persistent field storage appears in the field UI.
*/
protected function addPersistentFieldStorage() {
$field_storage = FieldStorageConfig::loadByName('node', $this->fieldName);
// Persist the field storage even if there are no fields.
$field_storage->set('persist_with_no_fields', TRUE)->save();
// Delete all instances of the field.
foreach ($field_storage->getBundles() as $node_type) {
// Delete all the body field instances.
$this->drupalGet('admin/structure/types/manage/' . $node_type . '/fields/node.' . $node_type . '.' . $this->fieldName);
$this->clickLink('Delete');
$this->submitForm([], 'Delete');
}
// Check "Re-use existing field" appears.
$this->drupalGet('admin/structure/types/manage/page/fields');
$this->assertSession()->pageTextContains('Re-use an existing field');
// Ensure that we test with a label that contains HTML.
$label = $this->randomString(4) . '<br/>' . $this->randomString(4);
// Add a new field for the orphaned storage.
$this->fieldUIAddExistingField("admin/structure/types/manage/page", $this->fieldName, $label);
}
/**
* Asserts field settings are as expected.
*
* @param string $bundle
* The bundle name for the field.
* @param string $field_name
* The field name for the field.
* @param string $string
* The settings text.
* @param string $entity_type
* The entity type for the field.
*
* @internal
*/
protected function assertFieldSettings(string $bundle, string $field_name, string $string = 'dummy test string', string $entity_type = 'node'): void {
// Assert field storage settings.
$field_storage = FieldStorageConfig::loadByName($entity_type, $field_name);
$this->assertSame($string, $field_storage->getSetting('test_field_storage_setting'), 'Field storage settings were found.');
// Assert field settings.
$field = FieldConfig::loadByName($entity_type, $bundle, $field_name);
$this->assertSame($string, $field->getSetting('test_field_setting'), 'Field settings were found.');
}
/**
* Tests that the field list administration page operates correctly.
*/
protected function fieldListAdminPage() {
$this->drupalGet('admin/reports/fields');
$this->assertSession()->pageTextContains($this->fieldName);
$this->assertSession()->linkByHrefExists('admin/structure/types/manage/' . $this->contentType . '/fields');
}
}

View File

@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\field\Entity\FieldConfig;
/**
* Tests the Field UI "Manage fields" screen.
*
* @group field_ui
* @group #slow
*/
class ManageFieldsMultipleTypesTest extends ManageFieldsFunctionalTestBase {
/**
* Tests that options are copied over when reusing a field.
*
* @dataProvider entityTypesProvider
*/
public function testReuseField($entity_type, $bundle1, $bundle2): void {
$field_name = 'test_reuse';
$label = $this->randomMachineName();
// Create field with pre-configured options.
$this->drupalGet($bundle1['path'] . "/fields/add-field");
$this->fieldUIAddNewField(NULL, $field_name, $label, 'field_ui:test_field_with_preconfigured_options:custom_options');
$new_label = $this->randomMachineName();
$this->fieldUIAddExistingField($bundle2['path'], "field_{$field_name}", $new_label);
$field = FieldConfig::loadByName($entity_type, $bundle2['id'], "field_{$field_name}");
$this->assertTrue($field->isRequired());
$this->assertEquals($new_label, $field->label());
$this->assertEquals('preconfigured_field_setting', $field->getSetting('test_field_setting'));
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$form_display = $display_repository->getFormDisplay($entity_type, $bundle2['id']);
$this->assertEquals('test_field_widget_multiple', $form_display->getComponent("field_{$field_name}")['type']);
$view_display = $display_repository->getViewDisplay($entity_type, $bundle2['id']);
$this->assertEquals('field_test_multiple', $view_display->getComponent("field_{$field_name}")['type']);
$this->assertEquals('altered dummy test string', $view_display->getComponent("field_{$field_name}")['settings']['test_formatter_setting_multiple']);
}
/**
* Tests that options are copied over when reusing a field.
*
* @dataProvider entityTypesProvider
*/
public function testReuseFieldMultipleDisplay($entity_type, $bundle1, $bundle2): void {
// Create additional form mode and enable it on both bundles.
EntityFormMode::create([
'id' => "{$entity_type}.little",
'label' => 'Little Form',
'targetEntityType' => $entity_type,
])->save();
$form_display = EntityFormDisplay::create([
'id' => "{$entity_type}.{$bundle1['id']}.little",
'targetEntityType' => $entity_type,
'status' => TRUE,
'bundle' => $bundle1['id'],
'mode' => 'little',
]);
$form_display->save();
EntityFormDisplay::create([
'id' => "{$entity_type}.{$bundle2['id']}.little",
'targetEntityType' => $entity_type,
'status' => TRUE,
'bundle' => $bundle2['id'],
'mode' => 'little',
])->save();
// Create additional view mode and enable it on both bundles.
EntityViewMode::create([
'id' => "{$entity_type}.little",
'targetEntityType' => $entity_type,
'status' => TRUE,
'enabled' => TRUE,
'label' => 'Little View Mode',
])->save();
$view_display = EntityViewDisplay::create([
'id' => "{$entity_type}.{$bundle1['id']}.little",
'targetEntityType' => $entity_type,
'status' => TRUE,
'bundle' => $bundle1['id'],
'mode' => 'little',
]);
$view_display->save();
EntityViewDisplay::create([
'id' => "{$entity_type}.{$bundle2['id']}.little",
'targetEntityType' => $entity_type,
'status' => TRUE,
'bundle' => $bundle2['id'],
'mode' => 'little',
])->save();
$field_name = 'test_reuse';
$label = $this->randomMachineName();
// Create field with pre-configured options.
$this->drupalGet($bundle1['path'] . "/fields/add-field");
$this->fieldUIAddNewField(NULL, $field_name, $label, 'field_ui:test_field_with_preconfigured_options:custom_options');
$view_display->setComponent("field_{$field_name}", [
'type' => 'field_test_default',
'region' => 'content',
])->save();
$form_display->setComponent("field_{$field_name}", [
'type' => 'test_field_widget',
'region' => 'content',
])->save();
$new_label = $this->randomMachineName();
$this->fieldUIAddExistingField($bundle2['path'], "field_{$field_name}", $new_label);
$field = FieldConfig::loadByName($entity_type, $bundle2['id'], "field_{$field_name}");
$this->assertTrue($field->isRequired());
$this->assertEquals($new_label, $field->label());
$this->assertEquals('preconfigured_field_setting', $field->getSetting('test_field_setting'));
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
// Ensure that the additional form display has correct settings.
$form_display = $display_repository->getFormDisplay($entity_type, $bundle2['id'], $form_display->getMode());
$this->assertEquals('test_field_widget', $form_display->getComponent("field_{$field_name}")['type']);
// Ensure that the additional view display has correct settings.
$view_display = $display_repository->getViewDisplay($entity_type, $bundle2['id'], $view_display->getMode());
$this->assertEquals('field_test_default', $view_display->getComponent("field_{$field_name}")['type']);
}
/**
* Data provider for testing Field UI with multiple entity types.
*
* @return array
* Test cases.
*/
public static function entityTypesProvider() {
return [
'node' => [
'entity_type' => 'node',
'bundle1' => [
'id' => 'article',
'path' => 'admin/structure/types/manage/article',
],
'bundle2' => [
'id' => 'page',
'path' => 'admin/structure/types/manage/page',
],
],
'taxonomy' => [
'entity_type' => 'taxonomy_term',
'bundle1' => [
'id' => 'tags',
'path' => 'admin/structure/taxonomy/manage/tags/overview',
],
'bundle2' => [
'id' => 'kittens',
'path' => 'admin/structure/taxonomy/manage/kittens/overview',
],
],
];
}
}

View File

@@ -0,0 +1,400 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
use Drupal\user\Entity\User;
// cSpell:ignore downlander
/**
* Tests the Manage Display page of a fieldable entity type.
*
* @group field_ui
* @group #slow
*/
class ManageFieldsTest extends BrowserTestBase {
use FieldUiTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'field_test',
'field_ui',
'field_ui_test',
'node',
'text',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to administer node fields, etc.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser(['administer node fields']);
$this->drupalLogin($this->adminUser);
$this->config('system.logging')
->set('error_level', ERROR_REPORTING_DISPLAY_ALL)
->save();
}
/**
* Tests drop button operations on the manage fields page.
*/
public function testFieldDropButtonOperations(): void {
$assert_session = $this->assertSession();
$node_type = $this->drupalCreateContentType();
$bundle = $node_type->id();
/** @var \Drupal\field\FieldStorageConfigInterface $storage */
$storage = $this->container->get('entity_type.manager')
->getStorage('field_storage_config')
->create([
'type' => 'string',
'field_name' => 'highlander',
'entity_type' => 'node',
]);
$storage->save();
$this->container->get('entity_type.manager')
->getStorage('field_config')
->create([
'field_storage' => $storage,
'bundle' => $bundle,
])
->save();
$this->drupalGet("/admin/structure/types/manage/{$bundle}/fields");
// Check that the summary element for the string field type exists and has
// the correct text (which comes from the FieldItemBase class).
$element = $assert_session->elementExists('css', '#highlander');
$summary = $assert_session->elementExists('css', '.field-settings-summary-cell > ul > li', $element);
$field_label = $this->container->get('plugin.manager.field.field_type')->getDefinitions()['string']['label'];
$this->assertEquals($field_label, $summary->getText());
// Add an entity reference field, and check that its summary is custom.
/** @var \Drupal\field\FieldStorageConfigInterface $storage */
$storage = $this->container->get('entity_type.manager')
->getStorage('field_storage_config')
->create([
'type' => 'entity_reference',
'field_name' => 'downlander',
'entity_type' => 'node',
'settings' => [
'target_type' => 'node',
],
]);
$storage->save();
$this->container->get('entity_type.manager')
->getStorage('field_config')
->create([
'field_storage' => $storage,
'bundle' => $bundle,
'entity_type' => 'node',
'settings' => [
'handler_settings' => [
'target_bundles' => [$bundle => $bundle],
],
],
])
->save();
$this->drupalGet("/admin/structure/types/manage/{$bundle}/fields");
$element = $assert_session->elementExists('css', '#downlander');
$custom_summary_text = 'Reference type: Content';
$allowed_bundles_text = "Content type: $bundle";
$this->assertStringContainsString($custom_summary_text, $element->getText());
$this->assertStringContainsString($allowed_bundles_text, $element->getText());
}
/**
* Tests adding a field.
*/
public function testAddField(): void {
$page = $this->getSession()->getPage();
$type = $this->drupalCreateContentType([
'name' => 'Article',
'type' => 'article',
]);
// Make sure field descriptions appear, both 1 line and multiple lines.
$this->drupalGet('/admin/structure/types/manage/' . $type->id() . '/fields/add-field');
$edit = [
'new_storage_type' => 'field_test_descriptions',
];
$this->submitForm($edit, 'Continue');
$this->assertSession()->pageTextContains('This one-line field description is important for testing');
$this->assertSession()->pageTextContains('This multiple line description needs to use an array');
$this->assertSession()->pageTextContains('This second line contains important information');
// Create a new field without actually saving it.
$this->fieldUIAddNewField('admin/structure/types/manage/' . $type->id(), 'test_field', 'Test field', 'test_field', [], [], FALSE);
// Assert that the field was not created.
$this->assertNull(FieldStorageConfig::loadByName('node', "field_test_field"));
$this->drupalGet('/admin/structure/types/manage/' . $type->id() . '/fields/add-field');
$edit = [
'new_storage_type' => 'test_field',
];
$this->submitForm($edit, 'Continue');
$edit = [
'label' => 'Test field',
'field_name' => 'test_field',
];
$this->submitForm($edit, 'Continue');
$this->assertSession()->statusMessageNotContains('Saved');
// Change the storage form values.
$edit = ['field_storage[subform][cardinality_number]' => 5];
$this->submitForm($edit, 'Update settings');
$this->assertSession()->statusMessageNotContains('Saved');
// Assert that the form values persist.
$this->assertEquals(5, $page->findField('field_storage[subform][cardinality_number]')->getValue());
// Try creating a field with the same machine name.
$this->drupalGet('/admin/structure/types/manage/' . $type->id() . '/fields/add-field');
$edit = [
'new_storage_type' => 'test_field',
];
$this->submitForm($edit, 'Continue');
$edit = [
'label' => 'Test field',
'field_name' => 'test_field',
];
$this->submitForm($edit, 'Continue');
// Assert that the values in the field storage form are reset.
$this->assertEquals(1, $page->findField('field_storage[subform][cardinality_number]')->getValue());
// Assert that the field is created with the new settings.
$this->submitForm([], 'Update settings');
$this->assertSession()->statusMessageNotContains('Saved');
$this->submitForm([], 'Save settings');
$this->assertSession()->statusMessageContains('Saved');
$this->assertEquals(1, FieldStorageConfig::loadByName('node', 'field_test_field')->getCardinality());
}
/**
* Tests multiple users adding a field with the same name.
*/
public function testAddFieldWithMultipleUsers(): void {
$page = $this->getSession()->getPage();
// Create two users.
$user1 = $this->drupalCreateUser(['administer node fields']);
$user2 = $this->drupalCreateUser(['administer node fields']);
$node_type = $this->drupalCreateContentType();
$bundle_path = '/admin/structure/types/manage/' . $node_type->id();
// Start adding a field as user 1, stop prior to saving, but keep the URL.
$this->drupalLogin($user1);
$this->drupalGet($bundle_path . '/fields/add-field');
$edit = [
'new_storage_type' => 'test_field',
];
$this->submitForm($edit, 'Continue');
$edit = [
'label' => 'Test field',
'field_name' => 'test_field',
];
$this->submitForm($edit, 'Continue');
// Make changes to the storage form.
$edit = ['field_storage[subform][cardinality_number]' => 5];
$storage_form_url = $this->getUrl();
$this->submitForm($edit, 'Update settings');
$this->drupalLogout();
// Actually add a field as user 2.
$this->drupalLogin($user2);
$this->drupalGet($bundle_path . '/fields/add-field');
$edit = [
'new_storage_type' => 'test_field',
];
$this->submitForm($edit, 'Continue');
$edit = [
'label' => 'Test field',
'field_name' => 'test_field',
];
$this->submitForm($edit, 'Continue');
$allowed_no_of_values = $page->findField('field_storage[subform][cardinality_number]')->getValue();
// Assert that the changes made by any user do not affect other users until
// the field is saved.
$this->assertEquals(1, $allowed_no_of_values);
$this->submitForm(['field_storage[subform][cardinality_number]' => 2], 'Update settings');
$this->submitForm([], 'Save settings');
$this->assertSession()->pageTextContains("Saved Test field configuration.");
$this->drupalLogout();
// Continue adding a field as user 1, using the URL saved previously.
$this->drupalLogin($user1);
$this->drupalGet($storage_form_url);
// Assert that the user can go on with configuring a field with a machine
// that is already taken.
$this->assertSession()->pageTextNotContains('error');
$this->submitForm([], 'Save settings');
// An error is thrown only after the final 'Save'.
$this->assertSession()->statusMessageContains("An error occurred while saving the field: 'field_storage_config' entity with ID 'node.field_test_field' already exists.");
}
/**
* Tests editing field when the field exists in temp store.
*/
public function testEditFieldWithLeftOverFieldInTempStore(): void {
$user = $this->drupalCreateUser(['administer node fields']);
$node_type = $this->drupalCreateContentType();
$bundle_path = '/admin/structure/types/manage/' . $node_type->id();
// Start adding a field but stop prior to saving.
$this->drupalLogin($user);
$this->drupalGet($bundle_path . '/fields/add-field');
$edit = [
'new_storage_type' => 'test_field',
];
$this->submitForm($edit, 'Continue');
$edit = [
'label' => 'Test field',
'field_name' => 'test_field',
];
$this->submitForm($edit, 'Continue');
/** @var \Drupal\field\FieldStorageConfigInterface $storage */
$storage = $this->container->get('entity_type.manager')
->getStorage('field_storage_config')
->create([
'type' => 'test_field',
'field_name' => 'test_field',
'entity_type' => 'node',
]);
$storage->save();
$this->container->get('entity_type.manager')
->getStorage('field_config')
->create([
'field_storage' => $storage,
'bundle' => $node_type->id(),
'entity_type' => 'node',
])
->save();
$this->drupalGet("$bundle_path/fields/node.{$node_type->id()}.test_field");
$this->submitForm([], 'Save settings');
$this->assertSession()->statusMessageContains('Saved test_field configuration.', 'status');
}
/**
* Tests creating entity reference field to non-bundleable entity type.
*/
public function testEntityReferenceToNonBundleableEntity(): void {
$type = $this->drupalCreateContentType([
'name' => 'kittens',
'type' => 'kittens',
]);
$bundle_path = 'admin/structure/types/manage/' . $type->id();
$field_name = 'field_user_reference';
$field_edit = [
'set_default_value' => '1',
"default_value_input[$field_name][0][target_id]" => $this->adminUser->label() . ' (' . $this->adminUser->id() . ')',
];
$this->fieldUIAddNewField($bundle_path, 'user_reference', NULL, 'field_ui:entity_reference:user', [], $field_edit);
$field = FieldConfig::loadByName('node', 'kittens', $field_name);
$this->assertEquals([['target_id' => $this->adminUser->id()]], $field->getDefaultValue(User::create(['name' => '1337'])));
}
/**
* Tests hook_form_field_storage_config_form_edit_alter().
*
* @group legacy
*/
public function testFieldStorageFormAlter(): void {
$this->container->get('module_installer')->install(['field_ui_test_deprecated']);
$this->rebuildContainer();
$node_type = $this->drupalCreateContentType();
$bundle = $node_type->id();
$this->expectDeprecation('The deprecated alter hook hook_form_field_storage_config_edit_form_alter() is implemented in these functions: field_ui_test_deprecated_form_field_storage_config_edit_form_alter. Use hook_form_field_config_edit_form_alter() instead. See https://www.drupal.org/node/3386675.');
$this->drupalGet("/admin/structure/types/manage/$bundle/fields/node.$bundle.body");
$this->assertSession()->elementTextContains('css', '#edit-field-storage', 'Greetings from the field_storage_config_edit_form() alter.');
}
/**
* Tests hook_form_field_storage_config_form_edit_alter().
*
* @group legacy
*/
public function testFieldTypeCardinalityAlter(): void {
$node_type = $this->drupalCreateContentType();
$bundle = $node_type->id();
/** @var \Drupal\field\FieldStorageConfigInterface $storage */
$storage = $this->container->get('entity_type.manager')
->getStorage('field_storage_config')
->create([
'type' => 'test_field',
'field_name' => 'field_test_field',
'entity_type' => 'node',
]);
$storage->save();
$this->container->get('entity_type.manager')
->getStorage('field_config')
->create([
'field_storage' => $storage,
'bundle' => $bundle,
'entity_type' => 'node',
])
->save();
$this->drupalGet("/admin/structure/types/manage/$bundle/fields/node.$bundle.field_test_field");
$this->assertSession()->elementTextContains('css', '#edit-field-storage', 'Greetings from Drupal\field_test\Plugin\Field\FieldType\TestItem::storageSettingsForm');
}
/**
* Tests hook_field_info_entity_type_ui_definitions_alter().
*/
public function testFieldUiDefinitionsAlter(): void {
$user = $this->drupalCreateUser(['administer node fields']);
$node_type = $this->drupalCreateContentType();
$this->drupalLogin($user);
$this->drupalGet('/admin/structure/types/manage/' . $node_type->id() . '/fields/add-field');
$this->assertSession()->pageTextContains('Boolean (overridden by alter)');
}
/**
* Ensure field category fallback works for field types without a description.
*/
public function testFieldCategoryFallbackWithoutDescription(): void {
$user = $this->drupalCreateUser(['administer node fields']);
$node_type = $this->drupalCreateContentType();
$this->drupalLogin($user);
$this->drupalGet('/admin/structure/types/manage/' . $node_type->id() . '/fields/add-field');
$field_type = $this->assertSession()->elementExists('xpath', '//label[text()="Test field"]');
$description_container = $field_type->getParent()->find('css', '.field-option__description');
$this->assertNotNull($description_container);
$this->assertEquals('', $description_container->getText());
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait;
use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
/**
* Tests the default value widget in Field UI.
*
* @group field_ui
*/
class DefaultValueWidgetTest extends WebDriverTestBase {
use TaxonomyTestTrait;
use FieldUiJSTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'field_ui',
'taxonomy',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a Content type and two test nodes.
$this->createContentType(['type' => 'test_content']);
$user = $this->drupalCreateUser([
'access content',
'administer content types',
'administer node fields',
]);
$this->drupalLogin($user);
}
/**
* Tests default value options on field config change.
*/
public function testDefaultValueOptionsForChangingBundles(): void {
$vocab_1 = $this->createVocabulary(['name' => 'Colors']);
$this->createTerm($vocab_1, ['name' => 'red']);
$this->createTerm($vocab_1, ['name' => 'green']);
$vocab_2 = $this->createVocabulary(['name' => 'Tags']);
$this->createTerm($vocab_2, ['name' => 'random tag 1']);
$this->createTerm($vocab_2, ['name' => 'random tag 2']);
$field_name = 'test_field';
$this->fieldUIAddNewFieldJS('admin/structure/types/manage/test_content', $field_name, $field_name, 'entity_reference', FALSE);
$page = $this->getSession()->getPage();
$page->findField('field_storage[subform][settings][target_type]')->selectOption('taxonomy_term');
$this->assertSession()->assertWaitOnAjaxRequest();
$page->findField('settings[handler_settings][target_bundles][' . $vocab_1->id() . ']')->check();
$this->assertSession()->assertWaitOnAjaxRequest();
$page->findField('set_default_value')->check();
$default_value_field = $page->findField('default_value_input[field_' . $field_name . '][0][target_id]');
$default_value_field->setValue('r');
$this->getSession()->getDriver()->keyDown($default_value_field->getXpath(), ' ');
$this->assertSession()->waitOnAutocomplete();
// Check the autocomplete results.
$results = $page->findAll('css', '.ui-autocomplete li');
$this->assertCount(2, $results);
$this->assertSession()->elementTextNotContains('css', '.ui-autocomplete li', 'random tag 1');
$this->assertSession()->elementTextContains('css', '.ui-autocomplete li', 'green');
}
}

View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\FunctionalJavascript;
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the bundle selection for view & form display modes.
*
* @group field_ui
*/
class DisplayModeBundleSelectionTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'field_ui',
'block',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType([
'name' => 'Article',
'type' => 'article',
]);
$this->drupalCreateContentType([
'name' => 'Page',
'type' => 'page',
]);
$this->drupalPlaceBlock('local_actions_block');
$user = $this->drupalCreateUser([
'administer display modes',
'administer node display',
'administer node form display',
]);
// Create a new form mode 'foobar' for content.
EntityFormMode::create([
'id' => 'node.foobar',
'targetEntityType' => 'node',
'label' => 'Foobar',
])->save();
$this->drupalLogin($user);
}
/**
* Tests the bundle selection.
*
* @param string $display_mode
* View or Form display mode.
* @param string $path
* Display mode path.
* @param string $custom_mode
* Custom mode to test.
*
* @dataProvider providerBundleSelection
*/
public function testBundleSelection($display_mode, $path, $custom_mode): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
// Add new display mode for content.
$this->drupalGet("/admin/structure/display-modes/$display_mode");
$this->assertNotEmpty($assert_session->waitForText("Add $display_mode mode"));
$this->clickLink("Add $display_mode mode for Content");
$this->assertNotEmpty($assert_session->waitForText("Add new Content $display_mode mode"));
$page->find('css', '[data-drupal-selector="edit-label"]')->setValue('test');
$page->find('css', '[data-drupal-selector="edit-bundles-by-entity-article"]')->check();
$page->find('css', '.ui-dialog-buttonset')->pressButton('Save');
// Verify that test display mode is selected for article content type.
$this->drupalGet("/admin/structure/types/manage/article/$path");
$page->find('css', '[data-drupal-selector="edit-modes"]')->pressButton('Custom display settings');
$checkbox = $page->find('css', '[data-drupal-selector="edit-display-modes-custom-test"]');
$this->assertTrue($checkbox->isChecked());
// Verify that test display mode is not selected for page content type.
$this->drupalGet("/admin/structure/types/manage/page/$path");
$page->find('css', '[data-drupal-selector="edit-modes"]')->pressButton('Custom display settings');
$checkbox = $page->find('css', '[data-drupal-selector="edit-display-modes-custom-test"]');
$this->assertFalse($checkbox->isChecked());
// Click Add view/form display mode button.
$this->drupalGet("/admin/structure/display-modes/$display_mode");
$this->assertNotEmpty($assert_session->waitForText("Add $display_mode mode"));
$this->clickLink("Add $display_mode mode");
$this->assertNotEmpty($assert_session->waitForText("Choose $display_mode mode entity type"));
// Add new view/form display mode for content.
$this->clickLink('Content');
$this->assertNotEmpty($assert_session->waitForText("Add new Content $display_mode mode"));
$page->find('css', '[data-drupal-selector="edit-label"]')->setValue('test2');
$page->find('css', '[data-drupal-selector="edit-bundles-by-entity-article"]')->check();
$page->find('css', '.ui-dialog-buttonset')->pressButton('Save');
// Verify that test2 display mode is selected for article content type.
$this->drupalGet("/admin/structure/types/manage/article/$path");
$page->find('css', '[data-drupal-selector="edit-modes"]')->pressButton('Custom display settings');
$checkbox = $page->find('css', '[data-drupal-selector="edit-display-modes-custom-test2"]');
$this->assertTrue($checkbox->isChecked());
// Verify that test2 display mode is not selected for page content type.
$this->drupalGet("/admin/structure/types/manage/page/$path");
$page->find('css', '[data-drupal-selector="edit-modes"]')->pressButton('Custom display settings');
$checkbox = $page->find('css', '[data-drupal-selector="edit-display-modes-custom-test2"]');
$this->assertFalse($checkbox->isChecked());
// Verify that display mode is not selected on article content type.
$this->drupalGet("/admin/structure/types/manage/article/$path");
$page->find('css', '[data-drupal-selector="edit-modes"]')->pressButton('Custom display settings');
$checkbox = $page->find('css', "[data-drupal-selector='edit-display-modes-custom-$custom_mode']");
$this->assertFalse($checkbox->isChecked());
// Edit existing display mode and enable it for article content type.
$this->drupalGet("/admin/structure/display-modes/$display_mode");
$this->assertNotEmpty($assert_session->waitForText("Add $display_mode mode"));
$page->find('xpath', '//ul[@class = "dropbutton"]/li[1]/a')->click();
$this->assertNotEmpty($assert_session->waitForText("This $display_mode mode will still be available for the rest of the Content types if not checked here, but it will not be enabled by default."));
$page->find('css', '[data-drupal-selector="edit-bundles-by-entity-article"]')->check();
$page->find('css', '.ui-dialog-buttonset')->pressButton('Save');
// Verify that display mode is selected on article content type.
$this->drupalGet("/admin/structure/types/manage/article/$path");
$page->find('css', '[data-drupal-selector="edit-modes"]')->pressButton('Custom display settings');
$checkbox = $page->find('css', "[data-drupal-selector='edit-display-modes-custom-$custom_mode']");
$this->assertTrue($checkbox->isChecked());
}
/**
* Data provider for testBundleSelection().
*/
public static function providerBundleSelection() {
return [
'view display' => ['view', 'display', 'full'],
'form display' => ['form', 'form-display', 'foobar'],
];
}
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\FunctionalJavascript;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the UI for entity displays.
*
* @group field_ui
*/
class EntityDisplayTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['field_ui', 'entity_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$entity = EntityTest::create([
'name' => 'The name for this entity',
'field_test_text' => [
['value' => 'The field test text value'],
],
]);
$entity->save();
$this->drupalLogin($this->drupalCreateUser([
'access administration pages',
'view test entity',
'administer entity_test content',
'administer entity_test fields',
'administer entity_test display',
'administer entity_test form display',
'view the administration theme',
]));
}
/**
* Tests the use of regions for entity form displays.
*/
public function testEntityForm(): void {
$this->drupalGet('entity_test/manage/1/edit');
$this->assertSession()->fieldExists('field_test_text[0][value]');
$this->drupalGet('entity_test/structure/entity_test/form-display');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
$this->getSession()->getPage()->pressButton('Show row weights');
$this->assertSession()->waitForElementVisible('css', '[name="fields[field_test_text][region]"]');
$this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'hidden');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Your settings have been saved.');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
$this->drupalGet('entity_test/manage/1/edit');
$this->assertSession()->fieldNotExists('field_test_text[0][value]');
}
/**
* Tests the use of regions for entity view displays.
*/
public function testEntityView(): void {
$this->drupalGet('entity_test/1');
$this->assertSession()->pageTextNotContains('The field test text value');
$this->drupalGet('entity_test/structure/entity_test/display');
$this->assertSession()->elementExists('css', '.region-content-message.region-empty');
$this->getSession()->getPage()->pressButton('Show row weights');
$this->assertSession()->waitForElementVisible('css', '[name="fields[field_test_text][region]"]');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
$this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Your settings have been saved.');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
$this->drupalGet('entity_test/1');
$this->assertSession()->pageTextContains('The field test text value');
}
/**
* Tests extra fields.
*/
public function testExtraFields(): void {
entity_test_create_bundle('bundle_with_extra_fields');
$this->drupalGet('entity_test/structure/bundle_with_extra_fields/display');
$this->assertSession()->waitForElement('css', '.tabledrag-handle');
$id = $this->getSession()->getPage()->find('css', '[name="form_build_id"]')->getValue();
$extra_field_row = $this->getSession()->getPage()->find('css', '#display-extra-field');
$disabled_region_row = $this->getSession()->getPage()->find('css', '.region-hidden-title');
$extra_field_row->find('css', '.handle')->dragTo($disabled_region_row);
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertSession()
->waitForElement('css', "[name='form_build_id']:not([value='$id'])");
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Your settings have been saved.');
}
}

View File

@@ -0,0 +1,505 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\FunctionalJavascript;
use Behat\Mink\Element\NodeElement;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait;
use Drupal\Tests\field_ui\Traits\FieldUiTestTrait;
/**
* Tests the Field UI "Manage display" and "Manage form display" screens.
*
* @group field_ui
*/
class ManageDisplayTest extends WebDriverTestBase {
use FieldUiTestTrait;
use FieldUiJSTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'field_ui',
'field_test',
'field_third_party_test',
'block',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* @var string
*/
protected $type;
/**
* @var \Drupal\Core\Entity\entityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $displayStorage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
// Create a test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer content types',
'administer node fields',
'administer node form display',
'administer node display',
'administer users',
'administer account settings',
'administer user display',
'bypass node access',
]);
$this->drupalLogin($admin_user);
// Create content type, with underscores.
$type_name = $this->randomMachineName(8) . '_test';
$type = $this->drupalCreateContentType(['name' => $type_name, 'type' => $type_name]);
$this->type = $type->id();
$this->entityTypeManager = $this->container->get('entity_type.manager');
}
/**
* Tests formatter settings.
*/
public function testFormatterUI(): void {
$manage_fields = 'admin/structure/types/manage/' . $this->type;
$manage_display = $manage_fields . '/display';
// Create a field, and a node with some data for the field.
$this->fieldUIAddNewFieldJS($manage_fields, 'test', 'Test field');
$display_id = 'node.' . $this->type . '.default';
$displayStorage = $this->entityTypeManager->getStorage('entity_view_display');
// Get the display options (formatter and settings) that were automatically
// assigned for the 'default' display.
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
$display = $displayStorage->loadUnchanged($display_id);
$display_options = $display->getComponent('field_test');
$format = $display_options['type'];
$default_settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings($format);
$setting_name = key($default_settings);
$setting_value = $display_options['settings'][$setting_name];
// Display the "Manage display" screen and check that the expected formatter
// is selected.
$this->drupalGet($manage_display);
$session = $this->getSession();
$assert_session = $this->assertSession();
$page = $session->getPage();
// Find commonly used elements in this test.
$button_save = $page->findButton('Save');
$field_test_format_type = $page->findField('fields[field_test][type]');
$field_test_drag_handle = $page->find('css', '#field-test .tabledrag-handle');
$field_test_settings = $page->find('css', 'input[name="field_test_settings_edit"]');
$weight_toggle = $page->find('css', '.tabledrag-toggle-weight');
// Assert the format type field is visible and contains the expected
// formatter.
$this->assertTrue($field_test_format_type->isVisible());
$this->assertEquals($format, $field_test_format_type->getValue());
$assert_session->responseContains("$setting_name: $setting_value");
// Validate the selectbox.
$this->assertFieldSelectOptions($field_test_format_type, [
'field_no_settings',
'field_empty_test',
'field_empty_setting',
'field_test_default',
'field_test_multiple',
'field_test_with_prepare_view',
'field_test_applicable',
]);
// Ensure that fields can be hidden directly by dragging the element.
$target = $page->find('css', '.region-hidden-message');
$field_test_drag_handle->dragTo($target);
$assert_session->assertExpectedAjaxRequest(1);
$button_save->click();
// Validate the changed display settings on the server.
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
$display = $displayStorage->loadUnchanged($display_id);
$this->assertNull($display->getComponent('field_test'));
// Switch to manual mode.
$weight_toggle->click();
$field_region = $page->findField('fields[field_test][region]');
// Change the region to content using the region field.
$this->assertEquals('hidden', $field_region->getValue());
$field_region->setValue('content');
// Confirm the region element retains focus after the AJAX update completes.
$this->assertJsCondition('document.activeElement === document.querySelector("[name=\'fields[field_test][region]\']")');
$button_save->click();
// Change the format for the test field.
$field_test_format_type->setValue('field_test_multiple');
$assert_session->assertExpectedAjaxRequest(1);
// Confirm the format element retains focus after the AJAX update completes.
$this->assertJsCondition('document.activeElement === document.querySelector("[name=\'fields[field_test][type]\']")');
$plugin_summary = $page->find('css', '#field-test .field-plugin-summary');
$this->assertStringContainsString("test_formatter_setting_multiple: dummy test string", $plugin_summary->getText(), 'The expected summary is displayed.');
// Submit the form and assert that
// hook_field_formatter_settings_summary_alter() is called.
$button_save->click();
$assert_session->responseContains('field_test_field_formatter_settings_summary_alter');
// Open the settings form for the test field.
$field_test_settings->click();
$assert_session->assertExpectedAjaxRequest(1);
// Assert that the field added in
// field_test_field_formatter_third_party_settings_form() is present.
$field_third_party = $page->findField('fields[field_test][settings_edit_form][third_party_settings][field_third_party_test][field_test_field_formatter_third_party_settings_form]');
$this->assertNotEmpty($field_third_party, 'The field added in hook_field_formatter_third_party_settings_form() is present on the settings form.');
// Change the value and submit the form to save the third party settings.
$field_third_party->setValue('foo');
$page->findButton('Update')->click();
$assert_session->assertExpectedAjaxRequest(2);
$button_save->click();
// Assert the third party settings.
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
$this->drupalGet($manage_display);
$id = 'node.' . $this->type . '.default';
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
$display = $displayStorage->loadUnchanged($id);
$this->assertEquals('foo', $display->getRenderer('field_test')->getThirdPartySetting('field_third_party_test', 'field_test_field_formatter_third_party_settings_form'));
$this->assertContains('field_third_party_test', $display->calculateDependencies()->getDependencies()['module'], 'The display has a dependency on field_third_party_test module.');
// Change the formatter to an empty setting and validate it's initialized
// correctly.
$field_test_format_type = $page->findField('fields[field_test][type]');
$field_test_format_type->setValue('field_empty_setting');
$assert_session->assertExpectedAjaxRequest(1);
$assert_session->responseNotContains('Default empty setting now has a value.');
$this->assertTrue($field_test_settings->isVisible());
// Set the empty_setting option to a non-empty value again and validate
// the formatting summary now display's this correctly.
$field_test_settings->click();
$assert_session->assertExpectedAjaxRequest(2);
$field_empty_setting = $page->findField('fields[field_test][settings_edit_form][settings][field_empty_setting]');
$field_empty_setting->setValue('non empty setting');
$page->findButton('Update')->click();
$assert_session->assertExpectedAjaxRequest(3);
$assert_session->responseContains('Default empty setting now has a value.');
// Test the settings form behavior. An edit button should be present since
// there are third party settings to configure.
$field_test_format_type->setValue('field_no_settings');
$this->assertTrue($field_test_settings->isVisible());
// Make sure we can save the third party settings when there are no settings
// available.
$field_test_settings->click();
$assert_session->assertExpectedAjaxRequest(4);
$page->findButton('Update')->click();
// When a module providing third-party settings to a formatter (or widget)
// is uninstalled, the formatter remains enabled but the provided settings,
// together with the corresponding form elements, are removed from the
// display component.
\Drupal::service('module_installer')->uninstall(['field_third_party_test']);
// Ensure the button is still there after the module has been disabled.
$this->drupalGet($manage_display);
$this->assertTrue($field_test_settings->isVisible());
// Ensure that third-party form elements are not present anymore.
$field_test_settings->click();
$assert_session->assertExpectedAjaxRequest(1);
$field_third_party = $page->findField('fields[field_test][settings_edit_form][third_party_settings][field_third_party_test][field_test_field_formatter_third_party_settings_form]');
$this->assertEmpty($field_third_party);
// Ensure that third-party settings were removed from the formatter.
/** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
$display = $displayStorage->loadUnchanged($display_id);
$component = $display->getComponent('field_test');
$this->assertArrayNotHasKey('field_third_party_test', $component['third_party_settings']);
}
/**
* Tests widget settings.
*/
public function testWidgetUI(): void {
// Admin Manage Fields page.
$manage_fields = 'admin/structure/types/manage/' . $this->type;
// Admin Manage Display page.
$manage_display = $manage_fields . '/form-display';
$form_storage = $this->entityTypeManager->getStorage('entity_form_display');
// Creates a new field that can be used with multiple formatters.
// Reference: Drupal\field_test\Plugin\Field\FieldWidget\TestFieldWidgetMultiple::isApplicable().
$this->fieldUIAddNewFieldJS($manage_fields, 'test', 'Test field');
// Get the display options (formatter and settings) that were automatically
// assigned for the 'default' display.
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
$display = $form_storage->loadUnchanged("node.{$this->type}.default");
$display_options = $display->getComponent('field_test');
$widget_type = $display_options['type'];
$default_settings = \Drupal::service('plugin.manager.field.widget')->getDefaultSettings($widget_type);
$setting_name = key($default_settings);
$setting_value = $display_options['settings'][$setting_name];
// Display the "Manage form display" screen and check if the expected
// widget is selected.
$this->drupalGet($manage_display);
$session = $this->getSession();
$assert_session = $this->assertSession();
$page = $session->getPage();
$field_test_settings = $page->find('css', 'input[name="field_test_settings_edit"]');
$field_test_type = $page->findField('fields[field_test][type]');
$button_save = $page->findButton('Save');
$this->assertEquals($widget_type, $field_test_type->getValue(), 'The expected widget is selected.');
$assert_session->responseContains("$setting_name: $setting_value");
// Check whether widget weights are respected.
$this->assertFieldSelectOptions($field_test_type, [
'test_field_widget',
'test_field_widget_multilingual',
'test_field_widget_multiple',
]);
$field_test_type->setValue('test_field_widget_multiple');
$assert_session->assertExpectedAjaxRequest(1);
$button_save->click();
$this->drupalGet($manage_display);
$widget_type = 'test_field_widget_multiple';
$default_settings = \Drupal::service('plugin.manager.field.widget')->getDefaultSettings($widget_type);
$setting_name = key($default_settings);
$setting_value = $default_settings[$setting_name];
$this->assertEquals($widget_type, $field_test_type->getValue(), 'The expected widget is selected.');
$assert_session->responseContains("$setting_name: $setting_value");
$button_save->click();
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
$display = $form_storage->loadUnchanged("node.{$this->type}.default");
$display_options = $display->getComponent('field_test');
$current_widget = $display_options['type'];
$current_setting_value = $display_options['settings'][$setting_name];
$this->assertEquals($current_widget, $widget_type, 'The widget was updated.');
$this->assertEquals($current_setting_value, $setting_value, 'The setting was updated.');
// Assert that hook_field_widget_settings_summary_alter() is called.
$assert_session->responseContains('field_test_field_widget_settings_summary_alter');
$field_test_settings->click();
$assert_session->assertExpectedAjaxRequest(1);
// Assert that the field added in
// field_test_field_widget_third_party_settings_form() is present.
$field_third_party_test = $page->findField('fields[field_test][settings_edit_form][third_party_settings][field_third_party_test][field_test_widget_third_party_settings_form]');
$this->assertNotEmpty($field_third_party_test, 'The field added in hook_field_widget_third_party_settings_form() is present on the settings form.');
$field_third_party_test->setValue('foo');
$page->findButton('Update')->click();
$assert_session->assertWaitOnAjaxRequest();
$button_save->click();
$this->drupalGet($manage_display);
// Assert the third party settings.
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
$display = $form_storage->loadUnchanged('node.' . $this->type . '.default');
$this->assertEquals('foo', $display->getRenderer('field_test')->getThirdPartySetting('field_third_party_test', 'field_test_widget_third_party_settings_form'));
$this->assertContains('field_third_party_test', $display->calculateDependencies()->getDependencies()['module'], 'Form display does not have a dependency on field_third_party_test module.');
// Creates a new field that can not be used with the multiple formatter.
// Reference: Drupal\field_test\Plugin\Field\FieldWidget\TestFieldWidgetMultiple::isApplicable().
$this->fieldUIAddNewFieldJS($manage_fields, 'onewidgetfield', 'One Widget Field');
// Go to the Manage Form Display.
$this->drupalGet($manage_display);
$field_onewidgetfield_type = $page->findField('fields[field_onewidgetfield][type]');
$field_test_drag_handle = $page->find('css', '#field-test .tabledrag-handle');
$field_region = $page->findField('fields[field_test][region]');
$weight_toggle = $page->find('css', '.tabledrag-toggle-weight');
$target = $page->find('css', '.region-hidden-message');
// Checks if the select elements contain the specified options.
$this->assertFieldSelectOptions($field_test_type, [
'test_field_widget',
'test_field_widget_multilingual',
'test_field_widget_multiple',
]);
$this->assertFieldSelectOptions($field_onewidgetfield_type, [
'test_field_widget',
'test_field_widget_multilingual',
]);
$field_test_drag_handle->dragTo($target);
$assert_session->assertWaitOnAjaxRequest();
$button_save->click();
// Validate the changed display settings on the server.
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
$display = $form_storage->loadUnchanged("node.{$this->type}.default");
$this->assertNull($display->getComponent('field_test'));
// Switch to manual mode.
$weight_toggle->click();
// Change the region to content using the region field.
$this->assertEquals('hidden', $field_region->getValue());
$field_region->setValue('content');
$button_save->click();
// Validate the change on the server.
$this->drupalGet($manage_display);
$display = EntityFormDisplay::load("node.{$this->type}.default");
$this->assertNotNull($display->getComponent('field_test'));
}
/**
* Checks if a select element contains the specified options.
*
* @param \Behat\Mink\Element\NodeElement $field
* The select field to validate.
* @param array $expected_options
* An array of expected options.
* @param string|null $selected
* The default value to validate.
*
* @internal
*/
protected function assertFieldSelectOptions(NodeElement $field, array $expected_options, ?string $selected = NULL): void {
/** @var \Behat\Mink\Element\NodeElement[] $select_options */
$select_options = $field->findAll('xpath', 'option');
// Validate the number of options.
$this->assertSameSize($expected_options, $select_options);
// Validate the options and expected order.
foreach ($select_options as $key => $option) {
$this->assertEquals($option->getAttribute('value'), $expected_options[$key]);
}
// Validate the default value if passed.
if (!is_null($selected)) {
$this->assertEquals($selected, $field->getValue());
}
}
/**
* Confirms that notifications to save appear when necessary.
*/
public function testNotAppliedUntilSavedWarning(): void {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Admin Manage Fields page.
$manage_fields = 'admin/structure/types/manage/' . $this->type;
$this->fieldUIAddNewFieldJS($manage_fields, 'test', 'Test field');
$manage_display = 'admin/structure/types/manage/' . $this->type . '/display';
$manage_form = 'admin/structure/types/manage/' . $this->type . '/form-display';
// Form display, change widget type.
$this->drupalGet($manage_form);
$assert_session->elementNotExists('css', '.tabledrag-changed-warning');
$assert_session->elementNotExists('css', 'abbr.tabledrag-changed');
$page->selectFieldOption('fields[uid][type]', 'options_buttons');
$this->assertNotNull($changed_warning = $assert_session->waitForElementVisible('css', '.tabledrag-changed-warning'));
$this->assertNotNull($assert_session->waitForElementVisible('css', ' #uid abbr.tabledrag-changed'));
$this->assertSame('* You have unsaved changes.', $changed_warning->getText());
// Form display, change widget settings.
$this->drupalGet($manage_form);
$edit_widget_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-uid-settings-edit"]');
$edit_widget_button->press();
$assert_session->waitForText('3rd party formatter settings form');
// Confirm the AJAX operation of opening the form does not result in the row
// being set as changed. New settings must be submitted for that to happen.
$assert_session->elementNotExists('css', 'abbr.tabledrag-changed');
$cancel_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-uid-settings-edit-form-actions-cancel-settings"]');
$cancel_button->press();
$assert_session->assertNoElementAfterWait('css', '[data-drupal-selector="edit-fields-uid-settings-edit-form-actions-cancel-settings"]');
$assert_session->elementNotExists('css', '.tabledrag-changed-warning');
$assert_session->elementNotExists('css', 'abbr.tabledrag-changed');
$edit_widget_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-uid-settings-edit"]');
$edit_widget_button->press();
$widget_field = $assert_session->waitForField('fields[uid][settings_edit_form][third_party_settings][field_third_party_test][field_test_widget_third_party_settings_form]');
$widget_field->setValue('honk');
$update_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-uid-settings-edit-form-actions-save-settings"]');
$update_button->press();
$assert_session->assertNoElementAfterWait('css', '[data-drupal-selector="edit-fields-field-test-settings-edit-form-actions-cancel-settings"]');
$this->assertNotNull($changed_warning = $assert_session->waitForElementVisible('css', '.tabledrag-changed-warning'));
$this->assertNotNull($assert_session->waitForElementVisible('css', ' #uid abbr.tabledrag-changed'));
$this->assertSame('* You have unsaved changes.', $changed_warning->getText());
// Content display, change formatter type.
$this->drupalGet($manage_display);
$assert_session->elementNotExists('css', '.tabledrag-changed-warning');
$assert_session->elementNotExists('css', 'abbr.tabledrag-changed');
$page->selectFieldOption('edit-fields-field-test-label', 'inline');
$this->assertNotNull($changed_warning = $assert_session->waitForElementVisible('css', '.tabledrag-changed-warning'));
$this->assertNotNull($assert_session->waitForElementVisible('css', ' #field-test abbr.tabledrag-changed'));
$this->assertSame('* You have unsaved changes.', $changed_warning->getText());
// Content display, change formatter settings.
$this->drupalGet($manage_display);
$assert_session->elementNotExists('css', '.tabledrag-changed-warning');
$assert_session->elementNotExists('css', 'abbr.tabledrag-changed');
$edit_formatter_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-field-test-settings-edit"]');
$edit_formatter_button->press();
$assert_session->waitForText('3rd party formatter settings form');
$cancel_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-field-test-settings-edit-form-actions-cancel-settings"]');
$cancel_button->press();
$assert_session->assertNoElementAfterWait('css', '[data-drupal-selector="edit-fields-field-test-settings-edit-form-actions-cancel-settings"]');
$assert_session->elementNotExists('css', '.tabledrag-changed-warning');
$assert_session->elementNotExists('css', 'abbr.tabledrag-changed');
$edit_formatter_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-field-test-settings-edit"]');
$edit_formatter_button->press();
$formatter_field = $assert_session->waitForField('fields[field_test][settings_edit_form][third_party_settings][field_third_party_test][field_test_field_formatter_third_party_settings_form]');
$formatter_field->setValue('honk');
$update_button = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-fields-field-test-settings-edit-form-actions-save-settings"]');
$update_button->press();
$assert_session->assertNoElementAfterWait('css', '[data-drupal-selector="edit-fields-field-test-settings-edit-form-actions-cancel-settings"]');
$this->assertNotNull($changed_warning = $assert_session->waitForElementVisible('css', '.tabledrag-changed-warning'));
$this->assertNotNull($assert_session->waitForElementVisible('css', ' #field-test abbr.tabledrag-changed'));
$this->assertSame('* You have unsaved changes.', $changed_warning->getText());
}
}

View File

@@ -0,0 +1,376 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\FunctionalJavascript;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait;
// cspell:ignore horserad
/**
* Tests the Field UI "Manage Fields" screens.
*
* @group field_ui
* @group #slow
*/
class ManageFieldsTest extends WebDriverTestBase {
use FieldUiJSTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'field_ui',
'field_test',
'block',
'options',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* @var string
*/
protected $type;
/**
* @var string
*/
protected $type2;
/**
* @var \Drupal\Core\Entity\entityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalPlaceBlock('local_actions_block');
// Create a test user.
$admin_user = $this->drupalCreateUser([
'access content',
'administer content types',
'administer node fields',
]);
$this->drupalLogin($admin_user);
$type = $this->drupalCreateContentType([
'name' => 'Article',
'type' => 'article',
]);
$this->type = $type->id();
$type2 = $this->drupalCreateContentType([
'name' => 'Basic Page',
'type' => 'page',
]);
$this->type2 = $type2->id();
$this->entityTypeManager = $this->container->get('entity_type.manager');
}
/**
* Tests re-using an existing field and the visibility of the re-use button.
*/
public function testReuseExistingField(): void {
$path = 'admin/structure/types/manage/article';
$path2 = 'admin/structure/types/manage/page';
$this->drupalGet($path2 . '/fields');
// The button should not be visible without any re-usable fields.
$this->assertSession()->linkNotExists('Re-use an existing field');
$field_label = 'Test field';
// Create a field, and a node with some data for the field.
$this->fieldUIAddNewFieldJS($path, 'test', $field_label);
// Add an existing field.
$this->fieldUIAddExistingFieldJS($path2, 'field_test', $field_label);
// Confirm the button is no longer visible after re-using the field.
$this->assertSession()->linkNotExists('Re-use an existing field');
}
/**
* Tests filter results in the re-use form.
*/
public function testFilterInReuseForm(): void {
$session = $this->getSession();
$page = $session->getPage();
$path = 'admin/structure/types/manage/article';
$path2 = 'admin/structure/types/manage/page';
$this->fieldUIAddNewFieldJS($path, 'horse', 'Horse');
$this->fieldUIAddNewFieldJS($path, 'horseradish', 'Horseradish', 'text');
$this->fieldUIAddNewFieldJS($path, 'carrot', 'Carrot', 'text');
$this->drupalGet($path2 . '/fields');
$this->assertSession()->linkExists('Re-use an existing field');
$this->clickLink('Re-use an existing field');
$this->assertSession()->waitForElementVisible('css', '#drupal-modal');
$filter = $this->assertSession()->waitForElementVisible('css', 'input[name="search"]');
$horse_field_row = $page->find('css', '.js-reuse-table tr[data-field-id="field_horse"]');
$horseradish_field_row = $page->find('css', '.js-reuse-table tr[data-field-id="field_horseradish"]');
$carrot_field_row = $page->find('css', '.js-reuse-table tr[data-field-id="field_carrot"]');
// Confirm every field is visible first.
$this->assertTrue($horse_field_row->isVisible());
$this->assertTrue($horseradish_field_row->isVisible());
$this->assertTrue($carrot_field_row->isVisible());
// Filter by 'horse' field name.
$filter->setValue('horse');
$session->wait(1000, "jQuery('[data-field-id=\"field_carrot\"]:visible').length == 0");
$this->assertTrue($horse_field_row->isVisible());
$this->assertTrue($horseradish_field_row->isVisible());
$this->assertFalse($carrot_field_row->isVisible());
// Filter even more so only 'horseradish' is visible.
$filter->setValue('horserad');
$session->wait(1000, "jQuery('[data-field-id=\"field_horse\"]:visible').length == 0");
$this->assertFalse($horse_field_row->isVisible());
$this->assertTrue($horseradish_field_row->isVisible());
$this->assertFalse($carrot_field_row->isVisible());
// Filter by field type but search with 'ext' instead of 'text' to
// confirm that contains-based search works.
$filter->setValue('ext');
$session->wait(1000, "jQuery('[data-field-id=\"field_horse\"]:visible').length == 0");
$session->wait(1000, "jQuery('[data-field-id=\"field_carrot\"]:visible').length == 1");
$this->assertFalse($horse_field_row->isVisible());
$this->assertTrue($horseradish_field_row->isVisible());
$this->assertTrue($carrot_field_row->isVisible());
// Ensure clearing brings all the results back.
$filter->setValue('');
$session->wait(1000, "jQuery('[data-field-id=\"field_horse\"]:visible').length == 1");
$this->assertTrue($horse_field_row->isVisible());
$this->assertTrue($horseradish_field_row->isVisible());
$this->assertTrue($carrot_field_row->isVisible());
}
/**
* Tests that field delete operation opens in modal.
*/
public function testFieldDelete(): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$this->drupalGet('admin/structure/types/manage/article/fields');
$page->find('css', '.dropbutton-toggle button')->click();
$page->clickLink('Delete');
// Asserts a dialog opens with the expected text.
$this->assertEquals('Are you sure you want to delete the field Body?', $assert_session->waitForElement('css', '.ui-dialog-title')->getText());
$page->find('css', '.ui-dialog-buttonset')->pressButton('Delete');
$assert_session->waitForText('The field Body has been deleted from the Article content type.');
}
/**
* Tests field add.
*/
public function testAddField(): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$this->drupalGet('admin/structure/types/manage/article/fields/add-field');
// Test validation.
$page->pressButton('Continue');
$assert_session->pageTextContains('You need to select a field type.');
$assert_session->pageTextNotContains('Choose an option below');
$this->assertNotEmpty($number_field = $page->find('xpath', '//*[text() = "Number"]')->getParent());
$number_field->click();
$this->assertTrue($assert_session->elementExists('css', '[name="new_storage_type"][value="number"]')->isSelected());
$page->pressButton('Continue');
$assert_session->pageTextContains('Choose an option below');
$field_name = 'test_field_1';
$page->fillField('label', $field_name);
$page->pressButton('Continue');
$assert_session->pageTextContains('You need to choose an option.');
$assert_session->elementNotExists('css', '[name="new_storage_type"].error');
$assert_session->elementExists('css', '[name="group_field_options_wrapper"].error');
$page->pressButton('Back');
// Try adding a field using a grouped field type.
$this->assertNotEmpty($email_field = $page->find('xpath', '//*[text() = "Email"]')->getParent());
$email_field->click();
$this->assertTrue($assert_session->elementExists('css', '[name="new_storage_type"][value="email"]')->isSelected());
$page->pressButton('Continue');
$assert_session->pageTextNotContains('Choose an option below');
$page->pressButton('Back');
$this->assertNotEmpty($text = $page->find('xpath', '//*[text() = "Plain text"]')->getParent());
$text->click();
$this->assertTrue($assert_session->elementExists('css', '[name="new_storage_type"][value="plain_text"]')->isSelected());
$page->pressButton('Continue');
$assert_session->pageTextContains('Choose an option below');
$page->fillField('label', $field_name);
$this->assertNotEmpty($text_plain = $page->find('xpath', '//*[text() = "Text (plain)"]')->getParent());
$text_plain->click();
$this->assertTrue($assert_session->elementExists('css', '[name="group_field_options_wrapper"][value="string"]')->isSelected());
$page->pressButton('Continue');
$this->assertMatchesRegularExpression('/.*article\/add-field\/node\/field_test_field_1.*/', $this->getUrl());
// Ensure the default value is reloaded when the field storage settings
// are changed.
$default_input_1_name = 'default_value_input[field_test_field_1][0][value]';
$default_input_1 = $assert_session->fieldExists($default_input_1_name);
$this->assertFalse($default_input_1->isVisible());
$default_value = $assert_session->fieldExists('set_default_value');
$default_value->check();
$assert_session->waitForElementVisible('xpath', $default_value->getXpath());
$default_input_1->setValue('There can be only one!');
$default_input_2_name = 'default_value_input[field_test_field_1][1][value]';
$assert_session->fieldNotExists($default_input_2_name);
$cardinality = $assert_session->fieldExists('field_storage[subform][cardinality_number]');
$cardinality->setValue(2);
$default_input_2 = $assert_session->waitForField($default_input_2_name);
// Ensure the default value for first input is retained.
$assert_session->fieldValueEquals($default_input_1_name, 'There can be only one!');
$page->findField($default_input_2_name)->setValue('But maybe also two?');
$cardinality->setValue('1');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->waitForElementRemoved('xpath', $default_input_2->getXpath());
// Ensure the first input retains its value.
$assert_session->fieldValueEquals($default_input_1_name, 'There can be only one!');
$cardinality->setValue(2);
$assert_session->waitForField($default_input_2_name);
// Ensure when the second input is added again it does not retain its value.
$assert_session->fieldValueEquals($default_input_2_name, '');
// Ensure changing the max length input will also reload the form.
$max_length_input = $assert_session->fieldExists('field_storage[subform][settings][max_length]');
$this->assertSame('255', $max_length_input->getValue());
$this->assertSame('255', $default_input_1->getAttribute('maxlength'));
$max_length_input->setValue('5');
$page->waitFor(5, function () use ($default_input_1) {
return $default_input_1->getAttribute('maxlength') === '5';
});
$this->assertSame('5', $default_input_1->getAttribute('maxlength'));
// Set a default value that is under the new limit.
$default_input_1->setValue('Five!');
$page->pressButton('Save settings');
$assert_session->pageTextContains('Saved ' . $field_name . ' configuration.');
$this->assertNotNull($field_storage = FieldStorageConfig::loadByName('node', "field_$field_name"));
$this->assertEquals('string', $field_storage->getType());
// Try adding a field using a non-grouped field type.
$this->drupalGet('admin/structure/types/manage/article/fields/add-field');
$this->assertNotEmpty($number_field = $page->find('xpath', '//*[text() = "Number"]')->getParent());
$number_field->click();
$this->assertTrue($assert_session->elementExists('css', '[name="new_storage_type"][value="number"]')->isSelected());
$page->pressButton('Continue');
$assert_session->pageTextContains('Choose an option below');
$this->assertNotEmpty($number_integer = $page->find('xpath', '//*[text() = "Number (integer)"]')->getParent());
$number_integer->click();
$this->assertTrue($assert_session->elementExists('css', '[name="group_field_options_wrapper"][value="integer"]')->isSelected());
$page->pressButton('Back');
$this->assertNotEmpty($test_field = $page->find('xpath', '//*[text() = "Test field"]')->getParent());
$test_field->click();
$this->assertTrue($assert_session->elementExists('css', '[name="new_storage_type"][value="test_field"]')->isSelected());
$page->pressButton('Continue');
$field_name = 'test_field_2';
$page->fillField('label', $field_name);
$assert_session->pageTextNotContains('Choose an option below');
$page->pressButton('Continue');
$this->assertMatchesRegularExpression('/.*article\/add-field\/node\/field_test_field_2.*/', $this->getUrl());
$page->pressButton('Save settings');
$assert_session->pageTextContains('Saved ' . $field_name . ' configuration.');
$this->assertNotNull($field_storage = FieldStorageConfig::loadByName('node', "field_$field_name"));
$this->assertEquals('test_field', $field_storage->getType());
}
/**
* Tests the order in which the field types appear in the form.
*/
public function testFieldTypeOrder(): void {
$this->drupalGet('admin/structure/types/manage/article/fields/add-field');
$page = $this->getSession()->getPage();
$field_type_categories = [
'selection_list',
'number',
];
foreach ($field_type_categories as $field_type_category) {
// Select the group card.
$group_field_card = $page->find('css', "[name='new_storage_type'][value='$field_type_category']")->getParent();
$group_field_card->click();
$page->pressButton('Continue');
$field_types = $page->findAll('css', '.subfield-option .option');
$field_type_labels = [];
foreach ($field_types as $field_type) {
$field_type_labels[] = $field_type->getText();
}
$expected_field_types = match ($field_type_category) {
'selection_list' => [
'List (text)',
'List (integer)',
'List (float)',
],
'number' => [
'Number (integer)',
'Number (decimal)',
'Number (float)',
],
};
// Assert that the field type options are displayed as per their weights.
$this->assertSame($expected_field_types, $field_type_labels);
// Return to the first step of the form.
$page->pressButton('Back');
}
}
/**
* Tests the form validation for allowed values field.
*/
public function testAllowedValuesFormValidation(): void {
FieldStorageConfig::create([
'field_name' => 'field_text',
'entity_type' => 'node',
'type' => 'text',
])->save();
FieldConfig::create([
'field_name' => 'field_text',
'entity_type' => 'node',
'bundle' => 'article',
])->save();
$this->drupalGet('/admin/structure/types/manage/article/fields/node.article.field_text');
$page = $this->getSession()->getPage();
$page->findField('edit-field-storage-subform-cardinality-number')->setValue('-11');
$this->assertSession()->assertExpectedAjaxRequest(1);
$page->findButton('Save settings')->click();
$this->assertSession()->pageTextContains('Limit must be higher than or equal to 1.');
}
/**
* Tests the form validation for label field.
*/
public function testLabelFieldFormValidation(): void {
$this->drupalGet('/admin/structure/types/manage/article/fields/add-field');
$page = $this->getSession()->getPage();
$page->findButton('Continue')->click();
$this->assertSession()->pageTextContains('You need to select a field type.');
$this->assertNotEmpty($boolean_field = $page->find('xpath', '//*[text() = "Boolean (overridden by alter)"]')->getParent());
$boolean_field->click();
$page->findButton('Continue')->click();
$page->findButton('Continue')->click();
$this->assertSession()->pageTextContains('Add new field: you need to provide a label.');
}
}

View File

@@ -0,0 +1,712 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Kernel;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Database;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\node\Entity\NodeType;
use Drupal\KernelTests\KernelTestBase;
use Drupal\user\Entity\Role;
/**
* Tests the entity display configuration entities.
*
* @group field_ui
*/
class EntityDisplayTest extends KernelTestBase {
/**
* Modules to install.
*
* @var string[]
*/
protected static $modules = [
'field_ui',
'field',
'entity_test',
'user',
'text',
'field_test',
'node',
'system',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('entity_test');
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installConfig(['field', 'node', 'user']);
}
/**
* Tests basic CRUD operations on entity display objects.
*/
public function testEntityDisplayCRUD(): void {
$display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
]);
$expected = [];
// Check that providing no 'weight' results in the highest current weight
// being assigned. The 'name' field's formatter has weight -5, therefore
// these follow.
$expected['component_1'] = ['weight' => -4, 'settings' => [], 'third_party_settings' => []];
$expected['component_2'] = ['weight' => -3, 'settings' => [], 'third_party_settings' => []];
$display->setComponent('component_1');
$display->setComponent('component_2');
$this->assertEquals($expected['component_1'], $display->getComponent('component_1'));
$this->assertEquals($expected['component_2'], $display->getComponent('component_2'));
// Check that arbitrary options are correctly stored.
$expected['component_3'] = ['weight' => 10, 'third_party_settings' => ['field_test' => ['foo' => 'bar']], 'settings' => []];
$display->setComponent('component_3', $expected['component_3']);
$this->assertEquals($expected['component_3'], $display->getComponent('component_3'));
// Check that the display can be properly saved and read back.
$display->save();
$display = EntityViewDisplay::load($display->id());
foreach (['component_1', 'component_2', 'component_3'] as $name) {
$expected[$name]['region'] = 'content';
$this->assertEquals($expected[$name], $display->getComponent($name));
}
// Ensure that third party settings were added to the config entity.
// These are added by entity_test_entity_presave() implemented in
// entity_test module.
$this->assertEquals('bar', $display->getThirdPartySetting('entity_test', 'foo'), 'Third party settings were added to the entity view display.');
// Check that getComponents() returns options for all components.
$expected['name'] = [
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
'settings' => [
'link_to_entity' => FALSE,
],
'third_party_settings' => [],
'region' => 'content',
];
$this->assertEquals($expected, $display->getComponents());
// Check that a component can be removed.
$display->removeComponent('component_3');
$this->assertNULL($display->getComponent('component_3'));
// Check that the removal is correctly persisted.
$display->save();
$display = EntityViewDisplay::load($display->id());
$this->assertNULL($display->getComponent('component_3'));
// Check that createCopy() creates a new component that can be correctly
// saved.
EntityViewMode::create([
'id' => $display->getTargetEntityTypeId() . '.other_view_mode',
'label' => 'Other',
'targetEntityType' => $display->getTargetEntityTypeId(),
])->save();
$new_display = $display->createCopy('other_view_mode');
$new_display->save();
$new_display = EntityViewDisplay::load($new_display->id());
$dependencies = $new_display->calculateDependencies()->getDependencies();
$this->assertEquals(['config' => ['core.entity_view_mode.entity_test.other_view_mode'], 'module' => ['entity_test']], $dependencies);
$this->assertEquals($display->getTargetEntityTypeId(), $new_display->getTargetEntityTypeId());
$this->assertEquals($display->getTargetBundle(), $new_display->getTargetBundle());
$this->assertEquals('other_view_mode', $new_display->getMode());
$this->assertEquals($display->getComponents(), $new_display->getComponents());
}
/**
* Tests sorting of components by name on basic CRUD operations.
*/
public function testEntityDisplayCRUDSort(): void {
$display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
]);
$display->setComponent('component_3');
$display->setComponent('component_1');
$display->setComponent('component_2');
$display->save();
$components = array_keys($display->getComponents());
// The name field is not configurable so will be added automatically.
$expected = [0 => 'component_1', 1 => 'component_2', 2 => 'component_3', 'name'];
$this->assertSame($expected, $components);
}
/**
* @covers \Drupal\Core\Entity\EntityDisplayRepository::getViewDisplay
*/
public function testEntityGetDisplay(): void {
$display_repository = $this->container->get('entity_display.repository');
// Check that getViewDisplay() returns a fresh object when no configuration
// entry exists.
$display = $display_repository->getViewDisplay('entity_test', 'entity_test');
$this->assertTrue($display->isNew());
// Add some components and save the display.
$display->setComponent('component_1', ['weight' => 10, 'settings' => []])
->save();
// Check that getViewDisplay() returns the correct object.
$display = $display_repository->getViewDisplay('entity_test', 'entity_test');
$this->assertFalse($display->isNew());
$this->assertEquals('entity_test.entity_test.default', $display->id());
$this->assertEquals(['weight' => 10, 'settings' => [], 'third_party_settings' => [], 'region' => 'content'], $display->getComponent('component_1'));
}
/**
* Tests the behavior of a field component within an entity display object.
*/
public function testExtraFieldComponent(): void {
entity_test_create_bundle('bundle_with_extra_fields');
$display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'bundle_with_extra_fields',
'mode' => 'default',
]);
// Check that the default visibility taken into account for extra fields
// unknown in the display.
$this->assertEquals(['weight' => 5, 'region' => 'content', 'settings' => [], 'third_party_settings' => []], $display->getComponent('display_extra_field'));
$this->assertNull($display->getComponent('display_extra_field_hidden'));
// Check that setting explicit options overrides the defaults.
$display->removeComponent('display_extra_field');
$display->setComponent('display_extra_field_hidden', ['weight' => 10]);
$this->assertNull($display->getComponent('display_extra_field'));
$this->assertEquals(['weight' => 10, 'settings' => [], 'third_party_settings' => []], $display->getComponent('display_extra_field_hidden'));
}
/**
* Tests the behavior of an extra field component with initial invalid values.
*/
public function testExtraFieldComponentInitialInvalidConfig(): void {
entity_test_create_bundle('bundle_with_extra_fields');
$display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'bundle_with_extra_fields',
'mode' => 'default',
// Add the extra field to the initial config, without a 'type'.
'content' => [
'display_extra_field' => [
'weight' => 5,
],
],
]);
// Check that the default visibility taken into account for extra fields
// unknown in the display that were included in the initial config.
$this->assertEquals(['weight' => 5, 'region' => 'content'], $display->getComponent('display_extra_field'));
$this->assertNull($display->getComponent('display_extra_field_hidden'));
// Check that setting explicit options overrides the defaults.
$display->removeComponent('display_extra_field');
$display->setComponent('display_extra_field_hidden', ['weight' => 10]);
$this->assertNull($display->getComponent('display_extra_field'));
$this->assertEquals(['weight' => 10, 'settings' => [], 'third_party_settings' => []], $display->getComponent('display_extra_field_hidden'));
}
/**
* Tests the behavior of a field component within an entity display object.
*/
public function testFieldComponent(): void {
$field_name = 'test_field';
// Create a field storage and a field.
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'test_field',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
]);
$field->save();
$display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
]);
// Check that providing no options results in default values being used.
$display->setComponent($field_name);
$field_type_info = \Drupal::service('plugin.manager.field.field_type')->getDefinition($field_storage->getType());
$default_formatter = $field_type_info['default_formatter'];
$formatter_settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings($default_formatter);
$expected = [
'weight' => -4,
'label' => 'above',
'type' => $default_formatter,
'settings' => $formatter_settings,
'third_party_settings' => [],
];
$this->assertEquals($expected, $display->getComponent($field_name));
// Check that the getFormatter() method returns the correct formatter plugin.
$formatter = $display->getRenderer($field_name);
$this->assertEquals($default_formatter, $formatter->getPluginId());
$this->assertEquals($formatter_settings, $formatter->getSettings());
// Check that the formatter is statically persisted.
$this->assertSame($formatter, $display->getRenderer($field_name));
// Check that changing the definition creates a new formatter.
$display->setComponent($field_name, [
'type' => 'field_test_multiple',
]);
$renderer = $display->getRenderer($field_name);
$this->assertEquals('field_test_multiple', $renderer->getPluginId());
$this->assertNotSame($formatter, $renderer);
// Check that the display has dependencies on the field and the module that
// provides the formatter.
$dependencies = $display->calculateDependencies()->getDependencies();
$this->assertEquals(['config' => ['field.field.entity_test.entity_test.test_field'], 'module' => ['entity_test', 'field_test']], $dependencies);
}
/**
* Tests the behavior of a field component for a base field.
*/
public function testBaseFieldComponent(): void {
$display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test_base_field_display',
'bundle' => 'entity_test_base_field_display',
'mode' => 'default',
]);
// Check that default options are correctly filled in.
$formatter_settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings('text_default');
$expected = [
'test_no_display' => NULL,
'test_display_configurable' => [
'label' => 'above',
'type' => 'text_default',
'settings' => $formatter_settings,
'third_party_settings' => [],
'weight' => 10,
'region' => 'content',
],
'test_display_non_configurable' => [
'label' => 'above',
'type' => 'text_default',
'settings' => $formatter_settings,
'third_party_settings' => [],
'weight' => 11,
'region' => 'content',
],
];
foreach ($expected as $field_name => $options) {
$this->assertEquals($options, $display->getComponent($field_name));
}
// Check that saving the display only writes data for fields whose display
// is configurable.
$display->save();
$config = $this->config('core.entity_view_display.' . $display->id());
$data = $config->get();
$this->assertFalse(isset($data['content']['test_no_display']));
$this->assertFalse(isset($data['hidden']['test_no_display']));
$this->assertEquals($expected['test_display_configurable'], $data['content']['test_display_configurable']);
$this->assertFalse(isset($data['content']['test_display_non_configurable']));
$this->assertFalse(isset($data['hidden']['test_display_non_configurable']));
// Check that defaults are correctly filled when loading the display.
$display = EntityViewDisplay::load($display->id());
foreach ($expected as $field_name => $options) {
$this->assertEquals($options, $display->getComponent($field_name));
}
// Check that data manually written for fields whose display is not
// configurable is discarded when loading the display.
$data['content']['test_display_non_configurable'] = $expected['test_display_non_configurable'];
$data['content']['test_display_non_configurable']['weight']++;
$config->setData($data)->save();
$display = EntityViewDisplay::load($display->id());
foreach ($expected as $field_name => $options) {
$this->assertEquals($options, $display->getComponent($field_name));
}
}
/**
* Tests deleting a bundle.
*/
public function testDeleteBundle(): void {
// Create a node bundle, display and form display object.
$type = NodeType::create([
'type' => 'article',
'name' => 'Article',
]);
$type->save();
node_add_body_field($type);
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getViewDisplay('node', 'article')->save();
$display_repository->getFormDisplay('node', 'article')->save();
// Delete the bundle.
$type->delete();
$display = EntityViewDisplay::load('node.article.default');
$this->assertFalse((bool) $display);
$form_display = EntityFormDisplay::load('node.article.default');
$this->assertFalse((bool) $form_display);
}
/**
* Tests deleting field.
*/
public function testDeleteField(): void {
$field_name = 'test_field';
// Create a field storage and a field.
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'test_field',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
]);
$field->save();
// Create default and teaser entity display.
EntityViewMode::create([
'id' => 'entity_test.teaser',
'label' => 'Teaser',
'targetEntityType' => 'entity_test',
])->save();
EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
])->setComponent($field_name)->save();
EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'teaser',
])->setComponent($field_name)->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
// Check the component exists.
$display = $display_repository->getViewDisplay('entity_test', 'entity_test');
$this->assertNotEmpty($display->getComponent($field_name));
$display = $display_repository->getViewDisplay('entity_test', 'entity_test', 'teaser');
$this->assertNotEmpty($display->getComponent($field_name));
// Delete the field.
$field->delete();
// Check that the component has been removed from the entity displays.
$display = $display_repository->getViewDisplay('entity_test', 'entity_test');
$this->assertNull($display->getComponent($field_name));
$display = $display_repository->getViewDisplay('entity_test', 'entity_test', 'teaser');
$this->assertNull($display->getComponent($field_name));
}
/**
* Tests \Drupal\Core\Entity\EntityDisplayBase::onDependencyRemoval().
*/
public function testOnDependencyRemoval(): void {
$this->enableModules(['field_plugins_test']);
$field_name = 'test_field';
// Create a field.
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'text',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
]);
$field->save();
EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
])->setComponent($field_name, ['type' => 'field_plugins_test_text_formatter'])->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
// Check the component exists and is of the correct type.
$display = $display_repository->getViewDisplay('entity_test', 'entity_test');
$this->assertEquals('field_plugins_test_text_formatter', $display->getComponent($field_name)['type']);
// Removing the field_plugins_test module should change the component to use
// the default formatter for test fields.
\Drupal::service('config.manager')->uninstall('module', 'field_plugins_test');
$display = $display_repository->getViewDisplay('entity_test', 'entity_test');
$this->assertEquals('text_default', $display->getComponent($field_name)['type']);
// Removing the text module should remove the field from the view display.
\Drupal::service('config.manager')->uninstall('module', 'text');
$display = $display_repository->getViewDisplay('entity_test', 'entity_test');
$this->assertNull($display->getComponent($field_name));
}
/**
* Ensure that entity view display changes invalidates cache tags.
*/
public function testEntityDisplayInvalidateCacheTags(): void {
$cache = \Drupal::cache();
$cache->set('cid', 'kittens', Cache::PERMANENT, ['config:entity_view_display_list']);
$display = EntityViewDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
]);
$display->setComponent('kitten');
$display->save();
$this->assertFalse($cache->get('cid'));
}
/**
* Tests getDisplayModeOptions().
*/
public function testGetDisplayModeOptions(): void {
NodeType::create([
'type' => 'article',
'name' => 'Article',
])->save();
EntityViewDisplay::create([
'targetEntityType' => 'node',
'bundle' => 'article',
'mode' => 'default',
])->setStatus(TRUE)->save();
$display_teaser = EntityViewDisplay::create([
'targetEntityType' => 'node',
'bundle' => 'article',
'mode' => 'teaser',
]);
$display_teaser->save();
EntityFormDisplay::create([
'targetEntityType' => 'user',
'bundle' => 'user',
'mode' => 'default',
])->setStatus(TRUE)->save();
$form_display_teaser = EntityFormDisplay::create([
'targetEntityType' => 'user',
'bundle' => 'user',
'mode' => 'register',
]);
$form_display_teaser->save();
// Test getViewModeOptionsByBundle().
$view_modes = \Drupal::service('entity_display.repository')->getViewModeOptionsByBundle('node', 'article');
$this->assertEquals(['default' => 'Default'], $view_modes);
$display_teaser->setStatus(TRUE)->save();
$view_modes = \Drupal::service('entity_display.repository')->getViewModeOptionsByBundle('node', 'article');
$this->assertEquals(['default' => 'Default', 'teaser' => 'Teaser'], $view_modes);
// Test getFormModeOptionsByBundle().
$form_modes = \Drupal::service('entity_display.repository')->getFormModeOptionsByBundle('user', 'user');
$this->assertEquals(['default' => 'Default'], $form_modes);
$form_display_teaser->setStatus(TRUE)->save();
$form_modes = \Drupal::service('entity_display.repository')->getFormModeOptionsByBundle('user', 'user');
$this->assertEquals(['default' => 'Default', 'register' => 'Register'], $form_modes);
}
/**
* Tests components dependencies additions.
*/
public function testComponentDependencies(): void {
$this->enableModules(['dblog', 'help']);
$this->installSchema('dblog', ['watchdog']);
$this->installEntitySchema('user');
/** @var \Drupal\user\RoleInterface[] $roles */
$roles = [];
// Create two arbitrary user roles.
for ($i = 0; $i < 2; $i++) {
$roles[$i] = Role::create([
'id' => $this->randomMachineName(),
'label' => $this->randomString(),
]);
$roles[$i]->save();
}
// Create a field of type 'test_field' attached to 'entity_test'.
$field_name = $this->randomMachineName();
FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'test_field',
])->save();
FieldConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'bundle' => 'entity_test',
])->save();
// Create a new form display without components.
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */
$form_display = EntityFormDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
]);
$form_display->save();
$dependencies = ['user.role.' . $roles[0]->id(), 'user.role.' . $roles[1]->id()];
// The config object should not depend on none of the two $roles.
$this->assertNoDependency('config', $dependencies[0], $form_display);
$this->assertNoDependency('config', $dependencies[1], $form_display);
// Add a widget of type 'test_field_widget'.
$component = [
'type' => 'test_field_widget',
'settings' => [
'test_widget_setting' => $this->randomString(),
'role' => $roles[0]->id(),
'role2' => $roles[1]->id(),
],
'third_party_settings' => [
'help' => ['foo' => 'bar'],
],
];
$form_display->setComponent($field_name, $component);
$form_display->save();
// Now, the form display should depend on both user roles $roles.
$this->assertDependency('config', $dependencies[0], $form_display);
$this->assertDependency('config', $dependencies[1], $form_display);
// The form display should depend on 'help' module.
$this->assertDependency('module', 'help', $form_display);
// Delete the first user role entity.
$roles[0]->delete();
// Reload the form display.
$form_display = EntityFormDisplay::load($form_display->id());
// The display exists.
$this->assertNotEmpty($form_display);
// The form display should not depend on $role[0] anymore.
$this->assertNoDependency('config', $dependencies[0], $form_display);
// The form display should depend on 'anonymous' user role.
$this->assertDependency('config', 'user.role.anonymous', $form_display);
// The form display should depend on 'help' module.
$this->assertDependency('module', 'help', $form_display);
// Manually trigger the removal of configuration belonging to the module
// because KernelTestBase::disableModules() is not aware of this.
$this->container->get('config.manager')->uninstall('module', 'help');
// Uninstall 'help' module.
$this->disableModules(['help']);
// Reload the form display.
$form_display = EntityFormDisplay::load($form_display->id());
// The display exists.
$this->assertNotEmpty($form_display);
// The component is still enabled.
$this->assertNotNull($form_display->getComponent($field_name));
// The form display should not depend on 'help' module anymore.
$this->assertNoDependency('module', 'help', $form_display);
// Delete the 2nd user role entity.
$roles[1]->delete();
// Reload the form display.
$form_display = EntityFormDisplay::load($form_display->id());
// The display exists.
$this->assertNotEmpty($form_display);
// The component has been disabled.
$this->assertNull($form_display->getComponent($field_name));
$this->assertTrue($form_display->get('hidden')[$field_name]);
// The correct warning message has been logged.
$arguments = ['@display' => 'Entity form display', '@id' => $form_display->id(), '@name' => $field_name];
$variables = Database::getConnection()->select('watchdog', 'w')
->fields('w', ['variables'])
->condition('type', 'system')
->condition('message', "@display '@id': Component '@name' was disabled because its settings depend on removed dependencies.")
->execute()
->fetchField();
$this->assertEquals($arguments, unserialize($variables));
}
/**
* Asserts that $key is a $type type dependency of $display config entity.
*
* @param string $type
* The dependency type: 'config', 'content', 'module' or 'theme'.
* @param string $key
* The string to be checked.
* @param \Drupal\Core\Entity\Display\EntityDisplayInterface $display
* The entity display object to get dependencies from.
*
* @internal
*/
protected function assertDependency(string $type, string $key, EntityDisplayInterface $display): void {
$this->assertDependencyHelper(TRUE, $type, $key, $display);
}
/**
* Asserts that $key is not a $type type dependency of $display config entity.
*
* @param string $type
* The dependency type: 'config', 'content', 'module' or 'theme'.
* @param string $key
* The string to be checked.
* @param \Drupal\Core\Entity\Display\EntityDisplayInterface $display
* The entity display object to get dependencies from.
*
* @internal
*/
protected function assertNoDependency(string $type, string $key, EntityDisplayInterface $display): void {
$this->assertDependencyHelper(FALSE, $type, $key, $display);
}
/**
* Provides a helper for dependency assertions.
*
* @param bool $assertion
* Assertion: positive or negative.
* @param string $type
* The dependency type: 'config', 'content', 'module' or 'theme'.
* @param string $key
* The string to be checked.
* @param \Drupal\Core\Entity\Display\EntityDisplayInterface $display
* The entity display object to get dependencies from.
*
* @internal
*/
protected function assertDependencyHelper(bool $assertion, string $type, string $key, EntityDisplayInterface $display): void {
$all_dependencies = $display->getDependencies();
$dependencies = !empty($all_dependencies[$type]) ? $all_dependencies[$type] : [];
$context = $display instanceof EntityViewDisplayInterface ? 'View' : 'Form';
$value = $assertion ? in_array($key, $dependencies) : !in_array($key, $dependencies);
$display_id = $display->id();
$message = $assertion ? "$context display '$display_id' depends on $type '$key'." : "$context display '$display_id' do not depend on $type '$key'.";
$this->assertTrue($value, $message);
}
}

View File

@@ -0,0 +1,313 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Kernel;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the entity display configuration entities.
*
* @group field_ui
*/
class EntityFormDisplayTest extends KernelTestBase {
/**
* Modules to install.
*
* @var string[]
*/
protected static $modules = [
'field_ui',
'field',
'entity_test',
'field_test',
'system',
'text',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('action');
$this->installConfig('user');
$this->installEntitySchema('entity_test');
}
/**
* @covers \Drupal\Core\Entity\EntityDisplayRepository::getFormDisplay
*/
public function testEntityGetFromDisplay(): void {
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
// Check that EntityDisplayRepositoryInterface::getFormDisplay() returns a
// fresh object when no configuration entry exists.
$form_display = $display_repository->getFormDisplay('entity_test', 'entity_test');
$this->assertTrue($form_display->isNew());
// Add some components and save the display.
$form_display->setComponent('component_1', ['weight' => 10])
->save();
// Check that EntityDisplayRepositoryInterface::getFormDisplay() returns the
// correct object.
$form_display = $display_repository->getFormDisplay('entity_test', 'entity_test');
$this->assertFalse($form_display->isNew());
$this->assertEquals('entity_test.entity_test.default', $form_display->id());
$this->assertEquals(['weight' => 10, 'settings' => [], 'third_party_settings' => [], 'region' => 'content'], $form_display->getComponent('component_1'));
}
/**
* Tests the behavior of a field component within an EntityFormDisplay object.
*/
public function testFieldComponent(): void {
// Create a field storage and a field.
$field_name = 'test_field';
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'test_field',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
]);
$field->save();
$form_display = EntityFormDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
]);
// Check that providing no options results in default values being used.
$form_display->setComponent($field_name);
$field_type_info = \Drupal::service('plugin.manager.field.field_type')->getDefinition($field_storage->getType());
$default_widget = $field_type_info['default_widget'];
$widget_settings = \Drupal::service('plugin.manager.field.widget')->getDefaultSettings($default_widget);
$expected = [
'weight' => 3,
'type' => $default_widget,
'settings' => $widget_settings,
'third_party_settings' => [],
];
$this->assertEquals($expected, $form_display->getComponent($field_name));
// Check that the getWidget() method returns the correct widget plugin.
$widget = $form_display->getRenderer($field_name);
$this->assertEquals($default_widget, $widget->getPluginId());
$this->assertEquals($widget_settings, $widget->getSettings());
// Check that the widget is statically persisted.
$this->assertSame($widget, $form_display->getRenderer($field_name));
// Check that changing the definition creates a new widget.
$form_display->setComponent($field_name, [
'type' => 'field_test_multiple',
]);
$renderer = $form_display->getRenderer($field_name);
$this->assertEquals('test_field_widget', $renderer->getPluginId());
$this->assertNotSame($widget, $renderer);
// Check that specifying an unknown widget (e.g. case of a disabled module)
// gets stored as is in the display, but results in the default widget being
// used.
$form_display->setComponent($field_name, [
'type' => 'unknown_widget',
]);
$options = $form_display->getComponent($field_name);
$this->assertEquals('unknown_widget', $options['type']);
$widget = $form_display->getRenderer($field_name);
$this->assertEquals($default_widget, $widget->getPluginId());
}
/**
* Tests the behavior of a field component for a base field.
*/
public function testBaseFieldComponent(): void {
$display = EntityFormDisplay::create([
'targetEntityType' => 'entity_test_base_field_display',
'bundle' => 'entity_test_base_field_display',
'mode' => 'default',
]);
// Check that default options are correctly filled in.
$formatter_settings = \Drupal::service('plugin.manager.field.widget')->getDefaultSettings('text_textfield');
$expected = [
'test_no_display' => NULL,
'test_display_configurable' => [
'type' => 'text_textfield',
'settings' => $formatter_settings,
'third_party_settings' => [],
'weight' => 10,
'region' => 'content',
],
'test_display_non_configurable' => [
'type' => 'text_textfield',
'settings' => $formatter_settings,
'third_party_settings' => [],
'weight' => 11,
'region' => 'content',
],
];
foreach ($expected as $field_name => $options) {
$this->assertEquals($options, $display->getComponent($field_name));
}
// Check that saving the display only writes data for fields whose display
// is configurable.
$display->save();
$config = $this->config('core.entity_form_display.' . $display->id());
$data = $config->get();
$this->assertFalse(isset($data['content']['test_no_display']));
$this->assertFalse(isset($data['hidden']['test_no_display']));
$this->assertEquals($expected['test_display_configurable'], $data['content']['test_display_configurable']);
$this->assertFalse(isset($data['content']['test_display_non_configurable']));
$this->assertFalse(isset($data['hidden']['test_display_non_configurable']));
// Check that defaults are correctly filled when loading the display.
$display = EntityFormDisplay::load($display->id());
foreach ($expected as $field_name => $options) {
$this->assertEquals($options, $display->getComponent($field_name));
}
// Check that data manually written for fields whose display is not
// configurable is discarded when loading the display.
$data['content']['test_display_non_configurable'] = $expected['test_display_non_configurable'];
$data['content']['test_display_non_configurable']['weight']++;
$config->setData($data)->save();
$display = EntityFormDisplay::load($display->id());
foreach ($expected as $field_name => $options) {
$this->assertEquals($options, $display->getComponent($field_name));
}
}
/**
* Tests deleting field.
*/
public function testDeleteField(): void {
$field_name = 'test_field';
// Create a field storage and a field.
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'test_field',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
]);
$field->save();
// Create default and compact entity display.
EntityFormMode::create([
'id' => 'entity_test.compact',
'label' => 'Compact',
'targetEntityType' => 'entity_test',
])->save();
EntityFormDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
])->setComponent($field_name)->save();
EntityFormDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'compact',
])->setComponent($field_name)->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
// Check the component exists.
$display = $display_repository->getFormDisplay('entity_test', 'entity_test');
$this->assertNotEmpty($display->getComponent($field_name));
$display = $display_repository->getFormDisplay('entity_test', 'entity_test', 'compact');
$this->assertNotEmpty($display->getComponent($field_name));
// Delete the field.
$field->delete();
// Check that the component has been removed from the entity displays.
$display = $display_repository->getFormDisplay('entity_test', 'entity_test');
$this->assertNull($display->getComponent($field_name));
$display = $display_repository->getFormDisplay('entity_test', 'entity_test', 'compact');
$this->assertNull($display->getComponent($field_name));
}
/**
* Tests \Drupal\Core\Entity\EntityDisplayBase::onDependencyRemoval().
*/
public function testOnDependencyRemoval(): void {
$this->enableModules(['field_plugins_test']);
$field_name = 'test_field';
// Create a field.
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'text',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
]);
$field->save();
EntityFormDisplay::create([
'targetEntityType' => 'entity_test',
'bundle' => 'entity_test',
'mode' => 'default',
])->setComponent($field_name, ['type' => 'field_plugins_test_text_widget'])->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
// Check the component exists and is of the correct type.
$display = $display_repository->getFormDisplay('entity_test', 'entity_test');
$this->assertEquals('field_plugins_test_text_widget', $display->getComponent($field_name)['type']);
// Removing the field_plugins_test module should change the component to use
// the default widget for test fields.
\Drupal::service('config.manager')->uninstall('module', 'field_plugins_test');
$display = $display_repository->getFormDisplay('entity_test', 'entity_test');
$this->assertEquals('text_textfield', $display->getComponent($field_name)['type']);
// Removing the text module should remove the field from the form display.
\Drupal::service('config.manager')->uninstall('module', 'text');
$display = $display_repository->getFormDisplay('entity_test', 'entity_test');
$this->assertNull($display->getComponent($field_name));
}
/**
* Tests the serialization and unserialization of the class.
*/
public function testSerialization(): void {
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$form_display = $display_repository->getFormDisplay('entity_test', 'entity_test');
// Make sure the langcode base field is visible in the original form
// display.
$this->assertNotEmpty($form_display->getComponent('langcode'));
// Remove the langcode.
$form_display->removeComponent('langcode');
$unserialized = unserialize(serialize($form_display));
// Verify that components are retained upon unserialization.
$this->assertEquals($form_display->getComponents(), $unserialized->getComponents());
}
}

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Traits;
/**
* Provides common functionality for the Field UI tests that depend on JS.
*/
trait FieldUiJSTestTrait {
/**
* Creates a new field through the Field UI.
*
* @param string|null $bundle_path
* Admin path of the bundle that the new field is to be attached to.
* @param string $field_name
* The field name of the new field storage.
* @param string|null $label
* (optional) The label of the new field. Defaults to a random string.
* @param string $field_type
* (optional) The field type of the new field storage. Defaults to
* 'test_field'.
* @param bool $save_settings
* (optional) Parameter for conditional execution of second and third step
* (Saving the storage settings and field settings). Defaults to 'TRUE'.
*
* @throws \Behat\Mink\Exception\ElementNotFoundException
*/
public function fieldUIAddNewFieldJS(?string $bundle_path, string $field_name, ?string $label = NULL, string $field_type = 'test_field', bool $save_settings = TRUE): void {
$label = $label ?: $field_name;
// Allow the caller to set a NULL path in case they navigated to the right
// page before calling this method.
if ($bundle_path !== NULL) {
$bundle_path = "$bundle_path/fields/add-field";
$this->drupalGet($bundle_path);
}
// First step: 'Add field' page.
$session = $this->getSession();
$page = $session->getPage();
$assert_session = $this->assertSession();
if ($assert_session->waitForElementVisible('css', "[name='new_storage_type'][value='$field_type']")) {
$page = $this->getSession()->getPage();
$field_card = $page->find('css', "[name='new_storage_type'][value='$field_type']")->getParent();
}
else {
$field_card = $this->getFieldFromGroupJS($field_type);
}
$field_card?->click();
$page->findButton('Continue')->click();
$field_label = $page->findField('edit-label');
$this->assertTrue($field_label->isVisible());
$field_label = $page->find('css', 'input[data-drupal-selector="edit-label"]');
$field_label->setValue($label);
$machine_name = $assert_session->waitForElementVisible('css', '[data-drupal-selector="edit-label"] + * .machine-name-value');
$this->assertNotEmpty($machine_name);
$page->findButton('Edit')->press();
$field_field_name = $page->findField('field_name');
$this->assertTrue($field_field_name->isVisible());
$field_field_name->setValue($field_name);
$page->findButton('Continue')->click();
$assert_session->waitForText("These settings apply to the $label field everywhere it is used.");
if ($save_settings) {
// Second step: Save field settings.
$page->findButton('Save settings')->click();
$assert_session->pageTextContains("Saved $label configuration.");
// Check that the field appears in the overview form.
$row = $page->find('css', '#field-' . $field_name);
$this->assertNotEmpty($row, 'Field was created and appears in the overview page.');
}
}
/**
* Adds an existing field through the Field UI.
*
* @param string $bundle_path
* Admin path of the bundle that the field is to be attached to.
* @param string $existing_storage_name
* The name of the existing field storage for which we want to add a new
* field.
* @param string|null $label
* (optional) The label of the new field. Defaults to a random string.
* @param array $field_edit
* (optional) $edit parameter for submitForm() on the second step
* ('Field settings' form).
*/
public function fieldUIAddExistingFieldJS(string $bundle_path, string $existing_storage_name, ?string $label = NULL, array $field_edit = []): void {
$label = $label ?: $this->randomMachineName();
$field_edit['edit-label'] = $label;
// First step: navigate to the re-use field page.
$this->drupalGet("{$bundle_path}/fields/");
// Confirm that the local action is visible.
$this->assertSession()->linkExists('Re-use an existing field');
$this->clickLink('Re-use an existing field');
// Wait for the modal to open.
$this->assertSession()->waitForElementVisible('css', '#drupal-modal');
$this->assertSession()->elementExists('css', "input[value=Re-use][name=$existing_storage_name]");
$this->click("input[value=Re-use][name=$existing_storage_name]");
// Set the main content to only the content region because the label can
// contain HTML which will be auto-escaped by Twig.
$this->assertSession()->responseContains('field-config-edit-form');
// Check that the page does not have double escaped HTML tags.
$this->assertSession()->responseNotContains('&amp;lt;');
// Second step: 'Field settings' form.
$this->submitForm($field_edit, 'Save settings');
$this->assertSession()->pageTextContains("Saved $label configuration.");
// Check that the field appears in the overview form.
$xpath = $this->assertSession()->buildXPathQuery("//table[@id=\"field-overview\"]//tr/td[1 and text() = :label]", [
':label' => $label,
]);
$this->assertSession()->elementExists('xpath', $xpath);
}
/**
* Helper function that returns the field card element if it is in a group.
*
* @param string $field_type
* The name of the field type.
*
* @return \Behat\Mink\Element\NodeElement|false|mixed|null
* Field card element within a group.
*/
public function getFieldFromGroupJS($field_type) {
$group_elements = $this->getSession()->getPage()->findAll('css', '.field-option-radio');
$groups = [];
foreach ($group_elements as $group_element) {
$groups[] = $group_element->getAttribute('value');
}
$field_card = NULL;
foreach ($groups as $group) {
$group_field_card = $this->getSession()->getPage()->find('css', "[name='new_storage_type'][value='$group']")->getParent();
$group_field_card->click();
$this->getSession()->getPage()->pressButton('Continue');
$field_card = $this->getSession()->getPage()->find('css', "[name='group_field_options_wrapper'][value='$field_type']");
if ($field_card) {
break;
}
$this->getSession()->getPage()->pressButton('Back');
}
return $field_card->getParent();
}
}

View File

@@ -0,0 +1,269 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Traits;
use Behat\Mink\Exception\ElementNotFoundException;
/**
* Provides common functionality for the Field UI test classes.
*/
trait FieldUiTestTrait {
/**
* Creates a new field through the Field UI.
*
* @param string $bundle_path
* Admin path of the bundle that the new field is to be attached to.
* @param string $field_name
* The field name of the new field storage.
* @param string $label
* (optional) The label of the new field. Defaults to a random string.
* @param string $field_type
* (optional) The field type of the new field storage. Defaults to
* 'test_field'.
* @param array $storage_edit
* (optional) $edit parameter for submitForm() on the second step
* ('Storage settings' form).
* @param array $field_edit
* (optional) $edit parameter for submitForm() on the third step ('Field
* settings' form).
* @param bool $save_settings
* (optional) Parameter for conditional execution of second and third step
* (Saving the storage settings and field settings). Defaults to 'TRUE'.
*/
public function fieldUIAddNewField($bundle_path, $field_name, $label = NULL, $field_type = 'test_field', array $storage_edit = [], array $field_edit = [], bool $save_settings = TRUE) {
// Generate a label containing only letters and numbers to prevent random
// test failure.
// See https://www.drupal.org/project/drupal/issues/3030902
$label = $label ?: $this->randomMachineName();
$initial_edit = [
'new_storage_type' => $field_type,
];
$second_edit = [
'label' => $label,
'field_name' => $field_name,
];
// Allow the caller to set a NULL path in case they navigated to the right
// page before calling this method.
if ($bundle_path !== NULL) {
$bundle_path = "$bundle_path/fields/add-field";
// First step: 'Add field' page.
$this->drupalGet($bundle_path);
}
else {
$bundle_path = $this->getUrl();
}
try {
// First check if the passed in field type is not part of a group.
$this->assertSession()->elementExists('css', "[name='new_storage_type'][value='$field_type']");
}
// If the element could not be found then it is probably in a group.
catch (ElementNotFoundException) {
// Call the helper function to confirm it is in a group.
$field_group = $this->getFieldFromGroup($field_type);
if ($field_group) {
// Pass in the group name as the new storage type.
$initial_edit['new_storage_type'] = $field_group;
$second_edit['group_field_options_wrapper'] = $field_type;
$this->drupalGet($bundle_path);
}
}
$this->submitForm($initial_edit, 'Continue');
$this->submitForm($second_edit, 'Continue');
// Assert that the field is not created.
$this->assertFieldDoesNotExist($bundle_path, $label);
if ($save_settings) {
$this->assertSession()->pageTextContains("These settings apply to the $label field everywhere it is used.");
// Test Breadcrumbs.
$this->getSession()->getPage()->findLink($label);
// Ensure that each array key in $storage_edit is prefixed with field_storage.
$prefixed_storage_edit = [];
foreach ($storage_edit as $key => $value) {
if (str_starts_with($key, 'field_storage')) {
$prefixed_storage_edit[$key] = $value;
continue;
}
// If the key starts with settings, it needs to be prefixed differently.
if (str_starts_with($key, 'settings[')) {
$prefixed_storage_edit[str_replace('settings[', 'field_storage[subform][settings][', $key)] = $value;
continue;
}
$prefixed_storage_edit['field_storage[subform][' . $key . ']'] = $value;
}
// Second step: 'Storage settings' form.
$this->submitForm($prefixed_storage_edit, 'Update settings');
// Third step: 'Field settings' form.
$this->submitForm($field_edit, 'Save settings');
$this->assertSession()->pageTextContains("Saved $label configuration.");
// Check that the field appears in the overview form.
$this->assertFieldExistsOnOverview($label);
}
}
/**
* Adds an existing field through the Field UI.
*
* @param string $bundle_path
* Admin path of the bundle that the field is to be attached to.
* @param string $existing_storage_name
* The name of the existing field storage for which we want to add a new
* field.
* @param string $label
* (optional) The label of the new field. Defaults to a random string.
* @param array $field_edit
* (optional) $edit parameter for submitForm() on the second step
* ('Field settings' form).
*/
public function fieldUIAddExistingField($bundle_path, $existing_storage_name, $label = NULL, array $field_edit = []) {
$label = $label ?: $this->randomMachineName();
$field_edit['edit-label'] = $label;
// First step: navigate to the re-use field page.
$this->drupalGet("{$bundle_path}/fields/");
// Confirm that the local action is visible.
$this->assertSession()->linkExists('Re-use an existing field');
$this->clickLink('Re-use an existing field');
$this->assertSession()->elementExists('css', "input[value=Re-use][name=$existing_storage_name]");
$this->click("input[value=Re-use][name=$existing_storage_name]");
// Set the main content to only the content region because the label can
// contain HTML which will be auto-escaped by Twig.
$this->assertSession()->responseContains('field-config-edit-form');
// Check that the page does not have double escaped HTML tags.
$this->assertSession()->responseNotContains('&amp;lt;');
// Second step: 'Field settings' form.
$this->submitForm($field_edit, 'Save settings');
$this->assertSession()->pageTextContains("Saved $label configuration.");
// Check that the field appears in the overview form.
$xpath = $this->assertSession()->buildXPathQuery("//table[@id=\"field-overview\"]//tr/td[1 and text() = :label]", [
':label' => $label,
]);
$this->assertSession()->elementExists('xpath', $xpath);
}
/**
* Deletes a field through the Field UI.
*
* @param string $bundle_path
* Admin path of the bundle that the field is to be deleted from.
* @param string $field_name
* The name of the field.
* @param string $label
* The label of the field.
* @param string $bundle_label
* The label of the bundle.
* @param string $source_label
* (optional) The label of the source entity type bundle.
*/
public function fieldUIDeleteField($bundle_path, $field_name, $label, $bundle_label, string $source_label = '') {
// Display confirmation form.
$this->drupalGet("$bundle_path/fields/$field_name/delete");
$this->assertSession()->pageTextContains("Are you sure you want to delete the field $label");
// Test Breadcrumbs.
$this->assertSession()->linkExists($label, 0, 'Field label is correct in the breadcrumb of the field delete page.');
// Submit confirmation form.
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("The field $label has been deleted from the $bundle_label $source_label");
// Check that the field does not appear in the overview form.
$xpath = $this->assertSession()->buildXPathQuery('//table[@id="field-overview"]//span[@class="label-field" and text()= :label]', [
':label' => $label,
]);
$this->assertSession()->elementNotExists('xpath', $xpath);
}
/**
* Helper function that returns the name of the group that a field is in.
*
* @param string $field_type
* The name of the field type.
*
* @return string
* Group name
*/
public function getFieldFromGroup($field_type) {
$group_elements = $this->getSession()->getPage()->findAll('css', '.field-option-radio');
$groups = [];
foreach ($group_elements as $group_element) {
$groups[] = $group_element->getAttribute('value');
}
foreach ($groups as $group) {
$test = [
'new_storage_type' => $group,
];
$this->submitForm($test, 'Continue');
try {
$this->assertSession()->elementExists('css', "[name='group_field_options_wrapper'][value='$field_type']");
$this->submitForm([], 'Back');
return $group;
}
catch (ElementNotFoundException) {
$this->submitForm([], 'Back');
continue;
}
}
return NULL;
}
/**
* Asserts that the field doesn't exist in the overview form.
*
* @param string $bundle_path
* The bundle path.
* @param string $label
* The field label.
*/
protected function assertFieldDoesNotExist(string $bundle_path, string $label) {
$original_url = $this->getUrl();
$this->drupalGet(explode('/fields', $bundle_path)[0] . '/fields');
$this->assertFieldDoesNotExistOnOverview($label);
$this->drupalGet($original_url);
}
/**
* Asserts that the field appears on the overview form.
*
* @param string $label
* The field label.
*
* @throws \Behat\Mink\Exception\ElementNotFoundException
*/
protected function assertFieldExistsOnOverview(string $label) {
$xpath = $this->assertSession()
->buildXPathQuery("//table[@id=\"field-overview\"]//tr/td[1 and text() = :label]", [
':label' => $label,
]);
$element = $this->getSession()->getPage()->find('xpath', $xpath);
if ($element === NULL) {
throw new ElementNotFoundException($this->getSession()->getDriver(), 'form field', 'label', $label);
}
}
/**
* Asserts that the field does not appear on the overview form.
*
* @param string $label
* The field label.
*/
protected function assertFieldDoesNotExistOnOverview(string $label) {
$xpath = $this->assertSession()
->buildXPathQuery("//table[@id=\"field-overview\"]//tr/td[1 and text() = :label]", [
':label' => $label,
]);
$element = $this->getSession()->getPage()->find('xpath', $xpath);
$this->assertSession()->assert($element === NULL, sprintf('A field "%s" appears on this page, but it should not.', $label));
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Unit;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\TempStore\PrivateTempStore;
use Drupal\field_ui\Form\FieldConfigEditForm;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\field_ui\Form\FieldConfigEditForm
*
* @group field_ui
*/
class FieldConfigEditFormTest extends UnitTestCase {
/**
* The field config edit form.
*
* @var \Drupal\field_ui\Form\FieldConfigEditForm|\PHPUnit\Framework\MockObject\MockObject
*/
protected $fieldConfigEditForm;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$entity_type_bundle_info = $this->createMock('\Drupal\Core\Entity\EntityTypeBundleInfoInterface');
$typed_data = $this->createMock('\Drupal\Core\TypedData\TypedDataManagerInterface');
$temp_store = $this->createMock(PrivateTempStore::class);
$element_info_manager = $this->createMock(ElementInfoManagerInterface::class);
$entity_display_repository = $this->createMock(EntityDisplayRepositoryInterface::class);
$this->fieldConfigEditForm = new FieldConfigEditForm($entity_type_bundle_info, $typed_data, $entity_display_repository, $temp_store, $element_info_manager);
}
/**
* @covers ::hasAnyRequired
*
* @dataProvider providerRequired
*/
public function testHasAnyRequired(array $element, bool $result): void {
$reflection = new \ReflectionClass('\Drupal\field_ui\Form\FieldConfigEditForm');
$method = $reflection->getMethod('hasAnyRequired');
$this->assertEquals($result, $method->invoke($this->fieldConfigEditForm, $element));
}
/**
* Provides test cases with required and optional elements.
*/
public static function providerRequired(): \Generator {
yield 'required' => [
[['#required' => TRUE]],
TRUE,
];
yield 'optional' => [
[['#required' => FALSE]],
FALSE,
];
yield 'required and optional' => [
[['#required' => TRUE], ['#required' => FALSE]],
TRUE,
];
yield 'empty' => [
[[], []],
FALSE,
];
yield 'multiple required' => [
[[['#required' => TRUE]], [['#required' => TRUE]]],
TRUE,
];
yield 'multiple optional' => [
[[['#required' => FALSE]], [['#required' => FALSE]]],
FALSE,
];
}
}

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Unit;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\field_ui\Element\FieldUiTable
*
* @group field_ui
*/
class FieldUiTableTest extends UnitTestCase {
/**
* @covers ::reduceOrder
*
* @dataProvider providerTestReduceOrder
*/
public function testReduceOrder($array, $expected): void {
$this->assertSame($expected, array_reduce($array, ['Drupal\field_ui\Element\FieldUiTable', 'reduceOrder']));
}
/**
* Provides test data for testReduceOrder().
*/
public static function providerTestReduceOrder() {
return [
'Flat' => [
'array' => [
[
'name' => 'foo',
],
[
'name' => 'bar',
],
[
'name' => 'baz',
],
],
'expected' => ['foo', 'bar', 'baz'],
],
'Nested' => [
'array' => [
[
'name' => 'foo',
'children' => [
[
'name' => 'bar',
'weight' => 0,
],
[
'name' => 'baz',
'weight' => -1,
],
],
],
[
'name' => 'biz',
],
],
'expected' => ['foo', 'baz', 'bar', 'biz'],
],
'Nested no name key' => [
'array' => [
[
'children' => [
[
'name' => 'foo',
'weight' => -1,
],
[
'name' => 'bar',
'weight' => 1,
],
[
'name' => 'baz',
'weight' => 0,
],
],
],
[
'name' => 'biz',
],
],
'expected' => ['foo', 'baz', 'bar', 'biz'],
],
];
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\field_ui\Unit;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\field_ui\FieldUI;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\field_ui\FieldUI
*
* @group field_ui
*/
class FieldUiTest extends UnitTestCase {
/**
* The path validator.
*
* @var \Drupal\Core\Path\PathValidatorInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $pathValidator;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->pathValidator = $this->createMock('Drupal\Core\Path\PathValidatorInterface');
$container = new ContainerBuilder();
$container->set('path.validator', $this->pathValidator);
\Drupal::setContainer($container);
}
/**
* @covers ::getNextDestination
*/
public function testGetNextDestination(): void {
$destinations = ['admin', 'admin/content'];
$expected_uri = 'base:admin';
$expected_query = [
'destinations' => ['admin/content'],
];
$actual = FieldUI::getNextDestination($destinations);
$this->assertSame($expected_uri, $actual->getUri());
$this->assertSame($expected_query, $actual->getOption('query'));
}
/**
* @covers ::getNextDestination
*/
public function testGetNextDestinationEmpty(): void {
$destinations = [];
$actual = FieldUI::getNextDestination($destinations);
$this->assertNull($actual);
}
/**
* @covers ::getNextDestination
*/
public function testGetNextDestinationRouteName(): void {
$destinations = [['route_name' => 'system.admin'], ['route_name' => 'system.admin_content']];
$expected_route_name = 'system.admin';
$expected_query = [
'destinations' => [['route_name' => 'system.admin_content']],
];
$actual = FieldUI::getNextDestination($destinations);
$this->assertSame($expected_route_name, $actual->getRouteName());
$this->assertSame($expected_query, $actual->getOption('query'));
}
}