view models

This commit is contained in:
2025-12-18 13:48:35 +01:00
parent cc2553876a
commit 91adbb9c83
71 changed files with 3119 additions and 359 deletions

View File

@@ -0,0 +1,149 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { RaceResultsService } from './RaceResultsService';
import { RacesApiClient } from '../../api/races/RacesApiClient';
import { RaceResultsDetailViewModel } from '../../view-models/RaceResultsDetailViewModel';
import { RaceWithSOFViewModel } from '../../view-models/RaceWithSOFViewModel';
import { ImportRaceResultsSummaryViewModel } from '../../view-models/ImportRaceResultsSummaryViewModel';
import type { RaceResultsDetailDTO, RaceWithSOFDTO } from '../../types/generated';
describe('RaceResultsService', () => {
let mockApiClient: Mocked<RacesApiClient>;
let service: RaceResultsService;
beforeEach(() => {
mockApiClient = {
getResultsDetail: vi.fn(),
getWithSOF: vi.fn(),
importResults: vi.fn(),
} as Mocked<RacesApiClient>;
service = new RaceResultsService(mockApiClient);
});
describe('getResultsDetail', () => {
it('should call apiClient.getResultsDetail and return RaceResultsDetailViewModel', async () => {
const raceId = 'race-123';
const currentUserId = 'user-456';
const mockDto: RaceResultsDetailDTO = {
raceId,
track: 'Test Track',
};
mockApiClient.getResultsDetail.mockResolvedValue(mockDto);
const result = await service.getResultsDetail(raceId, currentUserId);
expect(mockApiClient.getResultsDetail).toHaveBeenCalledWith(raceId);
expect(result).toBeInstanceOf(RaceResultsDetailViewModel);
expect(result.raceId).toBe(raceId);
expect(result.track).toBe('Test Track');
});
it('should handle undefined currentUserId', async () => {
const raceId = 'race-123';
const mockDto: RaceResultsDetailDTO = {
raceId,
track: 'Test Track',
};
mockApiClient.getResultsDetail.mockResolvedValue(mockDto);
const result = await service.getResultsDetail(raceId);
expect(result).toBeInstanceOf(RaceResultsDetailViewModel);
expect(result.currentUserId).toBe('');
});
it('should throw error when apiClient.getResultsDetail fails', async () => {
const raceId = 'race-123';
const currentUserId = 'user-456';
const error = new Error('API call failed');
mockApiClient.getResultsDetail.mockRejectedValue(error);
await expect(service.getResultsDetail(raceId, currentUserId)).rejects.toThrow('API call failed');
});
});
describe('getWithSOF', () => {
it('should call apiClient.getWithSOF and return RaceWithSOFViewModel', async () => {
const raceId = 'race-123';
const mockDto: RaceWithSOFDTO = {
id: raceId,
track: 'Test Track',
};
mockApiClient.getWithSOF.mockResolvedValue(mockDto);
const result = await service.getWithSOF(raceId);
expect(mockApiClient.getWithSOF).toHaveBeenCalledWith(raceId);
expect(result).toBeInstanceOf(RaceWithSOFViewModel);
expect(result.id).toBe(raceId);
expect(result.track).toBe('Test Track');
});
it('should throw error when apiClient.getWithSOF fails', async () => {
const raceId = 'race-123';
const error = new Error('API call failed');
mockApiClient.getWithSOF.mockRejectedValue(error);
await expect(service.getWithSOF(raceId)).rejects.toThrow('API call failed');
});
});
describe('importResults', () => {
it('should call apiClient.importResults and return ImportRaceResultsSummaryViewModel', async () => {
const raceId = 'race-123';
const input = { raceId, results: [{ position: 1 }] };
const mockDto = {
raceId,
importedCount: 10,
errors: ['Error 1'],
};
mockApiClient.importResults.mockResolvedValue(mockDto as any);
const result = await service.importResults(raceId, input);
expect(mockApiClient.importResults).toHaveBeenCalledWith(raceId, input);
expect(result).toBeInstanceOf(ImportRaceResultsSummaryViewModel);
expect(result.raceId).toBe(raceId);
expect(result.importedCount).toBe(10);
expect(result.errors).toEqual(['Error 1']);
});
it('should handle successful import with no errors', async () => {
const raceId = 'race-123';
const input = { raceId, results: [] };
const mockDto = {
raceId,
importedCount: 5,
errors: [],
};
mockApiClient.importResults.mockResolvedValue(mockDto as any);
const result = await service.importResults(raceId, input);
expect(result.importedCount).toBe(5);
expect(result.errors).toEqual([]);
});
it('should throw error when apiClient.importResults fails', async () => {
const raceId = 'race-123';
const input = { raceId, results: [] };
const error = new Error('API call failed');
mockApiClient.importResults.mockRejectedValue(error);
await expect(service.importResults(raceId, input)).rejects.toThrow('API call failed');
});
});
});

