mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-08 04:06:18 -04:00
feat(championship): surface teammate H2H bars on drivers view (#23)
Add reusable TeammateH2H component and teammatePairs helper that ranks intra-team battles by closeness, using existing hub teammate_wins data. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -150,4 +150,61 @@ describe('ChampionshipPage', () => {
|
||||
})
|
||||
expect(screen.getByText('No championship data')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders teammate battles section ordered by closest split', async () => {
|
||||
const h2hDrivers: ChampHubDriver[] = [
|
||||
driver({ driver_number: 1, name_acronym: 'VER', team_name: 'Red Bull', points: 200, position: 1 }),
|
||||
driver({
|
||||
driver_number: 4,
|
||||
name_acronym: 'NOR',
|
||||
full_name: 'Lando Norris',
|
||||
team_name: 'McLaren',
|
||||
team_colour: 'ff8000',
|
||||
points: 160,
|
||||
position: 2,
|
||||
teammate_wins: 6,
|
||||
teammate_losses: 5,
|
||||
cumulative: [18, 36, 54, 80, 120, 160],
|
||||
}),
|
||||
driver({
|
||||
driver_number: 81,
|
||||
name_acronym: 'PIA',
|
||||
full_name: 'Oscar Piastri',
|
||||
team_name: 'McLaren',
|
||||
team_colour: 'ff8000',
|
||||
points: 140,
|
||||
position: 3,
|
||||
teammate_wins: 5,
|
||||
teammate_losses: 6,
|
||||
cumulative: [12, 28, 45, 70, 110, 140],
|
||||
}),
|
||||
driver({
|
||||
driver_number: 11,
|
||||
name_acronym: 'PER',
|
||||
full_name: 'Sergio Perez',
|
||||
team_name: 'Red Bull',
|
||||
team_colour: '3671c6',
|
||||
points: 60,
|
||||
position: 4,
|
||||
teammate_wins: 1,
|
||||
teammate_losses: 9,
|
||||
cumulative: [5, 12, 20, 35, 50, 60],
|
||||
}),
|
||||
]
|
||||
mockFetchHub.mockResolvedValue({ ...hub, drivers: h2hDrivers })
|
||||
renderPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('champ-teammate-battles')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(screen.getByText('Teammate battles')).toBeInTheDocument()
|
||||
const rows = screen.getAllByTestId('teammate-h2h')
|
||||
expect(rows).toHaveLength(2)
|
||||
// McLaren 6–5 is closer than Red Bull 9–1 — McLaren row first.
|
||||
expect(rows[0]).toHaveTextContent('McLaren')
|
||||
expect(rows[0]).toHaveTextContent('6–5')
|
||||
expect(rows[1]).toHaveTextContent('Red Bull')
|
||||
expect(rows[1]).toHaveTextContent('9–1')
|
||||
})
|
||||
})
|
||||
|
||||
54
frontend/src/test/TeammateH2H.test.tsx
Normal file
54
frontend/src/test/TeammateH2H.test.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { TeammateH2H } from '../components/TeammateH2H'
|
||||
|
||||
describe('TeammateH2H', () => {
|
||||
it('renders score and TLAs', () => {
|
||||
render(
|
||||
<TeammateH2H
|
||||
teamName="Red Bull"
|
||||
teamColour="3671c6"
|
||||
driverATla="VER"
|
||||
driverBTla="PER"
|
||||
winsA={9}
|
||||
winsB={1}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('VER')).toBeInTheDocument()
|
||||
expect(screen.getByText('PER')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('h2h-score')).toHaveTextContent('9–1')
|
||||
expect(screen.getByText('Red Bull')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('sets proportional bar segment widths from wins', () => {
|
||||
render(
|
||||
<TeammateH2H
|
||||
teamName="McLaren"
|
||||
teamColour="ff8000"
|
||||
driverATla="NOR"
|
||||
driverBTla="PIA"
|
||||
winsA={6}
|
||||
winsB={4}
|
||||
/>,
|
||||
)
|
||||
const barA = screen.getByTestId('h2h-bar-a')
|
||||
const barB = screen.getByTestId('h2h-bar-b')
|
||||
expect(barA).toHaveStyle({ width: '60%' })
|
||||
expect(barB).toHaveStyle({ width: '40%' })
|
||||
})
|
||||
|
||||
it('shows extra driver note when provided', () => {
|
||||
render(
|
||||
<TeammateH2H
|
||||
teamName="Red Bull"
|
||||
teamColour="3671c6"
|
||||
driverATla="VER"
|
||||
driverBTla="PER"
|
||||
winsA={5}
|
||||
winsB={3}
|
||||
extraNote="+1"
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('+1')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
132
frontend/src/test/h2h.test.ts
Normal file
132
frontend/src/test/h2h.test.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { teammatePairs } from '../lib/h2h'
|
||||
import type { ChampHubDriver } from '../types'
|
||||
|
||||
function driver(over: Partial<ChampHubDriver>): ChampHubDriver {
|
||||
return {
|
||||
driver_number: 1,
|
||||
name_acronym: 'VER',
|
||||
full_name: 'Max Verstappen',
|
||||
team_name: 'Red Bull',
|
||||
team_colour: '3671c6',
|
||||
points: 200,
|
||||
position: 1,
|
||||
wins: 5,
|
||||
podiums: 8,
|
||||
poles: 4,
|
||||
form: [],
|
||||
cumulative: [],
|
||||
teammate_wins: 9,
|
||||
teammate_losses: 1,
|
||||
...over,
|
||||
}
|
||||
}
|
||||
|
||||
describe('teammatePairs', () => {
|
||||
it('returns empty for no drivers', () => {
|
||||
expect(teammatePairs([])).toEqual([])
|
||||
})
|
||||
|
||||
it('skips teams with a single driver', () => {
|
||||
const drivers = [
|
||||
driver({ driver_number: 1, team_name: 'Red Bull' }),
|
||||
driver({ driver_number: 44, name_acronym: 'HAM', team_name: 'Mercedes', points: 50 }),
|
||||
]
|
||||
expect(teammatePairs(drivers)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('pairs two-driver teams', () => {
|
||||
const drivers = [
|
||||
driver({ driver_number: 1, name_acronym: 'VER', points: 200, teammate_wins: 9, teammate_losses: 1 }),
|
||||
driver({
|
||||
driver_number: 11,
|
||||
name_acronym: 'PER',
|
||||
full_name: 'Sergio Perez',
|
||||
points: 60,
|
||||
teammate_wins: 1,
|
||||
teammate_losses: 9,
|
||||
}),
|
||||
]
|
||||
const pairs = teammatePairs(drivers)
|
||||
expect(pairs).toHaveLength(1)
|
||||
expect(pairs[0].teamName).toBe('Red Bull')
|
||||
expect(pairs[0].driverA.name_acronym).toBe('VER')
|
||||
expect(pairs[0].driverB.name_acronym).toBe('PER')
|
||||
expect(pairs[0].extraCount).toBe(0)
|
||||
expect(pairs[0].closeness).toBe(8)
|
||||
})
|
||||
|
||||
it('uses top two by points for teams with 3+ drivers', () => {
|
||||
const drivers = [
|
||||
driver({ driver_number: 1, name_acronym: 'VER', team_name: 'Red Bull', points: 200 }),
|
||||
driver({
|
||||
driver_number: 11,
|
||||
name_acronym: 'PER',
|
||||
team_name: 'Red Bull',
|
||||
points: 60,
|
||||
teammate_wins: 1,
|
||||
teammate_losses: 9,
|
||||
}),
|
||||
driver({
|
||||
driver_number: 99,
|
||||
name_acronym: 'LAW',
|
||||
full_name: 'Liam Lawson',
|
||||
team_name: 'Red Bull',
|
||||
points: 10,
|
||||
teammate_wins: 0,
|
||||
teammate_losses: 0,
|
||||
}),
|
||||
]
|
||||
const pairs = teammatePairs(drivers)
|
||||
expect(pairs).toHaveLength(1)
|
||||
expect(pairs[0].driverA.name_acronym).toBe('VER')
|
||||
expect(pairs[0].driverB.name_acronym).toBe('PER')
|
||||
expect(pairs[0].extraCount).toBe(1)
|
||||
})
|
||||
|
||||
it('sorts by closest battle first', () => {
|
||||
const drivers = [
|
||||
driver({
|
||||
driver_number: 1,
|
||||
name_acronym: 'VER',
|
||||
team_name: 'Red Bull',
|
||||
points: 200,
|
||||
teammate_wins: 10,
|
||||
teammate_losses: 0,
|
||||
}),
|
||||
driver({
|
||||
driver_number: 11,
|
||||
name_acronym: 'PER',
|
||||
team_name: 'Red Bull',
|
||||
points: 60,
|
||||
teammate_wins: 0,
|
||||
teammate_losses: 10,
|
||||
}),
|
||||
driver({
|
||||
driver_number: 4,
|
||||
name_acronym: 'NOR',
|
||||
team_name: 'McLaren',
|
||||
team_colour: 'ff8000',
|
||||
points: 160,
|
||||
teammate_wins: 6,
|
||||
teammate_losses: 5,
|
||||
}),
|
||||
driver({
|
||||
driver_number: 81,
|
||||
name_acronym: 'PIA',
|
||||
full_name: 'Oscar Piastri',
|
||||
team_name: 'McLaren',
|
||||
team_colour: 'ff8000',
|
||||
points: 140,
|
||||
teammate_wins: 5,
|
||||
teammate_losses: 6,
|
||||
}),
|
||||
]
|
||||
const pairs = teammatePairs(drivers)
|
||||
expect(pairs).toHaveLength(2)
|
||||
expect(pairs[0].teamName).toBe('McLaren')
|
||||
expect(pairs[0].closeness).toBe(1)
|
||||
expect(pairs[1].teamName).toBe('Red Bull')
|
||||
expect(pairs[1].closeness).toBe(10)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user