Commit 215b967a authored by alexpott's avatar alexpott

Issue #1493324 by tim.plunkett, dmsmidt, mgifford, bleen18, davidhernandez,...

Issue #1493324 by tim.plunkett, dmsmidt, mgifford, bleen18, davidhernandez, crasx, mparker17, stefan.r, YesCT, joelpittet, tstoeckler, larowlan, vijaycs85, swentel, rpayanm, Bojhan, LewisNyman, emma.maria, BarisW, njbarrett, rteijeiro, nod_, sun, joshtaylor, mrjmd, webchick, marcvangend, kattekrab, SKAUGHT, bowersox, andrewmacpherson, Manjit.Singh, RavindraSingh, Wim Leers, BLadwin, aspilicious, mortendk, mausolos, jessebeach, Gábor Hojtsy, anandps, falcon03, franz, andypost, rooby, rootwork, Cottser, Xano: Inline form errors for accessibility and UX
parent 3dd38370
......@@ -285,10 +285,13 @@ services:
arguments: ['@form_validator', '@form_submitter', '@form_cache', '@module_handler', '@event_dispatcher', '@request_stack', '@class_resolver', '@element_info', '@theme.manager', '@?csrf_token']
form_validator:
class: Drupal\Core\Form\FormValidator
arguments: ['@request_stack', '@string_translation', '@csrf_token', '@logger.channel.form']
arguments: ['@request_stack', '@string_translation', '@csrf_token', '@logger.channel.form', '@form_error_handler']
form_submitter:
class: Drupal\Core\Form\FormSubmitter
arguments: ['@request_stack', '@url_generator']
form_error_handler:
class: Drupal\Core\Form\FormErrorHandler
arguments: ['@string_translation', '@link_generator']
form_cache:
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']
......
......@@ -11,6 +11,7 @@
use Drupal\Component\Utility\Xss;
use Drupal\Core\Database\Database;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\FormElementHelper;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Render\Element;
use Drupal\Core\Template\Attribute;
......@@ -203,6 +204,12 @@ function template_preprocess_fieldset(&$variables) {
// Add the description's id to the fieldset aria attributes.
$variables['attributes']['aria-describedby'] = $description_id;
}
// Display any error messages.
$variables['errors'] = NULL;
if (!empty($element['#errors']) && empty($element['#error_no_message'])) {
$variables['errors'] = $element['#errors'];
}
}
/**
......@@ -407,7 +414,7 @@ function template_preprocess_form_element(&$variables) {
);
$variables['attributes'] = $element['#wrapper_attributes'];
// Add element #id for #type 'item'.
// Add element #id for #type 'item' and 'password_confirm'.
if (isset($element['#markup']) && !empty($element['#id'])) {
$variables['attributes']['id'] = $element['#id'];
}
......@@ -423,6 +430,12 @@ function template_preprocess_form_element(&$variables) {
// Pass elements disabled status to template.
$variables['disabled'] = !empty($element['#attributes']['disabled']) ? $element['#attributes']['disabled'] : NULL;
// Display any error messages.
$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 (!isset($element['#title'])) {
$element['#title_display'] = 'none';
......
......@@ -500,6 +500,12 @@ function template_preprocess_datetime_wrapper(&$variables) {
$variables['title'] = $element['#title'];
}
// Display any error messages.
$variables['errors'] = NULL;
if (!empty($element['#errors']) && empty($element['#error_no_message'])) {
$variables['errors'] = $element['#errors'];
}
if (!empty($element['#description'])) {
$variables['description'] = $element['#description'];
}
......
......@@ -265,6 +265,7 @@ public static function processDatelist(&$element, FormStateInterface $form_state
'#attributes' => $element['#attributes'],
'#options' => $options,
'#required' => $element['#required'],
'#error_no_message' => TRUE,
);
}
......
......@@ -267,6 +267,7 @@ public static function processDatetime(&$element, FormStateInterface $form_state
'#attributes' => $element['#attributes'] + $extra_attributes,
'#required' => $element['#required'],
'#size' => max(12, strlen($element['#value']['date'])),
'#error_no_message' => TRUE,
);
// Allows custom callbacks to alter the element.
......@@ -298,6 +299,7 @@ public static function processDatetime(&$element, FormStateInterface $form_state
'#attributes' => $element['#attributes'] + $extra_attributes,
'#required' => $element['#required'],
'#size' => 12,
'#error_no_message' => TRUE,
);
// Allows custom callbacks to alter the element.
......
<?php
/**
* @file
* Contains \Drupal\Core\Form\FormElementHelper.
*/
namespace Drupal\Core\Form;
use Drupal\Core\Render\Element;
/**
* Provides common functionality for form elements.
*/
class FormElementHelper {
/**
* Retrieves a form element.
*
* @param string $name
* The name of the form element. If the #parents property of your form
* element is ['foo', 'bar', 'baz'] then the name is 'foo][bar][baz'.
* @param array $form
* An associative array containing the structure of the form.
*
* @return array
* The form element.
*/
public static function getElementByName($name, array $form) {
foreach (Element::children($form) as $key) {
if (implode('][', $form[$key]['#parents']) === $name) {
return $form[$key];
}
elseif ($element = static::getElementByName($name, $form[$key])) {
return $element;
}
}
return [];
}
/**
* Returns the title for the element.
*
* If the element has no title, this will recurse through all children of the
* element until a title is found.
*
* @param array $element
* An associative array containing the properties of the form element.
*
* @return string
* The title of the element, or an empty string if none is found.
*/
public static function getElementTitle(array $element) {
$title = '';
if (isset($element['#title'])) {
$title = $element['#title'];
}
else {
foreach (Element::children($element) as $key) {
if ($title = static::getElementTitle($element[$key])) {
break;
}
}
}
return $title;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Form\FormErrorHandler.
*/
namespace Drupal\Core\Form;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\LinkGeneratorTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Url;
use Drupal\Core\Utility\LinkGeneratorInterface;
/**
* Handles form errors.
*/
class FormErrorHandler implements FormErrorHandlerInterface {
use StringTranslationTrait;
use LinkGeneratorTrait;
/**
* 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.
*/
public function __construct(TranslationInterface $string_translation, LinkGeneratorInterface $link_generator) {
$this->stringTranslation = $string_translation;
$this->linkGenerator = $link_generator;
}
/**
* {@inheritdoc}
*/
public function handleFormErrors(array &$form, FormStateInterface $form_state) {
// After validation check if there are errors.
if ($errors = $form_state->getErrors()) {
// Display error messages for each element.
$this->displayErrorMessages($form, $form_state);
// Loop through and assign each element its errors.
$this->setElementErrorsFromFormState($form, $form_state);
}
return $this;
}
/**
* 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) {
// We need to pass this through SafeMarkup::escape() so
// drupal_set_message() does not encode the links.
$error_links[] = SafeMarkup::escape($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)) {
$message = $this->formatPlural(count($error_links), '1 error has been found: !errors', '@count errors have been found: !errors', [
'!errors' => SafeMarkup::set(implode(', ', $error_links)),
]);
$this->drupalSetMessage($message, 'error');
}
}
/**
* Stores the errors of each element directly on the element.
*
* We must provide a way for non-form functions to check the errors for a
* specific element. The most common usage of this is a #pre_render callback.
*
* @param array $elements
* An associative array containing the structure of a form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
protected function setElementErrorsFromFormState(array &$elements, FormStateInterface &$form_state) {
// Recurse through all children.
foreach (Element::children($elements) as $key) {
if (isset($elements[$key]) && $elements[$key]) {
$this->setElementErrorsFromFormState($elements[$key], $form_state);
}
}
// Store the errors for this element on the element directly.
$elements['#errors'] = $form_state->getError($elements);
}
/**
* Wraps drupal_set_message().
*
* @codeCoverageIgnore
*/
protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
drupal_set_message($message, $type, $repeat);
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Form\FormErrorHandlerInterface.
*/
namespace Drupal\Core\Form;
/**
* Provides an interface for handling form errors.
*/
interface FormErrorHandlerInterface {
/**
* Handles form errors after form validation.
*
* @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.
*
* @return $this
*/
public function handleFormErrors(array &$form, FormStateInterface $form_state);
}
......@@ -1090,9 +1090,6 @@ public function setErrorByName($name, $message = '') {
$errors[$name] = $message;
$this->errors = $errors;
static::setAnyErrors();
if ($message) {
$this->drupalSetMessage($message, 'error');
}
}
}
......@@ -1119,7 +1116,7 @@ public function clearErrors() {
* {@inheritdoc}
*/
public function getError(array $element) {
if ($errors = $this->getErrors($this)) {
if ($errors = $this->getErrors()) {
$parents = array();
foreach ($element['#parents'] as $parent) {
$parents[] = $parent;
......@@ -1244,15 +1241,6 @@ public function cleanValues() {
return $this;
}
/**
* Wraps drupal_set_message().
*
* @return array|null
*/
protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
return drupal_set_message($message, $type, $repeat);
}
/**
* Wraps ModuleHandler::loadInclude().
*/
......
......@@ -417,7 +417,8 @@ public static function hasAnyErrors();
* 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
* instead to re-display the form to the user with the corresponding elements
* rendered with an 'error' CSS class (shown as red by default).
* rendered with an 'error' CSS class (shown as red by default) and the error
* message near the element.
*
* The standard behavior of this method can be changed if a button provides
* the #limit_validation_errors property. Multistep forms not wanting to
......
......@@ -44,6 +44,13 @@ class FormValidator implements FormValidatorInterface {
*/
protected $logger;
/**
* The form error handler.
*
* @var \Drupal\Core\Form\FormErrorHandlerInterface
*/
protected $formErrorHandler;
/**
* Constructs a new FormValidator.
*
......@@ -55,12 +62,15 @@ class FormValidator implements FormValidatorInterface {
* The CSRF token generator.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
* @param \Drupal\Core\Form\FormErrorHandlerInterface $form_error_handler
* The form error handler.
*/
public function __construct(RequestStack $request_stack, TranslationInterface $string_translation, CsrfTokenGenerator $csrf_token, LoggerInterface $logger) {
public function __construct(RequestStack $request_stack, TranslationInterface $string_translation, CsrfTokenGenerator $csrf_token, LoggerInterface $logger, FormErrorHandlerInterface $form_error_handler) {
$this->requestStack = $request_stack;
$this->stringTranslation = $string_translation;
$this->csrfToken = $csrf_token;
$this->logger = $logger;
$this->formErrorHandler = $form_error_handler;
}
/**
......@@ -184,8 +194,9 @@ protected function handleErrorsWithLimitedValidation(&$form, FormStateInterface
* The unique string identifying the form.
*/
protected function finalizeValidation(&$form, FormStateInterface &$form_state, $form_id) {
// After validation, loop through and assign each element its errors.
$this->setElementErrorsFromFormState($form, $form_state);
// Delegate handling of form errors to a service.
$this->formErrorHandler->handleFormErrors($form, $form_state);
// Mark this form as validated.
$form_state->setValidationComplete();
}
......@@ -394,26 +405,4 @@ protected function determineLimitValidationErrors(FormStateInterface &$form_stat
}
}
/**
* Stores the errors of each element directly on the element.
*
* We must provide a way for non-form functions to check the errors for a
* specific element. The most common usage of this is a #pre_render callback.
*
* @param array $elements
* An associative array containing the structure of a form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
protected function setElementErrorsFromFormState(array &$elements, FormStateInterface &$form_state) {
// Recurse through all children.
foreach (Element::children($elements) as $key) {
if (isset($elements[$key]) && $elements[$key]) {
$this->setElementErrorsFromFormState($elements[$key], $form_state);
}
}
// Store the errors for this element on the element directly.
$elements['#errors'] = $form_state->getError($elements);
}
}
......@@ -142,7 +142,7 @@ public static function getVisibleChildren(array $elements) {
}
// Skip value and hidden elements, since they are not rendered.
if (isset($child['#type']) && in_array($child['#type'], array('value', 'hidden'))) {
if (!static::isVisibleElement($child)) {
continue;
}
......@@ -152,6 +152,19 @@ public static function getVisibleChildren(array $elements) {
return array_keys($visible_children);
}
/**
* Determines if an element is visible.
*
* @param array $element
* The element to check for visibility.
*
* @return bool
* TRUE if the element is visible, otherwise FALSE.
*/
public static function isVisibleElement($element) {
return (!isset($element['#type']) || !in_array($element['#type'], ['value', 'hidden', 'token'])) && (!isset($element['#access']) || $element['#access']);
}
/**
* Sets HTML attributes based on element properties.
*
......
......@@ -86,6 +86,8 @@ public static function processCheckboxes(&$element, FormStateInterface $form_sta
'#default_value' => isset($value[$key]) ? $key : NULL,
'#attributes' => $element['#attributes'],
'#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
// Errors should only be shown on the parent checkboxes element.
'#error_no_message' => TRUE,
'#weight' => $weight,
);
}
......
......@@ -26,6 +26,7 @@ public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#markup' => '',
'#process' => array(
array($class, 'processPasswordConfirm'),
),
......@@ -53,6 +54,7 @@ public static function processPasswordConfirm(&$element, FormStateInterface $for
'#value' => empty($element['#value']) ? NULL : $element['#value']['pass1'],
'#required' => $element['#required'],
'#attributes' => array('class' => array('password-field', 'js-password-field')),
'#error_no_message' => TRUE,
);
$element['pass2'] = array(
'#type' => 'password',
......@@ -60,6 +62,7 @@ public static function processPasswordConfirm(&$element, FormStateInterface $for
'#value' => empty($element['#value']) ? NULL : $element['#value']['pass2'],
'#required' => $element['#required'],
'#attributes' => array('class' => array('password-confirm', 'js-password-confirm')),
'#error_no_message' => TRUE,
);
$element['#element_validate'] = array(array(get_called_class(), 'validatePasswordConfirm'));
$element['#tree'] = TRUE;
......
......@@ -83,6 +83,8 @@ public static function processRadios(&$element, FormStateInterface $form_state,
'#parents' => $element['#parents'],
'#id' => HtmlUtility::getUniqueId('edit-' . implode('-', $parents_for_id)),
'#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
// Errors should only be shown on the parent radios element.
'#error_no_message' => TRUE,
'#weight' => $weight,
);
}
......
......@@ -278,7 +278,7 @@ protected function doTestAuthoringInfo() {
'content_translation[created]' => '19/11/1978',
);
$this->drupalPostForm($entity->urlInfo('edit-form'), $edit, $this->getFormSubmitAction($entity, $langcode));
$this->assertTrue($this->xpath('//div[contains(@class, "error")]//ul'), 'Invalid values generate a list of form errors.');
$this->assertTrue($this->xpath('//div[contains(concat(" ", normalize-space(@class), " "), :class)]', array(':class' => ' messages--error ')), 'Invalid values generate a form error message.');
$metadata = $this->manager->getTranslationMetadata($entity->getTranslation($langcode));
$this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly kept.');
$this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly kept.');
......
......@@ -1163,7 +1163,7 @@ function file_managed_file_save_upload($element, FormStateInterface $form_state)
$destination = isset($element['#upload_location']) ? $element['#upload_location'] : NULL;
if (isset($destination) && !file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) {
\Drupal::logger('file')->notice('The upload directory %directory for the file field !name could not be created or is not accessible. A newly uploaded file could not be saved in this directory as a consequence, and the upload was canceled.', array('%directory' => $destination, '!name' => $element['#field_name']));
$form_state->setErrorByName($upload_name, t('The file could not be uploaded.'));
$form_state->setError($element, t('The file could not be uploaded.'));
return FALSE;
}
......@@ -1173,7 +1173,7 @@ function file_managed_file_save_upload($element, FormStateInterface $form_state)
if ($files_uploaded) {
if (!$files = file_save_upload($upload_name, $element['#upload_validators'], $destination)) {
\Drupal::logger('file')->notice('The file upload failed. %upload', array('%upload' => $upload_name));
$form_state->setErrorByName($upload_name, t('Files in the !name field were unable to be uploaded.', array('!name' => $element['#title'])));
$form_state->setError($element, t('Files in the !name field were unable to be uploaded.', array('!name' => $element['#title'])));
return array();
}
......
......@@ -233,6 +233,7 @@ public static function processManagedFile(&$element, FormStateInterface $form_st
'#multiple' => $element['#multiple'],
'#theme_wrappers' => [],
'#weight' => -10,
'#error_no_message' => TRUE,
];
if (!empty($fids) && $element['#files']) {
......@@ -328,7 +329,7 @@ public static function validateManagedFile(&$element, FormStateInterface $form_s
// Check required property based on the FID.
if ($element['#required'] && empty($element['fids']['#value']) && !in_array($clicked_button, ['upload_button', 'remove_button'])) {
$form_state->setError($element['upload'], t('!name field is required.', ['!name' => $element['#title']]));
$form_state->setError($element, t('!name is required.', ['!name' => $element['#title']]));
}
// Consolidate the array value of this field to array of FIDs.
......
......@@ -34,7 +34,8 @@ function testRequired() {
$edit = array();
$edit['title[0][value]'] = $this->randomMachineName();
$this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
$this->assertRaw(t('!title field is required.', array('!title' => $field->getLabel())), 'Node save failed when required file field was empty.');
$this->assertText('1 error has been found: ' . $field->label(), '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.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
......@@ -55,7 +56,8 @@ function testRequired() {
$edit = array();
$edit['title[0][value]'] = $this->randomMachineName();
$this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
$this->assertRaw(t('!title field is required.', array('!title' => $field->getLabel())), 'Node save failed when required multiple value file field was empty.');
$this->assertText('1 error has been found: ' . $field->label(), '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.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
......
......@@ -6,6 +6,8 @@
*/
namespace Drupal\shortcut\Tests;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\shortcut\Entity\ShortcutSet;
/**
......@@ -131,7 +133,9 @@ function testShortcutSetSwitchCreate() {
function testShortcutSetSwitchNoSetName() {
$edit = array('set' => 'new');
$this->drupalPostForm('user/' . $this->adminUser->id() . '/shortcuts', $edit, t('Change set'));
$this->assertText(t('The new set label is required.'));
$this->assertRaw(\Drupal::translation()->formatPlural(1, '1 error has been found: !errors', '@count errors have been found: !errors', [
'!errors' => SafeMarkup::set('<a href="#edit-label">Label</a>')
]));
$current_set = shortcut_current_displayed_set($this->adminUser);
$this->assertEqual($current_set->id(), $this->set->id(), 'Attempting to switch to a new shortcut set without providing a set name does not succeed.');
$this->assertFieldByXPath("//input[@name='label' and contains(concat(' ', normalize-space(@class), ' '), ' error ')]", NULL, 'The new set label field has the error class');
......
......@@ -113,6 +113,17 @@ abbr.ajax-changed {
margin-right: 0;
}
/* Inline error messages. */
.form-error-message:before {
content: '';
display: inline-block;
height: 14px;
width: 14px;
vertical-align: sub;
background: url(../../../misc/icons/ea2800/error.svg) no-repeat;
background-size: contain;
}
/**
* Inline items.
*/
......
......@@ -188,7 +188,7 @@ function testRequiredCheckboxesRadio() {
}
// Check the page for error messages.
$errors = $this->xpath('//div[contains(@class, "error")]//li');
$errors = $this->xpath('//div[contains(@class, "form-error-message")]//strong');
foreach ($errors as $error) {
$expected_key = array_search($error[0], $expected);
// If the error message is not one of the expected messages, fail.
......
......@@ -19,8 +19,20 @@
*/
class TriggeringElementProgrammedUnitTest extends KernelTestBase implements FormInterface {
/**
* {@inheritdoc}
*/
public static $modules = array('system');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', ['router']);
\Drupal::service('router.builder')->rebuild();
}
/**
* {@inheritdoc}
*/
......
......@@ -7,7 +7,9 @@
namespace Drupal\system\Tests\Form;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\simpletest\WebTestBase;
/**
......@@ -206,17 +208,33 @@ function testCustomRequiredError() {
$edit = array();
$this->drupalPostForm('form-test/validate-required', $edit, 'Submit');
$messages = [];
foreach (Element::children($form) as $key) {
if (isset($form[$key]['#required_error'])) {
$this->assertNoText(t('!name field is required.', array('!name' => $form[$key]['#title'])));
$this->assertText($form[$key]['#required_error']);
$messages[] = [
'title' => $form[$key]['#title'],
'message' => $form[$key]['#required_error'],
'key' => $key,
];
}
elseif (isset($form[$key]['#form_test_required_error'])) {
$this->assertNoText(t('!name field is required.', array('!name' => $form[$key]['#title'])));
$this->assertText($form[$key]['#form_test_required_error']);
$messages[] = [
'title' => $form[$key]['#title'],
'message' => $form[$key]['#form_test_required_error'],
'key' => $key,
];
}
elseif (!empty($form[$key]['#required'])) {
$messages[] = [
'title' => $form[$key]['#title'],
'message' => t('!name field is required.', ['!name' => $form[$key]['#title']]),
'key' => $key,
];
}
}
$this->assertNoText(t('An illegal choice has been detected. Please contact the site administrator.'));
$this->assertErrorMessages($messages);