import { render, screen } from '@testing-library/react' import { http, HttpResponse } from 'msw' import { MemoryRouter, Route, Routes } from 'react-router' import { describe, expect, it } from 'vitest' import { AuthProvider } from '../auth/AuthProvider' import { server } from '../test/server' import ScenarioShowPage from './ScenarioShowPage' const SCENARIO_ID = '11111111-1111-1111-1111-111111111111' /** * ScenarioShowPage renders under AuthProvider (same /api/me probe as every * authed page) plus a minimal own route harness carrying the `:id` param — * the real routes.tsx wiring is a separate test (14), so this page test does * not depend on it. */ function renderPage() { render( } /> , ) } describe('ScenarioShowPage', () => { it('renders each embedded bucket with its name, type, allocation and priority', 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({ '@context': '/api/contexts/Scenario', '@id': `/api/scenarios/${SCENARIO_ID}`, '@type': 'Scenario', name: 'Emergency Fund', description: 'Rainy day savings', buckets: [ { '@id': '/api/buckets/22222222-2222-2222-2222-222222222222', '@type': 'Bucket', name: 'Rent', type: 'need', priority: 1, sortOrder: 0, allocationType: 'fixed_limit', allocationValue: 10000, startingAmount: 3000, bufferMultiplier: '0.00', }, { '@id': '/api/buckets/33333333-3333-3333-3333-333333333333', '@type': 'Bucket', name: 'Dining Out', type: 'want', priority: 2, sortOrder: 1, allocationType: 'percentage', allocationValue: 2500, startingAmount: 1200, bufferMultiplier: '0.00', }, ], }), ), // Deliberately NOT stubbing /api/buckets — MSW's onUnhandledRequest: // 'error' fails this test if the page ever issues a second fetch, // enforcing the single-GET-with-embedded-buckets contract. ) renderPage() expect(await screen.findByText('Rent')).toBeInTheDocument() expect(screen.getByText('Dining Out')).toBeInTheDocument() expect(screen.getByText(/need/i)).toBeInTheDocument() expect(screen.getByText(/want/i)).toBeInTheDocument() // Allocation: formatted per the locked display contract — fixed_limit is // dollars-with-cents from CENTS (10000 -> '$100.00'), percentage is // percent from BASIS POINTS (2500 -> '25%'). expect(screen.getByText('$100.00')).toBeInTheDocument() expect(screen.getByText('25%')).toBeInTheDocument() // Priority: assert each bucket's own priority is shown somewhere within // its own rendered content, rather than a page-wide loose number match // (which could coincidentally match allocation/other digits instead). const rentPriorityMarker = await screen.findByText((_, element) => element?.textContent === 'Priority 1', ) const diningPriorityMarker = await screen.findByText((_, element) => element?.textContent === 'Priority 2', ) 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, ) }) })