Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

289 lines
10 KiB
JavaScript

/**
* 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');
});
});
});