forked from cardosofelipe/fast-next-template
- Add comprehensive test coverage for FormTextarea and FormSelect components to validate rendering, accessibility, props forwarding, error handling, and behavior. - Introduced function-scoped fixtures in e2e tests to ensure test isolation and address event loop issues with pytest-asyncio and SQLAlchemy.
449 lines
12 KiB
TypeScript
449 lines
12 KiB
TypeScript
/**
|
|
* Tests for FormSelect Component
|
|
* Verifies select field rendering, accessibility, and error handling
|
|
*/
|
|
|
|
import React from 'react';
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import { useForm, FormProvider } from 'react-hook-form';
|
|
import { FormSelect, type SelectOption } from '@/components/forms/FormSelect';
|
|
|
|
// Polyfill for Radix UI Select - jsdom doesn't support these browser APIs
|
|
beforeAll(() => {
|
|
Element.prototype.hasPointerCapture = jest.fn(() => false);
|
|
Element.prototype.setPointerCapture = jest.fn();
|
|
Element.prototype.releasePointerCapture = jest.fn();
|
|
Element.prototype.scrollIntoView = jest.fn();
|
|
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
|
});
|
|
|
|
// Helper wrapper component to provide form context
|
|
interface TestFormValues {
|
|
model: string;
|
|
category: string;
|
|
}
|
|
|
|
function TestWrapper({
|
|
children,
|
|
defaultValues = { model: '', category: '' },
|
|
}: {
|
|
children: (props: {
|
|
control: ReturnType<typeof useForm<TestFormValues>>['control'];
|
|
}) => React.ReactNode;
|
|
defaultValues?: Partial<TestFormValues>;
|
|
}) {
|
|
const form = useForm<TestFormValues>({
|
|
defaultValues: { model: '', category: '', ...defaultValues },
|
|
});
|
|
|
|
return <FormProvider {...form}>{children({ control: form.control })}</FormProvider>;
|
|
}
|
|
|
|
const mockOptions: SelectOption[] = [
|
|
{ value: 'claude-opus', label: 'Claude Opus' },
|
|
{ value: 'claude-sonnet', label: 'Claude Sonnet' },
|
|
{ value: 'claude-haiku', label: 'Claude Haiku' },
|
|
];
|
|
|
|
describe('FormSelect', () => {
|
|
describe('Basic Rendering', () => {
|
|
it('renders with label and select trigger', () => {
|
|
render(
|
|
<TestWrapper>
|
|
{({ control }) => (
|
|
<FormSelect
|
|
name="model"
|
|
control={control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
/>
|
|
)}
|
|
</TestWrapper>
|
|
);
|
|
|
|
expect(screen.getByText('Primary Model')).toBeInTheDocument();
|
|
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders with description', () => {
|
|
render(
|
|
<TestWrapper>
|
|
{({ control }) => (
|
|
<FormSelect
|
|
name="model"
|
|
control={control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
description="Main model used for this agent"
|
|
/>
|
|
)}
|
|
</TestWrapper>
|
|
);
|
|
|
|
expect(screen.getByText('Main model used for this agent')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders with custom placeholder', () => {
|
|
render(
|
|
<TestWrapper>
|
|
{({ control }) => (
|
|
<FormSelect
|
|
name="model"
|
|
control={control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
placeholder="Choose a model"
|
|
/>
|
|
)}
|
|
</TestWrapper>
|
|
);
|
|
|
|
expect(screen.getByText('Choose a model')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders default placeholder when none provided', () => {
|
|
render(
|
|
<TestWrapper>
|
|
{({ control }) => (
|
|
<FormSelect
|
|
name="model"
|
|
control={control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
/>
|
|
)}
|
|
</TestWrapper>
|
|
);
|
|
|
|
expect(screen.getByText('Select primary model')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Required Field', () => {
|
|
it('shows asterisk when required is true', () => {
|
|
render(
|
|
<TestWrapper>
|
|
{({ control }) => (
|
|
<FormSelect
|
|
name="model"
|
|
control={control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
required
|
|
/>
|
|
)}
|
|
</TestWrapper>
|
|
);
|
|
|
|
expect(screen.getByText('*')).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not show asterisk when required is false', () => {
|
|
render(
|
|
<TestWrapper>
|
|
{({ control }) => (
|
|
<FormSelect
|
|
name="model"
|
|
control={control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
required={false}
|
|
/>
|
|
)}
|
|
</TestWrapper>
|
|
);
|
|
|
|
expect(screen.queryByText('*')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Options Rendering', () => {
|
|
it('renders all options when opened', async () => {
|
|
render(
|
|
<TestWrapper>
|
|
{({ control }) => (
|
|
<FormSelect
|
|
name="model"
|
|
control={control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
/>
|
|
)}
|
|
</TestWrapper>
|
|
);
|
|
|
|
// Open the select using fireEvent (works better with Radix UI)
|
|
fireEvent.click(screen.getByRole('combobox'));
|
|
|
|
// Check all options are rendered
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('option', { name: 'Claude Opus' })).toBeInTheDocument();
|
|
});
|
|
expect(screen.getByRole('option', { name: 'Claude Sonnet' })).toBeInTheDocument();
|
|
expect(screen.getByRole('option', { name: 'Claude Haiku' })).toBeInTheDocument();
|
|
});
|
|
|
|
it('selects option when clicked', async () => {
|
|
render(
|
|
<TestWrapper>
|
|
{({ control }) => (
|
|
<FormSelect
|
|
name="model"
|
|
control={control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
/>
|
|
)}
|
|
</TestWrapper>
|
|
);
|
|
|
|
// Open the select and choose an option
|
|
fireEvent.click(screen.getByRole('combobox'));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('option', { name: 'Claude Sonnet' })).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('option', { name: 'Claude Sonnet' }));
|
|
|
|
// The selected value should now be displayed
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('combobox')).toHaveTextContent('Claude Sonnet');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Disabled State', () => {
|
|
it('disables select when disabled prop is true', () => {
|
|
render(
|
|
<TestWrapper>
|
|
{({ control }) => (
|
|
<FormSelect
|
|
name="model"
|
|
control={control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
disabled
|
|
/>
|
|
)}
|
|
</TestWrapper>
|
|
);
|
|
|
|
expect(screen.getByRole('combobox')).toBeDisabled();
|
|
});
|
|
|
|
it('enables select when disabled prop is false', () => {
|
|
render(
|
|
<TestWrapper>
|
|
{({ control }) => (
|
|
<FormSelect
|
|
name="model"
|
|
control={control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
disabled={false}
|
|
/>
|
|
)}
|
|
</TestWrapper>
|
|
);
|
|
|
|
expect(screen.getByRole('combobox')).not.toBeDisabled();
|
|
});
|
|
});
|
|
|
|
describe('Pre-selected Value', () => {
|
|
it('displays pre-selected value', () => {
|
|
render(
|
|
<TestWrapper defaultValues={{ model: 'claude-opus' }}>
|
|
{({ control }) => (
|
|
<FormSelect
|
|
name="model"
|
|
control={control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
/>
|
|
)}
|
|
</TestWrapper>
|
|
);
|
|
|
|
expect(screen.getByRole('combobox')).toHaveTextContent('Claude Opus');
|
|
});
|
|
});
|
|
|
|
describe('Accessibility', () => {
|
|
it('links label to select via htmlFor/id', () => {
|
|
render(
|
|
<TestWrapper>
|
|
{({ control }) => (
|
|
<FormSelect
|
|
name="model"
|
|
control={control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
/>
|
|
)}
|
|
</TestWrapper>
|
|
);
|
|
|
|
const label = screen.getByText('Primary Model');
|
|
const select = screen.getByRole('combobox');
|
|
|
|
expect(label).toHaveAttribute('for', 'model');
|
|
expect(select).toHaveAttribute('id', 'model');
|
|
});
|
|
|
|
it('sets aria-describedby with description ID when description exists', () => {
|
|
render(
|
|
<TestWrapper>
|
|
{({ control }) => (
|
|
<FormSelect
|
|
name="model"
|
|
control={control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
description="Choose the main model"
|
|
/>
|
|
)}
|
|
</TestWrapper>
|
|
);
|
|
|
|
const select = screen.getByRole('combobox');
|
|
expect(select).toHaveAttribute('aria-describedby', 'model-description');
|
|
});
|
|
});
|
|
|
|
describe('Custom ClassName', () => {
|
|
it('applies custom className to wrapper', () => {
|
|
const { container } = render(
|
|
<TestWrapper>
|
|
{({ control }) => (
|
|
<FormSelect
|
|
name="model"
|
|
control={control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
className="custom-class"
|
|
/>
|
|
)}
|
|
</TestWrapper>
|
|
);
|
|
|
|
expect(container.querySelector('.custom-class')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('displays error message when field has error', () => {
|
|
function TestComponent() {
|
|
const form = useForm<TestFormValues>({
|
|
defaultValues: { model: '', category: '' },
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
form.setError('model', { type: 'required', message: 'Model is required' });
|
|
}, [form]);
|
|
|
|
return (
|
|
<FormProvider {...form}>
|
|
<FormSelect
|
|
name="model"
|
|
control={form.control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
/>
|
|
</FormProvider>
|
|
);
|
|
}
|
|
|
|
render(<TestComponent />);
|
|
|
|
expect(screen.getByRole('alert')).toHaveTextContent('Model is required');
|
|
});
|
|
|
|
it('sets aria-invalid when error exists', () => {
|
|
function TestComponent() {
|
|
const form = useForm<TestFormValues>({
|
|
defaultValues: { model: '', category: '' },
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
form.setError('model', { type: 'required', message: 'Model is required' });
|
|
}, [form]);
|
|
|
|
return (
|
|
<FormProvider {...form}>
|
|
<FormSelect
|
|
name="model"
|
|
control={form.control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
/>
|
|
</FormProvider>
|
|
);
|
|
}
|
|
|
|
render(<TestComponent />);
|
|
|
|
expect(screen.getByRole('combobox')).toHaveAttribute('aria-invalid', 'true');
|
|
});
|
|
|
|
it('sets aria-describedby with error ID when error exists', () => {
|
|
function TestComponent() {
|
|
const form = useForm<TestFormValues>({
|
|
defaultValues: { model: '', category: '' },
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
form.setError('model', { type: 'required', message: 'Model is required' });
|
|
}, [form]);
|
|
|
|
return (
|
|
<FormProvider {...form}>
|
|
<FormSelect
|
|
name="model"
|
|
control={form.control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
/>
|
|
</FormProvider>
|
|
);
|
|
}
|
|
|
|
render(<TestComponent />);
|
|
|
|
expect(screen.getByRole('combobox')).toHaveAttribute('aria-describedby', 'model-error');
|
|
});
|
|
|
|
it('combines error and description IDs in aria-describedby', () => {
|
|
function TestComponent() {
|
|
const form = useForm<TestFormValues>({
|
|
defaultValues: { model: '', category: '' },
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
form.setError('model', { type: 'required', message: 'Model is required' });
|
|
}, [form]);
|
|
|
|
return (
|
|
<FormProvider {...form}>
|
|
<FormSelect
|
|
name="model"
|
|
control={form.control}
|
|
label="Primary Model"
|
|
options={mockOptions}
|
|
description="Choose the main model"
|
|
/>
|
|
</FormProvider>
|
|
);
|
|
}
|
|
|
|
render(<TestComponent />);
|
|
|
|
expect(screen.getByRole('combobox')).toHaveAttribute(
|
|
'aria-describedby',
|
|
'model-error model-description'
|
|
);
|
|
});
|
|
});
|
|
});
|