Refresh live sprint qualifying timing

This commit is contained in:
2026-07-03 12:43:45 -04:00
parent b7696c11de
commit 5408a45bbd
10 changed files with 585 additions and 53 deletions

View File

@@ -4,10 +4,13 @@ import {
compoundLetter,
extrapolateClock,
latestRaceControl,
liveSessionDisplay,
loadPinnedDrivers,
mergeVisibleSectors,
positionDeltaClass,
parseLiveStateEvent,
rcFlagClass,
rowsWithVisibleSectors,
savePinnedDrivers,
sortLiveTimingRows,
togglePin,
@@ -17,7 +20,8 @@ import {
tyreLabel,
windDirectionLabel,
} from '../lib/live'
import type { LiveStreamData } from '../types'
import type { LiveDriverData, LiveSectorData, LiveStreamData } from '../types'
import type { LiveTimingRow } from '../lib/live'
const snapshot: LiveStreamData = {
Drivers: {
@@ -106,6 +110,51 @@ const snapshot: LiveStreamData = {
Stints: {},
}
function sector(value: string, over: Partial<LiveSectorData> = {}): LiveSectorData {
return { Value: value, PersonalFastest: false, OverallFastest: false, ...over }
}
function timingRow(
number: string,
position: number,
driver: Partial<LiveDriverData> = {},
): LiveTimingRow {
return {
RacingNumber: number,
Position: position,
Driver: {
RacingNumber: number,
Position: position,
PrevPosition: position,
GapToLeader: '',
Interval: '',
LastLapTime: '',
LastLapPB: false,
LastLapOB: false,
BestLapTime: '',
BestLapPB: false,
BestLapOB: false,
BestLapNum: 0,
InPit: false,
PitOut: false,
Retired: false,
KnockedOut: false,
Cutoff: false,
OnFlyingLap: false,
NumberOfLaps: 0,
SpeedTrap: '',
Sectors: [],
...driver,
},
}
}
function rows(count: number, knockedOut = 0): LiveTimingRow[] {
return Array.from({ length: count }, (_, index) =>
timingRow(String(index + 1), index + 1, { KnockedOut: index >= count - knockedOut }),
)
}
describe('live transforms', () => {
it('parses live EventSource snapshots without changing PascalCase data', () => {
const parsed = parseLiveStateEvent(JSON.stringify({ is_live: true, data: snapshot }))
@@ -168,6 +217,91 @@ describe('track status mapping', () => {
})
})
describe('live qualifying display', () => {
it('puts the SQ1 cutoff after P17 for a 22-car sprint qualifying session', () => {
const display = liveSessionDisplay(
{ MeetingName: 'British Grand Prix', CircuitName: 'Silverstone', SessionType: 'Sprint Qualifying', SessionName: 'Sprint Qualifying' },
rows(22),
)
expect(display.phaseLabel).toBe('SQ1')
expect(display.cutoffPosition).toBe(17)
expect(display.advanceCount).toBe(17)
expect(display.atRiskStart).toBe(18)
expect(display.atRiskEnd).toBe(22)
})
it('keeps the normal Q1 cutoff after P15 for a 20-car qualifying session', () => {
const display = liveSessionDisplay(
{ MeetingName: 'Monaco Grand Prix', CircuitName: 'Monaco', SessionType: 'Qualifying', SessionName: 'Qualifying' },
rows(20),
)
expect(display.phaseLabel).toBe('Q1')
expect(display.cutoffPosition).toBe(15)
})
it('moves phase 2 cutoff after P10 once five cars are knocked out', () => {
const display = liveSessionDisplay(
{ MeetingName: 'Monaco Grand Prix', CircuitName: 'Monaco', SessionType: 'Qualifying', SessionName: 'Qualifying' },
rows(20, 5),
)
expect(display.phaseLabel).toBe('Q2')
expect(display.cutoffPosition).toBe(10)
})
it('shows no cutoff for race sessions or Q3', () => {
expect(
liveSessionDisplay(
{ MeetingName: 'Monaco Grand Prix', CircuitName: 'Monaco', SessionType: 'Race', SessionName: 'Race' },
rows(20),
).cutoffPosition,
).toBeNull()
expect(
liveSessionDisplay(
{ MeetingName: 'Monaco Grand Prix', CircuitName: 'Monaco', SessionType: 'Qualifying', SessionName: 'Q3' },
rows(10),
).cutoffPosition,
).toBeNull()
})
})
describe('visible sector display', () => {
it('holds S1 and S2 through temporary blanks while a flying lap is active', () => {
const first = [timingRow('4', 1, {
NumberOfLaps: 3,
OnFlyingLap: true,
Sectors: [sector('29.111'), sector('41.222'), sector('')],
})]
const held = mergeVisibleSectors({}, first)
const blank = [timingRow('4', 1, {
NumberOfLaps: 3,
OnFlyingLap: false,
Sectors: [sector(''), sector(''), sector('')],
})]
const next = mergeVisibleSectors(held, blank)
const visibleRows = rowsWithVisibleSectors(blank, next)
expect(visibleRows[0].Driver.Sectors[0].Value).toBe('29.111')
expect(visibleRows[0].Driver.Sectors[1].Value).toBe('41.222')
})
it('clears held sectors after the lap completes and the feed goes blank', () => {
const first = mergeVisibleSectors({}, [timingRow('4', 1, {
NumberOfLaps: 3,
LastLapTime: '1:30.000',
OnFlyingLap: true,
Sectors: [sector('29.111'), sector('41.222'), sector('20.333')],
})])
const next = mergeVisibleSectors(first, [timingRow('4', 1, {
NumberOfLaps: 4,
LastLapTime: '1:30.000',
OnFlyingLap: false,
Sectors: [sector(''), sector(''), sector('')],
})])
expect(next['4']).toBeUndefined()
})
})
describe('weather and stint helpers', () => {
it('maps wind direction degrees to compass points', () => {
expect(windDirectionLabel(0)).toBe('N')