/**
* Component: BookDate Book Picker Modal Tests
* Documentation: documentation/features/bookdate.md
*/
// @vitest-environment jsdom
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
const books = [
{ id: 'book-1', title: 'First Book', author: 'Author One', coverUrl: null },
{ id: 'book-2', title: 'Second Book', author: 'Author Two', coverUrl: null },
];
describe('BookPickerModal', () => {
afterEach(() => {
vi.unstubAllGlobals();
localStorage.clear();
});
it('loads books and confirms the selection', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ books }),
});
vi.stubGlobal('fetch', fetchMock);
localStorage.setItem('accessToken', 'token-123');
const onConfirm = vi.fn();
const onClose = vi.fn();
const { BookPickerModal } = await import('@/components/bookdate/BookPickerModal');
render(
);
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith('/api/bookdate/library', {
headers: { Authorization: 'Bearer token-123' },
});
});
const firstBookButton = await screen.findByRole('button', { name: /First Book/ });
fireEvent.click(firstBookButton);
fireEvent.click(screen.getByRole('button', { name: /Confirm Selection/ }));
expect(onConfirm).toHaveBeenCalledWith(['book-1']);
expect(onClose).toHaveBeenCalled();
});
it('disables additional selections once the max is reached', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ books }),
});
vi.stubGlobal('fetch', fetchMock);
localStorage.setItem('accessToken', 'token-456');
const { BookPickerModal } = await import('@/components/bookdate/BookPickerModal');
render(
);
const firstBookButton = await screen.findByRole('button', { name: /First Book/ });
fireEvent.click(firstBookButton);
expect(screen.getByText(/Maximum reached/)).toBeInTheDocument();
const secondBookButton = screen.getByRole('button', { name: /Second Book/ });
expect(secondBookButton).toBeDisabled();
});
it('shows an error when loading books fails', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: false,
json: async () => ({}),
});
vi.stubGlobal('fetch', fetchMock);
const { BookPickerModal } = await import('@/components/bookdate/BookPickerModal');
render(
);
expect(await screen.findByText('Failed to load library books')).toBeInTheDocument();
});
it('shows an empty search state when no books match', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ books }),
});
vi.stubGlobal('fetch', fetchMock);
const { BookPickerModal } = await import('@/components/bookdate/BookPickerModal');
render(
);
await screen.findByRole('button', { name: /First Book/ });
fireEvent.change(screen.getByPlaceholderText('Search books...'), { target: { value: 'missing' } });
expect(screen.getByText('No books match your search')).toBeInTheDocument();
});
it('clears selection and disables confirm', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ books }),
});
vi.stubGlobal('fetch', fetchMock);
const { BookPickerModal } = await import('@/components/bookdate/BookPickerModal');
render(
);
await screen.findByRole('button', { name: /First Book/ });
const clearButton = screen.getByRole('button', { name: 'Clear Selection' });
fireEvent.click(clearButton);
expect(screen.getByRole('button', { name: /Confirm Selection/ })).toBeDisabled();
});
});