refactor
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { DashboardController } from './DashboardController';
|
||||
import { DashboardService } from './DashboardService';
|
||||
import { DashboardOverviewDTO } from './dtos/DashboardOverviewDTO';
|
||||
|
||||
describe('DashboardController', () => {
|
||||
@@ -13,7 +11,7 @@ describe('DashboardController', () => {
|
||||
getDashboardOverview: vi.fn(),
|
||||
};
|
||||
|
||||
controller = new DashboardController(mockService as any);
|
||||
controller = new DashboardController(mockService as never);
|
||||
});
|
||||
|
||||
describe('getDashboardOverview', () => {
|
||||
|
||||
@@ -13,7 +13,6 @@ 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> {
|
||||
const presenter = await this.dashboardService.getDashboardOverview(driverId);
|
||||
return presenter.viewModel;
|
||||
return this.dashboardService.getDashboardOverview(driverId);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import { DashboardService } from './DashboardService';
|
||||
|
||||
// Import core interfaces
|
||||
import type { Logger } from '@core/shared/application/Logger';
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import type { DashboardOverviewResult } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
|
||||
import { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
|
||||
import { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
|
||||
import { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
|
||||
@@ -12,7 +13,8 @@ import { ILeagueMembershipRepository } from '@core/racing/domain/repositories/IL
|
||||
import { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
|
||||
import { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
import { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
|
||||
import { ImageServicePort } from '@core/media/application/ports/ImageServicePort';
|
||||
import { DashboardOverviewUseCase } from '@core/racing/application/use-cases/DashboardOverviewUseCase';
|
||||
|
||||
// Import concrete implementations
|
||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
@@ -24,25 +26,31 @@ import { InMemoryStandingRepository } from '@adapters/racing/persistence/inmemor
|
||||
import { InMemoryLeagueMembershipRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueMembershipRepository';
|
||||
import { InMemoryRaceRegistrationRepository } from '@adapters/racing/persistence/inmemory/InMemoryRaceRegistrationRepository';
|
||||
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
|
||||
import { DashboardOverviewPresenter } from './presenters/DashboardOverviewPresenter';
|
||||
|
||||
// Simple mock implementations for missing adapters
|
||||
class MockFeedRepository implements IFeedRepository {
|
||||
async getFeedForDriver(driverId: string, limit?: number) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async getFeedForDriver(_driverId: string, _limit?: number) {
|
||||
return [];
|
||||
}
|
||||
async getGlobalFeed(limit?: number) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async getGlobalFeed(_limit?: number) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
class MockSocialGraphRepository implements ISocialGraphRepository {
|
||||
async getFriends(driverId: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async getFriends(_driverId: string) {
|
||||
return [];
|
||||
}
|
||||
async getFriendIds(driverId: string) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async getFriendIds(_driverId: string) {
|
||||
return [];
|
||||
}
|
||||
async getSuggestedFriends(driverId: string, limit?: number) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async getSuggestedFriends(_driverId: string, _limit?: number) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -59,8 +67,11 @@ export const RACE_REGISTRATION_REPOSITORY_TOKEN = 'IRaceRegistrationRepository';
|
||||
export const FEED_REPOSITORY_TOKEN = 'IFeedRepository';
|
||||
export const SOCIAL_GRAPH_REPOSITORY_TOKEN = 'ISocialGraphRepository';
|
||||
export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
|
||||
export const DASHBOARD_OVERVIEW_USE_CASE_TOKEN = 'DashboardOverviewUseCase';
|
||||
export const DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN = 'DashboardOverviewOutputPort';
|
||||
|
||||
export const DashboardProviders: Provider[] = [
|
||||
DashboardOverviewPresenter,
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
@@ -113,4 +124,51 @@ export const DashboardProviders: Provider[] = [
|
||||
useFactory: (logger: Logger) => new InMemoryImageServiceAdapter(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
|
||||
useExisting: DashboardOverviewPresenter,
|
||||
},
|
||||
{
|
||||
provide: DASHBOARD_OVERVIEW_USE_CASE_TOKEN,
|
||||
useFactory: (
|
||||
driverRepo: IDriverRepository,
|
||||
raceRepo: IRaceRepository,
|
||||
resultRepo: IResultRepository,
|
||||
leagueRepo: ILeagueRepository,
|
||||
standingRepo: IStandingRepository,
|
||||
membershipRepo: ILeagueMembershipRepository,
|
||||
registrationRepo: IRaceRegistrationRepository,
|
||||
feedRepo: IFeedRepository,
|
||||
socialRepo: ISocialGraphRepository,
|
||||
imageService: ImageServicePort,
|
||||
output: UseCaseOutputPort<DashboardOverviewResult>,
|
||||
) =>
|
||||
new DashboardOverviewUseCase(
|
||||
driverRepo,
|
||||
raceRepo,
|
||||
resultRepo,
|
||||
leagueRepo,
|
||||
standingRepo,
|
||||
membershipRepo,
|
||||
registrationRepo,
|
||||
feedRepo,
|
||||
socialRepo,
|
||||
async (driverId: string) => imageService.getDriverAvatar(driverId),
|
||||
() => null,
|
||||
output,
|
||||
),
|
||||
inject: [
|
||||
DRIVER_REPOSITORY_TOKEN,
|
||||
RACE_REPOSITORY_TOKEN,
|
||||
RESULT_REPOSITORY_TOKEN,
|
||||
LEAGUE_REPOSITORY_TOKEN,
|
||||
STANDING_REPOSITORY_TOKEN,
|
||||
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
RACE_REGISTRATION_REPOSITORY_TOKEN,
|
||||
FEED_REPOSITORY_TOKEN,
|
||||
SOCIAL_GRAPH_REPOSITORY_TOKEN,
|
||||
IMAGE_SERVICE_TOKEN,
|
||||
DASHBOARD_OVERVIEW_OUTPUT_PORT_TOKEN,
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,80 +1,31 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
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';
|
||||
import type { IDriverRepository } from '@core/racing/domain/repositories/IDriverRepository';
|
||||
import type { IRaceRepository } from '@core/racing/domain/repositories/IRaceRepository';
|
||||
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
|
||||
import type { ILeagueRepository } from '@core/racing/domain/repositories/ILeagueRepository';
|
||||
import type { IStandingRepository } from '@core/racing/domain/repositories/IStandingRepository';
|
||||
import type { ILeagueMembershipRepository } from '@core/racing/domain/repositories/ILeagueMembershipRepository';
|
||||
import type { IRaceRegistrationRepository } from '@core/racing/domain/repositories/IRaceRegistrationRepository';
|
||||
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
|
||||
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
import type { IImageServicePort } from '@core/racing/application/ports/IImageServicePort';
|
||||
|
||||
// Tokens
|
||||
import {
|
||||
LOGGER_TOKEN,
|
||||
DRIVER_REPOSITORY_TOKEN,
|
||||
RACE_REPOSITORY_TOKEN,
|
||||
RESULT_REPOSITORY_TOKEN,
|
||||
LEAGUE_REPOSITORY_TOKEN,
|
||||
STANDING_REPOSITORY_TOKEN,
|
||||
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
RACE_REGISTRATION_REPOSITORY_TOKEN,
|
||||
FEED_REPOSITORY_TOKEN,
|
||||
SOCIAL_GRAPH_REPOSITORY_TOKEN,
|
||||
IMAGE_SERVICE_TOKEN,
|
||||
} from './DashboardProviders';
|
||||
import { LOGGER_TOKEN, DASHBOARD_OVERVIEW_USE_CASE_TOKEN } from './DashboardProviders';
|
||||
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
private readonly dashboardOverviewUseCase: DashboardOverviewUseCase;
|
||||
|
||||
constructor(
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
@Inject(DRIVER_REPOSITORY_TOKEN) private readonly driverRepository?: IDriverRepository,
|
||||
@Inject(RACE_REPOSITORY_TOKEN) private readonly raceRepository?: IRaceRepository,
|
||||
@Inject(RESULT_REPOSITORY_TOKEN) private readonly resultRepository?: IResultRepository,
|
||||
@Inject(LEAGUE_REPOSITORY_TOKEN) private readonly leagueRepository?: ILeagueRepository,
|
||||
@Inject(STANDING_REPOSITORY_TOKEN) private readonly standingRepository?: IStandingRepository,
|
||||
@Inject(LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN) private readonly leagueMembershipRepository?: ILeagueMembershipRepository,
|
||||
@Inject(RACE_REGISTRATION_REPOSITORY_TOKEN) private readonly raceRegistrationRepository?: IRaceRegistrationRepository,
|
||||
@Inject(FEED_REPOSITORY_TOKEN) private readonly feedRepository?: IFeedRepository,
|
||||
@Inject(SOCIAL_GRAPH_REPOSITORY_TOKEN) private readonly socialRepository?: ISocialGraphRepository,
|
||||
@Inject(IMAGE_SERVICE_TOKEN) private readonly imageService?: IImageServicePort,
|
||||
) {
|
||||
this.dashboardOverviewUseCase = new DashboardOverviewUseCase(
|
||||
driverRepository,
|
||||
raceRepository,
|
||||
resultRepository,
|
||||
leagueRepository,
|
||||
standingRepository,
|
||||
leagueMembershipRepository,
|
||||
raceRegistrationRepository,
|
||||
feedRepository,
|
||||
socialRepository,
|
||||
imageService,
|
||||
() => null, // getDriverStats
|
||||
);
|
||||
}
|
||||
@Inject(DASHBOARD_OVERVIEW_USE_CASE_TOKEN) private readonly dashboardOverviewUseCase: DashboardOverviewUseCase,
|
||||
private readonly dashboardOverviewPresenter: DashboardOverviewPresenter,
|
||||
) {}
|
||||
|
||||
async getDashboardOverview(driverId: string): Promise<DashboardOverviewPresenter> {
|
||||
async getDashboardOverview(driverId: string): Promise<DashboardOverviewDTO> {
|
||||
this.logger.debug('[DashboardService] Getting dashboard overview:', { driverId });
|
||||
|
||||
const result = await this.dashboardOverviewUseCase.execute({ driverId });
|
||||
|
||||
if (result.isErr()) {
|
||||
throw new Error(result.error?.message || 'Failed to get dashboard overview');
|
||||
throw new Error(result.unwrapErr().details?.message ?? 'Failed to get dashboard overview');
|
||||
}
|
||||
|
||||
const presenter = new DashboardOverviewPresenter();
|
||||
presenter.present(result.value as DashboardOverviewOutputPort);
|
||||
return presenter;
|
||||
return this.dashboardOverviewPresenter.getResponseModel();
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,10 @@ export class DashboardDriverSummaryDTO {
|
||||
@IsString()
|
||||
country!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
avatarUrl!: string;
|
||||
avatarUrl?: string | null;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@@ -52,13 +53,15 @@ export class DashboardRaceSummaryDTO {
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
leagueId!: string;
|
||||
leagueId?: string | null;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
leagueName!: string;
|
||||
leagueName?: string | null;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@@ -90,13 +93,15 @@ export class DashboardRecentResultDTO {
|
||||
@IsString()
|
||||
raceName!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
leagueId!: string;
|
||||
leagueId?: string | null;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
leagueName!: string;
|
||||
leagueName?: string | null;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@@ -120,17 +125,19 @@ export class DashboardLeagueStandingSummaryDTO {
|
||||
@IsString()
|
||||
leagueName!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
position!: number;
|
||||
position?: number | null;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
totalDrivers!: number;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
points!: number;
|
||||
points?: number | null;
|
||||
}
|
||||
|
||||
export class DashboardFeedItemSummaryDTO {
|
||||
@@ -191,9 +198,10 @@ export class DashboardFriendSummaryDTO {
|
||||
@IsString()
|
||||
country!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@ApiProperty({ nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
avatarUrl!: string;
|
||||
avatarUrl?: string | null;
|
||||
}
|
||||
|
||||
export class DashboardOverviewDTO {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user