Gold is the primary in both themes, Bricolage Grotesque carries the wordmark, titles and the score, and Movie A / Movie B wear the blue and magenta of three-strip Technicolor on their posters instead of colouring whole rows. The loop closes on screen - connectors light, a gold line draws back to Movie A, the row glows, then the result arrives - in place of confetti, on every play mode, scaled to the chain's length. Every page shares one header and hairline lists; copy is sentence case with one verb per flow, and the Home hero shows a real finished loop. Also clamps link counts at zero: an opponent who had not moved yet showed "-1 links".
561 lines
19 KiB
TypeScript
561 lines
19 KiB
TypeScript
import { render, screen, waitFor } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import type { TmdbMovieResult, TmdbPersonResult } from '@/types';
|
|
|
|
/**
|
|
* The ARIA 1.2 combobox that drives every "who was in..." / "what movie was
|
|
* ... in" search step. Requests are intercepted at the axios client and
|
|
* routed by URL - the same seam game-store.test.ts uses - so nothing reaches
|
|
* the network; searchMovies/searchPersons run for real against the mock.
|
|
*
|
|
* Real timers throughout: combining `vi.useFakeTimers()` with
|
|
* `@testing-library/user-event` deadlocks every `user.type`/`user.click` call
|
|
* in this project's toolchain (vitest 5.0.0 + user-event 14.6.7 + jsdom 30) -
|
|
* reproduced even for a bare `<input>` with no debounce involved, regardless
|
|
* of `delay: null` or an `advanceTimers` callback. `waitFor`/`findBy*` (which
|
|
* poll on real timers) stand in for fake-timer control of the 300ms debounce.
|
|
*/
|
|
const searchPersonsApi = vi.fn();
|
|
const searchMoviesApi = vi.fn();
|
|
|
|
type Handler = (params: Record<string, unknown> | undefined) => Promise<unknown>;
|
|
|
|
function route(method: string, url: string): Handler {
|
|
if (method === 'get' && url === '/persons/search') {
|
|
return (params) => searchPersonsApi((params as { q: string }).q);
|
|
}
|
|
if (method === 'get' && url === '/movies/search') {
|
|
return (params) => searchMoviesApi((params as { q: string }).q);
|
|
}
|
|
throw new Error(`test reached an unrouted request: ${method} ${url}`);
|
|
}
|
|
|
|
vi.mock('@/api/client', () => {
|
|
const call =
|
|
(method: string) =>
|
|
async (
|
|
url: string,
|
|
config?: { params?: Record<string, unknown> },
|
|
): Promise<{ data: unknown }> => ({
|
|
data: await route(method, url)(config?.params),
|
|
});
|
|
return {
|
|
default: {
|
|
get: call('get'),
|
|
post: call('post'),
|
|
put: call('put'),
|
|
delete: call('delete'),
|
|
},
|
|
};
|
|
});
|
|
|
|
const { default: SearchAutocomplete } = await import('./SearchAutocomplete');
|
|
|
|
const person = (id: number, name = `Person ${id}`): TmdbPersonResult => ({
|
|
id,
|
|
name,
|
|
profile_path: null,
|
|
popularity: 5,
|
|
known_for_department: 'Acting',
|
|
known_for: [],
|
|
adult: false,
|
|
});
|
|
const tmdbMovie = (id: number, title = `Movie ${id}`): TmdbMovieResult => ({
|
|
id,
|
|
title,
|
|
original_title: title,
|
|
overview: '',
|
|
poster_path: null,
|
|
backdrop_path: null,
|
|
release_date: '2000-01-01',
|
|
popularity: 5,
|
|
vote_average: 7,
|
|
vote_count: 10,
|
|
genre_ids: [],
|
|
adult: false,
|
|
});
|
|
|
|
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
/** Comfortably past the component's 300ms debounce. */
|
|
const pastDebounce = () => wait(400);
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
searchPersonsApi.mockResolvedValue({ page: 1, results: [], total_pages: 1, total_results: 0 });
|
|
searchMoviesApi.mockResolvedValue({ page: 1, results: [], total_pages: 1, total_results: 0 });
|
|
});
|
|
|
|
describe('SearchAutocomplete', () => {
|
|
it('does not search below the two-character threshold', async () => {
|
|
const user = userEvent.setup();
|
|
const onSelectActor = vi.fn();
|
|
render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={onSelectActor} onSelectMovie={vi.fn()} />,
|
|
);
|
|
|
|
await user.type(screen.getByRole('combobox'), 'a');
|
|
await pastDebounce();
|
|
|
|
expect(searchPersonsApi).not.toHaveBeenCalled();
|
|
expect(screen.queryByRole('listbox')).toBeNull();
|
|
}, 10000);
|
|
|
|
it('debounces before searching: not called right after typing, called once settled', async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={vi.fn()} onSelectMovie={vi.fn()} />,
|
|
);
|
|
await user.type(screen.getByRole('combobox'), 'ke');
|
|
|
|
expect(searchPersonsApi).not.toHaveBeenCalled();
|
|
await waitFor(() => expect(searchPersonsApi).toHaveBeenCalledWith('ke'));
|
|
}, 10000);
|
|
|
|
it('opens a labelled listbox of actor results, wired to the combobox', async () => {
|
|
searchPersonsApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [person(1, 'Keanu Reeves'), person(2, 'Carrie-Anne Moss')],
|
|
total_pages: 1,
|
|
total_results: 2,
|
|
});
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={vi.fn()} onSelectMovie={vi.fn()} />,
|
|
);
|
|
const input = screen.getByRole('combobox', { name: 'Search for an actor' });
|
|
await user.type(input, 'ke');
|
|
|
|
const listbox = await screen.findByRole('listbox', { name: 'Actor results' });
|
|
expect(input).toHaveAttribute('aria-expanded', 'true');
|
|
expect(input).toHaveAttribute('aria-controls', listbox.id);
|
|
expect(screen.getByText('Keanu Reeves')).toBeInTheDocument();
|
|
expect(screen.getByText('Carrie-Anne Moss')).toBeInTheDocument();
|
|
}, 10000);
|
|
|
|
it('filters out results that are not in the Acting department', async () => {
|
|
searchPersonsApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [
|
|
person(1, 'An Actor'),
|
|
{ ...person(2, 'A Director'), known_for_department: 'Directing' },
|
|
],
|
|
total_pages: 1,
|
|
total_results: 2,
|
|
});
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={vi.fn()} onSelectMovie={vi.fn()} />,
|
|
);
|
|
await user.type(screen.getByRole('combobox'), 'an');
|
|
|
|
await screen.findByText('An Actor');
|
|
expect(screen.queryByText('A Director')).toBeNull();
|
|
}, 10000);
|
|
|
|
it('searches movies in movie mode', async () => {
|
|
searchMoviesApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [tmdbMovie(1, 'The Matrix')],
|
|
total_pages: 1,
|
|
total_results: 1,
|
|
});
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="movie" onSelectActor={vi.fn()} onSelectMovie={vi.fn()} />,
|
|
);
|
|
await user.type(screen.getByRole('combobox', { name: 'Search for a movie' }), 'ma');
|
|
|
|
await waitFor(() => expect(searchMoviesApi).toHaveBeenCalledWith('ma'));
|
|
expect(searchPersonsApi).not.toHaveBeenCalled();
|
|
expect(
|
|
screen.getByRole('listbox', { name: 'Movie results' }),
|
|
).toBeInTheDocument();
|
|
expect(screen.getByText('The Matrix')).toBeInTheDocument();
|
|
}, 10000);
|
|
|
|
it('shows a no-match message when a search comes back empty', async () => {
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={vi.fn()} onSelectMovie={vi.fn()} />,
|
|
);
|
|
await user.type(screen.getByRole('combobox'), 'zz');
|
|
|
|
await screen.findByText('No matches. Try another spelling.');
|
|
}, 10000);
|
|
|
|
it('shows an error option when the search request fails', async () => {
|
|
searchPersonsApi.mockRejectedValue(new Error('network down'));
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={vi.fn()} onSelectMovie={vi.fn()} />,
|
|
);
|
|
await user.type(screen.getByRole('combobox'), 'ke');
|
|
|
|
await screen.findByText('Search failed. Please try again.');
|
|
}, 10000);
|
|
|
|
it('shows a loading indicator while the request is in flight, then hides it', async () => {
|
|
let resolveSearch: (v: unknown) => void = () => undefined;
|
|
searchPersonsApi.mockImplementation(
|
|
() => new Promise((resolve) => (resolveSearch = resolve)),
|
|
);
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={vi.fn()} onSelectMovie={vi.fn()} />,
|
|
);
|
|
await user.type(screen.getByRole('combobox'), 'ke');
|
|
|
|
await screen.findByLabelText('Searching…');
|
|
|
|
resolveSearch({ page: 1, results: [], total_pages: 1, total_results: 0 });
|
|
await waitFor(() => expect(screen.queryByLabelText('Searching…')).toBeNull());
|
|
}, 10000);
|
|
|
|
it('ignores a stale response that resolves after a newer search has started', async () => {
|
|
let resolveFirst: (v: unknown) => void = () => undefined;
|
|
let resolveSecond: (v: unknown) => void = () => undefined;
|
|
searchPersonsApi
|
|
.mockImplementationOnce(() => new Promise((resolve) => (resolveFirst = resolve)))
|
|
.mockImplementationOnce(() => new Promise((resolve) => (resolveSecond = resolve)));
|
|
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={vi.fn()} onSelectMovie={vi.fn()} />,
|
|
);
|
|
const input = screen.getByRole('combobox');
|
|
|
|
await user.type(input, 'ab');
|
|
await waitFor(() => expect(searchPersonsApi).toHaveBeenCalledTimes(1));
|
|
|
|
await user.type(input, 'c');
|
|
await waitFor(() => expect(searchPersonsApi).toHaveBeenCalledTimes(2));
|
|
|
|
// The newer ("abc") request settles first.
|
|
resolveSecond({
|
|
page: 1,
|
|
results: [person(2, 'Current Result')],
|
|
total_pages: 1,
|
|
total_results: 1,
|
|
});
|
|
await screen.findByText('Current Result');
|
|
|
|
// The stale ("ab") request settles after: its cleanup already ran, so it
|
|
// must not clobber the current results.
|
|
resolveFirst({
|
|
page: 1,
|
|
results: [person(1, 'Stale Result')],
|
|
total_pages: 1,
|
|
total_results: 1,
|
|
});
|
|
await wait(50);
|
|
|
|
expect(screen.getByText('Current Result')).toBeInTheDocument();
|
|
expect(screen.queryByText('Stale Result')).toBeNull();
|
|
}, 10000);
|
|
|
|
it('navigates results with ArrowDown/ArrowUp, wrapping at each end', async () => {
|
|
searchPersonsApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [person(1, 'First'), person(2, 'Second')],
|
|
total_pages: 1,
|
|
total_results: 2,
|
|
});
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={vi.fn()} onSelectMovie={vi.fn()} />,
|
|
);
|
|
const input = screen.getByRole('combobox');
|
|
await user.type(input, 'ke');
|
|
const listbox = await screen.findByRole('listbox');
|
|
|
|
expect(input).not.toHaveAttribute('aria-activedescendant');
|
|
|
|
await user.keyboard('{ArrowDown}');
|
|
expect(input).toHaveAttribute('aria-activedescendant', `${listbox.id}-option-0`);
|
|
|
|
await user.keyboard('{ArrowDown}');
|
|
expect(input).toHaveAttribute('aria-activedescendant', `${listbox.id}-option-1`);
|
|
|
|
// Wraps back to the first option.
|
|
await user.keyboard('{ArrowDown}');
|
|
expect(input).toHaveAttribute('aria-activedescendant', `${listbox.id}-option-0`);
|
|
|
|
// Wraps to the last option going up from the first.
|
|
await user.keyboard('{ArrowUp}');
|
|
expect(input).toHaveAttribute('aria-activedescendant', `${listbox.id}-option-1`);
|
|
}, 10000);
|
|
|
|
it('selects the active option on Enter, clears the query and closes the list', async () => {
|
|
searchPersonsApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [person(7, 'Seven')],
|
|
total_pages: 1,
|
|
total_results: 1,
|
|
});
|
|
const onSelectActor = vi.fn();
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={onSelectActor} onSelectMovie={vi.fn()} />,
|
|
);
|
|
const input = screen.getByRole('combobox');
|
|
await user.type(input, 'se');
|
|
await screen.findByText('Seven');
|
|
|
|
await user.keyboard('{ArrowDown}{Enter}');
|
|
|
|
expect(onSelectActor).toHaveBeenCalledWith(expect.objectContaining({ id: 7, name: 'Seven' }));
|
|
expect(input).toHaveValue('');
|
|
expect(input).toHaveAttribute('aria-expanded', 'false');
|
|
}, 10000);
|
|
|
|
it('does nothing on Enter when no option is active', async () => {
|
|
searchPersonsApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [person(7, 'Seven')],
|
|
total_pages: 1,
|
|
total_results: 1,
|
|
});
|
|
const onSelectActor = vi.fn();
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={onSelectActor} onSelectMovie={vi.fn()} />,
|
|
);
|
|
await user.type(screen.getByRole('combobox'), 'se');
|
|
await screen.findByText('Seven');
|
|
|
|
await user.keyboard('{Enter}');
|
|
expect(onSelectActor).not.toHaveBeenCalled();
|
|
}, 10000);
|
|
|
|
it('selects an actor result by clicking it', async () => {
|
|
searchPersonsApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [person(3, 'Clicked Actor')],
|
|
total_pages: 1,
|
|
total_results: 1,
|
|
});
|
|
const onSelectActor = vi.fn();
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={onSelectActor} onSelectMovie={vi.fn()} />,
|
|
);
|
|
await user.type(screen.getByRole('combobox'), 'cl');
|
|
await screen.findByText('Clicked Actor');
|
|
|
|
await user.click(screen.getByRole('button', { name: /Clicked Actor/ }));
|
|
expect(onSelectActor).toHaveBeenCalledWith(
|
|
expect.objectContaining({ id: 3, name: 'Clicked Actor' }),
|
|
);
|
|
}, 10000);
|
|
|
|
it("shows an actor's known-for titles when TMDB reports any", async () => {
|
|
searchPersonsApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [
|
|
{
|
|
...person(4, 'Prolific Actor'),
|
|
known_for: [tmdbMovie(1, 'Movie One'), tmdbMovie(2, 'Movie Two')],
|
|
},
|
|
],
|
|
total_pages: 1,
|
|
total_results: 1,
|
|
});
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={vi.fn()} onSelectMovie={vi.fn()} />,
|
|
);
|
|
await user.type(screen.getByRole('combobox'), 'pr');
|
|
|
|
await screen.findByText('Prolific Actor');
|
|
expect(screen.getByText('Known for: Movie One, Movie Two')).toBeInTheDocument();
|
|
}, 10000);
|
|
|
|
it('selects the active movie option on Enter in movie mode', async () => {
|
|
searchMoviesApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [tmdbMovie(9, 'Speed')],
|
|
total_pages: 1,
|
|
total_results: 1,
|
|
});
|
|
const onSelectMovie = vi.fn();
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="movie" onSelectActor={vi.fn()} onSelectMovie={onSelectMovie} />,
|
|
);
|
|
const input = screen.getByRole('combobox');
|
|
await user.type(input, 'sp');
|
|
await screen.findByText('Speed');
|
|
|
|
await user.keyboard('{ArrowDown}{Enter}');
|
|
|
|
expect(onSelectMovie).toHaveBeenCalledWith(
|
|
expect.objectContaining({ id: 9, title: 'Speed' }),
|
|
);
|
|
expect(input).toHaveValue('');
|
|
}, 10000);
|
|
|
|
it('selects a movie result by clicking it', async () => {
|
|
searchMoviesApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [tmdbMovie(9, 'Speed')],
|
|
total_pages: 1,
|
|
total_results: 1,
|
|
});
|
|
const onSelectMovie = vi.fn();
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="movie" onSelectActor={vi.fn()} onSelectMovie={onSelectMovie} />,
|
|
);
|
|
await user.type(screen.getByRole('combobox'), 'sp');
|
|
await screen.findByText('Speed');
|
|
|
|
await user.click(screen.getByRole('button', { name: /Speed/ }));
|
|
expect(onSelectMovie).toHaveBeenCalledWith(expect.objectContaining({ id: 9, title: 'Speed' }));
|
|
}, 10000);
|
|
|
|
it('closes and resets the active option on Escape', async () => {
|
|
searchPersonsApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [person(1, 'First')],
|
|
total_pages: 1,
|
|
total_results: 1,
|
|
});
|
|
const user = userEvent.setup();
|
|
render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={vi.fn()} onSelectMovie={vi.fn()} />,
|
|
);
|
|
const input = screen.getByRole('combobox');
|
|
await user.type(input, 'fi');
|
|
await screen.findByRole('listbox');
|
|
await user.keyboard('{ArrowDown}');
|
|
|
|
await user.keyboard('{Escape}');
|
|
expect(screen.queryByRole('listbox')).toBeNull();
|
|
expect(input).not.toHaveAttribute('aria-activedescendant');
|
|
}, 10000);
|
|
|
|
it('closes the dropdown when clicking outside the component', async () => {
|
|
searchPersonsApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [person(1, 'First')],
|
|
total_pages: 1,
|
|
total_results: 1,
|
|
});
|
|
const user = userEvent.setup();
|
|
render(
|
|
<div>
|
|
<SearchAutocomplete mode="actor" onSelectActor={vi.fn()} onSelectMovie={vi.fn()} />
|
|
<button type="button">outside</button>
|
|
</div>,
|
|
);
|
|
await user.type(screen.getByRole('combobox'), 'fi');
|
|
await screen.findByRole('listbox');
|
|
|
|
await user.click(screen.getByRole('button', { name: 'outside' }));
|
|
expect(screen.queryByRole('listbox')).toBeNull();
|
|
}, 10000);
|
|
|
|
it('reopens the dropdown on focus when there are already results', async () => {
|
|
searchPersonsApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [person(1, 'First')],
|
|
total_pages: 1,
|
|
total_results: 1,
|
|
});
|
|
const user = userEvent.setup();
|
|
render(
|
|
<div>
|
|
<SearchAutocomplete mode="actor" onSelectActor={vi.fn()} onSelectMovie={vi.fn()} />
|
|
<button type="button">outside</button>
|
|
</div>,
|
|
);
|
|
const input = screen.getByRole('combobox');
|
|
await user.type(input, 'fi');
|
|
await screen.findByRole('listbox');
|
|
await user.click(screen.getByRole('button', { name: 'outside' }));
|
|
expect(screen.queryByRole('listbox')).toBeNull();
|
|
|
|
await user.click(input);
|
|
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
}, 10000);
|
|
|
|
it('disables the input and forwards a custom placeholder', () => {
|
|
render(
|
|
<SearchAutocomplete
|
|
mode="actor"
|
|
onSelectActor={vi.fn()}
|
|
onSelectMovie={vi.fn()}
|
|
disabled
|
|
placeholder="Custom prompt"
|
|
/>,
|
|
);
|
|
const input = screen.getByRole('combobox');
|
|
expect(input).toBeDisabled();
|
|
expect(input).toHaveAttribute('placeholder', 'Custom prompt');
|
|
});
|
|
|
|
it('resets its query and results when the mode prop switches, and refocuses', async () => {
|
|
searchPersonsApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [person(1, 'First')],
|
|
total_pages: 1,
|
|
total_results: 1,
|
|
});
|
|
const user = userEvent.setup();
|
|
const { rerender } = render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={vi.fn()} onSelectMovie={vi.fn()} />,
|
|
);
|
|
const actorInput = screen.getByRole('combobox');
|
|
await user.type(actorInput, 'fi');
|
|
await screen.findByRole('listbox');
|
|
expect(actorInput).toHaveValue('fi');
|
|
|
|
rerender(
|
|
<SearchAutocomplete mode="movie" onSelectActor={vi.fn()} onSelectMovie={vi.fn()} />,
|
|
);
|
|
|
|
const movieInput = screen.getByRole('combobox', { name: 'Search for a movie' });
|
|
expect(movieInput).toHaveValue('');
|
|
expect(screen.queryByRole('listbox')).toBeNull();
|
|
expect(movieInput).toHaveFocus();
|
|
}, 10000);
|
|
|
|
it('does not search the new mode with the previous query after a selection flips the mode', async () => {
|
|
// Seen live in WP-19's smoke run: pick an actor, the mode flips to movie,
|
|
// and 300ms of debounce still hold the actor's name - so a movie search
|
|
// for "Daniel Craig" ran and opened a listbox under an empty input.
|
|
searchPersonsApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [person(1, 'Daniel Craig')],
|
|
total_pages: 1,
|
|
total_results: 1,
|
|
});
|
|
searchMoviesApi.mockResolvedValue({
|
|
page: 1,
|
|
results: [tmdbMovie(9, 'Daniel Craig - Permis de jouer')],
|
|
total_pages: 1,
|
|
total_results: 1,
|
|
});
|
|
const user = userEvent.setup();
|
|
const onSelectActor = vi.fn();
|
|
const { rerender } = render(
|
|
<SearchAutocomplete mode="actor" onSelectActor={onSelectActor} onSelectMovie={vi.fn()} />,
|
|
);
|
|
await user.type(screen.getByRole('combobox'), 'Daniel Craig');
|
|
await screen.findByText('Daniel Craig');
|
|
await user.click(screen.getByRole('button', { name: /Daniel Craig/ }));
|
|
expect(onSelectActor).toHaveBeenCalledTimes(1);
|
|
|
|
// The parent reacts to the selection by switching the mode, as the game does.
|
|
rerender(
|
|
<SearchAutocomplete mode="movie" onSelectActor={onSelectActor} onSelectMovie={vi.fn()} />,
|
|
);
|
|
await pastDebounce();
|
|
|
|
expect(searchMoviesApi).not.toHaveBeenCalled();
|
|
expect(screen.queryByRole('listbox')).toBeNull();
|
|
expect(screen.getByRole('combobox', { name: 'Search for a movie' })).toHaveValue('');
|
|
}, 10000);
|
|
});
|