Ver contenido
Guía de Implementación de Identificación de Errores: Patrones Completos para Validación de Formularios Accesible
Introducción
El manejo accesible de errores es crítico para la usabilidad de formularios. Esta guía cubre patrones completos de implementación para identificación de errores, sugerencias de errores y gestión de foco que cumplen con los requisitos de WCAG 3.3.1 y 3.3.3 mientras proporcionan una excelente experiencia de usuario para todos.
Más allá del cumplimiento de accesibilidad, el manejo de errores bien implementado reduce el abandono de formularios y las solicitudes de soporte al ayudar a los usuarios a completar formularios exitosamente en el primer intento.
Patrón Completo de Validación de Formularios
Estructura HTML con Todos los Atributos ARIA
<form id="registration-form" novalidate>
<!-- Resumen de Errores (mostrado después del envío con errores) -->
<div
id="error-summary"
class="error-summary"
role="alert"
aria-labelledby="error-summary-title"
tabindex="-1"
hidden
>
<h2 id="error-summary-title">
<svg aria-hidden="true" class="error-icon"><!-- icono --></svg>
Por favor corrige los siguientes errores:
</h2>
<ul id="error-list">
<!-- Enlaces de errores insertados dinámicamente -->
</ul>
</div>
<!-- Campos del Formulario -->
<div class="form-group">
<label for="nombre">
Nombre Completo
<span class="required" aria-hidden="true">*</span>
</label>
<input
type="text"
id="nombre"
name="nombre"
required
aria-required="true"
autocomplete="name"
>
<p id="nombre-error" class="error-message" hidden></p>
</div>
<div class="form-group">
<label for="email">
Correo Electrónico
<span class="required" aria-hidden="true">*</span>
</label>
<input
type="email"
id="email"
name="email"
required
aria-required="true"
aria-describedby="email-hint"
autocomplete="email"
>
<p id="email-hint" class="hint">Nunca compartiremos tu correo</p>
<p id="email-error" class="error-message" hidden></p>
</div>
<div class="form-group">
<label for="telefono">Número de Teléfono</label>
<input
type="tel"
id="telefono"
name="telefono"
pattern="[0-9]{10}"
aria-describedby="telefono-hint"
autocomplete="tel"
>
<p id="telefono-hint" class="hint">10 dígitos, solo números</p>
<p id="telefono-error" class="error-message" hidden></p>
</div>
<button type="submit">Crear Cuenta</button>
</form>
Estilos CSS para Estados de Error
/* Contenedor de resumen de errores */
.error-summary {
background-color: #fef2f2;
border: 2px solid #dc2626;
border-radius: 8px;
padding: 16px;
margin-bottom: 24px;
}
.error-summary:focus {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
.error-summary h2 {
color: #dc2626;
font-size: 1.125rem;
font-weight: 600;
margin: 0 0 12px 0;
display: flex;
align-items: center;
gap: 8px;
}
.error-summary ul {
margin: 0;
padding-left: 24px;
}
.error-summary li {
margin-bottom: 4px;
}
.error-summary a {
color: #dc2626;
text-decoration: underline;
}
.error-summary a:hover,
.error-summary a:focus {
color: #991b1b;
}
/* Grupo de formulario con error */
.form-group.has-error input,
.form-group.has-error select,
.form-group.has-error textarea {
border-color: #dc2626;
box-shadow: 0 0 0 1px #dc2626;
}
.form-group.has-error input:focus,
.form-group.has-error select:focus,
.form-group.has-error textarea:focus {
outline: 3px solid #2563eb;
outline-offset: 2px;
box-shadow: 0 0 0 1px #dc2626;
}
/* Estilo del mensaje de error */
.error-message {
color: #dc2626;
font-size: 0.875rem;
margin-top: 4px;
display: flex;
align-items: flex-start;
gap: 6px;
}
.error-message::before {
content: "⚠";
flex-shrink: 0;
}
/* Indicador de requerido */
.required {
color: #dc2626;
margin-left: 2px;
}
/* Texto de ayuda */
.hint {
color: #6b7280;
font-size: 0.875rem;
margin-top: 4px;
}
/* Estado oculto */
[hidden] {
display: none !important;
}
Implementación de Validación en JavaScript
Clase de Validación Completa
class FormValidator {
constructor(formElement, options = {}) {
this.form = formElement;
this.options = {
validateOnBlur: true,
validateOnInput: false,
showErrorSummary: true,
focusFirstError: true,
...options
};
this.errors = new Map();
this.validators = new Map();
this.init();
}
init() {
// Prevenir validación del navegador
this.form.setAttribute('novalidate', '');
// Configurar event listeners
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
if (this.options.validateOnBlur) {
this.form.addEventListener('blur', (e) => {
if (e.target.matches('input, select, textarea')) {
this.validateField(e.target);
}
}, true);
}
if (this.options.validateOnInput) {
this.form.addEventListener('input', (e) => {
if (e.target.matches('input, select, textarea')) {
// Debounce de validación de input
clearTimeout(e.target.validationTimeout);
e.target.validationTimeout = setTimeout(() => {
this.validateField(e.target);
}, 300);
}
});
}
// Limpiar error cuando el usuario comienza a escribir (después de validación inicial)
this.form.addEventListener('input', (e) => {
if (e.target.matches('input, select, textarea') &&
this.errors.has(e.target.id)) {
this.clearFieldError(e.target);
}
});
}
// Agregar validador personalizado para un campo
addValidator(fieldId, validator, message) {
if (!this.validators.has(fieldId)) {
this.validators.set(fieldId, []);
}
this.validators.get(fieldId).push({ validator, message });
}
validateField(field) {
const errors = [];
// Validación de requerido
if (field.hasAttribute('required') && !field.value.trim()) {
errors.push(this.getRequiredMessage(field));
}
// Validación de tipo (email, url, etc.)
if (field.value && field.type === 'email' && !this.isValidEmail(field.value)) {
errors.push('Por favor ingresa un correo electrónico válido (ej., [email protected])');
}
// Validación de patrón
if (field.value && field.pattern) {
const regex = new RegExp(`^${field.pattern}$`);
if (!regex.test(field.value)) {
errors.push(field.title || 'Por favor usa el formato solicitado');
}
}
// Validación de longitud mínima/máxima
if (field.value && field.minLength > 0 && field.value.length < field.minLength) {
errors.push(`Por favor ingresa al menos ${field.minLength} caracteres`);
}
if (field.value && field.maxLength > 0 && field.value.length > field.maxLength) {
errors.push(`Por favor ingresa no más de ${field.maxLength} caracteres`);
}
// Validadores personalizados
const customValidators = this.validators.get(field.id) || [];
for (const { validator, message } of customValidators) {
if (!validator(field.value, field)) {
errors.push(message);
}
}
// Actualizar estado del campo
if (errors.length > 0) {
this.showFieldError(field, errors[0]);
return false;
} else {
this.clearFieldError(field);
return true;
}
}
getRequiredMessage(field) {
const label = this.getFieldLabel(field);
return `${label} es requerido`;
}
getFieldLabel(field) {
const label = this.form.querySelector(`label[for="${field.id}"]`);
if (label) {
// Obtener contenido de texto sin el indicador de requerido
return label.textContent.replace('*', '').trim();
}
return field.name || field.id;
}
isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
showFieldError(field, message) {
const formGroup = field.closest('.form-group');
const errorElement = document.getElementById(`${field.id}-error`);
// Actualizar estado de error
this.errors.set(field.id, message);
// Actualizar atributos del campo
field.setAttribute('aria-invalid', 'true');
field.setAttribute('aria-describedby',
this.buildDescribedBy(field, `${field.id}-error`));
// Actualizar grupo del formulario
formGroup?.classList.add('has-error');
// Mostrar mensaje de error
if (errorElement) {
errorElement.textContent = message;
errorElement.hidden = false;
}
}
clearFieldError(field) {
const formGroup = field.closest('.form-group');
const errorElement = document.getElementById(`${field.id}-error`);
// Limpiar estado de error
this.errors.delete(field.id);
// Actualizar atributos del campo
field.removeAttribute('aria-invalid');
field.setAttribute('aria-describedby',
this.buildDescribedBy(field, null));
// Actualizar grupo del formulario
formGroup?.classList.remove('has-error');
// Ocultar mensaje de error
if (errorElement) {
errorElement.textContent = '';
errorElement.hidden = true;
}
}
buildDescribedBy(field, errorId) {
const hintId = `${field.id}-hint`;
const hintExists = document.getElementById(hintId);
const ids = [];
if (hintExists) ids.push(hintId);
if (errorId && this.errors.has(field.id)) ids.push(errorId);
return ids.length > 0 ? ids.join(' ') : null;
}
handleSubmit(e) {
e.preventDefault();
// Validar todos los campos
const fields = this.form.querySelectorAll('input, select, textarea');
let isValid = true;
fields.forEach(field => {
if (!this.validateField(field)) {
isValid = false;
}
});
if (!isValid) {
this.showErrorSummary();
return;
}
// Formulario válido - enviar
this.hideErrorSummary();
this.form.submit();
}
showErrorSummary() {
if (!this.options.showErrorSummary) {
if (this.options.focusFirstError) {
this.focusFirstError();
}
return;
}
const summary = document.getElementById('error-summary');
const errorList = document.getElementById('error-list');
if (!summary || !errorList) return;
// Construir lista de errores
errorList.innerHTML = '';
this.errors.forEach((message, fieldId) => {
const li = document.createElement('li');
const link = document.createElement('a');
link.href = `#${fieldId}`;
link.textContent = message;
link.addEventListener('click', (e) => {
e.preventDefault();
document.getElementById(fieldId)?.focus();
});
li.appendChild(link);
errorList.appendChild(li);
});
// Mostrar y enfocar resumen
summary.hidden = false;
summary.focus();
// Desplazar al resumen
summary.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
hideErrorSummary() {
const summary = document.getElementById('error-summary');
if (summary) {
summary.hidden = true;
}
}
focusFirstError() {
const firstErrorField = this.form.querySelector('[aria-invalid="true"]');
if (firstErrorField) {
firstErrorField.focus();
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}
// Uso
const form = document.getElementById('registration-form');
const validator = new FormValidator(form);
// Agregar validador personalizado
validator.addValidator('telefono',
(value) => !value || /^\d{10}$/.test(value),
'El número de teléfono debe tener exactamente 10 dígitos'
);
Implementación en React
Componente de Formulario Accesible
import React, { useState, useRef, useEffect } from 'react';
interface FormErrors {
[key: string]: string;
}
interface FormData {
nombre: string;
email: string;
telefono: string;
}
function FormularioAccesible() {
const [formData, setFormData] = useState<FormData>({
nombre: '',
email: '',
telefono: ''
});
const [errors, setErrors] = useState<FormErrors>({});
const [submitted, setSubmitted] = useState(false);
const errorSummaryRef = useRef<HTMLDivElement>(null);
const validateField = (name: string, value: string): string | null => {
switch (name) {
case 'nombre':
if (!value.trim()) return 'El nombre completo es requerido';
if (value.length < 2) return 'El nombre debe tener al menos 2 caracteres';
return null;
case 'email':
if (!value.trim()) return 'El correo electrónico es requerido';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return 'Por favor ingresa un correo válido (ej., [email protected])';
}
return null;
case 'telefono':
if (value && !/^\d{10}$/.test(value)) {
return 'El teléfono debe tener 10 dígitos';
}
return null;
default:
return null;
}
};
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
Object.keys(formData).forEach(key => {
const error = validateField(key, formData[key as keyof FormData]);
if (error) {
newErrors[key] = error;
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const error = validateField(name, value);
setErrors(prev => {
if (error) {
return { ...prev, [name]: error };
}
const { [name]: removed, ...rest } = prev;
return rest;
});
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Limpiar error cuando el usuario comienza a escribir (solo después de validación inicial)
if (errors[name]) {
setErrors(prev => {
const { [name]: removed, ...rest } = prev;
return rest;
});
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSubmitted(true);
if (validateForm()) {
console.log('Formulario enviado:', formData);
}
};
// Enfocar resumen de errores cuando existen errores después del envío
useEffect(() => {
if (submitted && Object.keys(errors).length > 0 && errorSummaryRef.current) {
errorSummaryRef.current.focus();
}
}, [errors, submitted]);
const errorCount = Object.keys(errors).length;
return (
<form onSubmit={handleSubmit} noValidate>
{/* Resumen de Errores */}
{errorCount > 0 && submitted && (
<div
ref={errorSummaryRef}
className="error-summary"
role="alert"
tabIndex={-1}
aria-labelledby="error-summary-title"
>
<h2 id="error-summary-title">
Por favor corrige {errorCount} error{errorCount !== 1 ? 'es' : ''}:
</h2>
<ul>
{Object.entries(errors).map(([field, message]) => (
<li key={field}>
<a
href={`#${field}`}
onClick={(e) => {
e.preventDefault();
document.getElementById(field)?.focus();
}}
>
{message}
</a>
</li>
))}
</ul>
</div>
)}
{/* Campo de Nombre */}
<div className={`form-group ${errors.nombre ? 'has-error' : ''}`}>
<label htmlFor="nombre">
Nombre Completo <span aria-hidden="true">*</span>
</label>
<input
type="text"
id="nombre"
name="nombre"
value={formData.nombre}
onChange={handleChange}
onBlur={handleBlur}
aria-required="true"
aria-invalid={errors.nombre ? 'true' : undefined}
aria-describedby={errors.nombre ? 'nombre-error' : undefined}
autoComplete="name"
/>
{errors.nombre && (
<p id="nombre-error" className="error-message" role="alert">
{errors.nombre}
</p>
)}
</div>
{/* Campo de Email */}
<div className={`form-group ${errors.email ? 'has-error' : ''}`}>
<label htmlFor="email">
Correo Electrónico <span aria-hidden="true">*</span>
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
aria-required="true"
aria-invalid={errors.email ? 'true' : undefined}
aria-describedby={`email-hint${errors.email ? ' email-error' : ''}`}
autoComplete="email"
/>
<p id="email-hint" className="hint">Nunca compartiremos tu correo</p>
{errors.email && (
<p id="email-error" className="error-message" role="alert">
{errors.email}
</p>
)}
</div>
{/* Campo de Teléfono */}
<div className={`form-group ${errors.telefono ? 'has-error' : ''}`}>
<label htmlFor="telefono">Número de Teléfono</label>
<input
type="tel"
id="telefono"
name="telefono"
value={formData.telefono}
onChange={handleChange}
onBlur={handleBlur}
aria-invalid={errors.telefono ? 'true' : undefined}
aria-describedby={`telefono-hint${errors.telefono ? ' telefono-error' : ''}`}
autoComplete="tel"
/>
<p id="telefono-hint" class="hint">10 dígitos, solo números</p>
{errors.telefono && (
<p id="telefono-error" className="error-message" role="alert">
{errors.telefono}
</p>
)}
</div>
<button type="submit">Crear Cuenta</button>
</form>
);
}
export default FormularioAccesible;
Implementación en Vue 3
Composable para Validación de Formularios
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'
interface FormData {
nombre: string
email: string
telefono: string
}
interface FormErrors {
[key: string]: string
}
const formData = ref<FormData>({
nombre: '',
email: '',
telefono: ''
})
const errors = ref<FormErrors>({})
const submitted = ref(false)
const errorSummaryRef = ref<HTMLDivElement | null>(null)
const errorCount = computed(() => Object.keys(errors.value).length)
function validateField(name: keyof FormData, value: string): string | null {
switch (name) {
case 'nombre':
if (!value.trim()) return 'El nombre completo es requerido'
if (value.length < 2) return 'El nombre debe tener al menos 2 caracteres'
return null
case 'email':
if (!value.trim()) return 'El correo electrónico es requerido'
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return 'Ingresa un correo válido (ej., [email protected])'
}
return null
case 'telefono':
if (value && !/^\d{10}$/.test(value)) {
return 'El teléfono debe tener 10 dígitos'
}
return null
default:
return null
}
}
function validateForm(): boolean {
const newErrors: FormErrors = {}
for (const key of Object.keys(formData.value) as (keyof FormData)[]) {
const error = validateField(key, formData.value[key])
if (error) {
newErrors[key] = error
}
}
errors.value = newErrors
return Object.keys(newErrors).length === 0
}
function handleBlur(e: FocusEvent) {
const target = e.target as HTMLInputElement
const name = target.name as keyof FormData
const error = validateField(name, target.value)
if (error) {
errors.value = { ...errors.value, [name]: error }
} else {
const { [name]: removed, ...rest } = errors.value
errors.value = rest
}
}
function handleInput(e: Event) {
const target = e.target as HTMLInputElement
const name = target.name as keyof FormData
// Limpiar error cuando el usuario escribe
if (errors.value[name]) {
const { [name]: removed, ...rest } = errors.value
errors.value = rest
}
}
async function handleSubmit() {
submitted.value = true
if (validateForm()) {
console.log('Formulario enviado:', formData.value)
} else {
await nextTick()
errorSummaryRef.value?.focus()
}
}
function focusField(fieldId: string) {
document.getElementById(fieldId)?.focus()
}
function getAriaDescribedBy(fieldName: string, hasHint: boolean = false): string | undefined {
const ids: string[] = []
if (hasHint) ids.push(`${fieldName}-hint`)
if (errors.value[fieldName]) ids.push(`${fieldName}-error`)
return ids.length > 0 ? ids.join(' ') : undefined
}
</script>
<template>
<form @submit.prevent="handleSubmit" novalidate>
<!-- Resumen de Errores -->
<div
v-if="errorCount > 0 && submitted"
ref="errorSummaryRef"
class="error-summary"
role="alert"
tabindex="-1"
aria-labelledby="error-summary-title"
>
<h2 id="error-summary-title">
Por favor corrige {{ errorCount }} error{{ errorCount !== 1 ? 'es' : '' }}:
</h2>
<ul>
<li v-for="(message, field) in errors" :key="field">
<a
:href="`#${field}`"
@click.prevent="focusField(field as string)"
>
{{ message }}
</a>
</li>
</ul>
</div>
<!-- Campo de Nombre -->
<div :class="['form-group', { 'has-error': errors.nombre }]">
<label for="nombre">
Nombre Completo <span aria-hidden="true">*</span>
</label>
<input
v-model="formData.nombre"
type="text"
id="nombre"
name="nombre"
@blur="handleBlur"
@input="handleInput"
aria-required="true"
:aria-invalid="errors.nombre ? 'true' : undefined"
:aria-describedby="getAriaDescribedBy('nombre')"
autocomplete="name"
>
<p v-if="errors.nombre" id="nombre-error" class="error-message" role="alert">
{{ errors.nombre }}
</p>
</div>
<!-- Campo de Email -->
<div :class="['form-group', { 'has-error': errors.email }]">
<label for="email">
Correo Electrónico <span aria-hidden="true">*</span>
</label>
<input
v-model="formData.email"
type="email"
id="email"
name="email"
@blur="handleBlur"
@input="handleInput"
aria-required="true"
:aria-invalid="errors.email ? 'true' : undefined"
:aria-describedby="getAriaDescribedBy('email', true)"
autocomplete="email"
>
<p id="email-hint" class="hint">Nunca compartiremos tu correo</p>
<p v-if="errors.email" id="email-error" class="error-message" role="alert">
{{ errors.email }}
</p>
</div>
<!-- Campo de Teléfono -->
<div :class="['form-group', { 'has-error': errors.telefono }]">
<label for="telefono">Número de Teléfono</label>
<input
v-model="formData.telefono"
type="tel"
id="telefono"
name="telefono"
@blur="handleBlur"
@input="handleInput"
:aria-invalid="errors.telefono ? 'true' : undefined"
:aria-describedby="getAriaDescribedBy('telefono', true)"
autocomplete="tel"
>
<p id="telefono-hint" class="hint">10 dígitos, solo números</p>
<p v-if="errors.telefono" id="telefono-error" class="error-message" role="alert">
{{ errors.telefono }}
</p>
</div>
<button type="submit">Crear Cuenta</button>
</form>
</template>
Pruebas Automatizadas
Tests de Playwright para Identificación de Errores
import { test, expect } from '@playwright/test';
test.describe('Identificación de Errores en Formularios', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/formulario');
});
test('muestra resumen de errores con enlaces después de envío inválido', async ({ page }) => {
// Enviar formulario vacío
await page.click('button[type="submit"]');
// El resumen de errores debe aparecer
const errorSummary = page.locator('[role="alert"]');
await expect(errorSummary).toBeVisible();
// Debe tener role="alert" para anuncio de lector de pantalla
await expect(errorSummary).toHaveAttribute('role', 'alert');
// Debe estar enfocado para acceso inmediato del lector de pantalla
await expect(errorSummary).toBeFocused();
// Debe contener enlaces a campos con errores
const errorLinks = errorSummary.locator('a');
const linkCount = await errorLinks.count();
expect(linkCount).toBeGreaterThan(0);
// Cada enlace debe navegar a su campo
const firstLink = errorLinks.first();
await firstLink.click();
const nombreInput = page.locator('#nombre');
await expect(nombreInput).toBeFocused();
});
test('errores en línea tienen atributos ARIA apropiados', async ({ page }) => {
// Enviar formulario vacío para activar errores
await page.click('button[type="submit"]');
// Verificar que campo requerido tiene aria-invalid
const nombreInput = page.locator('#nombre');
await expect(nombreInput).toHaveAttribute('aria-invalid', 'true');
// Verificar que aria-describedby apunta al mensaje de error
const describedBy = await nombreInput.getAttribute('aria-describedby');
expect(describedBy).toContain('nombre-error');
// El mensaje de error debe ser visible
const errorMessage = page.locator('#nombre-error');
await expect(errorMessage).toBeVisible();
await expect(errorMessage).not.toBeEmpty();
});
test('mensajes de error describen problemas específicos', async ({ page }) => {
// Ingresar email inválido
await page.fill('#email', 'no-es-email');
await page.locator('#email').blur();
// El error debe ser específico
const emailError = page.locator('#email-error');
await expect(emailError).toBeVisible();
const errorText = await emailError.textContent();
expect(errorText).toContain('válido');
expect(errorText).toContain('@'); // Debe incluir pista de formato
});
test('errores se limpian cuando el usuario corrige la entrada', async ({ page }) => {
// Enviar formulario vacío
await page.click('button[type="submit"]');
// Verificar que el error existe
const nombreInput = page.locator('#nombre');
await expect(nombreInput).toHaveAttribute('aria-invalid', 'true');
// Ingresar valor válido
await page.fill('#nombre', 'Juan Pérez');
// El error debe limpiarse
await expect(nombreInput).not.toHaveAttribute('aria-invalid');
const nombreError = page.locator('#nombre-error');
await expect(nombreError).toBeHidden();
});
test('sugerencias de error proporcionan orientación accionable', async ({ page }) => {
// Ingresar teléfono con formato incorrecto
await page.fill('#telefono', '12345');
await page.locator('#telefono').blur();
// El error debe sugerir formato correcto
const telefonoError = page.locator('#telefono-error');
await expect(telefonoError).toBeVisible();
const errorText = await telefonoError.textContent();
expect(errorText).toMatch(/10|dígitos/i); // Debe mencionar formato
});
test('lector de pantalla recibe anuncios de errores', async ({ page }) => {
// Enviar formulario vacío
await page.click('button[type="submit"]');
// El resumen de errores debe ser anunciado (role="alert" + aria-live)
const errorSummary = page.locator('[role="alert"]');
await expect(errorSummary).toHaveAttribute('role', 'alert');
// Los errores en línea también deben ser anunciados
const emailError = page.locator('#email-error[role="alert"]');
// Role alert implica aria-live="assertive"
await expect(emailError).toBeVisible();
});
});
Checklist de Resumen
- [ ] El resumen de errores aparece en la parte superior del formulario con role=“alert”
- [ ] El resumen de errores recibe el foco después del envío del formulario con errores
- [ ] El resumen de errores contiene enlaces a cada campo con errores
- [ ] Cada mensaje de error es descriptivo y accionable
- [ ] Los campos con errores tienen aria-invalid=“true”
- [ ] Los campos con errores tienen aria-describedby apuntando al mensaje de error
- [ ] Los mensajes de error aparecen cerca de sus campos asociados
- [ ] Los errores se describen en texto (no solo color o iconos)
- [ ] Las sugerencias de error proporcionan pistas de formato cuando se conocen
- [ ] Los errores se limpian cuando el usuario corrige la entrada
- [ ] La gestión de foco ayuda a los usuarios a navegar a los errores
- [ ] Los lectores de pantalla anuncian los errores cuando aparecen