import { describe, it, expect } from 'vitest'; import { GetDashboardOverviewUseCase } from '@gridpilot/racing/application/use-cases/GetDashboardOverviewUseCase'; import type { IDashboardOverviewPresenter, DashboardOverviewViewModel, DashboardFeedItemSummaryViewModel, } from '@gridpilot/racing/application/presenters/IDashboardOverviewPresenter'; class FakeDashboardOverviewPresenter implements IDashboardOverviewPresenter { viewModel: DashboardOverviewViewModel | null = null; present(viewModel: DashboardOverviewViewModel): void { this.viewModel = viewModel; } getViewModel(): DashboardOverviewViewModel | null { return this.viewModel; } } interface TestImageService { getDriverAvatar(driverId: string): string; } function createTestImageService(): TestImageService { return { getDriverAvatar: (driverId: string) => `avatar-${driverId}`, }; } 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 = { id: driverId, name: 'Alice Racer', country: 'US' }; const leagues = [ { id: 'league-1', name: 'Alpha League' }, { id: 'league-2', name: 'Beta League' }, ]; const now = Date.now(); const races = [ { id: 'race-1', leagueId: 'league-1', track: 'Monza', car: 'GT3', scheduledAt: new Date(now + 60 * 60 * 1000), status: 'scheduled' as const, }, { id: 'race-2', leagueId: 'league-1', track: 'Spa', car: 'GT3', scheduledAt: new Date(now + 2 * 60 * 60 * 1000), status: 'scheduled' as const, }, { id: 'race-3', leagueId: 'league-2', track: 'Silverstone', car: 'GT4', scheduledAt: new Date(now + 3 * 60 * 60 * 1000), status: 'scheduled' as const, }, { id: 'race-4', leagueId: 'league-2', track: 'Imola', car: 'GT4', scheduledAt: new Date(now + 4 * 60 * 60 * 1000), status: 'scheduled' as const, }, ]; const results: unknown[] = []; const memberships = [ { leagueId: 'league-1', driverId, status: 'active', }, { leagueId: 'league-2', driverId, status: 'active', }, ]; const registeredRaceIds = new Set(['race-1', 'race-3']); const feedItems: DashboardFeedItemSummaryViewModel[] = []; const friends: Array<{ id: string }> = []; const driverRepository: { findById: (id: string) => Promise<{ id: string; name: string; country: string } | null>; } = { findById: async (id: string) => (id === driver.id ? driver : null), }; const raceRepository: { findAll: () => Promise< Array<{ id: string; leagueId: string; track: string; car: string; scheduledAt: Date; status: 'scheduled'; }> >; } = { findAll: async () => races, }; const resultRepository: { findAll: () => Promise; } = { findAll: async () => results, }; const leagueRepository: { findAll: () => Promise>; } = { findAll: async () => leagues, }; const standingRepository: { findByLeagueId: (leagueId: string) => Promise; } = { findByLeagueId: async () => [], }; const leagueMembershipRepository: { getMembership: ( leagueId: string, driverIdParam: string, ) => Promise<{ leagueId: string; driverId: string; status: string } | null>; } = { getMembership: async (leagueId: string, driverIdParam: string) => { return ( memberships.find( (m) => m.leagueId === leagueId && m.driverId === driverIdParam, ) ?? null ); }, }; const raceRegistrationRepository: { isRegistered: (raceId: string, driverIdParam: string) => Promise; } = { isRegistered: async (raceId: string, driverIdParam: string) => { if (driverIdParam !== driverId) return false; return registeredRaceIds.has(raceId); }, }; const feedRepository: { getFeedForDriver: (driverIdParam: string) => Promise; } = { getFeedForDriver: async () => feedItems, }; const socialRepository: { getFriends: (driverIdParam: string) => Promise>; } = { getFriends: async () => friends, }; 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, presenter, ); // When await useCase.execute({ driverId }); 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 = { id: driverId, name: 'Result Driver', country: 'DE' }; const leagues = [ { id: 'league-A', name: 'Results League A' }, { id: 'league-B', name: 'Results League B' }, ]; const raceOld = { id: 'race-old', leagueId: 'league-A', track: 'Old Circuit', car: 'GT3', scheduledAt: new Date('2024-01-01T10:00:00Z'), status: 'completed' as const, }; const raceNew = { id: 'race-new', leagueId: 'league-B', track: 'New Circuit', car: 'GT4', scheduledAt: new Date('2024-02-01T10:00:00Z'), status: 'completed' as const, }; const races = [raceOld, raceNew]; const results = [ { id: 'result-old', raceId: raceOld.id, driverId, position: 5, incidents: 3, }, { id: 'result-new', raceId: raceNew.id, driverId, position: 2, incidents: 1, }, ]; const memberships = [ { leagueId: 'league-A', driverId, status: 'active', }, { leagueId: 'league-B', driverId, status: 'active', }, ]; const standingsByLeague = new Map< string, Array<{ leagueId: string; driverId: string; position: number; points: number }> >(); standingsByLeague.set('league-A', [ { leagueId: 'league-A', driverId, position: 3, points: 50 }, { leagueId: 'league-A', driverId: 'other-1', position: 1, points: 80 }, ]); standingsByLeague.set('league-B', [ { leagueId: 'league-B', driverId, position: 1, points: 100 }, { leagueId: 'league-B', driverId: 'other-2', position: 2, points: 90 }, ]); const driverRepository: { findById: (id: string) => Promise<{ id: string; name: string; country: string } | null>; } = { findById: async (id: string) => (id === driver.id ? driver : null), }; const raceRepository: { findAll: () => Promise; } = { findAll: async () => races, }; const resultRepository: { findAll: () => Promise; } = { findAll: async () => results, }; const leagueRepository: { findAll: () => Promise; } = { findAll: async () => leagues, }; const standingRepository: { findByLeagueId: (leagueId: string) => Promise>; } = { findByLeagueId: async (leagueId: string) => standingsByLeague.get(leagueId) ?? [], }; const leagueMembershipRepository: { getMembership: ( leagueId: string, driverIdParam: string, ) => Promise<{ leagueId: string; driverId: string; status: string } | null>; } = { getMembership: async (leagueId: string, driverIdParam: string) => { return ( memberships.find( (m) => m.leagueId === leagueId && m.driverId === driverIdParam, ) ?? null ); }, }; const raceRegistrationRepository: { isRegistered: (raceId: string, driverIdParam: string) => Promise; } = { isRegistered: async () => false, }; const feedRepository: { getFeedForDriver: (driverIdParam: string) => Promise; } = { getFeedForDriver: async () => [], }; const socialRepository: { getFriends: (driverIdParam: string) => Promise>; } = { getFriends: async () => [], }; 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, presenter, ); // When await useCase.execute({ driverId }); 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 = { id: driverId, name: 'New Racer', country: 'FR' }; const driverRepository: { findById: (id: string) => Promise<{ id: string; name: string; country: string } | null>; } = { findById: async (id: string) => (id === driver.id ? driver : null), }; const raceRepository: { findAll: () => Promise } = { findAll: async () => [], }; const resultRepository: { findAll: () => Promise } = { findAll: async () => [], }; const leagueRepository: { findAll: () => Promise } = { findAll: async () => [], }; const standingRepository: { findByLeagueId: (leagueId: string) => Promise; } = { findByLeagueId: async () => [], }; const leagueMembershipRepository: { getMembership: (leagueId: string, driverIdParam: string) => Promise; } = { getMembership: async () => null, }; const raceRegistrationRepository: { isRegistered: (raceId: string, driverIdParam: string) => Promise; } = { isRegistered: async () => false, }; const feedRepository: { getFeedForDriver: (driverIdParam: string) => Promise; } = { getFeedForDriver: async () => [], }; const socialRepository: { getFriends: (driverIdParam: string) => Promise>; } = { getFriends: async () => [], }; 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, presenter, ); // When await useCase.execute({ driverId }); 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([]); }); });