import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {useState} from 'react';
import {describe, expect, it, vi} from 'vitest';
import GbifNameInput from './GbifNameInput';
const maize = {key: 1, canonicalName: 'Zea mays', scientificName: 'Zea mays L.', rank: 'SPECIES'};
const mexicana = {key: 2, canonicalName: 'Zea mexicana', scientificName: 'Zea mexicana (Schrad.) Kuntze', rank: 'SPECIES'};
function stubSuggestions(matches) {
const fetch = vi.fn(async () => ({ok: true, status: 200, json: async () => matches}));
vi.stubGlobal('fetch', fetch);
return fetch;
}
// The field is controlled by its form, so give it somewhere to keep its value.
function Field({initial}) {
const [value, setValue] = useState(initial);
return ;
}
function setup(initial = '') {
const user = userEvent.setup();
render();
return {user, input: screen.getByRole('combobox')};
}
describe('GbifNameInput', () => {
it('does not search for a name already on the form', async () => {
const fetch = stubSuggestions([maize]);
setup('Zea mays');
await new Promise((resolve) => setTimeout(resolve, 400));
expect(fetch).not.toHaveBeenCalled();
});
it('does not search for fewer than three letters', async () => {
const fetch = stubSuggestions([maize]);
const {user, input} = setup();
await user.type(input, 'Ze');
await new Promise((resolve) => setTimeout(resolve, 400));
expect(fetch).not.toHaveBeenCalled();
});
it('searches the given url for what is typed', async () => {
const fetch = stubSuggestions([maize]);
const {user, input} = setup();
await user.type(input, 'Zea');
await screen.findByRole('option');
expect(fetch).toHaveBeenLastCalledWith('/gbif?term=Zea', expect.anything());
});
it('lists each match with its scientific name and rank', async () => {
stubSuggestions([maize]);
const {user, input} = setup();
await user.type(input, 'Zea');
expect(await screen.findByRole('option')).toHaveTextContent('Zea mays (Zea mays L.) – SPECIES');
});
it('fills in the canonical name of a match clicked', async () => {
stubSuggestions([maize, mexicana]);
const {user, input} = setup();
await user.type(input, 'Zea');
await user.click(await screen.findByRole('option', {name: /Zea mexicana/}));
expect(input).toHaveValue('Zea mexicana');
});
it('closes the list once a match is chosen', async () => {
stubSuggestions([maize, mexicana]);
const {user, input} = setup();
await user.type(input, 'Zea');
await user.click(await screen.findByRole('option', {name: /Zea mexicana/}));
expect(screen.queryByRole('option')).not.toBeInTheDocument();
});
it('does not search again for a name just chosen', async () => {
const fetch = stubSuggestions([maize]);
const {user, input} = setup();
await user.type(input, 'Zea');
await user.click(await screen.findByRole('option'));
await new Promise((resolve) => setTimeout(resolve, 400));
expect(fetch).toHaveBeenCalledTimes(1);
});
it('chooses the highlighted match with the arrow keys and Enter', async () => {
stubSuggestions([maize, mexicana]);
const {user, input} = setup();
await user.type(input, 'Zea');
await screen.findAllByRole('option');
await user.keyboard('{ArrowDown}{ArrowDown}{Enter}');
expect(input).toHaveValue('Zea mexicana');
});
it('leaves what was typed alone when Enter is pressed with nothing highlighted', async () => {
stubSuggestions([maize]);
const {user, input} = setup();
await user.type(input, 'Zea');
await screen.findByRole('option');
await user.keyboard('{Enter}');
expect(input).toHaveValue('Zea');
});
it('closes the list with Escape', async () => {
stubSuggestions([maize]);
const {user, input} = setup();
await user.type(input, 'Zea');
await screen.findByRole('option');
await user.keyboard('{Escape}');
expect(screen.queryByRole('option')).not.toBeInTheDocument();
});
it('closes the list when the field loses focus', async () => {
stubSuggestions([maize]);
const {user, input} = setup();
await user.type(input, 'Zea');
await screen.findByRole('option');
await user.tab();
expect(screen.queryByRole('option')).not.toBeInTheDocument();
});
});