import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { MemoryRouter } from 'react-router-dom' import AddPage from '../src/pages/AddPage.js' import { api, ApiError } from '../src/api.js' import type { Candidate, ReleasePreview } from '../src/types.js' vi.mock('../src/api.js', async (importOriginal) => { const actual = await importOriginal() return { ...actual, api: { ...actual.api, lookupSearch: vi.fn(), getReleasePreview: vi.fn(), addToCollection: vi.fn() } } }) const candidate: Candidate = { id: 1001, artist: 'The Cinematic Orchestra', title: 'Motion', year: 1999, formats: ['Vinyl'], labels: ['Ninja Tune'], country: 'UK', catno: 'ZEN012', thumbUrl: null, } const preview: ReleasePreview = { release: { ...candidate, genres: [], tracklist: [], coverUrl: null, barcodes: [] }, duplicate: false, ripMatch: 'not_ripped', matchCandidates: [], } // The api module is mocked directly, so mocks resolve with parsed bodies. function jsonOk(body: unknown) { return Promise.resolve(body) } beforeEach(() => { vi.mocked(api.lookupSearch).mockReset() vi.mocked(api.getReleasePreview).mockReset() vi.mocked(api.addToCollection).mockReset() }) function renderAdd(initialEntry = '/add') { return render( ) } describe('AddPage', () => { it('searches discogs and shows candidate cards', async () => { vi.mocked(api.lookupSearch).mockResolvedValue(jsonOk({ candidates: [candidate] }) as never) renderAdd() await userEvent.type(screen.getByPlaceholderText(/artist and title/i), 'cinematic orchestra motion{Enter}') await waitFor(() => expect(api.lookupSearch).toHaveBeenCalledWith('cinematic orchestra motion', undefined) ) expect(await screen.findByRole('button', { name: /motion/i })).toBeTruthy() }) it('passes the format filter and pre-filled query from the URL', async () => { vi.mocked(api.lookupSearch).mockResolvedValue(jsonOk({ candidates: [] }) as never) renderAdd('/add?q=5021592210629') expect((screen.getByPlaceholderText(/artist and title/i) as HTMLInputElement).value).toBe( '5021592210629' ) await userEvent.selectOptions(screen.getByLabelText(/format/i), 'Vinyl') await userEvent.click(screen.getByRole('button', { name: /^search$/i })) await waitFor(() => expect(api.lookupSearch).toHaveBeenCalledWith('5021592210629', 'Vinyl')) }) it('shows a not-found message', async () => { vi.mocked(api.lookupSearch).mockRejectedValue(new ApiError(404, 'not_found')) renderAdd() await userEvent.type(screen.getByPlaceholderText(/artist and title/i), 'zzz{Enter}') await waitFor(() => expect(screen.getByText(/nothing found/i)).toBeTruthy()) }) it('selecting a candidate loads the confirm view and adds', async () => { vi.mocked(api.lookupSearch).mockResolvedValue(jsonOk({ candidates: [candidate] }) as never) vi.mocked(api.getReleasePreview).mockResolvedValue(jsonOk(preview) as never) vi.mocked(api.addToCollection).mockResolvedValue(jsonOk({}) as never) renderAdd() await userEvent.type(screen.getByPlaceholderText(/artist and title/i), 'motion{Enter}') await userEvent.click(await screen.findByRole('button', { name: /motion/i })) await waitFor(() => expect(screen.getByText('Not ripped yet')).toBeTruthy()) await userEvent.click(screen.getByRole('button', { name: /add to collection/i })) await waitFor(() => expect(api.addToCollection).toHaveBeenCalledWith({ releaseId: 1001 })) }) })