core tests
This commit is contained in:
75
core/racing/domain/services/ChampionshipAggregator.test.ts
Normal file
75
core/racing/domain/services/ChampionshipAggregator.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ChampionshipAggregator } from './ChampionshipAggregator';
|
||||
import type { DropScoreApplier } from './DropScoreApplier';
|
||||
import { Points } from '../value-objects/Points';
|
||||
|
||||
describe('ChampionshipAggregator', () => {
|
||||
const mockDropScoreApplier = {
|
||||
apply: vi.fn(),
|
||||
} as unknown as DropScoreApplier;
|
||||
|
||||
const aggregator = new ChampionshipAggregator(mockDropScoreApplier);
|
||||
|
||||
it('should aggregate points and sort standings by total points', () => {
|
||||
const seasonId = 'season-1';
|
||||
const championship = {
|
||||
id: 'champ-1',
|
||||
dropScorePolicy: { strategy: 'none' },
|
||||
} as any;
|
||||
|
||||
const eventPointsByEventId = {
|
||||
'event-1': [
|
||||
{
|
||||
participant: { id: 'p1', type: 'driver' },
|
||||
totalPoints: 10,
|
||||
basePoints: 10,
|
||||
bonusPoints: 0,
|
||||
penaltyPoints: 0
|
||||
},
|
||||
{
|
||||
participant: { id: 'p2', type: 'driver' },
|
||||
totalPoints: 20,
|
||||
basePoints: 20,
|
||||
bonusPoints: 0,
|
||||
penaltyPoints: 0
|
||||
},
|
||||
],
|
||||
'event-2': [
|
||||
{
|
||||
participant: { id: 'p1', type: 'driver' },
|
||||
totalPoints: 15,
|
||||
basePoints: 15,
|
||||
bonusPoints: 0,
|
||||
penaltyPoints: 0
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
vi.mocked(mockDropScoreApplier.apply).mockImplementation((policy, events) => {
|
||||
const total = events.reduce((sum, e) => sum + e.points, 0);
|
||||
return {
|
||||
totalPoints: total,
|
||||
counted: events,
|
||||
dropped: [],
|
||||
};
|
||||
});
|
||||
|
||||
const standings = aggregator.aggregate({
|
||||
seasonId,
|
||||
championship,
|
||||
eventPointsByEventId,
|
||||
});
|
||||
|
||||
expect(standings).toHaveLength(2);
|
||||
|
||||
// p1 should be first (10 + 15 = 25 points)
|
||||
expect(standings[0].participant.id).toBe('p1');
|
||||
expect(standings[0].totalPoints.toNumber()).toBe(25);
|
||||
expect(standings[0].position.toNumber()).toBe(1);
|
||||
|
||||
// p2 should be second (20 points)
|
||||
expect(standings[1].participant.id).toBe('p2');
|
||||
expect(standings[1].totalPoints.toNumber()).toBe(20);
|
||||
expect(standings[1].position.toNumber()).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -59,7 +59,7 @@ export class ChampionshipAggregator {
|
||||
totalPoints,
|
||||
resultsCounted,
|
||||
resultsDropped,
|
||||
position: 0,
|
||||
position: 1,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
74
core/racing/domain/services/SeasonScheduleGenerator.test.ts
Normal file
74
core/racing/domain/services/SeasonScheduleGenerator.test.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SeasonScheduleGenerator } from './SeasonScheduleGenerator';
|
||||
import { SeasonSchedule } from '../value-objects/SeasonSchedule';
|
||||
import { RecurrenceStrategy } from '../value-objects/RecurrenceStrategy';
|
||||
import { RaceTimeOfDay } from '../value-objects/RaceTimeOfDay';
|
||||
import { WeekdaySet } from '../value-objects/WeekdaySet';
|
||||
import { LeagueTimezone } from '../value-objects/LeagueTimezone';
|
||||
import { MonthlyRecurrencePattern } from '../value-objects/MonthlyRecurrencePattern';
|
||||
|
||||
describe('SeasonScheduleGenerator', () => {
|
||||
it('should generate weekly slots', () => {
|
||||
const startDate = new Date(2024, 0, 1); // Monday, Jan 1st 2024
|
||||
const schedule = new SeasonSchedule({
|
||||
startDate,
|
||||
plannedRounds: 4,
|
||||
timeOfDay: new RaceTimeOfDay(20, 0),
|
||||
timezone: LeagueTimezone.create('UTC'),
|
||||
recurrence: RecurrenceStrategy.weekly(WeekdaySet.fromArray(['Mon'])),
|
||||
});
|
||||
|
||||
const slots = SeasonScheduleGenerator.generateSlots(schedule);
|
||||
|
||||
expect(slots).toHaveLength(4);
|
||||
expect(slots[0].roundNumber).toBe(1);
|
||||
expect(slots[0].scheduledAt.getHours()).toBe(20);
|
||||
expect(slots[0].scheduledAt.getMinutes()).toBe(0);
|
||||
expect(slots[0].scheduledAt.getFullYear()).toBe(2024);
|
||||
expect(slots[0].scheduledAt.getMonth()).toBe(0);
|
||||
expect(slots[0].scheduledAt.getDate()).toBe(1);
|
||||
|
||||
expect(slots[1].roundNumber).toBe(2);
|
||||
expect(slots[1].scheduledAt.getDate()).toBe(8);
|
||||
expect(slots[2].roundNumber).toBe(3);
|
||||
expect(slots[2].scheduledAt.getDate()).toBe(15);
|
||||
expect(slots[3].roundNumber).toBe(4);
|
||||
expect(slots[3].scheduledAt.getDate()).toBe(22);
|
||||
});
|
||||
|
||||
it('should generate slots every 2 weeks', () => {
|
||||
const startDate = new Date(2024, 0, 1);
|
||||
const schedule = new SeasonSchedule({
|
||||
startDate,
|
||||
plannedRounds: 2,
|
||||
timeOfDay: new RaceTimeOfDay(20, 0),
|
||||
timezone: LeagueTimezone.create('UTC'),
|
||||
recurrence: RecurrenceStrategy.everyNWeeks(2, WeekdaySet.fromArray(['Mon'])),
|
||||
});
|
||||
|
||||
const slots = SeasonScheduleGenerator.generateSlots(schedule);
|
||||
|
||||
expect(slots).toHaveLength(2);
|
||||
expect(slots[0].scheduledAt.getDate()).toBe(1);
|
||||
expect(slots[1].scheduledAt.getDate()).toBe(15);
|
||||
});
|
||||
|
||||
it('should generate monthly slots (nth weekday)', () => {
|
||||
const startDate = new Date(2024, 0, 1);
|
||||
const schedule = new SeasonSchedule({
|
||||
startDate,
|
||||
plannedRounds: 2,
|
||||
timeOfDay: new RaceTimeOfDay(20, 0),
|
||||
timezone: LeagueTimezone.create('UTC'),
|
||||
recurrence: RecurrenceStrategy.monthlyNthWeekday(MonthlyRecurrencePattern.create(1, 'Mon')),
|
||||
});
|
||||
|
||||
const slots = SeasonScheduleGenerator.generateSlots(schedule);
|
||||
|
||||
expect(slots).toHaveLength(2);
|
||||
expect(slots[0].scheduledAt.getMonth()).toBe(0);
|
||||
expect(slots[0].scheduledAt.getDate()).toBe(1);
|
||||
expect(slots[1].scheduledAt.getMonth()).toBe(1);
|
||||
expect(slots[1].scheduledAt.getDate()).toBe(5);
|
||||
});
|
||||
});
|
||||
50
core/racing/domain/services/SkillLevelService.test.ts
Normal file
50
core/racing/domain/services/SkillLevelService.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { SkillLevelService } from './SkillLevelService';
|
||||
|
||||
describe('SkillLevelService', () => {
|
||||
describe('getSkillLevel', () => {
|
||||
it('should return pro for rating >= 3000', () => {
|
||||
expect(SkillLevelService.getSkillLevel(3000)).toBe('pro');
|
||||
expect(SkillLevelService.getSkillLevel(5000)).toBe('pro');
|
||||
});
|
||||
|
||||
it('should return advanced for rating >= 2500 and < 3000', () => {
|
||||
expect(SkillLevelService.getSkillLevel(2500)).toBe('advanced');
|
||||
expect(SkillLevelService.getSkillLevel(2999)).toBe('advanced');
|
||||
});
|
||||
|
||||
it('should return intermediate for rating >= 1800 and < 2500', () => {
|
||||
expect(SkillLevelService.getSkillLevel(1800)).toBe('intermediate');
|
||||
expect(SkillLevelService.getSkillLevel(2499)).toBe('intermediate');
|
||||
});
|
||||
|
||||
it('should return beginner for rating < 1800', () => {
|
||||
expect(SkillLevelService.getSkillLevel(1799)).toBe('beginner');
|
||||
expect(SkillLevelService.getSkillLevel(500)).toBe('beginner');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTeamPerformanceLevel', () => {
|
||||
it('should return beginner for null rating', () => {
|
||||
expect(SkillLevelService.getTeamPerformanceLevel(null)).toBe('beginner');
|
||||
});
|
||||
|
||||
it('should return pro for rating >= 4500', () => {
|
||||
expect(SkillLevelService.getTeamPerformanceLevel(4500)).toBe('pro');
|
||||
});
|
||||
|
||||
it('should return advanced for rating >= 3000 and < 4500', () => {
|
||||
expect(SkillLevelService.getTeamPerformanceLevel(3000)).toBe('advanced');
|
||||
expect(SkillLevelService.getTeamPerformanceLevel(4499)).toBe('advanced');
|
||||
});
|
||||
|
||||
it('should return intermediate for rating >= 2000 and < 3000', () => {
|
||||
expect(SkillLevelService.getTeamPerformanceLevel(2000)).toBe('intermediate');
|
||||
expect(SkillLevelService.getTeamPerformanceLevel(2999)).toBe('intermediate');
|
||||
});
|
||||
|
||||
it('should return beginner for rating < 2000', () => {
|
||||
expect(SkillLevelService.getTeamPerformanceLevel(1999)).toBe('beginner');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { AverageStrengthOfFieldCalculator } from './StrengthOfFieldCalculator';
|
||||
|
||||
describe('AverageStrengthOfFieldCalculator', () => {
|
||||
const calculator = new AverageStrengthOfFieldCalculator();
|
||||
|
||||
it('should calculate average SOF and round it', () => {
|
||||
const ratings = [
|
||||
{ driverId: 'd1', rating: 1500 },
|
||||
{ driverId: 'd2', rating: 2000 },
|
||||
{ driverId: 'd3', rating: 1750 },
|
||||
];
|
||||
|
||||
const sof = calculator.calculate(ratings);
|
||||
|
||||
expect(sof).toBe(1750);
|
||||
});
|
||||
|
||||
it('should handle rounding correctly', () => {
|
||||
const ratings = [
|
||||
{ driverId: 'd1', rating: 1000 },
|
||||
{ driverId: 'd2', rating: 1001 },
|
||||
];
|
||||
|
||||
const sof = calculator.calculate(ratings);
|
||||
|
||||
expect(sof).toBe(1001); // (1000 + 1001) / 2 = 1000.5 -> 1001
|
||||
});
|
||||
|
||||
it('should return null for empty ratings', () => {
|
||||
expect(calculator.calculate([])).toBeNull();
|
||||
});
|
||||
|
||||
it('should filter out non-positive ratings', () => {
|
||||
const ratings = [
|
||||
{ driverId: 'd1', rating: 1500 },
|
||||
{ driverId: 'd2', rating: 0 },
|
||||
{ driverId: 'd3', rating: -100 },
|
||||
];
|
||||
|
||||
const sof = calculator.calculate(ratings);
|
||||
|
||||
expect(sof).toBe(1500);
|
||||
});
|
||||
|
||||
it('should return null if all ratings are non-positive', () => {
|
||||
const ratings = [
|
||||
{ driverId: 'd1', rating: 0 },
|
||||
{ driverId: 'd2', rating: -500 },
|
||||
];
|
||||
|
||||
expect(calculator.calculate(ratings)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user