diff --git a/website/src/components/SearchPage/fields/AutoCompleteField.spec.tsx b/website/src/components/SearchPage/fields/AutoCompleteField.spec.tsx new file mode 100644 index 000000000..1510ea6e6 --- /dev/null +++ b/website/src/components/SearchPage/fields/AutoCompleteField.spec.tsx @@ -0,0 +1,208 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +import { AutoCompleteField } from './AutoCompleteField'; +import { lapisClientHooks } from '../../../services/serviceHooks.ts'; +import { type MetadataFilter } from '../../../types/config.ts'; + +vi.mock('../../../services/serviceHooks.ts'); +vi.mock('../../../clientLogger.ts', () => ({ + getClientLogger: () => ({ + error: vi.fn(), + }), +})); + +const mockUseAggregated = vi.fn(); +// @ts-expect-error because mockReturnValue is not defined in the type definition +lapisClientHooks.mockReturnValue({ + zodiosHooks: { + useAggregated: mockUseAggregated, + }, +}); + +describe('AutoCompleteField', () => { + const field: MetadataFilter = { + name: 'testField', + label: 'Test Field', + type: 'string', + autocomplete: true, + }; + const setAFieldValue = vi.fn(); + const lapisUrl = 'https://example.com/api'; + const lapisSearchParameters = { param1: 'value1' }; + + beforeEach(() => { + setAFieldValue.mockClear(); + }); + + it('renders input and shows all all options on empty input', async () => { + mockUseAggregated.mockReturnValue({ + data: { + data: [ + { testField: 'Option 1', count: 10 }, + { testField: 'Option 2', count: 20 }, + ], + }, + isLoading: false, + error: null, + mutate: vi.fn(), + }); + + render( + , + ); + + const input = screen.getByLabelText('Test Field'); + expect(input).toBeInTheDocument(); + + fireEvent.focus(input); + + const options = await screen.findAllByRole('option'); + expect(options).toHaveLength(2); + expect(options[0]).toHaveTextContent('Option 1(10)'); + expect(options[1]).toHaveTextContent('Option 2(20)'); + }); + + it('filters options based on query', async () => { + mockUseAggregated.mockReturnValue({ + data: { + data: [ + { testField: 'Option 1', count: 10 }, + { testField: 'Option 2', count: 20 }, + ], + }, + isLoading: false, + error: null, + mutate: vi.fn(), + }); + render( + , + ); + + const input = screen.getByLabelText('Test Field'); + fireEvent.focus(input); + + fireEvent.change(input, { target: { value: 'Option 2' } }); + + const options = await screen.findAllByRole('option'); + expect(options).toHaveLength(1); + expect(options[0]).toHaveTextContent('Option 2(20)'); + }); + + it('displays loading state when aggregated endpoint is in isLoading state', () => { + mockUseAggregated.mockReturnValueOnce({ + data: null, + isLoading: true, + error: null, + mutate: vi.fn(), + }); + + render( + , + ); + + const input = screen.getByLabelText('Test Field'); + fireEvent.focus(input); + + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); + + it('displays error message when aggregated returns an error', () => { + mockUseAggregated.mockReturnValueOnce({ + data: null, + isLoading: false, + error: { message: 'Error message', stack: 'Error stack' }, + mutate: vi.fn(), + }); + + render( + , + ); + + const input = screen.getByLabelText('Test Field'); + fireEvent.focus(input); + + expect(screen.getByText('No options available')).toBeInTheDocument(); + }); + + it('calls setAFieldValue, when an option is selected', async () => { + mockUseAggregated.mockReturnValue({ + data: { + data: [ + { testField: 'Option 1', count: 10 }, + { testField: 'Option 2', count: 20 }, + ], + }, + isLoading: false, + error: null, + mutate: vi.fn(), + }); + render( + , + ); + + const input = screen.getByLabelText('Test Field'); + fireEvent.focus(input); + + const options = await screen.findAllByRole('option'); + fireEvent.click(options[0]); + + expect(setAFieldValue).toHaveBeenCalledWith('testField', 'Option 1'); + }); + + it('clears input value on clear button click', async () => { + mockUseAggregated.mockReturnValue({ + data: { + data: [ + { testField: 'Option 1', count: 10 }, + { testField: 'Option 2', count: 20 }, + ], + }, + isLoading: false, + error: null, + mutate: vi.fn(), + }); + render( + , + ); + + const input = screen.getByLabelText('Test Field'); + fireEvent.focus(input); + + const clearButton = screen.getByLabelText('Clear'); + fireEvent.click(clearButton); + + expect(setAFieldValue).toHaveBeenCalledWith('testField', ''); + }); +}); diff --git a/website/src/components/SearchPage/fields/AutoCompleteField.tsx b/website/src/components/SearchPage/fields/AutoCompleteField.tsx index 9d71fd81b..25b7a1299 100644 --- a/website/src/components/SearchPage/fields/AutoCompleteField.tsx +++ b/website/src/components/SearchPage/fields/AutoCompleteField.tsx @@ -105,6 +105,7 @@ export const AutoCompleteField = ({ setQuery(''); setAFieldValue(field.name, ''); }} + aria-label='Clear' >