View File

@@ -1,13 +1,17 @@
import { RacesApiClient } from '../../api/races/RacesApiClient';
import { RaceResultsDetailViewModel } from '../../view-models/RaceResultsDetailViewModel';
import { RaceWithSOFViewModel } from '../../view-models/RaceWithSOFViewModel';
import { ImportRaceResultsSummaryViewModel } from '../../view-models/ImportRaceResultsSummaryViewModel';
// TODO: Move these types to apps/website/lib/types/generated when available
type ImportRaceResultsInputDto = { raceId: string; results: Array<any> };
// TODO: Move this type to apps/website/lib/types/generated when available
type ImportRaceResultsInputDto = { raceId: string; results: Array<unknown> };
// Note: RaceWithSOFViewModel and ImportRaceResultsSummaryViewModel are defined in presenters
// These will need to be converted to proper view models
type RaceWithSOFViewModel = any; // TODO: Create proper view model
type ImportRaceResultsSummaryViewModel = any; // TODO: Create proper view model
// TODO: Move this type to apps/website/lib/types/generated when available
type ImportRaceResultsSummaryDto = {
raceId: string;
importedCount: number;
errors: string[];
};
/**
* Race Results Service
@@ -29,20 +33,18 @@ export class RaceResultsService {
}
/**
* Get race with strength of field calculation
* TODO: Create RaceWithSOFViewModel and use it here
*/
async getWithSOF(raceId: string): Promise<RaceWithSOFViewModel> {
const dto = await this.apiClient.getWithSOF(raceId);
return dto; // TODO: return new RaceWithSOFViewModel(dto);
}
* Get race with strength of field calculation
*/
async getWithSOF(raceId: string): Promise<RaceWithSOFViewModel> {
const dto = await this.apiClient.getWithSOF(raceId);
return new RaceWithSOFViewModel(dto);
}
/**
* Import race results and get summary
* TODO: Create ImportRaceResultsSummaryViewModel and use it here
*/
async importResults(raceId: string, input: ImportRaceResultsInputDto): Promise<ImportRaceResultsSummaryViewModel> {
const dto = await this.apiClient.importResults(raceId, input);
return dto; // TODO: return new ImportRaceResultsSummaryViewModel(dto);
}
* Import race results and get summary
*/
async importResults(raceId: string, input: ImportRaceResultsInputDto): Promise<ImportRaceResultsSummaryViewModel> {
const dto = await this.apiClient.importResults(raceId, input) as ImportRaceResultsSummaryDto;
return new ImportRaceResultsSummaryViewModel(dto);
}
}

View File

