mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-08 04:06:18 -04:00
Generate deterministic template headlines server-side for each replay chapter kind, expose Chapter.headline in the race-hub payload, and render a horizontal chapter strip on the story view with active-chapter highlighting, click-to-jump, and a 90-second tour mode built on the existing scrubber playback. Co-authored-by: Cursor <cursoragent@cursor.com>
88 lines
2.8 KiB
TypeScript
88 lines
2.8 KiB
TypeScript
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||
import { fireEvent, render, screen } from '@testing-library/react'
|
||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||
import { ChapterStrip } from '../components/ChapterStrip'
|
||
import type { Chapter } from '../types'
|
||
|
||
const chapters: Chapter[] = [
|
||
{
|
||
kind: 'start',
|
||
title: 'Start',
|
||
headline: 'Lights out — the field charges into Turn 1',
|
||
start_lap: 1,
|
||
end_lap: 1,
|
||
start_time: '2025-05-25T13:00:00Z',
|
||
end_time: '2025-05-25T13:01:00Z',
|
||
driver_numbers: [],
|
||
},
|
||
{
|
||
kind: 'safety_car',
|
||
title: 'Safety Car (L12-L15)',
|
||
headline: 'Sainz incident brings out the Safety Car — leaders dive for the pits',
|
||
start_lap: 12,
|
||
end_lap: 15,
|
||
start_time: '2025-05-25T13:12:00Z',
|
||
end_time: '2025-05-25T13:15:00Z',
|
||
driver_numbers: [55],
|
||
},
|
||
]
|
||
|
||
const tMin = new Date('2025-05-25T13:00:00Z').getTime()
|
||
const tMax = new Date('2025-05-25T13:20:00Z').getTime()
|
||
const tRange = tMax - tMin
|
||
|
||
function renderStrip(
|
||
overrides: Partial<{
|
||
scrubTime: number | null
|
||
onChapterClick: (index: number, scrub: number) => void
|
||
}> = {},
|
||
) {
|
||
const onChapterClick = overrides.onChapterClick ?? vi.fn()
|
||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||
render(
|
||
<QueryClientProvider client={queryClient}>
|
||
<ChapterStrip
|
||
chapters={chapters}
|
||
scrubTime={overrides.scrubTime ?? null}
|
||
tMin={tMin}
|
||
tRange={tRange}
|
||
tourActive={false}
|
||
tourChapterIndex={null}
|
||
onChapterClick={onChapterClick}
|
||
onTourToggle={vi.fn()}
|
||
/>
|
||
</QueryClientProvider>,
|
||
)
|
||
return { onChapterClick }
|
||
}
|
||
|
||
describe('ChapterStrip', () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks()
|
||
})
|
||
|
||
it('renders chapter headlines and lap ranges', () => {
|
||
renderStrip()
|
||
expect(screen.getByTestId('chapter-strip')).toBeInTheDocument()
|
||
expect(screen.getByText('Lights out — the field charges into Turn 1')).toBeInTheDocument()
|
||
expect(screen.getByText('L12–L15')).toBeInTheDocument()
|
||
})
|
||
|
||
it('highlights the active chapter from scrub time', () => {
|
||
const scrub = (new Date('2025-05-25T13:13:00Z').getTime() - tMin) / tRange
|
||
renderStrip({ scrubTime: scrub })
|
||
expect(screen.getByTestId('chapter-card-1')).toHaveClass('active')
|
||
expect(screen.getByTestId('chapter-card-0')).not.toHaveClass('active')
|
||
})
|
||
|
||
it('calls click handler to jump scrubber', () => {
|
||
const onChapterClick = vi.fn<(index: number, scrub: number) => void>()
|
||
renderStrip({ onChapterClick })
|
||
fireEvent.click(screen.getByTestId('chapter-card-1'))
|
||
expect(onChapterClick).toHaveBeenCalledTimes(1)
|
||
const [index, scrub] = onChapterClick.mock.calls[0]
|
||
expect(index).toBe(1)
|
||
expect(scrub).toBeCloseTo(0.6, 2)
|
||
})
|
||
})
|