This commit is contained in:
2025-12-12 23:49:56 +01:00
parent cae81b1088
commit 8f1db21fb1
29 changed files with 879 additions and 399 deletions

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest'
import { OverlayAction } from '../../../../packages/automation/application/ports/IOverlaySyncPort'
import { OverlayAction } from '../../../../packages/automation/application/ports/OverlaySyncPort'
import { IAutomationLifecycleEmitter, LifecycleCallback } from '../../../../packages/automation/infrastructure/adapters/IAutomationLifecycleEmitter'
import { OverlaySyncService } from '../../../../packages/automation/application/services/OverlaySyncService'
@@ -11,7 +11,7 @@ class MockLifecycleEmitter implements IAutomationLifecycleEmitter {
offLifecycle(cb: LifecycleCallback): void {
this.callbacks.delete(cb)
}
async emit(event: { type: string; actionId: string; timestamp: number }) {
async emit(event: { type: 'panel-attached'|'modal-opened'|'action-started'|'action-complete'|'action-failed'|'panel-missing'; actionId: string; timestamp: number }) {
for (const cb of Array.from(this.callbacks)) {
cb(event)
}

View File

@@ -56,7 +56,7 @@ describe('CompleteRaceCreationUseCase', () => {
const state = CheckoutState.ready();
vi.mocked(mockCheckoutService.extractCheckoutInfo).mockResolvedValue(
Result.ok({ price: undefined, state, buttonHtml: '<a>n/a</a>' })
Result.ok({ price: null, state, buttonHtml: '<a>n/a</a>' })
);
const result = await useCase.execute('test-session-123');

View File

@@ -4,12 +4,12 @@ import { Result } from '@gridpilot/shared-result';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
import type { ICheckoutService } from '@gridpilot/automation/application/ports/ICheckoutService';
import type { ICheckoutConfirmationPort } from '@gridpilot/automation/application/ports/ICheckoutConfirmationPort';
import type { CheckoutServicePort } from '@gridpilot/automation/application/ports/CheckoutServicePort';
import type { CheckoutConfirmationPort } from '@gridpilot/automation/application/ports/CheckoutConfirmationPort';
describe('ConfirmCheckoutUseCase - Enhanced with Confirmation Port', () => {
let mockCheckoutService: ICheckoutService;
let mockConfirmationPort: ICheckoutConfirmationPort;
let mockCheckoutService: CheckoutServicePort;
let mockConfirmationPort: CheckoutConfirmationPort;
let useCase: ConfirmCheckoutUseCase;
beforeEach(() => {

View File

@@ -3,7 +3,7 @@ import { Result } from '../../../../packages/shared/result/Result';
import { ConfirmCheckoutUseCase } from '../../../../packages/automation/application/use-cases/ConfirmCheckoutUseCase';
import type { CheckoutServicePort } from '../../../../packages/automation/application/ports/CheckoutServicePort';
import type { CheckoutConfirmationPort } from '../../../../packages/automation/application/ports/CheckoutConfirmationPort';
import type { CheckoutInfoDTODTO } from '../../../../packages/automation/application/dto/CheckoutInfoDTODTO';
import type { CheckoutInfoDTO } from '../../../../packages/automation/application/dto/CheckoutInfoDTO';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState, CheckoutStateEnum } from '@gridpilot/automation/domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';

View File

@@ -8,10 +8,10 @@ import type { IResultRepository } from '@gridpilot/racing/domain/repositories/IR
import type { IPenaltyRepository } from '@gridpilot/racing/domain/repositories/IPenaltyRepository';
import type { IChampionshipStandingRepository } from '@gridpilot/racing/domain/repositories/IChampionshipStandingRepository';
import type { Season } from '@gridpilot/racing/domain/entities/Season';
import { Season } from '@gridpilot/racing/domain/entities/Season';
import type { LeagueScoringConfig } from '@gridpilot/racing/domain/entities/LeagueScoringConfig';
import type { Race } from '@gridpilot/racing/domain/entities/Race';
import type { Result } from '@gridpilot/racing/domain/entities/Result';
import { Race } from '@gridpilot/racing/domain/entities/Race';
import { Result } from '@gridpilot/racing/domain/entities/Result';
import type { Penalty } from '@gridpilot/racing/domain/entities/Penalty';
import type { ChampionshipStanding } from '@gridpilot/racing/domain/entities/ChampionshipStanding';
import type { ChampionshipConfig } from '@gridpilot/racing/domain/types/ChampionshipConfig';
@@ -34,6 +34,30 @@ class InMemorySeasonRepository implements ISeasonRepository {
return this.seasons.filter((s) => s.leagueId === leagueId);
}
async create(season: Season): Promise<Season> {
this.seasons.push(season);
return season;
}
async add(season: Season): Promise<void> {
this.seasons.push(season);
}
async update(season: Season): Promise<void> {
const index = this.seasons.findIndex((s) => s.id === season.id);
if (index >= 0) {
this.seasons[index] = season;
}
}
async listByLeague(leagueId: string): Promise<Season[]> {
return this.seasons.filter((s) => s.leagueId === leagueId);
}
async listActiveByLeague(leagueId: string): Promise<Season[]> {
return this.seasons.filter((s) => s.leagueId === leagueId && s.status === 'active');
}
seedSeason(season: Season): void {
this.seasons.push(season);
}
@@ -46,6 +70,16 @@ class InMemoryLeagueScoringConfigRepository implements ILeagueScoringConfigRepos
return this.configs.find((c) => c.seasonId === seasonId) || null;
}
async save(config: LeagueScoringConfig): Promise<LeagueScoringConfig> {
const index = this.configs.findIndex((c) => c.id === config.id);
if (index >= 0) {
this.configs[index] = config;
} else {
this.configs.push(config);
}
return config;
}
seedConfig(config: LeagueScoringConfig): void {
this.configs.push(config);
}
@@ -113,10 +147,60 @@ class InMemoryRaceRepository implements IRaceRepository {
class InMemoryResultRepository implements IResultRepository {
private results: Result[] = [];
async findById(id: string): Promise<Result | null> {
return this.results.find((r) => r.id === id) || null;
}
async findAll(): Promise<Result[]> {
return [...this.results];
}
async findByRaceId(raceId: string): Promise<Result[]> {
return this.results.filter((r) => r.raceId === raceId);
}
async findByDriverId(driverId: string): Promise<Result[]> {
return this.results.filter((r) => r.driverId === driverId);
}
async findByDriverIdAndLeagueId(driverId: string, leagueId: string): Promise<Result[]> {
return this.results.filter((r) => r.driverId === driverId && r.raceId.startsWith(leagueId));
}
async create(result: Result): Promise<Result> {
this.results.push(result);
return result;
}
async createMany(results: Result[]): Promise<Result[]> {
this.results.push(...results);
return results;
}
async update(result: Result): Promise<Result> {
const index = this.results.findIndex((r) => r.id === result.id);
if (index >= 0) {
this.results[index] = result;
}
return result;
}
async delete(id: string): Promise<void> {
this.results = this.results.filter((r) => r.id !== id);
}
async deleteByRaceId(raceId: string): Promise<void> {
this.results = this.results.filter((r) => r.raceId !== raceId);
}
async exists(id: string): Promise<boolean> {
return this.results.some((r) => r.id === id);
}
async existsByRaceId(raceId: string): Promise<boolean> {
return this.results.some((r) => r.raceId === raceId);
}
seedResult(result: Result): void {
this.results.push(result);
}
@@ -146,6 +230,41 @@ class InMemoryPenaltyRepository implements IPenaltyRepository {
return [...this.penalties];
}
async findById(id: string): Promise<Penalty | null> {
return this.penalties.find((p) => p.id === id) || null;
}
async findByDriverId(driverId: string): Promise<Penalty[]> {
return this.penalties.filter((p) => p.driverId === driverId);
}
async findByProtestId(protestId: string): Promise<Penalty[]> {
return this.penalties.filter((p) => p.protestId === protestId);
}
async findPending(): Promise<Penalty[]> {
return this.penalties.filter((p) => p.status === 'pending');
}
async findIssuedBy(stewardId: string): Promise<Penalty[]> {
return this.penalties.filter((p) => p.issuedBy === stewardId);
}
async create(penalty: Penalty): Promise<void> {
this.penalties.push(penalty);
}
async update(penalty: Penalty): Promise<void> {
const index = this.penalties.findIndex((p) => p.id === penalty.id);
if (index >= 0) {
this.penalties[index] = penalty;
}
}
async exists(id: string): Promise<boolean> {
return this.penalties.some((p) => p.id === id);
}
seedPenalty(penalty: Penalty): void {
this.penalties.push(penalty);
}
@@ -267,7 +386,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
championshipAggregator,
);
const season: Season = {
const season = Season.create({
id: seasonId,
leagueId,
gameId: 'iracing',
@@ -277,7 +396,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
order: 1,
startDate: new Date('2025-01-01'),
endDate: new Date('2025-12-31'),
};
});
seasonRepository.seedSeason(season);
@@ -292,7 +411,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
leagueScoringConfigRepository.seedConfig(leagueScoringConfig);
const races: Race[] = [
{
Race.create({
id: 'race-1-sprint',
leagueId,
scheduledAt: new Date('2025-02-01'),
@@ -300,8 +419,8 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
car: 'Car A',
sessionType: 'race',
status: 'completed',
},
{
}),
Race.create({
id: 'race-1-main',
leagueId,
scheduledAt: new Date('2025-02-01'),
@@ -309,8 +428,8 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
car: 'Car A',
sessionType: 'race',
status: 'completed',
},
{
}),
Race.create({
id: 'race-2-sprint',
leagueId,
scheduledAt: new Date('2025-03-01'),
@@ -318,8 +437,8 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
car: 'Car A',
sessionType: 'race',
status: 'completed',
},
{
}),
Race.create({
id: 'race-2-main',
leagueId,
scheduledAt: new Date('2025-03-01'),
@@ -327,8 +446,8 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
car: 'Car A',
sessionType: 'race',
status: 'completed',
},
{
}),
Race.create({
id: 'race-3-sprint',
leagueId,
scheduledAt: new Date('2025-04-01'),
@@ -336,8 +455,8 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
car: 'Car A',
sessionType: 'race',
status: 'completed',
},
{
}),
Race.create({
id: 'race-3-main',
leagueId,
scheduledAt: new Date('2025-04-01'),
@@ -345,7 +464,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
car: 'Car A',
sessionType: 'race',
status: 'completed',
},
}),
];
races.forEach((race) => raceRepository.seedRace(race));
@@ -392,7 +511,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
let resultIdCounter = 1;
for (const raceData of resultsData) {
raceData.finishingOrder.forEach((driverId, index) => {
const result: Result = {
const result = Result.create({
id: `result-${resultIdCounter++}`,
raceId: raceData.raceId,
driverId,
@@ -400,7 +519,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
fastestLap: driverId === raceData.fastestLapDriverId ? 90000 : 91000 + index * 100,
incidents: 0,
startPosition: index + 1,
};
});
resultRepository.seedResult(result);
});
}
@@ -423,7 +542,7 @@ describe('RecalculateChampionshipStandingsUseCase', () => {
sorted.map((r) => r.participant.id),
);
const leader = rows[0];
const leader = rows[0]!;
expect(leader.resultsCounted).toBeLessThanOrEqual(6);
expect(leader.resultsDropped).toBeGreaterThanOrEqual(0);
});

View File

@@ -1,8 +1,8 @@
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import { StartAutomationSessionUseCase } from '../../../../packages/automation/application/use-cases/StartAutomationSessionUseCase';
import { IAutomationEngine } from '../../../../packages/automation/application/ports/IAutomationEngine';
import { IScreenAutomation } from '../../../../packages/automation/application/ports/IScreenAutomation';
import { ISessionRepository } from '../../../../packages/automation/application/ports/ISessionRepository';
import { AutomationEnginePort as IAutomationEngine } from '../../../../packages/automation/application/ports/AutomationEnginePort';
import { IBrowserAutomation as IScreenAutomation } from '../../../../packages/automation/application/ports/ScreenAutomationPort';
import { SessionRepositoryPort as ISessionRepository } from '../../../../packages/automation/application/ports/SessionRepositoryPort';
import { AutomationSession } from '@gridpilot/automation/domain/entities/AutomationSession';
describe('StartAutomationSessionUseCase', () => {

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { VerifyAuthenticatedPageUseCase } from '../../../../packages/automation/application/use-cases/VerifyAuthenticatedPageUseCase';
import { IAuthenticationService } from '../../../../packages/automation/application/ports/IAuthenticationService';
import { AuthenticationServicePort as IAuthenticationService } from '../../../../packages/automation/application/ports/AuthenticationServicePort';
import { Result } from '../../../../packages/shared/result/Result';
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';