feat: scan flow page with candidates, confirm view and error guidance
This commit is contained in:
161
web/test/scanPage.test.tsx
Normal file
161
web/test/scanPage.test.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
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 ScanPage from '../src/pages/ScanPage.js'
|
||||
import type { Candidate, ReleasePreview } from '../src/types.js'
|
||||
|
||||
vi.mock('../src/components/Scanner.js', () => ({
|
||||
default: ({ onDetect }: { onDetect: (code: string) => void }) => (
|
||||
<button type="button" onClick={() => onDetect('5021592210629')}>
|
||||
fake-scan
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../src/api.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../src/api.js')>()
|
||||
return {
|
||||
...actual,
|
||||
api: {
|
||||
...actual.api,
|
||||
lookupBarcode: vi.fn(),
|
||||
getReleasePreview: vi.fn(),
|
||||
addToCollection: vi.fn(),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
import { api, ApiError } from '../src/api.js'
|
||||
|
||||
const candidate: Candidate = {
|
||||
id: 1001,
|
||||
artist: 'The Cinematic Orchestra',
|
||||
title: 'Motion',
|
||||
year: 1999,
|
||||
formats: ['CD'],
|
||||
labels: ['Ninja Tune'],
|
||||
country: 'UK',
|
||||
catno: 'ZENCD012',
|
||||
thumbUrl: null,
|
||||
}
|
||||
|
||||
const preview: ReleasePreview = {
|
||||
release: { ...candidate, genres: [], tracklist: [{ position: '1', title: 'Overture' }], coverUrl: null, barcodes: ['5021592210629'] },
|
||||
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.lookupBarcode).mockReset()
|
||||
vi.mocked(api.getReleasePreview).mockReset()
|
||||
vi.mocked(api.addToCollection).mockReset()
|
||||
})
|
||||
|
||||
function renderScan() {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={['/scan']}>
|
||||
<ScanPage />
|
||||
</MemoryRouter>
|
||||
)
|
||||
}
|
||||
|
||||
describe('ScanPage flow', () => {
|
||||
it('scan → candidates → confirm → added', async () => {
|
||||
vi.mocked(api.lookupBarcode).mockResolvedValue(jsonOk({ candidates: [candidate] }) as never)
|
||||
vi.mocked(api.getReleasePreview).mockResolvedValue(jsonOk(preview) as never)
|
||||
vi.mocked(api.addToCollection).mockResolvedValue(
|
||||
jsonOk({
|
||||
id: 1,
|
||||
discogsReleaseId: 1001,
|
||||
title: 'Motion',
|
||||
artist: 'The Cinematic Orchestra',
|
||||
year: 1999,
|
||||
formats: ['CD'],
|
||||
genres: [],
|
||||
labels: ['Ninja Tune'],
|
||||
tracklist: [],
|
||||
catno: 'ZENCD012',
|
||||
country: 'UK',
|
||||
artworkUrl: null,
|
||||
barcodes: ['5021592210629'],
|
||||
dateAdded: '2026-08-29',
|
||||
ripOverride: null,
|
||||
ripStatus: 'not_ripped',
|
||||
}) as never
|
||||
)
|
||||
|
||||
renderScan()
|
||||
await userEvent.click(screen.getByRole('button', { name: 'fake-scan' }))
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: /motion/i })).toBeTruthy())
|
||||
await userEvent.click(screen.getByRole('button', { name: /motion/i }))
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Not ripped yet')).toBeTruthy())
|
||||
expect(screen.getByText('Overture')).toBeTruthy()
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /add to collection/i }))
|
||||
await waitFor(() => expect(screen.getByText(/added to collection/i)).toBeTruthy())
|
||||
expect(api.addToCollection).toHaveBeenCalledWith({
|
||||
releaseId: 1001,
|
||||
barcode: '5021592210629',
|
||||
})
|
||||
})
|
||||
|
||||
it('shows not-found guidance with a manual-search escape hatch', async () => {
|
||||
vi.mocked(api.lookupBarcode).mockRejectedValue(new ApiError(404, 'not_found'))
|
||||
renderScan()
|
||||
await userEvent.click(screen.getByRole('button', { name: 'fake-scan' }))
|
||||
await waitFor(() => expect(screen.getByText(/nothing found/i)).toBeTruthy())
|
||||
expect(screen.getByRole('link', { name: /search manually/i }).getAttribute('href')).toBe(
|
||||
'/add?q=5021592210629'
|
||||
)
|
||||
})
|
||||
|
||||
it('sends the picked match candidate for ambiguous rip matches', async () => {
|
||||
vi.mocked(api.lookupBarcode).mockResolvedValue(jsonOk({ candidates: [candidate] }) as never)
|
||||
vi.mocked(api.getReleasePreview).mockResolvedValue(
|
||||
jsonOk({
|
||||
...preview,
|
||||
ripMatch: 'ambiguous',
|
||||
matchCandidates: [{ id: 77, title: 'Motion', artist: 'Somebody Else' }],
|
||||
}) as never
|
||||
)
|
||||
vi.mocked(api.addToCollection).mockResolvedValue(jsonOk({}) as never)
|
||||
|
||||
renderScan()
|
||||
await userEvent.click(screen.getByRole('button', { name: 'fake-scan' }))
|
||||
await userEvent.click(await screen.findByRole('button', { name: /motion/i }))
|
||||
await waitFor(() => expect(screen.getByRole('radio', { name: /somebody else/i })).toBeTruthy())
|
||||
await userEvent.click(screen.getByRole('radio', { name: /somebody else/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /add to collection/i }))
|
||||
await waitFor(() => expect(api.addToCollection).toHaveBeenCalledWith({
|
||||
releaseId: 1001,
|
||||
barcode: '5021592210629',
|
||||
matchAlbumId: 77,
|
||||
}))
|
||||
})
|
||||
|
||||
it('shows a duplicate warning from the preview', async () => {
|
||||
vi.mocked(api.lookupBarcode).mockResolvedValue(jsonOk({ candidates: [candidate] }) as never)
|
||||
vi.mocked(api.getReleasePreview).mockResolvedValue(jsonOk({ ...preview, duplicate: true }) as never)
|
||||
renderScan()
|
||||
await userEvent.click(screen.getByRole('button', { name: 'fake-scan' }))
|
||||
await userEvent.click(await screen.findByRole('button', { name: /motion/i }))
|
||||
await waitFor(() => expect(screen.getByText(/already in your collection/i)).toBeTruthy())
|
||||
})
|
||||
|
||||
it('links to settings when no discogs token is configured', async () => {
|
||||
vi.mocked(api.lookupBarcode).mockRejectedValue(new ApiError(409, 'no_discogs_token'))
|
||||
renderScan()
|
||||
await userEvent.click(screen.getByRole('button', { name: 'fake-scan' }))
|
||||
await waitFor(() => expect(screen.getByText(/discogs token/i)).toBeTruthy())
|
||||
expect(screen.getByRole('link', { name: /settings/i }).getAttribute('href')).toBe('/settings')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user