/** * Tests for utils/form-validation.js * * Tests the FormValidator class and built-in validator functions * used for inline form validation with ARIA support. */ import '@js/utils/form-validation.js'; const FormValidator = window.FormValidator; const validators = window.formValidators; describe('FormValidator', () => { let form; let input; let validator; beforeEach(() => { form = document.createElement('form'); input = document.createElement('input'); input.type = 'text'; input.id = 'test-field'; form.appendChild(input); document.body.appendChild(form); validator = new FormValidator(); }); afterEach(() => { document.body.removeChild(form); }); describe('addValidation', () => { it('creates an error element next to the field', () => { validator.addValidation(input, () => null); const errorEl = document.getElementById('test-field-error'); expect(errorEl).not.toBeNull(); expect(errorEl.className).toBe('ldr-field-error'); }); it('sets aria-describedby on the field', () => { validator.addValidation(input, () => null); expect(input.getAttribute('aria-describedby')).toContain('test-field-error'); }); it('sets aria-live on error element', () => { validator.addValidation(input, () => null); const errorEl = document.getElementById('test-field-error'); expect(errorEl.getAttribute('aria-live')).toBe('polite'); }); it('accepts selector string instead of element', () => { validator.addValidation('#test-field', () => null); const errorEl = document.getElementById('test-field-error'); expect(errorEl).not.toBeNull(); }); it('does nothing for non-existent selector', () => { expect(() => { validator.addValidation('#nonexistent', () => null); }).not.toThrow(); }); it('generates an id for fields without one', () => { const noIdInput = document.createElement('input'); noIdInput.type = 'text'; form.appendChild(noIdInput); validator.addValidation(noIdInput, () => null); expect(noIdInput.id).toMatch(/^form-field-/); }); }); describe('validateField', () => { it('returns true when validator returns null (valid)', () => { validator.addValidation(input, () => null); expect(validator.validateField(input)).toBe(true); }); it('returns false when validator returns error message', () => { validator.addValidation(input, () => 'Error!'); expect(validator.validateField(input)).toBe(false); }); it('adds ldr-field-invalid class on error', () => { validator.addValidation(input, () => 'Error!'); validator.validateField(input); expect(input.classList.contains('ldr-field-invalid')).toBe(true); }); it('sets aria-invalid on error', () => { validator.addValidation(input, () => 'Error!'); validator.validateField(input); expect(input.getAttribute('aria-invalid')).toBe('true'); }); it('displays error message in error element', () => { validator.addValidation(input, () => 'Field is required'); validator.validateField(input); const errorEl = document.getElementById('test-field-error'); expect(errorEl.textContent).toBe('Field is required'); expect(errorEl.style.display).toBe('block'); }); it('clears error state when field becomes valid', () => { validator.addValidation(input, (val) => (val ? null : 'Required')); input.value = ''; validator.validateField(input); expect(input.classList.contains('ldr-field-invalid')).toBe(true); input.value = 'filled'; validator.validateField(input); expect(input.classList.contains('ldr-field-invalid')).toBe(false); expect(input.hasAttribute('aria-invalid')).toBe(false); }); it('returns true for unregistered elements', () => { const unknownInput = document.createElement('input'); expect(validator.validateField(unknownInput)).toBe(true); }); }); describe('validateAll', () => { it('returns true when all fields are valid', () => { const input2 = document.createElement('input'); input2.id = 'field-2'; form.appendChild(input2); validator.addValidation(input, () => null); validator.addValidation(input2, () => null); expect(validator.validateAll()).toBe(true); }); it('returns false when any field is invalid', () => { const input2 = document.createElement('input'); input2.id = 'field-2'; form.appendChild(input2); validator.addValidation(input, () => null); validator.addValidation(input2, () => 'Error'); expect(validator.validateAll()).toBe(false); }); it('validates all fields (not short-circuit)', () => { const input2 = document.createElement('input'); input2.id = 'field-2'; form.appendChild(input2); validator.addValidation(input, () => 'Error 1'); validator.addValidation(input2, () => 'Error 2'); validator.validateAll(); // Both should show errors expect(input.classList.contains('ldr-field-invalid')).toBe(true); expect(input2.classList.contains('ldr-field-invalid')).toBe(true); }); }); describe('clearErrors', () => { it('removes error classes and messages from all fields', () => { validator.addValidation(input, () => 'Error'); validator.validateField(input); expect(input.classList.contains('ldr-field-invalid')).toBe(true); validator.clearErrors(); expect(input.classList.contains('ldr-field-invalid')).toBe(false); expect(input.hasAttribute('aria-invalid')).toBe(false); const errorEl = document.getElementById('test-field-error'); expect(errorEl.textContent).toBe(''); expect(errorEl.style.display).toBe('none'); }); }); describe('showError', () => { it('shows custom error on a field', () => { validator.addValidation(input, () => null); validator.showError(input, 'Custom error'); expect(input.classList.contains('ldr-field-invalid')).toBe(true); expect(input.getAttribute('aria-invalid')).toBe('true'); const errorEl = document.getElementById('test-field-error'); expect(errorEl.textContent).toBe('Custom error'); }); it('accepts selector string', () => { validator.addValidation(input, () => null); validator.showError('#test-field', 'Custom error'); expect(input.classList.contains('ldr-field-invalid')).toBe(true); }); it('does nothing for non-existent selector', () => { expect(() => { validator.showError('#nonexistent', 'Error'); }).not.toThrow(); }); }); }); describe('Built-in validators', () => { describe('required', () => { const required = validators.required(); it('returns null for non-empty value', () => { expect(required('hello')).toBeNull(); }); it('returns error for empty string', () => { expect(required('')).toBe('This field is required'); }); it('returns error for whitespace-only string', () => { expect(required(' ')).toBe('This field is required'); }); it('returns error for null/undefined', () => { expect(required(null)).toBe('This field is required'); expect(required(undefined)).toBe('This field is required'); }); it('accepts custom message', () => { const custom = validators.required('Please fill this in'); expect(custom('')).toBe('Please fill this in'); }); }); describe('minLength', () => { const minLen = validators.minLength(3); it('returns null for values meeting minimum', () => { expect(minLen('abc')).toBeNull(); expect(minLen('abcd')).toBeNull(); }); it('returns error for short values', () => { expect(minLen('ab')).toBe('Must be at least 3 characters'); }); it('returns null for empty value (let required handle it)', () => { expect(minLen('')).toBeNull(); expect(minLen(null)).toBeNull(); }); it('accepts custom message', () => { const custom = validators.minLength(5, 'Too short!'); expect(custom('abc')).toBe('Too short!'); }); }); describe('maxLength', () => { const maxLen = validators.maxLength(5); it('returns null for values within limit', () => { expect(maxLen('abc')).toBeNull(); expect(maxLen('abcde')).toBeNull(); }); it('returns error for values exceeding limit', () => { expect(maxLen('abcdef')).toBe('Must be no more than 5 characters'); }); it('returns null for empty value', () => { expect(maxLen('')).toBeNull(); expect(maxLen(null)).toBeNull(); }); }); describe('pattern', () => { const emailLike = validators.pattern(/^[^@]+@[^@]+$/, 'Invalid email'); it('returns null for matching values', () => { expect(emailLike('user@example.com')).toBeNull(); }); it('returns error for non-matching values', () => { expect(emailLike('not-an-email')).toBe('Invalid email'); }); it('returns null for empty value', () => { expect(emailLike('')).toBeNull(); expect(emailLike(null)).toBeNull(); }); it('uses default message when none provided', () => { const digits = validators.pattern(/^\d+$/); expect(digits('abc')).toBe('Invalid format'); }); }); });