diff --git a/frontend/src/pages/ScenarioShowPage.test.tsx b/frontend/src/pages/ScenarioShowPage.test.tsx index f5340c0..2be3109 100644 --- a/frontend/src/pages/ScenarioShowPage.test.tsx +++ b/frontend/src/pages/ScenarioShowPage.test.tsx @@ -98,4 +98,44 @@ describe('ScenarioShowPage', () => { expect(rentPriorityMarker).toBeInTheDocument() expect(diningPriorityMarker).toBeInTheDocument() }) + + it('renders a distinct not-found state when the scenario 404s (e.g. not owned)', async () => { + server.use( + http.get('http://localhost/api/me', () => + HttpResponse.json({ id: 'u1', email: 'a@b.com' }), + ), + http.get(`http://localhost/api/scenarios/${SCENARIO_ID}`, () => + HttpResponse.json({ message: 'Not Found' }, { status: 404 }), + ), + ) + + renderPage() + + expect(await screen.findByText(/not found/i)).toBeInTheDocument() + expect( + screen.queryByText(/could not load this scenario/i), + ).not.toBeInTheDocument() + + // Lock in the accessibility semantics distinction from the error state + // (role="alert") — a copy-paste that swaps this to "alert" should fail + // here even if the visible copy still says "not found". + expect(await screen.findByRole('status')).toHaveTextContent(/not found/i) + }) + + it('renders the generic error state for a non-404 failure', async () => { + server.use( + http.get('http://localhost/api/me', () => + HttpResponse.json({ id: 'u1', email: 'a@b.com' }), + ), + http.get(`http://localhost/api/scenarios/${SCENARIO_ID}`, () => + HttpResponse.json({ message: 'Internal Server Error' }, { status: 500 }), + ), + ) + + renderPage() + + expect(await screen.findByRole('alert')).toHaveTextContent( + /could not load this scenario/i, + ) + }) }) diff --git a/frontend/src/pages/ScenarioShowPage.tsx b/frontend/src/pages/ScenarioShowPage.tsx index dfa8910..c5c5882 100644 --- a/frontend/src/pages/ScenarioShowPage.tsx +++ b/frontend/src/pages/ScenarioShowPage.tsx @@ -3,7 +3,7 @@ import { useParams } from 'react-router' import AuthedLayout from '../components/layout/AuthedLayout' import DigitalProgressBar from '../components/ui/DigitalProgressBar' import Panel from '../components/ui/Panel' -import { api } from '../lib/api' +import { ApiError, api } from '../lib/api' import { bucketCapacity } from '../lib/bucketCapacity' import { formatAllocation } from '../lib/bucketDisplay' import type { Bucket, Scenario } from '../types/api' @@ -11,6 +11,7 @@ import type { Bucket, Scenario } from '../types/api' type LoadState = | { status: 'loading' } | { status: 'error' } + | { status: 'not-found' } | { status: 'ready'; scenario: Scenario } function BucketCard({ bucket }: { bucket: Bucket }) { @@ -54,8 +55,15 @@ export default function ScenarioShowPage() { setState({ status: 'ready', scenario }) } }) - .catch(() => { - if (active) { + .catch((error: unknown) => { + if (!active) { + return + } + // Owner scoping (#50) returns 404 for a scenario you don't own — surface + // it as a distinct not-found state rather than a generic failure. + if (error instanceof ApiError && error.status === 404) { + setState({ status: 'not-found' }) + } else { setState({ status: 'error' }) } }) @@ -80,6 +88,12 @@ export default function ScenarioShowPage() {
)} + {state.status === 'not-found' && ( ++ Scenario not found. +
+ )} + {state.status === 'ready' && ( <>