presenter refactoring
This commit is contained in:
@@ -13,6 +13,7 @@ export class DashboardController {
|
||||
@ApiQuery({ name: 'driverId', description: 'Driver ID' })
|
||||
@ApiResponse({ status: 200, description: 'Dashboard overview', type: DashboardOverviewDTO })
|
||||
async getDashboardOverview(@Query('driverId') driverId: string): Promise<DashboardOverviewDTO> {
|
||||
return this.dashboardService.getDashboardOverview(driverId);
|
||||
const presenter = await this.dashboardService.getDashboardOverview(driverId);
|
||||
return presenter.viewModel;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
|
||||
import type { DashboardOverviewOutputPort } from '@core/racing/application/ports/output/DashboardOverviewOutputPort';
|
||||
import { DashboardOverviewDTO } from './dtos/DashboardOverviewDTO';
|
||||
import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter';
|
||||
|
||||
// Core imports
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
@@ -64,7 +64,7 @@ export class DashboardService {
|
||||
);
|
||||
}
|
||||
|
||||
async getDashboardOverview(driverId: string): Promise<DashboardOverviewDTO> {
|
||||
async getDashboardOverview(driverId: string): Promise<DashboardOverviewPresenter> {
|
||||
this.logger.debug('[DashboardService] Getting dashboard overview:', { driverId });
|
||||
|
||||
const result = await this.dashboardOverviewUseCase.execute({ driverId });
|
||||
@@ -73,6 +73,8 @@ export class DashboardService {
|
||||
throw new Error(result.error?.message || 'Failed to get dashboard overview');
|
||||
}
|
||||
|
||||
return plainToClass(DashboardOverviewDTO, result.value);
|
||||
const presenter = new DashboardOverviewPresenter();
|
||||
presenter.present(result.value as DashboardOverviewOutputPort);
|
||||
return presenter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { DashboardOverviewPresenter } from './DashboardOverviewPresenter';
|
||||
import type { DashboardOverviewOutputPort } from '@core/racing/application/ports/output/DashboardOverviewOutputPort';
|
||||
|
||||
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: {
|
||||
id: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'League 1',
|
||||
track: 'Spa',
|
||||
car: 'GT3',
|
||||
scheduledAt: '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,
|
||||
},
|
||||
],
|
||||
leagueStandingsSummaries: [
|
||||
{
|
||||
leagueId: 'league-1',
|
||||
leagueName: 'League 1',
|
||||
position: 1,
|
||||
totalDrivers: 20,
|
||||
points: 150,
|
||||
},
|
||||
],
|
||||
feedSummary: {
|
||||
notificationCount: 3,
|
||||
items: [
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
friends: [
|
||||
{
|
||||
id: 'friend-1',
|
||||
name: 'Friend One',
|
||||
country: 'US',
|
||||
avatarUrl: 'https://example.com/friend.jpg',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
describe('DashboardOverviewPresenter', () => {
|
||||
let presenter: DashboardOverviewPresenter;
|
||||
|
||||
beforeEach(() => {
|
||||
presenter = new DashboardOverviewPresenter();
|
||||
});
|
||||
|
||||
it('maps DashboardOverviewOutputPort to DashboardOverviewDTO correctly', () => {
|
||||
const output = createOutput();
|
||||
|
||||
presenter.present(output);
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import type { DashboardOverviewOutputPort } from '@core/racing/application/ports/output/DashboardOverviewOutputPort';
|
||||
import {
|
||||
DashboardOverviewDTO,
|
||||
DashboardDriverSummaryDTO,
|
||||
DashboardRaceSummaryDTO,
|
||||
DashboardRecentResultDTO,
|
||||
DashboardLeagueStandingSummaryDTO,
|
||||
DashboardFeedSummaryDTO,
|
||||
DashboardFeedItemSummaryDTO,
|
||||
DashboardFriendSummaryDTO,
|
||||
} from '../dtos/DashboardOverviewDTO';
|
||||
|
||||
export class DashboardOverviewPresenter {
|
||||
private result: DashboardOverviewDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
present(output: DashboardOverviewOutputPort): void {
|
||||
const currentDriver: DashboardDriverSummaryDTO | null = output.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,
|
||||
}
|
||||
: 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 myUpcomingRaces: DashboardRaceSummaryDTO[] = output.myUpcomingRaces.map(mapRace);
|
||||
const otherUpcomingRaces: DashboardRaceSummaryDTO[] = output.otherUpcomingRaces.map(mapRace);
|
||||
const upcomingRaces: DashboardRaceSummaryDTO[] = output.upcomingRaces.map(mapRace);
|
||||
|
||||
const nextRace: DashboardRaceSummaryDTO | null = output.nextRace ? mapRace(output.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 leagueStandingsSummaries: DashboardLeagueStandingSummaryDTO[] =
|
||||
output.leagueStandingsSummaries.map(standing => ({
|
||||
leagueId: standing.leagueId,
|
||||
leagueName: standing.leagueName,
|
||||
position: standing.position,
|
||||
totalDrivers: standing.totalDrivers,
|
||||
points: standing.points,
|
||||
}));
|
||||
|
||||
const feedItems: DashboardFeedItemSummaryDTO[] = output.feedSummary.items.map(item => ({
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
headline: item.headline,
|
||||
body: item.body,
|
||||
timestamp: item.timestamp,
|
||||
ctaLabel: item.ctaLabel,
|
||||
ctaHref: item.ctaHref,
|
||||
}));
|
||||
|
||||
const feedSummary: DashboardFeedSummaryDTO = {
|
||||
notificationCount: output.feedSummary.notificationCount,
|
||||
items: feedItems,
|
||||
};
|
||||
|
||||
const friends: DashboardFriendSummaryDTO[] = output.friends.map(friend => ({
|
||||
id: friend.id,
|
||||
name: friend.name,
|
||||
country: friend.country,
|
||||
avatarUrl: friend.avatarUrl,
|
||||
}));
|
||||
|
||||
this.result = {
|
||||
currentDriver,
|
||||
myUpcomingRaces,
|
||||
otherUpcomingRaces,
|
||||
upcomingRaces,
|
||||
activeLeaguesCount: output.activeLeaguesCount,
|
||||
nextRace,
|
||||
recentResults,
|
||||
leagueStandingsSummaries,
|
||||
feedSummary,
|
||||
friends,
|
||||
};
|
||||
}
|
||||
|
||||
get viewModel(): DashboardOverviewDTO {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
|
||||
getViewModel(): DashboardOverviewDTO | null {
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user