This commit is contained in:
2025-12-21 19:53:22 +01:00
parent f2d8a23583
commit 3c64f328e2
105 changed files with 3191 additions and 1706 deletions

View File

@@ -1,120 +1,137 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { DashboardOverviewPresenter } from './DashboardOverviewPresenter';
import type { DashboardOverviewOutputPort } from '@core/racing/application/ports/output/DashboardOverviewOutputPort';
import type { DashboardOverviewResult } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
import { Driver } from '@core/racing/domain/entities/Driver';
import { Race } from '@core/racing/domain/entities/Race';
import { League } from '@core/racing/domain/entities/League';
import { Standing } from '@core/racing/domain/entities/Standing';
import { Result as RaceResult } from '@core/racing/domain/entities/result/Result';
import type { FeedItem } from '@core/social/domain/types/FeedItem';
const createOutput = (): DashboardOverviewOutputPort => ({
currentDriver: {
id: 'driver-1',
name: 'Test Driver',
country: 'DE',
avatarUrl: 'https://example.com/avatar.jpg',
rating: 2500,
globalRank: 42,
totalRaces: 10,
wins: 3,
podiums: 5,
consistency: 90,
},
myUpcomingRaces: [
{
id: 'race-1',
leagueId: 'league-1',
leagueName: 'League 1',
track: 'Spa',
car: 'GT3',
scheduledAt: '2025-01-01T10:00:00Z',
status: 'scheduled',
isMyLeague: true,
},
],
otherUpcomingRaces: [
{
id: 'race-2',
leagueId: 'league-2',
leagueName: 'League 2',
track: 'Monza',
car: 'GT3',
scheduledAt: '2025-01-02T10:00:00Z',
status: 'scheduled',
isMyLeague: false,
},
],
upcomingRaces: [
{
id: 'race-1',
leagueId: 'league-1',
leagueName: 'League 1',
track: 'Spa',
car: 'GT3',
scheduledAt: '2025-01-01T10:00:00Z',
status: 'scheduled',
isMyLeague: true,
},
{
id: 'race-2',
leagueId: 'league-2',
leagueName: 'League 2',
track: 'Monza',
car: 'GT3',
scheduledAt: '2025-01-02T10:00:00Z',
status: 'scheduled',
isMyLeague: false,
},
],
activeLeaguesCount: 2,
nextRace: {
const createOutput = (): DashboardOverviewResult => {
const driver = Driver.create({ id: 'driver-1', iracingId: '12345', name: 'Test Driver', country: 'DE' });
const league1 = League.create({ id: 'league-1', name: 'League 1', description: 'First league', ownerId: 'owner-1' });
const league2 = League.create({ id: 'league-2', name: 'League 2', description: 'Second league', ownerId: 'owner-2' });
const league3 = League.create({ id: 'league-3', name: 'League 3', description: 'Third league', ownerId: 'owner-3' });
const race1 = Race.create({
id: 'race-1',
leagueId: 'league-1',
leagueName: 'League 1',
track: 'Spa',
car: 'GT3',
scheduledAt: '2025-01-01T10:00:00Z',
scheduledAt: new Date('2025-01-01T10:00:00Z'),
status: 'scheduled',
isMyLeague: true,
},
recentResults: [
{
raceId: 'race-3',
raceName: 'Nürburgring',
leagueId: 'league-3',
leagueName: 'League 3',
finishedAt: '2024-12-01T10:00:00Z',
position: 1,
incidents: 0,
});
const race2 = Race.create({
id: 'race-2',
leagueId: 'league-2',
track: 'Monza',
car: 'GT3',
scheduledAt: new Date('2025-01-02T10:00:00Z'),
status: 'scheduled',
});
const race3 = Race.create({
id: 'race-3',
leagueId: 'league-3',
track: 'Nürburgring',
car: 'GT3',
scheduledAt: new Date('2024-12-01T10:00:00Z'),
status: 'completed',
});
const standing1 = Standing.create({ leagueId: 'league-1', driverId: 'driver-1', position: 1, points: 150 });
const result1 = RaceResult.create({
id: 'result-1',
raceId: 'race-3',
driverId: 'driver-1',
position: 1,
fastestLap: 120,
incidents: 0,
startPosition: 1,
});
const feedItem: FeedItem = {
id: 'feed-1',
type: 'friend-joined-league',
headline: 'You won a race',
body: 'Congrats!',
timestamp: new Date('2024-12-02T10:00:00Z'),
ctaLabel: 'View',
ctaHref: '/races/race-3',
};
const friend = Driver.create({ id: 'friend-1', iracingId: '67890', name: 'Friend One', country: 'US' });
return {
currentDriver: {
driver,
avatarUrl: 'https://example.com/avatar.jpg',
rating: 2500,
globalRank: 42,
totalRaces: 10,
wins: 3,
podiums: 5,
consistency: 90,
},
],
leagueStandingsSummaries: [
{
leagueId: 'league-1',
leagueName: 'League 1',
position: 1,
totalDrivers: 20,
points: 150,
},
],
feedSummary: {
notificationCount: 3,
items: [
myUpcomingRaces: [
{
id: 'feed-1',
type: 'race_result' as any,
headline: 'You won a race',
body: 'Congrats!',
timestamp: '2024-12-02T10:00:00Z',
ctaLabel: 'View',
ctaHref: '/races/race-3',
race: race1,
league: league1,
isMyLeague: true,
},
],
},
friends: [
{
id: 'friend-1',
name: 'Friend One',
country: 'US',
avatarUrl: 'https://example.com/friend.jpg',
otherUpcomingRaces: [
{
race: race2,
league: league2,
isMyLeague: false,
},
],
upcomingRaces: [
{
race: race1,
league: league1,
isMyLeague: true,
},
{
race: race2,
league: league2,
isMyLeague: false,
},
],
activeLeaguesCount: 2,
nextRace: {
race: race1,
league: league1,
isMyLeague: true,
},
],
});
recentResults: [
{
race: race3,
league: league3,
result: result1,
},
],
leagueStandingsSummaries: [
{
league: league1,
standing: standing1,
totalDrivers: 20,
},
],
feedSummary: {
notificationCount: 3,
items: [feedItem],
},
friends: [
{
driver: friend,
avatarUrl: 'https://example.com/friend.jpg',
},
],
};
};
describe('DashboardOverviewPresenter', () => {
let presenter: DashboardOverviewPresenter;
@@ -123,44 +140,23 @@ describe('DashboardOverviewPresenter', () => {
presenter = new DashboardOverviewPresenter();
});
it('maps DashboardOverviewOutputPort to DashboardOverviewDTO correctly', () => {
it('maps DashboardOverviewResult to DashboardOverviewDTO correctly', () => {
const output = createOutput();
presenter.present(output);
const dto = presenter.getResponseModel();
const viewModel = presenter.viewModel;
expect(viewModel.activeLeaguesCount).toBe(2);
expect(viewModel.currentDriver?.id).toBe('driver-1');
expect(viewModel.myUpcomingRaces[0].id).toBe('race-1');
expect(viewModel.otherUpcomingRaces[0].id).toBe('race-2');
expect(viewModel.upcomingRaces).toHaveLength(2);
expect(viewModel.nextRace?.id).toBe('race-1');
expect(viewModel.recentResults[0].raceId).toBe('race-3');
expect(viewModel.leagueStandingsSummaries[0].leagueId).toBe('league-1');
expect(viewModel.feedSummary.notificationCount).toBe(3);
expect(viewModel.feedSummary.items[0].id).toBe('feed-1');
expect(viewModel.friends[0].id).toBe('friend-1');
expect(dto.activeLeaguesCount).toBe(2);
expect(dto.currentDriver?.id).toBe('driver-1');
expect(dto.myUpcomingRaces[0].id).toBe('race-1');
expect(dto.otherUpcomingRaces[0].id).toBe('race-2');
expect(dto.upcomingRaces).toHaveLength(2);
expect(dto.nextRace?.id).toBe('race-1');
expect(dto.recentResults[0].raceId).toBe('race-3');
expect(dto.leagueStandingsSummaries[0].leagueId).toBe('league-1');
expect(dto.feedSummary.notificationCount).toBe(3);
expect(dto.feedSummary.items[0].id).toBe('feed-1');
expect(dto.friends[0].id).toBe('friend-1');
});
it('reset clears state and causes viewModel to throw', () => {
const output = createOutput();
presenter.present(output);
expect(presenter.viewModel).toBeDefined();
presenter.reset();
expect(() => presenter.viewModel).toThrow('Presenter not presented');
});
it('getViewModel returns null when not presented', () => {
expect(presenter.getViewModel()).toBeNull();
});
it('getViewModel returns same DTO after present', () => {
const output = createOutput();
presenter.present(output);
expect(presenter.getViewModel()).toEqual(presenter.viewModel);
});
});

View File

@@ -1,4 +1,7 @@
import type { DashboardOverviewOutputPort } from '@core/racing/application/ports/output/DashboardOverviewOutputPort';
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
import type {
DashboardOverviewResult,
} from '@core/racing/application/use-cases/DashboardOverviewUseCase';
import {
DashboardOverviewDTO,
DashboardDriverSummaryDTO,
@@ -10,93 +13,89 @@ import {
DashboardFriendSummaryDTO,
} from '../dtos/DashboardOverviewDTO';
export class DashboardOverviewPresenter {
private result: DashboardOverviewDTO | null = null;
export class DashboardOverviewPresenter implements UseCaseOutputPort<DashboardOverviewResult> {
private responseModel: DashboardOverviewDTO | null = null;
reset() {
this.result = null;
}
present(output: DashboardOverviewOutputPort): void {
const currentDriver: DashboardDriverSummaryDTO | null = output.currentDriver
present(result: DashboardOverviewResult): void {
const currentDriver: DashboardDriverSummaryDTO | null = result.currentDriver
? {
id: output.currentDriver.id,
name: output.currentDriver.name,
country: output.currentDriver.country,
avatarUrl: output.currentDriver.avatarUrl,
rating: output.currentDriver.rating,
globalRank: output.currentDriver.globalRank,
totalRaces: output.currentDriver.totalRaces,
wins: output.currentDriver.wins,
podiums: output.currentDriver.podiums,
consistency: output.currentDriver.consistency,
id: result.currentDriver.driver.id,
name: String(result.currentDriver.driver.name),
country: String(result.currentDriver.driver.country),
avatarUrl: result.currentDriver.avatarUrl,
rating: result.currentDriver.rating,
globalRank: result.currentDriver.globalRank,
totalRaces: result.currentDriver.totalRaces,
wins: result.currentDriver.wins,
podiums: result.currentDriver.podiums,
consistency: result.currentDriver.consistency,
}
: null;
const mapRace = (race: typeof output.myUpcomingRaces[number]): DashboardRaceSummaryDTO => ({
id: race.id,
leagueId: race.leagueId,
leagueName: race.leagueName,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt,
status: race.status,
isMyLeague: race.isMyLeague,
const mapRace = (raceSummary: DashboardOverviewResult['myUpcomingRaces'][number]): DashboardRaceSummaryDTO => ({
id: raceSummary.race.id,
leagueId: raceSummary.league?.id ? String(raceSummary.league.id) : null,
leagueName: raceSummary.league?.name ? String(raceSummary.league.name) : null,
track: String(raceSummary.race.track),
car: String(raceSummary.race.car),
scheduledAt: raceSummary.race.scheduledAt.toISOString(),
status: raceSummary.race.status as 'scheduled' | 'running' | 'completed' | 'cancelled',
isMyLeague: raceSummary.isMyLeague,
});
const myUpcomingRaces: DashboardRaceSummaryDTO[] = output.myUpcomingRaces.map(mapRace);
const otherUpcomingRaces: DashboardRaceSummaryDTO[] = output.otherUpcomingRaces.map(mapRace);
const upcomingRaces: DashboardRaceSummaryDTO[] = output.upcomingRaces.map(mapRace);
const myUpcomingRaces: DashboardRaceSummaryDTO[] = result.myUpcomingRaces.map(mapRace);
const otherUpcomingRaces: DashboardRaceSummaryDTO[] = result.otherUpcomingRaces.map(mapRace);
const upcomingRaces: DashboardRaceSummaryDTO[] = result.upcomingRaces.map(mapRace);
const nextRace: DashboardRaceSummaryDTO | null = output.nextRace ? mapRace(output.nextRace) : null;
const nextRace: DashboardRaceSummaryDTO | null = result.nextRace ? mapRace(result.nextRace) : null;
const recentResults: DashboardRecentResultDTO[] = output.recentResults.map(result => ({
raceId: result.raceId,
raceName: result.raceName,
leagueId: result.leagueId,
leagueName: result.leagueName,
finishedAt: result.finishedAt,
position: result.position,
incidents: result.incidents,
const recentResults: DashboardRecentResultDTO[] = result.recentResults.map(resultSummary => ({
raceId: resultSummary.race.id,
raceName: String(resultSummary.race.track),
leagueId: resultSummary.league?.id ? String(resultSummary.league.id) : null,
leagueName: resultSummary.league?.name ? String(resultSummary.league.name) : null,
finishedAt: resultSummary.race.scheduledAt.toISOString(),
position: Number(resultSummary.result.position),
incidents: Number(resultSummary.result.incidents),
}));
const leagueStandingsSummaries: DashboardLeagueStandingSummaryDTO[] =
output.leagueStandingsSummaries.map(standing => ({
leagueId: standing.leagueId,
leagueName: standing.leagueName,
position: standing.position,
result.leagueStandingsSummaries.map(standing => ({
leagueId: String(standing.league.id),
leagueName: String(standing.league.name),
position: standing.standing?.position ? Number(standing.standing.position) : null,
totalDrivers: standing.totalDrivers,
points: standing.points,
points: standing.standing?.points ? Number(standing.standing.points) : null,
}));
const feedItems: DashboardFeedItemSummaryDTO[] = output.feedSummary.items.map(item => ({
const feedItems: DashboardFeedItemSummaryDTO[] = result.feedSummary.items.map(item => ({
id: item.id,
type: item.type,
type: String(item.type),
headline: item.headline,
body: item.body,
timestamp: item.timestamp,
ctaLabel: item.ctaLabel,
ctaHref: item.ctaHref,
body: item.body ?? '',
timestamp: item.timestamp.toISOString(),
ctaLabel: item.ctaLabel ?? '',
ctaHref: item.ctaHref ?? '',
}));
const feedSummary: DashboardFeedSummaryDTO = {
notificationCount: output.feedSummary.notificationCount,
notificationCount: result.feedSummary.notificationCount,
items: feedItems,
};
const friends: DashboardFriendSummaryDTO[] = output.friends.map(friend => ({
id: friend.id,
name: friend.name,
country: friend.country,
const friends: DashboardFriendSummaryDTO[] = result.friends.map(friend => ({
id: friend.driver.id,
name: String(friend.driver.name),
country: String(friend.driver.country),
avatarUrl: friend.avatarUrl,
}));
this.result = {
this.responseModel = {
currentDriver,
myUpcomingRaces,
otherUpcomingRaces,
upcomingRaces,
activeLeaguesCount: output.activeLeaguesCount,
activeLeaguesCount: result.activeLeaguesCount,
nextRace,
recentResults,
leagueStandingsSummaries,
@@ -105,12 +104,8 @@ export class DashboardOverviewPresenter {
};
}
get viewModel(): DashboardOverviewDTO {
if (!this.result) throw new Error('Presenter not presented');
return this.result;
}
getViewModel(): DashboardOverviewDTO | null {
return this.result;
getResponseModel(): DashboardOverviewDTO {
if (!this.responseModel) throw new Error('Presenter not presented');
return this.responseModel;
}
}