Files
gridpilot.gg/apps/website/lib/builders/view-data/RaceStewardingViewDataBuilder.test.ts
Marc Mintel 108cfbcd65
Some checks failed
Contract Testing / contract-tests (pull_request) Failing after 5m55s
Contract Testing / contract-snapshot (pull_request) Has been skipped
view data tests
2026-01-22 18:22:08 +01:00

842 lines
24 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { RaceStewardingViewDataBuilder } from './RaceStewardingViewDataBuilder';
describe('RaceStewardingViewDataBuilder', () => {
describe('happy paths', () => {
it('should transform API DTO to RaceStewardingViewData correctly', () => {
const apiDto = {
race: {
id: 'race-123',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-456',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: 'Penalty applied',
},
],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-5',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
},
pendingCount: 1,
resolvedCount: 1,
penaltiesCount: 1,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result).toEqual({
race: {
id: 'race-123',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-456',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: 'Penalty applied',
},
],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-5',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
},
pendingCount: 1,
resolvedCount: 1,
penaltiesCount: 1,
});
});
it('should handle empty protests and penalties', () => {
const apiDto = {
race: {
id: 'race-456',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-789',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingProtests).toHaveLength(0);
expect(result.resolvedProtests).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.pendingCount).toBe(0);
expect(result.resolvedCount).toBe(0);
expect(result.penaltiesCount).toBe(0);
});
it('should handle multiple protests and penalties', () => {
const apiDto = {
race: {
id: 'race-789',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-101',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [
{
id: 'protest-3',
protestingDriverId: 'driver-5',
accusedDriverId: 'driver-6',
incident: {
lap: 15,
description: 'Contact at turn 7',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: 'Penalty applied',
},
],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-7',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
{
id: 'penalty-2',
driverId: 'driver-8',
type: 'grid_penalty',
value: 3,
reason: 'Qualifying infringement',
notes: null,
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
'driver-6': { id: 'driver-6', name: 'Driver 6' },
'driver-7': { id: 'driver-7', name: 'Driver 7' },
'driver-8': { id: 'driver-8', name: 'Driver 8' },
},
pendingCount: 2,
resolvedCount: 1,
penaltiesCount: 2,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingProtests).toHaveLength(2);
expect(result.resolvedProtests).toHaveLength(1);
expect(result.penalties).toHaveLength(2);
});
});
describe('data transformation', () => {
it('should preserve all DTO fields in the output', () => {
const apiDto = {
race: {
id: 'race-102',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-103',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [],
penalties: [],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
},
pendingCount: 1,
resolvedCount: 0,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.race?.id).toBe(apiDto.race.id);
expect(result.race?.track).toBe(apiDto.race.track);
expect(result.race?.scheduledAt).toBe(apiDto.race.scheduledAt);
expect(result.league?.id).toBe(apiDto.league.id);
expect(result.pendingCount).toBe(apiDto.pendingCount);
expect(result.resolvedCount).toBe(apiDto.resolvedCount);
expect(result.penaltiesCount).toBe(apiDto.penaltiesCount);
});
it('should not modify the input DTO', () => {
const apiDto = {
race: {
id: 'race-104',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-105',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 0,
};
const originalDto = { ...apiDto };
RaceStewardingViewDataBuilder.build(apiDto);
expect(apiDto).toEqual(originalDto);
});
});
describe('edge cases', () => {
it('should handle null API DTO', () => {
const result = RaceStewardingViewDataBuilder.build(null);
expect(result.race).toBeNull();
expect(result.league).toBeNull();
expect(result.pendingProtests).toHaveLength(0);
expect(result.resolvedProtests).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.driverMap).toEqual({});
expect(result.pendingCount).toBe(0);
expect(result.resolvedCount).toBe(0);
expect(result.penaltiesCount).toBe(0);
});
it('should handle undefined API DTO', () => {
const result = RaceStewardingViewDataBuilder.build(undefined);
expect(result.race).toBeNull();
expect(result.league).toBeNull();
expect(result.pendingProtests).toHaveLength(0);
expect(result.resolvedProtests).toHaveLength(0);
expect(result.penalties).toHaveLength(0);
expect(result.driverMap).toEqual({});
expect(result.pendingCount).toBe(0);
expect(result.resolvedCount).toBe(0);
expect(result.penaltiesCount).toBe(0);
});
it('should handle race without league', () => {
const apiDto = {
race: {
id: 'race-106',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.league).toBeNull();
});
it('should handle protests without proof video', () => {
const apiDto = {
race: {
id: 'race-107',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-108',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: null,
decisionNotes: null,
},
],
resolvedProtests: [],
penalties: [],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
},
pendingCount: 1,
resolvedCount: 0,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingProtests[0].proofVideoUrl).toBeNull();
});
it('should handle protests without decision notes', () => {
const apiDto = {
race: {
id: 'race-109',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-110',
},
pendingProtests: [],
resolvedProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
penalties: [],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
},
pendingCount: 0,
resolvedCount: 1,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.resolvedProtests[0].decisionNotes).toBeNull();
});
it('should handle penalties without notes', () => {
const apiDto = {
race: {
id: 'race-111',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-112',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-1',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: null,
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 1,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.penalties[0].notes).toBeNull();
});
it('should handle penalties without value', () => {
const apiDto = {
race: {
id: 'race-113',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-114',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-1',
type: 'disqualification',
value: null,
reason: 'Technical infringement',
notes: null,
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 1,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.penalties[0].value).toBe(0);
});
it('should handle penalties without reason', () => {
const apiDto = {
race: {
id: 'race-115',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-116',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-1',
type: 'warning',
value: 0,
reason: null,
notes: null,
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 1,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.penalties[0].reason).toBe('');
});
it('should handle different protest statuses', () => {
const apiDto = {
race: {
id: 'race-117',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-118',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: 'Penalty applied',
},
{
id: 'protest-3',
protestingDriverId: 'driver-5',
accusedDriverId: 'driver-6',
incident: {
lap: 15,
description: 'Contact at turn 7',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'rejected',
proofVideoUrl: 'video-url',
decisionNotes: 'Insufficient evidence',
},
],
penalties: [],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
'driver-6': { id: 'driver-6', name: 'Driver 6' },
},
pendingCount: 1,
resolvedCount: 2,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingProtests[0].status).toBe('pending');
expect(result.resolvedProtests[0].status).toBe('resolved');
expect(result.resolvedProtests[1].status).toBe('rejected');
});
it('should handle different penalty types', () => {
const apiDto = {
race: {
id: 'race-119',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-120',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-1',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
{
id: 'penalty-2',
driverId: 'driver-2',
type: 'grid_penalty',
value: 3,
reason: 'Qualifying infringement',
notes: null,
},
{
id: 'penalty-3',
driverId: 'driver-3',
type: 'points_deduction',
value: 10,
reason: 'Dangerous driving',
notes: null,
},
{
id: 'penalty-4',
driverId: 'driver-4',
type: 'disqualification',
value: 0,
reason: 'Technical infringement',
notes: null,
},
{
id: 'penalty-5',
driverId: 'driver-5',
type: 'warning',
value: 0,
reason: 'Minor infraction',
notes: null,
},
{
id: 'penalty-6',
driverId: 'driver-6',
type: 'license_points',
value: 2,
reason: 'Multiple incidents',
notes: null,
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
'driver-6': { id: 'driver-6', name: 'Driver 6' },
},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 6,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.penalties[0].type).toBe('time_penalty');
expect(result.penalties[1].type).toBe('grid_penalty');
expect(result.penalties[2].type).toBe('points_deduction');
expect(result.penalties[3].type).toBe('disqualification');
expect(result.penalties[4].type).toBe('warning');
expect(result.penalties[5].type).toBe('license_points');
});
it('should handle empty driver map', () => {
const apiDto = {
race: {
id: 'race-121',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-122',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 0,
resolvedCount: 0,
penaltiesCount: 0,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.driverMap).toEqual({});
});
it('should handle count values from DTO', () => {
const apiDto = {
race: {
id: 'race-123',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-124',
},
pendingProtests: [],
resolvedProtests: [],
penalties: [],
driverMap: {},
pendingCount: 5,
resolvedCount: 10,
penaltiesCount: 3,
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingCount).toBe(5);
expect(result.resolvedCount).toBe(10);
expect(result.penaltiesCount).toBe(3);
});
it('should calculate counts from arrays when not provided', () => {
const apiDto = {
race: {
id: 'race-125',
track: 'Test Track',
scheduledAt: '2024-01-01T10:00:00Z',
},
league: {
id: 'league-126',
},
pendingProtests: [
{
id: 'protest-1',
protestingDriverId: 'driver-1',
accusedDriverId: 'driver-2',
incident: {
lap: 5,
description: 'Contact at turn 3',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'pending',
proofVideoUrl: 'video-url',
decisionNotes: null,
},
],
resolvedProtests: [
{
id: 'protest-2',
protestingDriverId: 'driver-3',
accusedDriverId: 'driver-4',
incident: {
lap: 10,
description: 'Contact at turn 5',
},
filedAt: '2024-01-01T10:00:00Z',
status: 'resolved',
proofVideoUrl: 'video-url',
decisionNotes: 'Penalty applied',
},
],
penalties: [
{
id: 'penalty-1',
driverId: 'driver-5',
type: 'time_penalty',
value: 5,
reason: 'Track limits',
notes: 'Warning issued',
},
],
driverMap: {
'driver-1': { id: 'driver-1', name: 'Driver 1' },
'driver-2': { id: 'driver-2', name: 'Driver 2' },
'driver-3': { id: 'driver-3', name: 'Driver 3' },
'driver-4': { id: 'driver-4', name: 'Driver 4' },
'driver-5': { id: 'driver-5', name: 'Driver 5' },
},
};
const result = RaceStewardingViewDataBuilder.build(apiDto);
expect(result.pendingCount).toBe(1);
expect(result.resolvedCount).toBe(1);
expect(result.penaltiesCount).toBe(1);
});
});
});