@@ -0,0 +1,134 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { RaceService } from './RaceService';
import { RacesApiClient } from '../../api/races/RacesApiClient';
import { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel';
import { RacesPageViewModel } from '../../view-models/RacesPageViewModel';
import { RaceStatsViewModel } from '../../view-models/RaceStatsViewModel';
describe('RaceService', () => {
let mockApiClient: Mocked<RacesApiClient>;
let service: RaceService;
beforeEach(() => {
mockApiClient = {
getDetail: vi.fn(),
getPageData: vi.fn(),
getTotal: vi.fn(),
} as Mocked<RacesApiClient>;
service = new RaceService(mockApiClient);
});
describe('getRaceDetail', () => {
it('should call apiClient.getDetail and return RaceDetailViewModel', async () => {
const raceId = 'race-123';
const driverId = 'driver-456';
const mockDto = {
race: { id: raceId, track: 'Test Track' },
league: { id: 'league-1', name: 'Test League' },
entryList: [],
registration: { isRegistered: true, canRegister: false },
userResult: null,
};
mockApiClient.getDetail.mockResolvedValue(mockDto as any);
const result = await service.getRaceDetail(raceId, driverId);
expect(mockApiClient.getDetail).toHaveBeenCalledWith(raceId, driverId);
expect(result).toBeInstanceOf(RaceDetailViewModel);
expect(result.race?.id).toBe(raceId);
});
it('should throw error when apiClient.getDetail fails', async () => {
const raceId = 'race-123';
const driverId = 'driver-456';
const error = new Error('API call failed');
mockApiClient.getDetail.mockRejectedValue(error);
await expect(service.getRaceDetail(raceId, driverId)).rejects.toThrow('API call failed');
});
});
describe('getRacesPageData', () => {
it('should call apiClient.getPageData and return RacesPageViewModel with transformed data', async () => {
const mockDto = {
races: [
{
id: 'race-1',
track: 'Monza',
car: 'Ferrari',
scheduledAt: '2023-10-01T10:00:00Z',
status: 'upcoming',
leagueId: 'league-1',
leagueName: 'Test League',
},
{
id: 'race-2',
track: 'Silverstone',
car: 'Mercedes',
scheduledAt: '2023-09-15T10:00:00Z',
status: 'completed',
leagueId: 'league-1',
leagueName: 'Test League',
},
],
};
mockApiClient.getPageData.mockResolvedValue(mockDto as any);
const result = await service.getRacesPageData();
expect(mockApiClient.getPageData).toHaveBeenCalled();
expect(result).toBeInstanceOf(RacesPageViewModel);
expect(result.upcomingRaces).toHaveLength(1);
expect(result.completedRaces).toHaveLength(1);
expect(result.totalCount).toBe(2);
expect(result.upcomingRaces[0].title).toBe('Monza - Ferrari');
expect(result.completedRaces[0].title).toBe('Silverstone - Mercedes');
});
it('should handle empty races array', async () => {
const mockDto = { races: [] };
mockApiClient.getPageData.mockResolvedValue(mockDto as any);
const result = await service.getRacesPageData();
expect(result.upcomingRaces).toHaveLength(0);
expect(result.completedRaces).toHaveLength(0);
expect(result.totalCount).toBe(0);
});
it('should throw error when apiClient.getPageData fails', async () => {
const error = new Error('API call failed');
mockApiClient.getPageData.mockRejectedValue(error);
await expect(service.getRacesPageData()).rejects.toThrow('API call failed');
});
});
describe('getRacesTotal', () => {
it('should call apiClient.getTotal and return RaceStatsViewModel', async () => {
const mockDto = { totalRaces: 42 };
mockApiClient.getTotal.mockResolvedValue(mockDto as any);
const result = await service.getRacesTotal();
expect(mockApiClient.getTotal).toHaveBeenCalled();
expect(result).toBeInstanceOf(RaceStatsViewModel);
expect(result.totalRaces).toBe(42);
expect(result.formattedTotalRaces).toBe('42');
});
it('should throw error when apiClient.getTotal fails', async () => {
const error = new Error('API call failed');
mockApiClient.getTotal.mockRejectedValue(error);
await expect(service.getRacesTotal()).rejects.toThrow('API call failed');
});
});
});

View File

@@ -1,9 +1,20 @@
import { RacesApiClient } from '../../api/races/RacesApiClient';
import { RaceDetailViewModel } from '../../view-models/RaceDetailViewModel';
import { RacesPageViewModel } from '../../view-models/RacesPageViewModel';
import { RaceStatsViewModel } from '../../view-models/RaceStatsViewModel';
// TODO: Move these types to apps/website/lib/types/generated when available
type RacesPageDataDto = { races: Array<any> };
type RaceStatsDto = { totalRaces: number };
type RacesPageDataRaceDTO = {
id: string;
track: string;
car: string;
scheduledAt: string;
status: string;
leagueId: string;
leagueName: string;
};
type RacesPageDataDto = { races: RacesPageDataRaceDTO[] };
type RaceStatsDTO = { totalRaces: number };
/**
* Race Service
@@ -28,18 +39,51 @@ export class RaceService {
}
/**
* Get races page data
* TODO: Add view model transformation when view model is available
* Get races page data with view model transformation
*/
async getRacesPageData(): Promise<RacesPageDataDto> {
return this.apiClient.getPageData();
async getRacesPageData(): Promise<RacesPageViewModel> {
const dto = await this.apiClient.getPageData();
return new RacesPageViewModel(this.transformRacesPageData(dto));
}
/**
* Get total races statistics
* TODO: Add view model transformation when view model is available
* Get total races statistics with view model transformation
*/
async getRacesTotal(): Promise<RaceStatsDto> {
return this.apiClient.getTotal();
async getRacesTotal(): Promise<RaceStatsViewModel> {
const dto: RaceStatsDTO = await this.apiClient.getTotal();
return new RaceStatsViewModel(dto);
}
/**
* Transform API races page data to view model format
*/
private transformRacesPageData(dto: RacesPageDataDto): {
upcomingRaces: Array<{ id: string; title: string; scheduledTime: string; status: string }>;
completedRaces: Array<{ id: string; title: string; scheduledTime: string; status: string }>;
totalCount: number;
} {
const upcomingRaces = dto.races
.filter(race => race.status !== 'completed')
.map(race => ({
id: race.id,
title: `${race.track} - ${race.car}`,
scheduledTime: race.scheduledAt,
status: race.status,
}));
const completedRaces = dto.races
.filter(race => race.status === 'completed')
.map(race => ({
id: race.id,
title: `${race.track} - ${race.car}`,
scheduledTime: race.scheduledAt,
status: race.status,
}));
return {
upcomingRaces,
completedRaces,
totalCount: dto.races.length,
};
}
}