import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { useForm } from 'react-hook-form';
import FormField from './FormField';
describe('FormField', () => {
it('label htmlFor matches input id (the WCAG 1.3.1 contract)', () => {
render(
,
);
const label = screen.getByText('Email');
const input = screen.getByLabelText('Email');
// Programmatic label association — what screen readers use.
expect(input).toBeInTheDocument();
expect(label).toHaveAttribute('for', input.id);
// useId() gives a non-empty id by definition.
expect(input.id).toMatch(/^field-/);
});
it('two siblings get independent ids (no collision)', () => {
render(
<>
>,
);
const a = screen.getByLabelText('Name');
const b = screen.getByLabelText('Description');
expect(a.id).not.toBe(b.id);
});
it('required surfaces the asterisk + aria-required on the child', () => {
render(
,
);
expect(screen.getByText('*')).toBeInTheDocument();
expect(screen.getByLabelText(/Email/)).toHaveAttribute('aria-required', 'true');
});
it('description wires aria-describedby to the child', () => {
render(
,
);
const input = screen.getByLabelText('Token');
const desc = screen.getByText(/Paste the API key/);
expect(input.getAttribute('aria-describedby')).toContain(desc.id);
});
it('error sets aria-invalid + role=alert + extends aria-describedby', () => {
render(
,
);
const input = screen.getByLabelText('Email');
expect(input).toHaveAttribute('aria-invalid', 'true');
const err = screen.getByRole('alert');
expect(err).toHaveTextContent('Must be a valid email address');
expect(input.getAttribute('aria-describedby')).toContain(err.id);
});
it('composes cleanly with react-hook-form register() — spread + clone preserves both', () => {
function Form({ onSubmit }: { onSubmit: (v: { name: string }) => void }) {
const { register, handleSubmit } = useForm<{ name: string }>();
return (
);
}
let captured = '';
render(