import {
cleanup,
fireEvent,
screen,
waitFor,
} from '@testing-library/react';
import { renderWithIntl } from '@/utils/test/renderWithIntl';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import NavSearch from './NavSearch';
const mocks = vi.hoisted(() => ({
search: vi.fn(async (query: string) => {
if (!query) return [];
return [
{ type: 'note', title: 'Test Note', url: '/note/1' },
{ type: 'board', title: 'Test Board', url: '/board/1' },
];
}),
}));
// Mock icons
vi.mock('@heroicons/react/24/outline', () => ({
Squares2X2Icon: () => ,
DocumentTextIcon: () => ,
PuzzledIcon: () => ,
MagnifyingGlassIcon: () => ,
}));
// Mock search action
vi.mock('@/app/(authenticated)/lib/actions', () => ({
search: mocks.search,
}));
describe('NavSearch', () => {
beforeEach(() => {
cleanup();
vi.useFakeTimers({ toFake: ['setTimeout'], shouldAdvanceTime: true });
});
// Radix FocusScope dispatches a CustomEvent from a setTimeout(0) on unmount; flush it
// while jsdom is alive or it fires after teardown with a non-jsdom Event.
afterEach(() => {
cleanup();
vi.runOnlyPendingTimers();
vi.useRealTimers();
});
it('show searching text when search is loading', async () => {
mocks.search.mockImplementationOnce(() => {
setTimeout(() => {}, 1000);
return new Promise(() => {});
});
renderWithIntl();
fireEvent.click(screen.getByRole('button', { name: /Search anything/i }));
const input = await screen.findByPlaceholderText(
/Search boards and notes/i,
);
fireEvent.change(input, { target: { value: 'test' } });
// skip 1s debounce time
vi.advanceTimersByTime(1000);
await waitFor(() => {
expect(screen.getByText(/Searching.../i)).toBeInTheDocument();
});
});
it('renders search button and modal', () => {
renderWithIntl();
expect(screen.getByText(/Search anything/i)).toBeInTheDocument();
});
it('shows empty results message when no results', async () => {
renderWithIntl();
// Open modal by simulating click
fireEvent.click(screen.getByRole('button', { name: /Search anything/i }));
vi.advanceTimersByTime(350);
// Input is empty, so should show empty results
expect(await screen.findByText(/Quickly find boards/i)).toBeInTheDocument();
});
it('shows search results when input is typed', async () => {
renderWithIntl();
fireEvent.click(screen.getByRole('button', { name: /Search anything/i }));
const input = await screen.findByPlaceholderText(
/Search boards and notes/i,
);
fireEvent.change(input, { target: { value: 'test' } });
vi.advanceTimersByTime(350);
expect(await screen.findByText('Test Note')).toBeInTheDocument();
expect(await screen.findByText('Test Board')).toBeInTheDocument();
expect(screen.getByTestId('note-icon')).toBeInTheDocument();
expect(screen.getByTestId('chalkboard-icon')).toBeInTheDocument();
});
it('shows navigation instructions in modal footer', async () => {
renderWithIntl();
fireEvent.click(screen.getByRole('button', { name: /Search anything/i }));
const input = await screen.findByPlaceholderText(
/Search boards and notes/i,
);
fireEvent.change(input, { target: { value: 'test' } });
vi.advanceTimersByTime(350);
expect(await screen.findByText(/Navigate/i)).toBeInTheDocument();
expect(await screen.findByText(/Open/i)).toBeInTheDocument();
});
it('navigates results with keyboard', async () => {
renderWithIntl();
fireEvent.click(screen.getByRole('button', { name: /Search anything/i }));
const input = await screen.findByPlaceholderText(
/Search boards and notes/i,
);
fireEvent.change(input, { target: { value: 'test' } });
vi.advanceTimersByTime(350);
expect(await screen.findByText('Test Note')).toBeInTheDocument();
expect(await screen.findByText('Test Board')).toBeInTheDocument();
// Simulate arrow down key
fireEvent.keyDown(input, { key: 'ArrowDown' });
// After ArrowDown, the first result should be focused
const firstResult = screen.getByText('Test Note');
expect(document.activeElement).toBe(firstResult.closest('a'));
// Simulate Enter key on focused result
fireEvent.keyDown(document.activeElement as Element, { key: 'Enter' });
// Since we can't actually navigate, just ensure focus remains
expect(document.activeElement).toBe(firstResult.closest('a'));
// Simulate Backspace to return focus to input
fireEvent.keyDown(document.activeElement as Element, { key: 'Backspace' });
expect(document.activeElement).toBe(input);
// Simulate ArrowDown twice to cycle to the second result
fireEvent.keyDown(input, { key: 'ArrowDown' });
fireEvent.keyDown(input, { key: 'ArrowDown' });
const secondResult = screen.getByText('Test Board');
expect(document.activeElement).toBe(secondResult.closest('a'));
});
});