Commit e55a7d8d authored by webchick's avatar webchick

Issue #2578561 by tim.plunkett, joelpittet, Bojhan, Fabianx, xjm, cilefen,...

Issue #2578561 by tim.plunkett, joelpittet, Bojhan, Fabianx, xjm, cilefen, David_Rothstein, DamienMcKenna: Move "Inline Form Errors" functionality to optional module and restore D7-style form errors by default
parent 40fa14ba
...@@ -341,7 +341,6 @@ services: ...@@ -341,7 +341,6 @@ services:
arguments: ['@request_stack', '@url_generator'] arguments: ['@request_stack', '@url_generator']
form_error_handler: form_error_handler:
class: Drupal\Core\Form\FormErrorHandler class: Drupal\Core\Form\FormErrorHandler
arguments: ['@string_translation', '@link_generator', '@renderer']
form_cache: form_cache:
class: Drupal\Core\Form\FormCache class: Drupal\Core\Form\FormCache
arguments: ['@app.root', '@keyvalue.expirable', '@module_handler', '@current_user', '@csrf_token', '@logger.channel.form', '@request_stack', '@page_cache_request_policy'] arguments: ['@app.root', '@keyvalue.expirable', '@module_handler', '@current_user', '@csrf_token', '@logger.channel.form', '@request_stack', '@page_cache_request_policy']
......
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
use Drupal\Component\Utility\Xss; use Drupal\Component\Utility\Xss;
use Drupal\Core\Database\Database; use Drupal\Core\Database\Database;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\FormElementHelper;
use Drupal\Core\Form\OptGroup; use Drupal\Core\Form\OptGroup;
use Drupal\Core\Render\Element; use Drupal\Core\Render\Element;
use Drupal\Core\Template\Attribute; use Drupal\Core\Template\Attribute;
...@@ -222,11 +221,8 @@ function template_preprocess_fieldset(&$variables) { ...@@ -222,11 +221,8 @@ function template_preprocess_fieldset(&$variables) {
$variables['attributes']['aria-describedby'] = $description_id; $variables['attributes']['aria-describedby'] = $description_id;
} }
// Display any error messages. // Suppress error messages.
$variables['errors'] = NULL; $variables['errors'] = NULL;
if (!empty($element['#errors']) && empty($element['#error_no_message'])) {
$variables['errors'] = $element['#errors'];
}
} }
/** /**
...@@ -257,11 +253,8 @@ function template_preprocess_details(&$variables) { ...@@ -257,11 +253,8 @@ function template_preprocess_details(&$variables) {
$variables['children'] = (isset($element['#children'])) ? $element['#children'] : ''; $variables['children'] = (isset($element['#children'])) ? $element['#children'] : '';
$variables['value'] = (isset($element['#value'])) ? $element['#value'] : ''; $variables['value'] = (isset($element['#value'])) ? $element['#value'] : '';
// Display any error messages. // Suppress error messages.
$variables['errors'] = NULL; $variables['errors'] = NULL;
if (!empty($element['#errors']) && empty($element['#error_no_message'])) {
$variables['errors'] = $element['#errors'];
}
} }
/** /**
...@@ -437,7 +430,7 @@ function template_preprocess_form_element(&$variables) { ...@@ -437,7 +430,7 @@ function template_preprocess_form_element(&$variables) {
); );
$variables['attributes'] = $element['#wrapper_attributes']; $variables['attributes'] = $element['#wrapper_attributes'];
// Add element #id for #type 'item' and 'password_confirm'. // Add element #id for #type 'item'.
if (isset($element['#markup']) && !empty($element['#id'])) { if (isset($element['#markup']) && !empty($element['#id'])) {
$variables['attributes']['id'] = $element['#id']; $variables['attributes']['id'] = $element['#id'];
} }
...@@ -453,11 +446,8 @@ function template_preprocess_form_element(&$variables) { ...@@ -453,11 +446,8 @@ function template_preprocess_form_element(&$variables) {
// Pass elements disabled status to template. // Pass elements disabled status to template.
$variables['disabled'] = !empty($element['#attributes']['disabled']) ? $element['#attributes']['disabled'] : NULL; $variables['disabled'] = !empty($element['#attributes']['disabled']) ? $element['#attributes']['disabled'] : NULL;
// Display any error messages. // Suppress error messages.
$variables['errors'] = NULL; $variables['errors'] = NULL;
if (!empty($element['#errors']) && empty($element['#error_no_message'])) {
$variables['errors'] = $element['#errors'];
}
// If #title is not set, we don't display any label. // If #title is not set, we don't display any label.
if (!isset($element['#title'])) { if (!isset($element['#title'])) {
......
...@@ -546,11 +546,8 @@ function template_preprocess_datetime_wrapper(&$variables) { ...@@ -546,11 +546,8 @@ function template_preprocess_datetime_wrapper(&$variables) {
$variables['title'] = $element['#title']; $variables['title'] = $element['#title'];
} }
// Display any error messages. // Suppress error messages.
$variables['errors'] = NULL; $variables['errors'] = NULL;
if (!empty($element['#errors']) && empty($element['#error_no_message'])) {
$variables['errors'] = $element['#errors'];
}
if (!empty($element['#description'])) { if (!empty($element['#description'])) {
$variables['description'] = $element['#description']; $variables['description'] = $element['#description'];
......
...@@ -8,44 +8,12 @@ ...@@ -8,44 +8,12 @@
namespace Drupal\Core\Form; namespace Drupal\Core\Form;
use Drupal\Core\Render\Element; use Drupal\Core\Render\Element;
use Drupal\Core\Routing\LinkGeneratorTrait;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Url;
use Drupal\Core\Utility\LinkGeneratorInterface;
/** /**
* Handles form errors. * Handles form errors.
*/ */
class FormErrorHandler implements FormErrorHandlerInterface { class FormErrorHandler implements FormErrorHandlerInterface {
use StringTranslationTrait;
use LinkGeneratorTrait;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a new FormErrorHandler.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
* @param \Drupal\Core\Utility\LinkGeneratorInterface $link_generator
* The link generation service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
*/
public function __construct(TranslationInterface $string_translation, LinkGeneratorInterface $link_generator, RendererInterface $renderer) {
$this->stringTranslation = $string_translation;
$this->linkGenerator = $link_generator;
$this->renderer = $renderer;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
...@@ -71,51 +39,12 @@ public function handleFormErrors(array &$form, FormStateInterface $form_state) { ...@@ -71,51 +39,12 @@ public function handleFormErrors(array &$form, FormStateInterface $form_state) {
* The current state of the form. * The current state of the form.
*/ */
protected function displayErrorMessages(array $form, FormStateInterface $form_state) { protected function displayErrorMessages(array $form, FormStateInterface $form_state) {
$error_links = [];
$errors = $form_state->getErrors(); $errors = $form_state->getErrors();
// Loop through all form errors and check if we need to display a link.
foreach ($errors as $name => $error) {
$form_element = FormElementHelper::getElementByName($name, $form);
$title = FormElementHelper::getElementTitle($form_element);
// Only show links to erroneous elements that are visible.
$is_visible_element = Element::isVisibleElement($form_element);
// Only show links for elements that have a title themselves or have
// children with a title.
$has_title = !empty($title);
// Only show links for elements with an ID.
$has_id = !empty($form_element['#id']);
// Do not show links to elements with suppressed messages. Most often
// their parent element is used for inline errors.
if (!empty($form_element['#error_no_message'])) {
unset($errors[$name]);
}
elseif ($is_visible_element && $has_title && $has_id) {
$error_links[] = $this->l($title, Url::fromRoute('<none>', [], ['fragment' => $form_element['#id'], 'external' => TRUE]));
unset($errors[$name]);
}
}
// Set normal error messages for all remaining errors. // Loop through all form errors and set an error message.
foreach ($errors as $error) { foreach ($errors as $error) {
$this->drupalSetMessage($error, 'error'); $this->drupalSetMessage($error, 'error');
} }
if (!empty($error_links)) {
$render_array = [
[
'#markup' => $this->formatPlural(count($error_links), '1 error has been found: ', '@count errors have been found: '),
],
[
'#theme' => 'item_list',
'#items' => $error_links,
'#context' => ['list_style' => 'comma-list'],
],
];
$message = $this->renderer->renderPlain($render_array);
$this->drupalSetMessage($message, 'error');
}
} }
/** /**
......
...@@ -417,8 +417,7 @@ public static function hasAnyErrors(); ...@@ -417,8 +417,7 @@ public static function hasAnyErrors();
* indicate which element needs to be changed and provide an error message. * indicate which element needs to be changed and provide an error message.
* This causes the Form API to not execute the form submit handlers, and * This causes the Form API to not execute the form submit handlers, and
* instead to re-display the form to the user with the corresponding elements * instead to re-display the form to the user with the corresponding elements
* rendered with an 'error' CSS class (shown as red by default) and the error * rendered with an 'error' CSS class (shown as red by default).
* message near the element.
* *
* The standard behavior of this method can be changed if a button provides * The standard behavior of this method can be changed if a button provides
* the #limit_validation_errors property. Multistep forms not wanting to * the #limit_validation_errors property. Multistep forms not wanting to
......
...@@ -317,7 +317,7 @@ protected function doTestAuthoringInfo() { ...@@ -317,7 +317,7 @@ protected function doTestAuthoringInfo() {
'content_translation[created]' => '19/11/1978', 'content_translation[created]' => '19/11/1978',
); );
$this->drupalPostForm($entity->urlInfo('edit-form'), $edit, $this->getFormSubmitAction($entity, $langcode)); $this->drupalPostForm($entity->urlInfo('edit-form'), $edit, $this->getFormSubmitAction($entity, $langcode));
$this->assertTrue($this->xpath('//div[contains(concat(" ", normalize-space(@class), " "), :class)]', array(':class' => ' messages--error ')), 'Invalid values generate a form error message.'); $this->assertTrue($this->xpath('//div[contains(@class, "error")]//ul'), 'Invalid values generate a list of form errors.');
$metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode)); $metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
$this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly kept.'); $this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly kept.');
$this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly kept.'); $this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly kept.');
......
...@@ -548,7 +548,9 @@ function testDatelistWidget() { ...@@ -548,7 +548,9 @@ function testDatelistWidget() {
$this->drupalPostForm(NULL, $edit, t('Save')); $this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertResponse(200); $this->assertResponse(200);
$this->assertText(t($expected)); foreach ($expected as $expected_text) {
$this->assertText(t($expected_text));
}
} }
// Test the widget for complete input with zeros as part of selections. // Test the widget for complete input with zeros as part of selections.
...@@ -589,13 +591,27 @@ function testDatelistWidget() { ...@@ -589,13 +591,27 @@ function testDatelistWidget() {
protected function datelistDataProvider() { protected function datelistDataProvider() {
return [ return [
// Year only selected, validation error on Month, Day, Hour, Minute. // Year only selected, validation error on Month, Day, Hour, Minute.
[['year' => 2012, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''], '4 errors have been found: MonthDayHourMinute'], [['year' => 2012, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''], [
'A value must be selected for month.',
'A value must be selected for day.',
'A value must be selected for hour.',
'A value must be selected for minute.',
]],
// Year and Month selected, validation error on Day, Hour, Minute. // Year and Month selected, validation error on Day, Hour, Minute.
[['year' => 2012, 'month' => '12', 'day' => '', 'hour' => '', 'minute' => ''], '3 errors have been found: DayHourMinute'], [['year' => 2012, 'month' => '12', 'day' => '', 'hour' => '', 'minute' => ''], [
'A value must be selected for day.',
'A value must be selected for hour.',
'A value must be selected for minute.',
]],
// Year, Month and Day selected, validation error on Hour, Minute. // Year, Month and Day selected, validation error on Hour, Minute.
[['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => ''], '2 errors have been found: HourMinute'], [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => ''], [
'A value must be selected for hour.',
'A value must be selected for minute.',
]],
// Year, Month, Day and Hour selected, validation error on Minute only. // Year, Month, Day and Hour selected, validation error on Minute only.
[['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => ''], '1 error has been found: Minute'], [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => ''], [
'A value must be selected for minute.',
]],
]; ];
} }
......
...@@ -275,7 +275,6 @@ public function testFieldAdminHandler() { ...@@ -275,7 +275,6 @@ public function testFieldAdminHandler() {
$this->drupalPostForm('node/add/' . $this->type, $edit, t('Save')); $this->drupalPostForm('node/add/' . $this->type, $edit, t('Save'));
// Assert that entity reference autocomplete field is validated. // Assert that entity reference autocomplete field is validated.
$this->assertText(t('1 error has been found: Test Entity Reference Field'), 'Node save failed when required entity reference field was not correctly filled.');
$this->assertText(t('There are no entities matching "@entity"', ['@entity' => 'Test'])); $this->assertText(t('There are no entities matching "@entity"', ['@entity' => 'Test']));
$edit = array( $edit = array(
...@@ -286,7 +285,6 @@ public function testFieldAdminHandler() { ...@@ -286,7 +285,6 @@ public function testFieldAdminHandler() {
// Assert the results multiple times to avoid sorting problem of nodes with // Assert the results multiple times to avoid sorting problem of nodes with
// the same title. // the same title.
$this->assertText(t('1 error has been found: Test Entity Reference Field'));
$this->assertText(t('Multiple entities match this reference;')); $this->assertText(t('Multiple entities match this reference;'));
$this->assertText(t("@node1", ['@node1' => $node1->getTitle() . ' (' . $node1->id() . ')'])); $this->assertText(t("@node1", ['@node1' => $node1->getTitle() . ' (' . $node1->id() . ')']));
$this->assertText(t("@node2", ['@node2' => $node2->getTitle() . ' (' . $node2->id() . ')'])); $this->assertText(t("@node2", ['@node2' => $node2->getTitle() . ' (' . $node2->id() . ')']));
......
...@@ -390,7 +390,7 @@ public static function validateManagedFile(&$element, FormStateInterface $form_s ...@@ -390,7 +390,7 @@ public static function validateManagedFile(&$element, FormStateInterface $form_s
if ($element['#required'] && empty($element['fids']['#value']) && !in_array($clicked_button, ['upload_button', 'remove_button'])) { if ($element['#required'] && empty($element['fids']['#value']) && !in_array($clicked_button, ['upload_button', 'remove_button'])) {
// We expect the field name placeholder value to be wrapped in t() // We expect the field name placeholder value to be wrapped in t()
// here, so it won't be escaped again as it's already marked safe. // here, so it won't be escaped again as it's already marked safe.
$form_state->setError($element, t('@name is required.', ['@name' => $element['#title']])); $form_state->setError($element, t('@name field is required.', ['@name' => $element['#title']]));
} }
// Consolidate the array value of this field to array of FIDs. // Consolidate the array value of this field to array of FIDs.
......
...@@ -35,8 +35,7 @@ function testRequired() { ...@@ -35,8 +35,7 @@ function testRequired() {
$edit = array(); $edit = array();
$edit['title[0][value]'] = $this->randomMachineName(); $edit['title[0][value]'] = $this->randomMachineName();
$this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish')); $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
$this->assertText('1 error has been found: ' . $field->label(), 'Node save failed when required file field was empty.'); $this->assertRaw(t('@title field is required.', array('@title' => $field->getLabel())), 'Node save failed when required file field was empty.');
$this->assertIdentical(1, count($this->xpath('//div[contains(concat(" ", normalize-space(@class), " "), :class)]//a', [':class' => ' messages--error '])), 'There is one link in the error message.');
// Create a new node with the uploaded file. // Create a new node with the uploaded file.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name); $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
...@@ -57,8 +56,7 @@ function testRequired() { ...@@ -57,8 +56,7 @@ function testRequired() {
$edit = array(); $edit = array();
$edit['title[0][value]'] = $this->randomMachineName(); $edit['title[0][value]'] = $this->randomMachineName();
$this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish')); $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
$this->assertText('1 error has been found: ' . $field->label(), 'Node save failed when required multiple value file field was empty.'); $this->assertRaw(t('@title field is required.', array('@title' => $field->getLabel())), 'Node save failed when required multiple value file field was empty.');
$this->assertIdentical(1, count($this->xpath('//div[contains(concat(" ", normalize-space(@class), " "), :class)]//a', [':class' => ' messages--error '])), 'There is one link in the error message.');
// Create a new node with the uploaded file into the multivalue field. // Create a new node with the uploaded file into the multivalue field.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name); $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
......
type: module
name: Inline Form Errors
description: 'Enables inline form errors.'
version: VERSION
core: 8.x
package: Core (Experimental)
<?php
/**
* @file
* Enables inline form errors.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function inline_form_errors_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.inline_form_errors':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Inline Form Errors module provides an experimental approach to form errors, placing the error messages next to the elements themselves. For more information, see the <a href=":inline_form_error">online documentation for the Inline Form Errors module</a>.', [':inline_form_error' => 'https://www.drupal.org/documentation/modules/inline_form_error']) . '</p>';
return $output;
}
}
/**
* Implements hook_preprocess_HOOK() for form element templates.
*/
function inline_form_errors_preprocess_form_element(&$variables) {
_inline_form_errors_set_errors($variables);
}
/**
* Implements hook_preprocess_HOOK() for details element templates.
*/
function inline_form_errors_preprocess_details(&$variables) {
_inline_form_errors_set_errors($variables);
}
/**
* Implements hook_preprocess_HOOK() for fieldset element templates.
*/
function inline_form_errors_preprocess_fieldset(&$variables) {
_inline_form_errors_set_errors($variables);
}
/**
* Implements hook_preprocess_HOOK() for datetime form wrapper templates.
*/
function inline_form_errors_preprocess_datetime_wrapper(&$variables) {
_inline_form_errors_set_errors($variables);
}
/**
* Populates form errors in the template.
*/
function _inline_form_errors_set_errors(&$variables) {
$element = $variables['element'];
if (!empty($element['#errors']) && empty($element['#error_no_message'])) {
$variables['errors'] = $element['#errors'];
}
}
<?php
/**
* @file
* Contains \Drupal\inline_form_errors\FormErrorHandler.
*/
namespace Drupal\inline_form_errors;
use Drupal\Core\Form\FormElementHelper;
use Drupal\Core\Form\FormErrorHandler as CoreFormErrorHandler;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\LinkGeneratorTrait;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Url;
use Drupal\Core\Utility\LinkGeneratorInterface;
/**
* Produces inline form errors.
*/
class FormErrorHandler extends CoreFormErrorHandler {
use StringTranslationTrait;
use LinkGeneratorTrait;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a new FormErrorHandler.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
* @param \Drupal\Core\Utility\LinkGeneratorInterface $link_generator
* The link generation service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
*/
public function __construct(TranslationInterface $string_translation, LinkGeneratorInterface $link_generator, RendererInterface $renderer) {
$this->stringTranslation = $string_translation;
$this->linkGenerator = $link_generator;
$this->renderer = $renderer;
}
/**
* Loops through and displays all form errors.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
protected function displayErrorMessages(array $form, FormStateInterface $form_state) {
$error_links = [];
$errors = $form_state->getErrors();
// Loop through all form errors and check if we need to display a link.
foreach ($errors as $name => $error) {
$form_element = FormElementHelper::getElementByName($name, $form);
$title = FormElementHelper::getElementTitle($form_element);
// Only show links to erroneous elements that are visible.
$is_visible_element = Element::isVisibleElement($form_element);
// Only show links for elements that have a title themselves or have
// children with a title.
$has_title = !empty($title);
// Only show links for elements with an ID.
$has_id = !empty($form_element['#id']);
// Do not show links to elements with suppressed messages. Most often
// their parent element is used for inline errors.
if (!empty($form_element['#error_no_message'])) {
unset($errors[$name]);
}
elseif ($is_visible_element && $has_title && $has_id) {
$error_links[] = $this->l($title, Url::fromRoute('<none>', [], ['fragment' => $form_element['#id'], 'external' => TRUE]));
unset($errors[$name]);
}
}
// Set normal error messages for all remaining errors.
foreach ($errors as $error) {
$this->drupalSetMessage($error, 'error');
}
if (!empty($error_links)) {
$render_array = [
[
'#markup' => $this->formatPlural(count($error_links), '1 error has been found: ', '@count errors have been found: '),
],
[
'#theme' => 'item_list',
'#items' => $error_links,
'#context' => ['list_style' => 'comma-list'],
],
];
$message = $this->renderer->renderPlain($render_array);
$this->drupalSetMessage($message, 'error');
}
}
}
<?php
/**
* @file
* Contains \Drupal\inline_form_errors\InlineFormErrorsServiceProvider.
*/
namespace Drupal\inline_form_errors;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
use Symfony\Component\DependencyInjection\Reference;
/**
* Overrides the form_error_handler service to enable inline form errors.
*/
class InlineFormErrorsServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
$container->getDefinition('form_error_handler')
->setClass(FormErrorHandler::class)
->setArguments([new Reference('string_translation'), new Reference('link_generator'), new Reference('renderer')]);
}
}
<?php
/**
* @file
* Contains \Drupal\Tests\inline_form_errors\Unit\FormErrorHandlerTest.
*/
namespace Drupal\Tests\inline_form_errors\Unit;
use Drupal\Core\Form\FormState;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Utility\LinkGeneratorInterface;
use Drupal\inline_form_errors\FormErrorHandler;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\inline_form_errors\FormErrorHandler
* @group InlineFormErrors
*/
class FormErrorHandlerTest extends UnitTestCase {
/**
* @covers ::handleFormErrors
* @covers ::displayErrorMessages
*/
public function testDisplayErrorMessagesInline() {
$link_generator = $this->getMock(LinkGeneratorInterface::class);
$link_generator->expects($this->any())
->method('generate')
->willReturnArgument(0);
$renderer = $this->getMock(RendererInterface::class);
$form_error_handler = $this->getMockBuilder(FormErrorHandler::class)
->setConstructorArgs([$this->getStringTranslationStub(), $link_generator, $renderer])
->setMethods(['drupalSetMessage'])
->getMock();
$form_error_handler->expects($this->at(0))
->method('drupalSetMessage')
->with('no title given', 'error');
$form_error_handler->expects($this->at(1))
->method('drupalSetMessage')
->with('element is invisible', 'error');
$form_error_handler->expects($this->at(2))
->method('drupalSetMessage')
->with('this missing element is invalid', 'error');
$form_error_handler->expects($this->at(3))
->method('drupalSetMessage')
->with('3 errors have been found: <ul-comma-list-mock><li-mock>Test 1</li-mock><li-mock>Test 2 &amp; a half</li-mock><li-mock>Test 3</li-mock></ul-comma-list-mock>', 'error');
$renderer->expects($this->any())
->method('renderPlain')
->will($this->returnCallback(function ($render_array) {
return $render_array[0]['#markup'] . '<ul-comma-list-mock><li-mock>' . implode(array_map('htmlspecialchars', $render_array[1]['#items']), '</li-mock><li-mock>') . '</li-mock></ul-comma-list-mock>';
}));
$form = [
'#parents' => [],
];
$form['test1'] = [
'#type' => 'textfield',
'#title' => 'Test 1',
'#parents' => ['test1'],
'#id' => 'edit-test1',
];
$form['test2'] = [
'#type' => 'textfield',
'#title' => 'Test 2 & a half',
'#parents' => ['test2'],
'#id' => 'edit-test2',
];
$form['fieldset'] = [
'#parents' => ['fieldset'],
'test3' => [
'#type' => 'textfield',
'#title' => 'Test 3',
'#parents' => ['fieldset', 'test3'],
'#id' => 'edit-test3',
],
];
$form['test4'] = [
'#type' => 'textfield',
'#title' => 'Test 4',
'#parents' => ['test4'],
'#id' => 'edit-test4',
'#error_no_message' => TRUE,
];
$form['test5'] = [
'#type' => 'textfield',
'#parents' => ['test5'],
'#id' => 'edit-test5',
];
$form['test6'] = [
'#type' => 'value',
'#title' => 'Test 6',
'#parents' => ['test6'],
'#id' => 'edit-test6',
];
$form_state = new FormState();
$form_state->setErrorByName('test1', 'invalid');
$form_state->setErrorByName('test2', 'invalid');
$form_state->setErrorByName('fieldset][test3', 'invalid');
$form_state->setErrorByName('test4', 'no error message');
$form_state->setErrorByName('test5', 'no title given');
$form_state->setErrorByName('test6', 'element is invisible');
$form_state->setErrorByName('missing_element', 'this missing element is invalid');
$form_error_handler->handleFormErrors($form, $form_state);
$this->assertSame('invalid', $form['test1']['#errors']);
}