From 21a4c69972f7cd28ad817a482bc84c7d5d764390 Mon Sep 17 00:00:00 2001 From: Samu Date: Thu, 3 Sep 2026 19:24:57 +0200 Subject: [PATCH] fix: reset-to-auto in both rip branches, visible mutation failures, distinct invalid-token error --- web/src/pages/AddPage.tsx | 8 +++- web/src/pages/ItemPage.tsx | 81 +++++++++++++++++++++++++++----------- web/src/pages/ScanPage.tsx | 11 ++++++ web/src/scan/reducer.ts | 2 +- web/test/item.test.tsx | 15 +++++++ web/test/scanPage.test.tsx | 8 ++++ 6 files changed, 101 insertions(+), 24 deletions(-) diff --git a/web/src/pages/AddPage.tsx b/web/src/pages/AddPage.tsx index 7e3fc08..9dff8bf 100644 --- a/web/src/pages/AddPage.tsx +++ b/web/src/pages/AddPage.tsx @@ -10,6 +10,7 @@ function toErrorKind(err: unknown): ScanErrorKind { if (err instanceof ApiError) { if (err.code === 'not_found') return 'not_found' if (err.code === 'no_discogs_token') return 'no_discogs_token' + if (err.code === 'discogs_auth') return 'discogs_auth' if (err.code === 'discogs_rate_limited' || err.status === 429) return 'rate_limited' } return 'server' @@ -179,11 +180,16 @@ export default function AddPage() { {state.phase === 'error' && (

- {state.kind === 'not_found' ? 'Nothing found' : 'Search failed'} + {state.kind === 'not_found' + ? 'Nothing found' + : state.kind === 'discogs_auth' + ? 'Discogs rejected your token' + : 'Search failed'}

{state.kind === 'not_found' && 'Try different spelling, or add the year.'} {state.kind === 'no_discogs_token' && 'Add your Discogs token in Settings first.'} + {state.kind === 'discogs_auth' && 'Check your Discogs token in Settings.'} {state.kind === 'rate_limited' && 'Discogs is rate limiting us. Try again shortly.'}

- {item.ripOverride !== null && ( - - )} - + ) : ( )} + {item.ripOverride !== null && ( + + )}
diff --git a/web/src/pages/ScanPage.tsx b/web/src/pages/ScanPage.tsx index 995c51f..1317318 100644 --- a/web/src/pages/ScanPage.tsx +++ b/web/src/pages/ScanPage.tsx @@ -11,6 +11,7 @@ function toErrorKind(err: unknown): ScanErrorKind { if (err instanceof ApiError) { if (err.code === 'not_found') return 'not_found' if (err.code === 'no_discogs_token') return 'no_discogs_token' + if (err.code === 'discogs_auth') return 'discogs_auth' if (err.code === 'discogs_rate_limited' || err.status === 429) return 'rate_limited' } return 'server' @@ -185,6 +186,16 @@ export default function ScanPage() { )} + {state.phase === 'error' && state.kind === 'discogs_auth' && ( +
+

Discogs rejected your token

+

Check your Discogs token in Settings.

+ + Settings + +
+ )} + {state.phase === 'error' && state.kind === 'server' && (

Lookup failed

diff --git a/web/src/scan/reducer.ts b/web/src/scan/reducer.ts index c009246..2042d74 100644 --- a/web/src/scan/reducer.ts +++ b/web/src/scan/reducer.ts @@ -1,6 +1,6 @@ import type { Candidate, Item, ReleasePreview } from '../types.js' -export type ScanErrorKind = 'not_found' | 'no_discogs_token' | 'rate_limited' | 'server' +export type ScanErrorKind = 'not_found' | 'no_discogs_token' | 'discogs_auth' | 'rate_limited' | 'server' export type ScanState = | { phase: 'scan' } diff --git a/web/test/item.test.tsx b/web/test/item.test.tsx index 25e67c2..1557a8b 100644 --- a/web/test/item.test.tsx +++ b/web/test/item.test.tsx @@ -127,4 +127,19 @@ describe('ItemPage', () => { const linkBtn = await screen.findByRole('button', { name: /^link$/i }) expect(linkBtn).toHaveProperty('disabled', true) }) + + it('shows Reset to auto for a manual not-ripped override', async () => { + vi.mocked(api.getItem).mockResolvedValue({ ...item, ripOverride: false, ripStatus: 'not_ripped' } as never) + renderItem() + await waitFor(() => expect(screen.getByText(/not ripped yet \(manually set\)/i)).toBeTruthy()) + expect(screen.getByRole('button', { name: /reset to auto/i })).toBeTruthy() + expect(screen.getByRole('button', { name: /mark ripped/i })).toBeTruthy() + }) + + it('shows an error when a rip toggle fails', async () => { + vi.mocked(api.setRip).mockRejectedValue(new TypeError('fetch failed')) + renderItem() + await userEvent.click(await screen.findByRole('button', { name: /mark ripped/i })) + await waitFor(() => expect(screen.getByText(/didn't work/i)).toBeTruthy()) + }) }) diff --git a/web/test/scanPage.test.tsx b/web/test/scanPage.test.tsx index bb6eb66..2f5a65c 100644 --- a/web/test/scanPage.test.tsx +++ b/web/test/scanPage.test.tsx @@ -177,4 +177,12 @@ describe('ScanPage flow', () => { await waitFor(() => expect(screen.getByText(/added to collection/i)).toBeTruthy()) expect(api.addToCollection).toHaveBeenCalledTimes(1) }) + + it('distinguishes an invalid discogs token with a settings link', async () => { + vi.mocked(api.lookupBarcode).mockRejectedValue(new ApiError(502, 'discogs_auth')) + renderScan() + await userEvent.click(screen.getByRole('button', { name: 'fake-scan' })) + await waitFor(() => expect(screen.getByText(/rejected your token/i)).toBeTruthy()) + expect(screen.getByRole('link', { name: /settings/i }).getAttribute('href')).toBe('/settings') + }) })