This commit is contained in:
2025-12-16 15:42:38 +01:00
parent 29410708c8
commit 362894d1a5
147 changed files with 780 additions and 375 deletions

View File

@@ -0,0 +1,62 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { CompleteOnboardingPresenter } from './CompleteOnboardingPresenter';
import type { CompleteDriverOnboardingResultDTO } from '../../../../../core/racing/application/presenters/ICompleteDriverOnboardingPresenter';
describe('CompleteOnboardingPresenter', () => {
let presenter: CompleteOnboardingPresenter;
beforeEach(() => {
presenter = new CompleteOnboardingPresenter();
});
describe('present', () => {
it('should map successful core DTO to API view model', () => {
const dto: CompleteDriverOnboardingResultDTO = {
success: true,
driverId: 'driver-123',
};
presenter.present(dto);
const result = presenter.viewModel;
expect(result).toEqual({
success: true,
driverId: 'driver-123',
errorMessage: undefined,
});
});
it('should map failed core DTO to API view model', () => {
const dto: CompleteDriverOnboardingResultDTO = {
success: false,
errorMessage: 'Driver already exists',
};
presenter.present(dto);
const result = presenter.viewModel;
expect(result).toEqual({
success: false,
driverId: undefined,
errorMessage: 'Driver already exists',
});
});
});
describe('reset', () => {
it('should reset the result', () => {
const dto: CompleteDriverOnboardingResultDTO = {
success: true,
driverId: 'driver-123',
};
presenter.present(dto);
expect(presenter.viewModel).toBeDefined();
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
});

View File

@@ -0,0 +1,23 @@
import { CompleteOnboardingOutput } from '../dto/DriverDto';
import type { ICompleteDriverOnboardingPresenter, CompleteDriverOnboardingResultDTO } from '../../../../../core/racing/application/presenters/ICompleteDriverOnboardingPresenter';
export class CompleteOnboardingPresenter implements ICompleteDriverOnboardingPresenter {
private result: CompleteOnboardingOutput | null = null;
reset() {
this.result = null;
}
present(dto: CompleteDriverOnboardingResultDTO) {
this.result = {
success: dto.success,
driverId: dto.driverId,
errorMessage: dto.errorMessage,
};
}
get viewModel(): CompleteOnboardingOutput {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,46 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { DriverRegistrationStatusPresenter } from './DriverRegistrationStatusPresenter';
describe('DriverRegistrationStatusPresenter', () => {
let presenter: DriverRegistrationStatusPresenter;
beforeEach(() => {
presenter = new DriverRegistrationStatusPresenter();
});
describe('present', () => {
it('should map parameters to view model for registered driver', () => {
presenter.present(true, 'race-123', 'driver-456');
const result = presenter.viewModel;
expect(result).toEqual({
isRegistered: true,
raceId: 'race-123',
driverId: 'driver-456',
});
});
it('should map parameters to view model for unregistered driver', () => {
presenter.present(false, 'race-789', 'driver-101');
const result = presenter.viewModel;
expect(result).toEqual({
isRegistered: false,
raceId: 'race-789',
driverId: 'driver-101',
});
});
});
describe('reset', () => {
it('should reset the result', () => {
presenter.present(true, 'race-123', 'driver-456');
expect(presenter.viewModel).toBeDefined();
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
});

View File

@@ -0,0 +1,28 @@
import { DriverRegistrationStatusViewModel } from '../dto/DriverDto';
import type { IDriverRegistrationStatusPresenter } from '../../../../../core/racing/application/presenters/IDriverRegistrationStatusPresenter';
export class DriverRegistrationStatusPresenter implements IDriverRegistrationStatusPresenter {
private result: DriverRegistrationStatusViewModel | null = null;
present(isRegistered: boolean, raceId: string, driverId: string) {
this.result = {
isRegistered,
raceId,
driverId,
};
}
getViewModel(): DriverRegistrationStatusViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
// For consistency with other presenters
reset() {
this.result = null;
}
get viewModel(): DriverRegistrationStatusViewModel {
return this.getViewModel();
}
}

View File

@@ -0,0 +1,41 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { DriverStatsPresenter } from './DriverStatsPresenter';
import type { TotalDriversResultDTO } from '../../../../../core/racing/application/presenters/ITotalDriversPresenter';
describe('DriverStatsPresenter', () => {
let presenter: DriverStatsPresenter;
beforeEach(() => {
presenter = new DriverStatsPresenter();
});
describe('present', () => {
it('should map core DTO to API view model correctly', () => {
const dto: TotalDriversResultDTO = {
totalDrivers: 42,
};
presenter.present(dto);
const result = presenter.viewModel;
expect(result).toEqual({
totalDrivers: 42,
});
});
});
describe('reset', () => {
it('should reset the result', () => {
const dto: TotalDriversResultDTO = {
totalDrivers: 10,
};
presenter.present(dto);
expect(presenter.viewModel).toBeDefined();
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
});

View File

@@ -0,0 +1,21 @@
import { DriverStatsDto } from '../dto/DriverDto';
import type { ITotalDriversPresenter, TotalDriversResultDTO } from '../../../../../core/racing/application/presenters/ITotalDriversPresenter';
export class DriverStatsPresenter implements ITotalDriversPresenter {
private result: DriverStatsDto | null = null;
reset() {
this.result = null;
}
present(dto: TotalDriversResultDTO) {
this.result = {
totalDrivers: dto.totalDrivers,
};
}
get viewModel(): DriverStatsDto {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}

View File

@@ -0,0 +1,159 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { DriversLeaderboardPresenter } from './DriversLeaderboardPresenter';
import type { DriversLeaderboardResultDTO } from '../../../../../core/racing/application/presenters/IDriversLeaderboardPresenter';
describe('DriversLeaderboardPresenter', () => {
let presenter: DriversLeaderboardPresenter;
beforeEach(() => {
presenter = new DriversLeaderboardPresenter();
});
describe('present', () => {
it('should map core DTO to API view model correctly', () => {
const dto: DriversLeaderboardResultDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver One',
country: 'US',
iracingId: '12345',
joinedAt: new Date('2023-01-01'),
},
{
id: 'driver-2',
name: 'Driver Two',
country: 'DE',
iracingId: '67890',
joinedAt: new Date('2023-01-02'),
},
],
rankings: [
{ driverId: 'driver-1', rating: 2500, overallRank: 1 },
{ driverId: 'driver-2', rating: 2400, overallRank: 2 },
],
stats: {
'driver-1': { racesCompleted: 50, wins: 10, podiums: 20 },
'driver-2': { racesCompleted: 40, wins: 5, podiums: 15 },
},
avatarUrls: {
'driver-1': 'https://example.com/avatar1.png',
'driver-2': 'https://example.com/avatar2.png',
},
};
presenter.present(dto);
const result = presenter.viewModel;
expect(result.drivers).toHaveLength(2);
expect(result.drivers[0]).toEqual({
id: 'driver-1',
name: 'Driver One',
rating: 2500,
skillLevel: 'Pro',
nationality: 'US',
racesCompleted: 50,
wins: 10,
podiums: 20,
isActive: true,
rank: 1,
avatarUrl: 'https://example.com/avatar1.png',
});
expect(result.drivers[1]).toEqual({
id: 'driver-2',
name: 'Driver Two',
rating: 2400,
skillLevel: 'Pro',
nationality: 'DE',
racesCompleted: 40,
wins: 5,
podiums: 15,
isActive: true,
rank: 2,
avatarUrl: 'https://example.com/avatar2.png',
});
expect(result.totalRaces).toBe(90);
expect(result.totalWins).toBe(15);
expect(result.activeCount).toBe(2);
});
it('should sort drivers by rating descending', () => {
const dto: DriversLeaderboardResultDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver One',
country: 'US',
iracingId: '12345',
joinedAt: new Date(),
},
{
id: 'driver-2',
name: 'Driver Two',
country: 'DE',
iracingId: '67890',
joinedAt: new Date(),
},
],
rankings: [
{ driverId: 'driver-1', rating: 2400, overallRank: 2 },
{ driverId: 'driver-2', rating: 2500, overallRank: 1 },
],
stats: {},
avatarUrls: {},
};
presenter.present(dto);
const result = presenter.viewModel;
expect(result.drivers[0].id).toBe('driver-2'); // Higher rating first
expect(result.drivers[1].id).toBe('driver-1');
});
it('should handle missing stats gracefully', () => {
const dto: DriversLeaderboardResultDTO = {
drivers: [
{
id: 'driver-1',
name: 'Driver One',
country: 'US',
iracingId: '12345',
joinedAt: new Date(),
},
],
rankings: [
{ driverId: 'driver-1', rating: 2500, overallRank: 1 },
],
stats: {}, // No stats
avatarUrls: {},
};
presenter.present(dto);
const result = presenter.viewModel;
expect(result.drivers[0].racesCompleted).toBe(0);
expect(result.drivers[0].wins).toBe(0);
expect(result.drivers[0].podiums).toBe(0);
});
});
describe('reset', () => {
it('should reset the result', () => {
const dto: DriversLeaderboardResultDTO = {
drivers: [],
rankings: [],
stats: {},
avatarUrls: {},
};
presenter.present(dto);
expect(presenter.viewModel).toBeDefined();
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
});
});

View File

@@ -0,0 +1,49 @@
import { DriversLeaderboardViewModel, DriverLeaderboardItemViewModel } from '../dto/DriverDto';
import type { IDriversLeaderboardPresenter, DriversLeaderboardResultDTO } from '../../../../../core/racing/application/presenters/IDriversLeaderboardPresenter';
export class DriversLeaderboardPresenter implements IDriversLeaderboardPresenter {
private result: DriversLeaderboardViewModel | null = null;
reset() {
this.result = null;
}
present(dto: DriversLeaderboardResultDTO) {
const drivers: DriverLeaderboardItemViewModel[] = dto.drivers.map(driver => {
const ranking = dto.rankings.find(r => r.driverId === driver.id);
const stats = dto.stats[driver.id];
const avatarUrl = dto.avatarUrls[driver.id];
return {
id: driver.id,
name: driver.name,
rating: ranking?.rating ?? 0,
skillLevel: 'Pro', // TODO: map from domain
nationality: driver.country,
racesCompleted: stats?.racesCompleted ?? 0,
wins: stats?.wins ?? 0,
podiums: stats?.podiums ?? 0,
isActive: true, // TODO: determine from domain
rank: ranking?.overallRank ?? 0,
avatarUrl,
};
});
// Calculate totals
const totalRaces = drivers.reduce((sum, d) => sum + (d.racesCompleted ?? 0), 0);
const totalWins = drivers.reduce((sum, d) => sum + (d.wins ?? 0), 0);
const activeCount = drivers.filter(d => d.isActive).length;
this.result = {
drivers: drivers.sort((a, b) => (b.rating ?? 0) - (a.rating ?? 0)),
totalRaces,
totalWins,
activeCount,
};
}
get viewModel(): DriversLeaderboardViewModel {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
}