Files
gridpilot.gg/core/racing/application/DashboardOverviewUseCase.test.ts
2025-12-16 12:14:06 +01:00

653 lines
26 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { GetDashboardOverviewUseCase } from '@core/racing/application/use-cases/GetDashboardOverviewUseCase';
import { Driver } from '@core/racing/domain/entities/Driver';
import { Race } from '@core/racing/domain/entities/Race';
import { Result } from '@core/racing/domain/entities/Result';
import { League } from '@core/racing/domain/entities/League';
import { Standing } from '@core/racing/domain/entities/Standing';
import { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
import type { FeedItem } from '@core/social/domain/types/FeedItem';
import type {
IDashboardOverviewPresenter,
DashboardOverviewViewModel,
DashboardFeedItemSummaryViewModel,
} from '@core/racing/application/presenters/IDashboardOverviewPresenter';
class FakeDashboardOverviewPresenter implements IDashboardOverviewPresenter {
viewModel: DashboardOverviewViewModel | null = null;
reset(): void {
this.viewModel = null;
}
present(viewModel: DashboardOverviewViewModel): void {
this.viewModel = viewModel;
}
getViewModel(): DashboardOverviewViewModel | null {
return this.viewModel;
}
}
interface TestImageService {
getDriverAvatar(driverId: string): string;
getTeamLogo(teamId: string): string;
getLeagueCover(leagueId: string): string;
getLeagueLogo(leagueId: string): string;
}
function createTestImageService(): TestImageService {
return {
getDriverAvatar: (driverId: string) => `avatar-${driverId}`,
getTeamLogo: (teamId: string) => `team-logo-${teamId}`,
getLeagueCover: (leagueId: string) => `league-cover-${leagueId}`,
getLeagueLogo: (leagueId: string) => `league-logo-${leagueId}`,
};
}
describe('GetDashboardOverviewUseCase', () => {
it('partitions upcoming races into myUpcomingRaces and otherUpcomingRaces and selects nextRace from myUpcomingRaces', async () => {
// Given a driver with memberships in two leagues and future races with mixed registration
const driverId = 'driver-1';
const driver = Driver.create({ id: driverId, iracingId: '12345', name: 'Alice Racer', country: 'US' });
const leagues = [
League.create({ id: 'league-1', name: 'Alpha League', description: 'First league', ownerId: 'owner-1' }),
League.create({ id: 'league-2', name: 'Beta League', description: 'Second league', ownerId: 'owner-2' }),
];
const now = Date.now();
const races = [
Race.create({
id: 'race-1',
leagueId: 'league-1',
track: 'Monza',
car: 'GT3',
scheduledAt: new Date(now + 60 * 60 * 1000),
status: 'scheduled',
}),
Race.create({
id: 'race-2',
leagueId: 'league-1',
track: 'Spa',
car: 'GT3',
scheduledAt: new Date(now + 2 * 60 * 60 * 1000),
status: 'scheduled',
}),
Race.create({
id: 'race-3',
leagueId: 'league-2',
track: 'Silverstone',
car: 'GT4',
scheduledAt: new Date(now + 3 * 60 * 60 * 1000),
status: 'scheduled',
}),
Race.create({
id: 'race-4',
leagueId: 'league-2',
track: 'Imola',
car: 'GT4',
scheduledAt: new Date(now + 4 * 60 * 60 * 1000),
status: 'scheduled',
}),
];
const results: Result[] = [];
const memberships = [
LeagueMembership.create({
leagueId: 'league-1',
driverId,
role: 'member',
status: 'active',
}),
LeagueMembership.create({
leagueId: 'league-2',
driverId,
role: 'member',
status: 'active',
}),
];
const registeredRaceIds = new Set<string>(['race-1', 'race-3']);
const feedItems: FeedItem[] = [];
const friends: Driver[] = [];
const driverRepository = {
findById: async (id: string): Promise<Driver | null> => (id === driver.id ? driver : null),
findByIRacingId: async (): Promise<Driver | null> => null,
findAll: async (): Promise<Driver[]> => [],
create: async (): Promise<Driver> => { throw new Error('Not implemented'); },
update: async (): Promise<Driver> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByIRacingId: async (): Promise<boolean> => false,
};
const raceRepository = {
findById: async (): Promise<Race | null> => null,
findAll: async (): Promise<Race[]> => races,
findByLeagueId: async (): Promise<Race[]> => [],
findUpcomingByLeagueId: async (): Promise<Race[]> => [],
findCompletedByLeagueId: async (): Promise<Race[]> => [],
findByStatus: async (): Promise<Race[]> => [],
findByDateRange: async (): Promise<Race[]> => [],
create: async (): Promise<Race> => { throw new Error('Not implemented'); },
update: async (): Promise<Race> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
};
const resultRepository = {
findById: async (): Promise<Result | null> => null,
findAll: async (): Promise<Result[]> => results,
findByRaceId: async (): Promise<Result[]> => [],
findByDriverId: async (): Promise<Result[]> => [],
findByDriverIdAndLeagueId: async (): Promise<Result[]> => [],
create: async (): Promise<Result> => { throw new Error('Not implemented'); },
createMany: async (): Promise<Result[]> => { throw new Error('Not implemented'); },
update: async (): Promise<Result> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByRaceId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByRaceId: async (): Promise<boolean> => false,
};
const leagueRepository = {
findById: async (): Promise<League | null> => null,
findAll: async (): Promise<League[]> => leagues,
findByOwnerId: async (): Promise<League[]> => [],
create: async (): Promise<League> => { throw new Error('Not implemented'); },
update: async (): Promise<League> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
searchByName: async (): Promise<League[]> => [],
};
const standingRepository = {
findByLeagueId: async (): Promise<Standing[]> => [],
findByDriverIdAndLeagueId: async (): Promise<Standing | null> => null,
findAll: async (): Promise<Standing[]> => [],
save: async (): Promise<Standing> => { throw new Error('Not implemented'); },
saveMany: async (): Promise<Standing[]> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByLeagueId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
recalculate: async (): Promise<Standing[]> => [],
};
const leagueMembershipRepository = {
getMembership: async (leagueId: string, driverIdParam: string): Promise<LeagueMembership | null> => {
return (
memberships.find(
(m) => m.leagueId === leagueId && m.driverId === driverIdParam,
) ?? null
);
},
getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
getJoinRequests: async (): Promise<any[]> => [],
saveMembership: async (): Promise<LeagueMembership> => { throw new Error('Not implemented'); },
removeMembership: async (): Promise<void> => { throw new Error('Not implemented'); },
saveJoinRequest: async (): Promise<any> => { throw new Error('Not implemented'); },
removeJoinRequest: async (): Promise<void> => { throw new Error('Not implemented'); },
};
const raceRegistrationRepository = {
isRegistered: async (raceId: string, driverIdParam: string): Promise<boolean> => {
if (driverIdParam !== driverId) return false;
return registeredRaceIds.has(raceId);
},
getRegisteredDrivers: async (): Promise<string[]> => [],
getRegistrationCount: async (): Promise<number> => 0,
register: async (): Promise<void> => { throw new Error('Not implemented'); },
withdraw: async (): Promise<void> => { throw new Error('Not implemented'); },
getDriverRegistrations: async (): Promise<string[]> => [],
clearRaceRegistrations: async (): Promise<void> => { throw new Error('Not implemented'); },
};
const feedRepository = {
getFeedForDriver: async (): Promise<FeedItem[]> => feedItems,
getGlobalFeed: async (): Promise<FeedItem[]> => [],
};
const socialRepository = {
getFriends: async (): Promise<Driver[]> => friends,
getFriendIds: async (): Promise<string[]> => [],
getSuggestedFriends: async (): Promise<Driver[]> => [],
};
const imageService = createTestImageService();
const getDriverStats = (id: string) =>
id === driverId
? {
rating: 1600,
wins: 5,
podiums: 12,
totalRaces: 40,
overallRank: 42,
consistency: 88,
}
: null;
const presenter = new FakeDashboardOverviewPresenter();
const useCase = new GetDashboardOverviewUseCase(
driverRepository,
raceRepository,
resultRepository,
leagueRepository,
standingRepository,
leagueMembershipRepository,
raceRegistrationRepository,
feedRepository,
socialRepository,
imageService,
getDriverStats,
);
// When
await useCase.execute({ driverId }, presenter);
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();
const vm = viewModel!;
// Then myUpcomingRaces only contains registered races from the driver's leagues
expect(vm.myUpcomingRaces.map((r) => r.id)).toEqual(['race-1', 'race-3']);
// And otherUpcomingRaces contains the other upcoming races in those leagues
expect(vm.otherUpcomingRaces.map((r) => r.id)).toEqual(['race-2', 'race-4']);
// And nextRace is the earliest upcoming race from myUpcomingRaces
expect(vm.nextRace).not.toBeNull();
expect(vm.nextRace!.id).toBe('race-1');
});
it('builds recentResults sorted by date descending and leagueStandingsSummaries from standings', async () => {
// Given completed races with results and standings
const driverId = 'driver-2';
const driver = Driver.create({ id: driverId, iracingId: '67890', name: 'Result Driver', country: 'DE' });
const leagues = [
League.create({ id: 'league-A', name: 'Results League A', description: 'League A', ownerId: 'owner-A' }),
League.create({ id: 'league-B', name: 'Results League B', description: 'League B', ownerId: 'owner-B' }),
];
const raceOld = Race.create({
id: 'race-old',
leagueId: 'league-A',
track: 'Old Circuit',
car: 'GT3',
scheduledAt: new Date('2024-01-01T10:00:00Z'),
status: 'completed',
});
const raceNew = Race.create({
id: 'race-new',
leagueId: 'league-B',
track: 'New Circuit',
car: 'GT4',
scheduledAt: new Date('2024-02-01T10:00:00Z'),
status: 'completed',
});
const races = [raceOld, raceNew];
const results = [
Result.create({
id: 'result-old',
raceId: raceOld.id,
driverId,
position: 5,
fastestLap: 120,
incidents: 3,
startPosition: 5,
}),
Result.create({
id: 'result-new',
raceId: raceNew.id,
driverId,
position: 2,
fastestLap: 115,
incidents: 1,
startPosition: 2,
}),
];
const memberships = [
LeagueMembership.create({
leagueId: 'league-A',
driverId,
role: 'member',
status: 'active',
}),
LeagueMembership.create({
leagueId: 'league-B',
driverId,
role: 'member',
status: 'active',
}),
];
const standingsByLeague = new Map<
string,
Standing[]
>();
standingsByLeague.set('league-A', [
Standing.create({ leagueId: 'league-A', driverId, position: 3, points: 50 }),
Standing.create({ leagueId: 'league-A', driverId: 'other-1', position: 1, points: 80 }),
]);
standingsByLeague.set('league-B', [
Standing.create({ leagueId: 'league-B', driverId, position: 1, points: 100 }),
Standing.create({ leagueId: 'league-B', driverId: 'other-2', position: 2, points: 90 }),
]);
const driverRepository = {
findById: async (id: string): Promise<Driver | null> => (id === driver.id ? driver : null),
findByIRacingId: async (): Promise<Driver | null> => null,
findAll: async (): Promise<Driver[]> => [],
create: async (): Promise<Driver> => { throw new Error('Not implemented'); },
update: async (): Promise<Driver> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByIRacingId: async (): Promise<boolean> => false,
};
const raceRepository = {
findById: async (): Promise<Race | null> => null,
findAll: async (): Promise<Race[]> => races,
findByLeagueId: async (): Promise<Race[]> => [],
findUpcomingByLeagueId: async (): Promise<Race[]> => [],
findCompletedByLeagueId: async (): Promise<Race[]> => [],
findByStatus: async (): Promise<Race[]> => [],
findByDateRange: async (): Promise<Race[]> => [],
create: async (): Promise<Race> => { throw new Error('Not implemented'); },
update: async (): Promise<Race> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
};
const resultRepository = {
findById: async (): Promise<Result | null> => null,
findAll: async (): Promise<Result[]> => results,
findByRaceId: async (): Promise<Result[]> => [],
findByDriverId: async (): Promise<Result[]> => [],
findByDriverIdAndLeagueId: async (): Promise<Result[]> => [],
create: async (): Promise<Result> => { throw new Error('Not implemented'); },
createMany: async (): Promise<Result[]> => { throw new Error('Not implemented'); },
update: async (): Promise<Result> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByRaceId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByRaceId: async (): Promise<boolean> => false,
};
const leagueRepository = {
findById: async (): Promise<League | null> => null,
findAll: async (): Promise<League[]> => leagues,
findByOwnerId: async (): Promise<League[]> => [],
create: async (): Promise<League> => { throw new Error('Not implemented'); },
update: async (): Promise<League> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
searchByName: async (): Promise<League[]> => [],
};
const standingRepository = {
findByLeagueId: async (leagueId: string): Promise<Standing[]> =>
standingsByLeague.get(leagueId) ?? [],
findByDriverIdAndLeagueId: async (): Promise<Standing | null> => null,
findAll: async (): Promise<Standing[]> => [],
save: async (): Promise<Standing> => { throw new Error('Not implemented'); },
saveMany: async (): Promise<Standing[]> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByLeagueId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
recalculate: async (): Promise<Standing[]> => [],
};
const leagueMembershipRepository = {
getMembership: async (leagueId: string, driverIdParam: string): Promise<LeagueMembership | null> => {
return (
memberships.find(
(m) => m.leagueId === leagueId && m.driverId === driverIdParam,
) ?? null
);
},
getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
getJoinRequests: async (): Promise<any[]> => [],
saveMembership: async (): Promise<LeagueMembership> => { throw new Error('Not implemented'); },
removeMembership: async (): Promise<void> => { throw new Error('Not implemented'); },
saveJoinRequest: async (): Promise<any> => { throw new Error('Not implemented'); },
removeJoinRequest: async (): Promise<void> => { throw new Error('Not implemented'); },
};
const raceRegistrationRepository = {
isRegistered: async (): Promise<boolean> => false,
getRegisteredDrivers: async (): Promise<string[]> => [],
getRegistrationCount: async (): Promise<number> => 0,
register: async (): Promise<void> => { throw new Error('Not implemented'); },
withdraw: async (): Promise<void> => { throw new Error('Not implemented'); },
getDriverRegistrations: async (): Promise<string[]> => [],
clearRaceRegistrations: async (): Promise<void> => { throw new Error('Not implemented'); },
};
const feedRepository = {
getFeedForDriver: async (): Promise<FeedItem[]> => [],
getGlobalFeed: async (): Promise<FeedItem[]> => [],
};
const socialRepository = {
getFriends: async (): Promise<Driver[]> => [],
getFriendIds: async (): Promise<string[]> => [],
getSuggestedFriends: async (): Promise<Driver[]> => [],
};
const imageService = createTestImageService();
const getDriverStats = (id: string) =>
id === driverId
? {
rating: 1800,
wins: 3,
podiums: 7,
totalRaces: 20,
overallRank: 10,
consistency: 92,
}
: null;
const presenter = new FakeDashboardOverviewPresenter();
const useCase = new GetDashboardOverviewUseCase(
driverRepository,
raceRepository,
resultRepository,
leagueRepository,
standingRepository,
leagueMembershipRepository,
raceRegistrationRepository,
feedRepository,
socialRepository,
imageService,
getDriverStats,
);
// When
await useCase.execute({ driverId }, presenter);
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();
const vm = viewModel!;
// Then recentResults are sorted by finishedAt descending (newest first)
expect(vm.recentResults.length).toBe(2);
expect(vm.recentResults[0]!.raceId).toBe('race-new');
expect(vm.recentResults[1]!.raceId).toBe('race-old');
// And leagueStandingsSummaries reflect the driver's position and points per league
const summariesByLeague = new Map(
vm.leagueStandingsSummaries.map((s) => [s.leagueId, s]),
);
const summaryA = summariesByLeague.get('league-A');
const summaryB = summariesByLeague.get('league-B');
expect(summaryA).toBeDefined();
expect(summaryA!.position).toBe(3);
expect(summaryA!.points).toBe(50);
expect(summaryA!.totalDrivers).toBe(2);
expect(summaryB).toBeDefined();
expect(summaryB!.position).toBe(1);
expect(summaryB!.points).toBe(100);
expect(summaryB!.totalDrivers).toBe(2);
});
it('returns empty collections and safe defaults when driver has no races or standings', async () => {
// Given a driver with no related data
const driverId = 'driver-empty';
const driver = Driver.create({ id: driverId, iracingId: '11111', name: 'New Racer', country: 'FR' });
const driverRepository = {
findById: async (id: string): Promise<Driver | null> => (id === driver.id ? driver : null),
findByIRacingId: async (): Promise<Driver | null> => null,
findAll: async (): Promise<Driver[]> => [],
create: async (): Promise<Driver> => { throw new Error('Not implemented'); },
update: async (): Promise<Driver> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByIRacingId: async (): Promise<boolean> => false,
};
const raceRepository = {
findById: async (): Promise<Race | null> => null,
findAll: async (): Promise<Race[]> => [],
findByLeagueId: async (): Promise<Race[]> => [],
findUpcomingByLeagueId: async (): Promise<Race[]> => [],
findCompletedByLeagueId: async (): Promise<Race[]> => [],
findByStatus: async (): Promise<Race[]> => [],
findByDateRange: async (): Promise<Race[]> => [],
create: async (): Promise<Race> => { throw new Error('Not implemented'); },
update: async (): Promise<Race> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
};
const resultRepository = {
findById: async (): Promise<Result | null> => null,
findAll: async (): Promise<Result[]> => [],
findByRaceId: async (): Promise<Result[]> => [],
findByDriverId: async (): Promise<Result[]> => [],
findByDriverIdAndLeagueId: async (): Promise<Result[]> => [],
create: async (): Promise<Result> => { throw new Error('Not implemented'); },
createMany: async (): Promise<Result[]> => { throw new Error('Not implemented'); },
update: async (): Promise<Result> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByRaceId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
existsByRaceId: async (): Promise<boolean> => false,
};
const leagueRepository = {
findById: async (): Promise<League | null> => null,
findAll: async (): Promise<League[]> => [],
findByOwnerId: async (): Promise<League[]> => [],
create: async (): Promise<League> => { throw new Error('Not implemented'); },
update: async (): Promise<League> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
searchByName: async (): Promise<League[]> => [],
};
const standingRepository = {
findByLeagueId: async (): Promise<Standing[]> => [],
findByDriverIdAndLeagueId: async (): Promise<Standing | null> => null,
findAll: async (): Promise<Standing[]> => [],
save: async (): Promise<Standing> => { throw new Error('Not implemented'); },
saveMany: async (): Promise<Standing[]> => { throw new Error('Not implemented'); },
delete: async (): Promise<void> => { throw new Error('Not implemented'); },
deleteByLeagueId: async (): Promise<void> => { throw new Error('Not implemented'); },
exists: async (): Promise<boolean> => false,
recalculate: async (): Promise<Standing[]> => [],
};
const leagueMembershipRepository = {
getMembership: async (): Promise<LeagueMembership | null> => null,
getLeagueMembers: async (): Promise<LeagueMembership[]> => [],
getJoinRequests: async (): Promise<any[]> => [],
saveMembership: async (): Promise<LeagueMembership> => { throw new Error('Not implemented'); },
removeMembership: async (): Promise<void> => { throw new Error('Not implemented'); },
saveJoinRequest: async (): Promise<any> => { throw new Error('Not implemented'); },
removeJoinRequest: async (): Promise<void> => { throw new Error('Not implemented'); },
};
const raceRegistrationRepository = {
isRegistered: async (): Promise<boolean> => false,
getRegisteredDrivers: async (): Promise<string[]> => [],
getRegistrationCount: async (): Promise<number> => 0,
register: async (): Promise<void> => { throw new Error('Not implemented'); },
withdraw: async (): Promise<void> => { throw new Error('Not implemented'); },
getDriverRegistrations: async (): Promise<string[]> => [],
clearRaceRegistrations: async (): Promise<void> => { throw new Error('Not implemented'); },
};
const feedRepository = {
getFeedForDriver: async (): Promise<FeedItem[]> => [],
getGlobalFeed: async (): Promise<FeedItem[]> => [],
};
const socialRepository = {
getFriends: async (): Promise<Driver[]> => [],
getFriendIds: async (): Promise<string[]> => [],
getSuggestedFriends: async (): Promise<Driver[]> => [],
};
const imageService = createTestImageService();
const getDriverStats = () => null;
const presenter = new FakeDashboardOverviewPresenter();
const useCase = new GetDashboardOverviewUseCase(
driverRepository,
raceRepository,
resultRepository,
leagueRepository,
standingRepository,
leagueMembershipRepository,
raceRegistrationRepository,
feedRepository,
socialRepository,
imageService,
getDriverStats,
);
// When
await useCase.execute({ driverId }, presenter);
const viewModel = presenter.getViewModel();
expect(viewModel).not.toBeNull();
const vm = viewModel!;
// Then collections are empty and no errors are thrown
expect(vm.myUpcomingRaces).toEqual([]);
expect(vm.otherUpcomingRaces).toEqual([]);
expect(vm.nextRace).toBeNull();
expect(vm.recentResults).toEqual([]);
expect(vm.leagueStandingsSummaries).toEqual([]);
expect(vm.friends).toEqual([]);
expect(vm.feedSummary.notificationCount).toBe(0);
expect(vm.feedSummary.items).toEqual([]);
});
});