diff --git a/frontend/src/pages/RacePreviewPage.tsx b/frontend/src/pages/RacePreviewPage.tsx index ca6d68a..babef61 100644 --- a/frontend/src/pages/RacePreviewPage.tsx +++ b/frontend/src/pages/RacePreviewPage.tsx @@ -58,35 +58,67 @@ function SectionState({ return <>{children} } +function circuitIdentity(meeting: Meeting): { name: string; place: string } { + const name = meeting.circuit_short_name?.trim() || meeting.meeting_name + const location = meeting.location?.trim() ?? '' + const country = meeting.country_name?.trim() ?? '' + const parts: string[] = [] + if (location) parts.push(location) + if (country && country !== location) parts.push(country) + return { name, place: parts.join(' · ') } +} + function TrackOutlineCard({ + meeting, outline, loading, error, accent, }: { + meeting: Meeting outline: TrackOutline | null | undefined loading: boolean error: Error | null accent: string }) { const outlinePath = useMemo(() => buildOutlinePath(outline?.points ?? []), [outline]) + const identity = circuitIdentity(meeting) + + let body: ReactNode + if (loading) { + body =
Loading…
+ } else if (error) { + body = ( +
+ {error instanceof Error ? error.message : 'Failed to load'} +
+ ) + } else if (outlinePath) { + body = ( +
+ + + + +
+ ) + } else { + body = ( +
+
+

{identity.name}

+ {identity.place &&

{identity.place}

} +

+ A GPS track outline is not available in the local cache. +

+
+ ) + } return (

Circuit

- -
- - - - -
-
+ {body}
) } @@ -408,6 +440,7 @@ export function RacePreviewPage() {
{ expect(screen.getByTestId('preview-header')).toHaveTextContent('Monaco') expect(screen.getByTestId('preview-schedule')).toHaveTextContent('FP1') expect(screen.getByTestId('preview-track-card')).toBeInTheDocument() + expect(screen.getByRole('img', { name: 'Circuit outline' })).toBeInTheDocument() + expect(screen.queryByTestId('preview-track-fallback')).not.toBeInTheDocument() await waitFor(() => { expect(screen.getByTestId('preview-last-year-card')).toHaveTextContent('VER') @@ -273,5 +275,75 @@ describe('RacePreviewPage', () => { await waitFor(() => { expect(screen.getByTestId('preview-last-year-card')).toHaveTextContent('First time on the calendar') }) + + const fallback = screen.getByTestId('preview-track-fallback') + expect(screen.getByTestId('preview-track-card')).toContainElement(fallback) + expect(fallback).toHaveTextContent('Monaco') + expect(fallback).toHaveTextContent('A GPS track outline is not available in the local cache.') + expect(screen.queryByRole('img', { name: 'Circuit outline' })).not.toBeInTheDocument() + }) + + it('shows a designed circuit fallback when the track outline is unavailable', async () => { + const italianGp: Meeting = { + ...upcomingMeeting, + meeting_name: 'Italian Grand Prix', + meeting_official_name: 'Formula 1 Pirelli Gran Premio d\'Italia', + location: 'Monza', + country_name: 'Italy', + country_code: 'ITA', + circuit_short_name: 'Monza', + } + + mockFetchMeetings.mockImplementation(async (year: number) => { + if (year === 2099) return [italianGp] + if (year === 2098) return [{ ...italianGp, meeting_key: 90, year: 2098 }] + return [] + }) + mockFetchSessions.mockImplementation(async (meetingKey: number) => { + if (meetingKey === 100) return sessions + if (meetingKey === 90) return [priorRaceSession] + return [] + }) + mockFetchTrackOutline.mockResolvedValue(null) + + renderPage() + + await waitFor(() => { + expect(screen.getByTestId('preview-track-fallback')).toBeInTheDocument() + }) + + const card = screen.getByTestId('preview-track-card') + const fallback = screen.getByTestId('preview-track-fallback') + expect(card).toContainElement(fallback) + expect(card).toHaveTextContent('Circuit') + expect(fallback).toHaveTextContent('Monza') + expect(fallback).toHaveTextContent('Italy') + expect(fallback).toHaveTextContent('A GPS track outline is not available in the local cache.') + expect(fallback).not.toHaveTextContent(/unknown/i) + expect(screen.queryByRole('img', { name: 'Circuit outline' })).not.toBeInTheDocument() + expect(card).not.toHaveTextContent('Track outline unavailable for this circuit') + }) + + it('keeps a thrown outline request distinct from the designed unavailable state', async () => { + mockFetchMeetings.mockImplementation(async (year: number) => { + if (year === 2099) return [upcomingMeeting] + if (year === 2098) return [priorMeeting] + return [] + }) + mockFetchSessions.mockImplementation(async (meetingKey: number) => { + if (meetingKey === 100) return sessions + if (meetingKey === 90) return [priorRaceSession] + return [] + }) + mockFetchTrackOutline.mockRejectedValue(new Error('outline request failed')) + + renderPage() + + await waitFor(() => { + expect(screen.getByTestId('preview-track-card')).toHaveTextContent('outline request failed') + }) + + expect(screen.queryByTestId('preview-track-fallback')).not.toBeInTheDocument() + expect(screen.queryByRole('img', { name: 'Circuit outline' })).not.toBeInTheDocument() }) })