/** * View Data Layer Tests - Dashboard Functionality * * This test file covers the view data layer for dashboard functionality. * * The view data layer is responsible for: * - DTO → UI model mapping * - Formatting, sorting, and grouping * - Derived fields and defaults * - UI-specific semantics * * This layer isolates the UI from API churn by providing a stable interface * between the API layer and the presentation layer. * * Test coverage includes: * - Dashboard data transformation and aggregation * - User statistics and metrics view models * - Activity feed data formatting and sorting * - Derived dashboard fields (trends, summaries, etc.) * - Default values and fallbacks for dashboard views * - Dashboard-specific formatting (dates, numbers, percentages, etc.) * - Data grouping and categorization for dashboard components * - Real-time data updates and state management */ import { DashboardViewDataBuilder } from '@/lib/builders/view-data/DashboardViewDataBuilder'; import { DashboardDateDisplay } from '@/lib/display-objects/DashboardDateDisplay'; import { DashboardCountDisplay } from '@/lib/display-objects/DashboardCountDisplay'; import { DashboardRankDisplay } from '@/lib/display-objects/DashboardRankDisplay'; import { DashboardConsistencyDisplay } from '@/lib/display-objects/DashboardConsistencyDisplay'; import { DashboardLeaguePositionDisplay } from '@/lib/display-objects/DashboardLeaguePositionDisplay'; import { RatingDisplay } from '@/lib/display-objects/RatingDisplay'; import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO'; import type { DashboardDriverSummaryDTO } from '@/lib/types/generated/DashboardDriverSummaryDTO'; import type { DashboardRaceSummaryDTO } from '@/lib/types/generated/DashboardRaceSummaryDTO'; import type { DashboardFeedSummaryDTO } from '@/lib/types/generated/DashboardFeedSummaryDTO'; import type { DashboardFriendSummaryDTO } from '@/lib/types/generated/DashboardFriendSummaryDTO'; import type { DashboardLeagueStandingSummaryDTO } from '@/lib/types/generated/DashboardLeagueStandingSummaryDTO'; describe('DashboardViewDataBuilder', () => { describe('happy paths', () => { it('should transform DashboardOverviewDTO to DashboardViewData correctly', () => { const dashboardDTO: DashboardOverviewDTO = { currentDriver: { id: 'driver-123', name: 'John Doe', country: 'USA', avatarUrl: 'https://example.com/avatar.jpg', rating: 1234.56, globalRank: 42, totalRaces: 150, wins: 25, podiums: 60, consistency: 85, }, myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 3, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 5, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.currentDriver).toEqual({ name: 'John Doe', avatarUrl: 'https://example.com/avatar.jpg', country: 'USA', rating: '1,235', rank: '42', totalRaces: '150', wins: '25', podiums: '60', consistency: '85%', }); expect(result.nextRace).toBeNull(); expect(result.upcomingRaces).toEqual([]); expect(result.leagueStandings).toEqual([]); expect(result.feedItems).toEqual([]); expect(result.friends).toEqual([]); expect(result.activeLeaguesCount).toBe('3'); expect(result.friendCount).toBe('0'); expect(result.hasUpcomingRaces).toBe(false); expect(result.hasLeagueStandings).toBe(false); expect(result.hasFeedItems).toBe(false); expect(result.hasFriends).toBe(false); }); it('should handle missing currentDriver gracefully', () => { const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 0, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.currentDriver).toEqual({ name: '', avatarUrl: '', country: '', rating: '0.0', rank: '0', totalRaces: '0', wins: '0', podiums: '0', consistency: '0%', }); }); it('should handle null/undefined driver fields', () => { const dashboardDTO: DashboardOverviewDTO = { currentDriver: { id: 'driver-123', name: 'Jane Smith', country: 'Canada', rating: null, globalRank: null, totalRaces: 0, wins: 0, podiums: 0, consistency: null, }, myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 0, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.currentDriver.rating).toBe('0'); expect(result.currentDriver.rank).toBe('0'); expect(result.currentDriver.consistency).toBe('0%'); }); it('should handle nextRace with all fields', () => { const now = new Date(); const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24 hours from now const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 1, nextRace: { id: 'race-456', leagueId: 'league-789', leagueName: 'Pro League', track: 'Monza', car: 'Ferrari 488 GT3', scheduledAt: futureDate.toISOString(), status: 'scheduled', isMyLeague: true, }, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.nextRace).not.toBeNull(); expect(result.nextRace?.id).toBe('race-456'); expect(result.nextRace?.track).toBe('Monza'); expect(result.nextRace?.car).toBe('Ferrari 488 GT3'); expect(result.nextRace?.scheduledAt).toBe(futureDate.toISOString()); expect(result.nextRace?.isMyLeague).toBe(true); expect(result.nextRace?.formattedDate).toBeDefined(); expect(result.nextRace?.formattedTime).toBeDefined(); expect(result.nextRace?.timeUntil).toBeDefined(); }); it('should handle upcomingRaces with multiple races', () => { const now = new Date(); const race1Date = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000); // 2 days from now const race2Date = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000); // 5 days from now const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [ { id: 'race-1', track: 'Spa', car: 'Porsche 911 GT3', scheduledAt: race1Date.toISOString(), status: 'scheduled', isMyLeague: true, }, { id: 'race-2', track: 'Nürburgring', car: 'Audi R8 LMS', scheduledAt: race2Date.toISOString(), status: 'scheduled', isMyLeague: false, }, ], activeLeaguesCount: 2, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.upcomingRaces).toHaveLength(2); expect(result.upcomingRaces[0].id).toBe('race-1'); expect(result.upcomingRaces[0].track).toBe('Spa'); expect(result.upcomingRaces[0].isMyLeague).toBe(true); expect(result.upcomingRaces[1].id).toBe('race-2'); expect(result.upcomingRaces[1].track).toBe('Nürburgring'); expect(result.upcomingRaces[1].isMyLeague).toBe(false); expect(result.hasUpcomingRaces).toBe(true); }); it('should handle leagueStandings with multiple leagues', () => { const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 2, nextRace: null, recentResults: [], leagueStandingsSummaries: [ { leagueId: 'league-1', leagueName: 'Rookie League', position: 5, totalDrivers: 50, points: 1250, }, { leagueId: 'league-2', leagueName: 'Pro League', position: 12, totalDrivers: 100, points: 890, }, ], feedSummary: { notificationCount: 0, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.leagueStandings).toHaveLength(2); expect(result.leagueStandings[0].leagueId).toBe('league-1'); expect(result.leagueStandings[0].leagueName).toBe('Rookie League'); expect(result.leagueStandings[0].position).toBe('#5'); expect(result.leagueStandings[0].points).toBe('1250'); expect(result.leagueStandings[0].totalDrivers).toBe('50'); expect(result.leagueStandings[1].leagueId).toBe('league-2'); expect(result.leagueStandings[1].leagueName).toBe('Pro League'); expect(result.leagueStandings[1].position).toBe('#12'); expect(result.leagueStandings[1].points).toBe('890'); expect(result.leagueStandings[1].totalDrivers).toBe('100'); expect(result.hasLeagueStandings).toBe(true); }); it('should handle feedItems with all fields', () => { const now = new Date(); const timestamp = new Date(now.getTime() - 30 * 60 * 1000); // 30 minutes ago const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 1, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 3, items: [ { id: 'feed-1', type: 'race_result', headline: 'Race completed', body: 'You finished 3rd in the Pro League race', timestamp: timestamp.toISOString(), ctaLabel: 'View Results', ctaHref: '/races/123', }, { id: 'feed-2', type: 'league_update', headline: 'League standings updated', body: 'You moved up 2 positions', timestamp: timestamp.toISOString(), }, ], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.feedItems).toHaveLength(2); expect(result.feedItems[0].id).toBe('feed-1'); expect(result.feedItems[0].type).toBe('race_result'); expect(result.feedItems[0].headline).toBe('Race completed'); expect(result.feedItems[0].body).toBe('You finished 3rd in the Pro League race'); expect(result.feedItems[0].timestamp).toBe(timestamp.toISOString()); expect(result.feedItems[0].formattedTime).toBe('Past'); expect(result.feedItems[0].ctaLabel).toBe('View Results'); expect(result.feedItems[0].ctaHref).toBe('/races/123'); expect(result.feedItems[1].id).toBe('feed-2'); expect(result.feedItems[1].type).toBe('league_update'); expect(result.feedItems[1].headline).toBe('League standings updated'); expect(result.feedItems[1].body).toBe('You moved up 2 positions'); expect(result.feedItems[1].ctaLabel).toBeUndefined(); expect(result.feedItems[1].ctaHref).toBeUndefined(); expect(result.hasFeedItems).toBe(true); }); it('should handle friends with avatar URLs', () => { const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 1, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [ { id: 'friend-1', name: 'Alice', country: 'UK', avatarUrl: 'https://example.com/alice.jpg', }, { id: 'friend-2', name: 'Bob', country: 'Germany', avatarUrl: undefined, }, ], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.friends).toHaveLength(2); expect(result.friends[0].id).toBe('friend-1'); expect(result.friends[0].name).toBe('Alice'); expect(result.friends[0].country).toBe('UK'); expect(result.friends[0].avatarUrl).toBe('https://example.com/alice.jpg'); expect(result.friends[1].id).toBe('friend-2'); expect(result.friends[1].name).toBe('Bob'); expect(result.friends[1].country).toBe('Germany'); expect(result.friends[1].avatarUrl).toBe(''); expect(result.friendCount).toBe('2'); expect(result.hasFriends).toBe(true); }); it('should handle empty arrays and zero counts', () => { const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 0, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.upcomingRaces).toEqual([]); expect(result.leagueStandings).toEqual([]); expect(result.feedItems).toEqual([]); expect(result.friends).toEqual([]); expect(result.activeLeaguesCount).toBe('0'); expect(result.friendCount).toBe('0'); expect(result.hasUpcomingRaces).toBe(false); expect(result.hasLeagueStandings).toBe(false); expect(result.hasFeedItems).toBe(false); expect(result.hasFriends).toBe(false); }); }); describe('data transformation', () => { it('should preserve all DTO fields in the output', () => { const dashboardDTO: DashboardOverviewDTO = { currentDriver: { id: 'driver-123', name: 'John Doe', country: 'USA', avatarUrl: 'https://example.com/avatar.jpg', rating: 1234.56, globalRank: 42, totalRaces: 150, wins: 25, podiums: 60, consistency: 85, }, myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 3, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 5, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.currentDriver.name).toBe(dashboardDTO.currentDriver?.name); expect(result.currentDriver.country).toBe(dashboardDTO.currentDriver?.country); expect(result.currentDriver.avatarUrl).toBe(dashboardDTO.currentDriver?.avatarUrl); expect(result.activeLeaguesCount).toBe(dashboardDTO.activeLeaguesCount.toString()); }); it('should not modify the input DTO', () => { const dashboardDTO: DashboardOverviewDTO = { currentDriver: { id: 'driver-123', name: 'John Doe', country: 'USA', rating: 1234.56, globalRank: 42, totalRaces: 150, wins: 25, podiums: 60, consistency: 85, }, myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 3, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 5, items: [], }, friends: [], }; const originalDTO = JSON.parse(JSON.stringify(dashboardDTO)); DashboardViewDataBuilder.build(dashboardDTO); expect(dashboardDTO).toEqual(originalDTO); }); it('should transform all numeric fields to formatted strings', () => { const dashboardDTO: DashboardOverviewDTO = { currentDriver: { id: 'driver-123', name: 'John Doe', country: 'USA', rating: 1234.56, globalRank: 42, totalRaces: 150, wins: 25, podiums: 60, consistency: 85, }, myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 3, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 5, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(typeof result.currentDriver.rating).toBe('string'); expect(typeof result.currentDriver.rank).toBe('string'); expect(typeof result.currentDriver.totalRaces).toBe('string'); expect(typeof result.currentDriver.wins).toBe('string'); expect(typeof result.currentDriver.podiums).toBe('string'); expect(typeof result.currentDriver.consistency).toBe('string'); expect(typeof result.activeLeaguesCount).toBe('string'); expect(typeof result.friendCount).toBe('string'); }); it('should handle large numbers correctly', () => { const dashboardDTO: DashboardOverviewDTO = { currentDriver: { id: 'driver-123', name: 'John Doe', country: 'USA', rating: 999999.99, globalRank: 1, totalRaces: 10000, wins: 2500, podiums: 5000, consistency: 99.9, }, myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 100, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.currentDriver.rating).toBe('1,000,000'); expect(result.currentDriver.totalRaces).toBe('10000'); expect(result.currentDriver.wins).toBe('2500'); expect(result.currentDriver.podiums).toBe('5000'); expect(result.currentDriver.consistency).toBe('99.9%'); expect(result.activeLeaguesCount).toBe('100'); }); }); describe('edge cases', () => { it('should handle missing optional fields in driver', () => { const dashboardDTO: DashboardOverviewDTO = { currentDriver: { id: 'driver-123', name: 'John Doe', country: 'USA', totalRaces: 100, wins: 20, podiums: 40, }, myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 1, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.currentDriver.avatarUrl).toBe(''); expect(result.currentDriver.rating).toBe('0'); expect(result.currentDriver.rank).toBe('0'); expect(result.currentDriver.consistency).toBe('0%'); }); it('should handle race with missing optional fields', () => { const now = new Date(); const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 1, nextRace: { id: 'race-456', track: 'Monza', car: 'Ferrari 488 GT3', scheduledAt: futureDate.toISOString(), status: 'scheduled', isMyLeague: true, }, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.nextRace).not.toBeNull(); expect(result.nextRace?.leagueId).toBeUndefined(); expect(result.nextRace?.leagueName).toBeUndefined(); }); it('should handle feed item with missing optional fields', () => { const now = new Date(); const timestamp = new Date(now.getTime() - 60 * 60 * 1000); // 1 hour ago const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 1, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 1, items: [ { id: 'feed-1', type: 'notification', headline: 'New notification', timestamp: timestamp.toISOString(), }, ], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.feedItems[0].body).toBeUndefined(); expect(result.feedItems[0].ctaLabel).toBeUndefined(); expect(result.feedItems[0].ctaHref).toBeUndefined(); }); it('should handle friend with missing avatarUrl', () => { const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 1, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [ { id: 'friend-1', name: 'Alice', country: 'UK', }, ], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.friends[0].avatarUrl).toBe(''); }); it('should handle league standing with null position', () => { const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 1, nextRace: null, recentResults: [], leagueStandingsSummaries: [ { leagueId: 'league-1', leagueName: 'Test League', position: null as any, totalDrivers: 50, points: 1000, }, ], feedSummary: { notificationCount: 0, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.leagueStandings[0].position).toBe('-'); }); it('should handle race with empty track and car', () => { const now = new Date(); const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 1, nextRace: { id: 'race-456', track: '', car: '', scheduledAt: futureDate.toISOString(), status: 'scheduled', isMyLeague: true, }, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.nextRace?.track).toBe(''); expect(result.nextRace?.car).toBe(''); }); }); describe('derived fields', () => { it('should correctly calculate hasUpcomingRaces', () => { const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [ { id: 'race-1', track: 'Spa', car: 'Porsche', scheduledAt: new Date().toISOString(), status: 'scheduled', isMyLeague: true, }, ], activeLeaguesCount: 1, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.hasUpcomingRaces).toBe(true); }); it('should correctly calculate hasLeagueStandings', () => { const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 1, nextRace: null, recentResults: [], leagueStandingsSummaries: [ { leagueId: 'league-1', leagueName: 'Test League', position: 5, totalDrivers: 50, points: 1000, }, ], feedSummary: { notificationCount: 0, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.hasLeagueStandings).toBe(true); }); it('should correctly calculate hasFeedItems', () => { const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 1, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 1, items: [ { id: 'feed-1', type: 'notification', headline: 'Test', timestamp: new Date().toISOString(), }, ], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.hasFeedItems).toBe(true); }); it('should correctly calculate hasFriends', () => { const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 1, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [ { id: 'friend-1', name: 'Alice', country: 'UK', }, ], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.hasFriends).toBe(true); }); it('should correctly calculate friendCount', () => { const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 1, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [ { id: 'friend-1', name: 'Alice', country: 'UK' }, { id: 'friend-2', name: 'Bob', country: 'Germany' }, { id: 'friend-3', name: 'Charlie', country: 'France' }, ], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.friendCount).toBe('3'); }); }); }); describe('DashboardDateDisplay', () => { describe('happy paths', () => { it('should format future date correctly', () => { const now = new Date(); const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24 hours from now const result = DashboardDateDisplay.format(futureDate); expect(result.date).toMatch(/^[A-Za-z]{3}, [A-Za-z]{3} \d{1,2}, \d{4}$/); expect(result.time).toMatch(/^\d{2}:\d{2}$/); expect(result.relative).toBe('1d'); }); it('should format date less than 24 hours correctly', () => { const now = new Date(); const futureDate = new Date(now.getTime() + 6 * 60 * 60 * 1000); // 6 hours from now const result = DashboardDateDisplay.format(futureDate); expect(result.relative).toBe('6h'); }); it('should format date more than 24 hours correctly', () => { const now = new Date(); const futureDate = new Date(now.getTime() + 48 * 60 * 60 * 1000); // 2 days from now const result = DashboardDateDisplay.format(futureDate); expect(result.relative).toBe('2d'); }); it('should format past date correctly', () => { const now = new Date(); const pastDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 24 hours ago const result = DashboardDateDisplay.format(pastDate); expect(result.relative).toBe('Past'); }); it('should format current date correctly', () => { const now = new Date(); const result = DashboardDateDisplay.format(now); expect(result.relative).toBe('Now'); }); it('should format date with leading zeros in time', () => { const date = new Date('2024-01-15T05:03:00'); const result = DashboardDateDisplay.format(date); expect(result.time).toBe('05:03'); }); }); describe('edge cases', () => { it('should handle midnight correctly', () => { const date = new Date('2024-01-15T00:00:00'); const result = DashboardDateDisplay.format(date); expect(result.time).toBe('00:00'); }); it('should handle end of day correctly', () => { const date = new Date('2024-01-15T23:59:59'); const result = DashboardDateDisplay.format(date); expect(result.time).toBe('23:59'); }); it('should handle different days of week', () => { const date = new Date('2024-01-15'); // Monday const result = DashboardDateDisplay.format(date); expect(result.date).toContain('Mon'); }); it('should handle different months', () => { const date = new Date('2024-01-15'); const result = DashboardDateDisplay.format(date); expect(result.date).toContain('Jan'); }); }); }); describe('DashboardCountDisplay', () => { describe('happy paths', () => { it('should format positive numbers correctly', () => { expect(DashboardCountDisplay.format(0)).toBe('0'); expect(DashboardCountDisplay.format(1)).toBe('1'); expect(DashboardCountDisplay.format(100)).toBe('100'); expect(DashboardCountDisplay.format(1000)).toBe('1000'); }); it('should handle null values', () => { expect(DashboardCountDisplay.format(null)).toBe('0'); }); it('should handle undefined values', () => { expect(DashboardCountDisplay.format(undefined)).toBe('0'); }); }); describe('edge cases', () => { it('should handle negative numbers', () => { expect(DashboardCountDisplay.format(-1)).toBe('-1'); expect(DashboardCountDisplay.format(-100)).toBe('-100'); }); it('should handle large numbers', () => { expect(DashboardCountDisplay.format(999999)).toBe('999999'); expect(DashboardCountDisplay.format(1000000)).toBe('1000000'); }); it('should handle decimal numbers', () => { expect(DashboardCountDisplay.format(1.5)).toBe('1.5'); expect(DashboardCountDisplay.format(100.99)).toBe('100.99'); }); }); }); describe('DashboardRankDisplay', () => { describe('happy paths', () => { it('should format rank correctly', () => { expect(DashboardRankDisplay.format(1)).toBe('1'); expect(DashboardRankDisplay.format(42)).toBe('42'); expect(DashboardRankDisplay.format(100)).toBe('100'); }); }); describe('edge cases', () => { it('should handle rank 0', () => { expect(DashboardRankDisplay.format(0)).toBe('0'); }); it('should handle large ranks', () => { expect(DashboardRankDisplay.format(999999)).toBe('999999'); }); }); }); describe('DashboardConsistencyDisplay', () => { describe('happy paths', () => { it('should format consistency correctly', () => { expect(DashboardConsistencyDisplay.format(0)).toBe('0%'); expect(DashboardConsistencyDisplay.format(50)).toBe('50%'); expect(DashboardConsistencyDisplay.format(100)).toBe('100%'); }); }); describe('edge cases', () => { it('should handle decimal consistency', () => { expect(DashboardConsistencyDisplay.format(85.5)).toBe('85.5%'); expect(DashboardConsistencyDisplay.format(99.9)).toBe('99.9%'); }); it('should handle negative consistency', () => { expect(DashboardConsistencyDisplay.format(-10)).toBe('-10%'); }); }); }); describe('DashboardLeaguePositionDisplay', () => { describe('happy paths', () => { it('should format position correctly', () => { expect(DashboardLeaguePositionDisplay.format(1)).toBe('#1'); expect(DashboardLeaguePositionDisplay.format(5)).toBe('#5'); expect(DashboardLeaguePositionDisplay.format(100)).toBe('#100'); }); it('should handle null values', () => { expect(DashboardLeaguePositionDisplay.format(null)).toBe('-'); }); it('should handle undefined values', () => { expect(DashboardLeaguePositionDisplay.format(undefined)).toBe('-'); }); }); describe('edge cases', () => { it('should handle position 0', () => { expect(DashboardLeaguePositionDisplay.format(0)).toBe('#0'); }); it('should handle large positions', () => { expect(DashboardLeaguePositionDisplay.format(999)).toBe('#999'); }); }); }); describe('RatingDisplay', () => { describe('happy paths', () => { it('should format rating correctly', () => { expect(RatingDisplay.format(0)).toBe('0'); expect(RatingDisplay.format(1234.56)).toBe('1,235'); expect(RatingDisplay.format(9999.99)).toBe('10,000'); }); it('should handle null values', () => { expect(RatingDisplay.format(null)).toBe('—'); }); it('should handle undefined values', () => { expect(RatingDisplay.format(undefined)).toBe('—'); }); }); describe('edge cases', () => { it('should round down correctly', () => { expect(RatingDisplay.format(1234.4)).toBe('1,234'); }); it('should round up correctly', () => { expect(RatingDisplay.format(1234.6)).toBe('1,235'); }); it('should handle decimal ratings', () => { expect(RatingDisplay.format(1234.5)).toBe('1,235'); }); it('should handle large ratings', () => { expect(RatingDisplay.format(999999.99)).toBe('1,000,000'); }); }); }); describe('Dashboard View Data - Cross-Component Consistency', () => { describe('common patterns', () => { it('should all use consistent formatting for numeric values', () => { const dashboardDTO: DashboardOverviewDTO = { currentDriver: { id: 'driver-123', name: 'John Doe', country: 'USA', rating: 1234.56, globalRank: 42, totalRaces: 150, wins: 25, podiums: 60, consistency: 85, }, myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 3, nextRace: null, recentResults: [], leagueStandingsSummaries: [ { leagueId: 'league-1', leagueName: 'Test League', position: 5, totalDrivers: 50, points: 1250, }, ], feedSummary: { notificationCount: 0, items: [], }, friends: [ { id: 'friend-1', name: 'Alice', country: 'UK' }, { id: 'friend-2', name: 'Bob', country: 'Germany' }, ], }; const result = DashboardViewDataBuilder.build(dashboardDTO); // All numeric values should be formatted as strings expect(typeof result.currentDriver.rating).toBe('string'); expect(typeof result.currentDriver.rank).toBe('string'); expect(typeof result.currentDriver.totalRaces).toBe('string'); expect(typeof result.currentDriver.wins).toBe('string'); expect(typeof result.currentDriver.podiums).toBe('string'); expect(typeof result.currentDriver.consistency).toBe('string'); expect(typeof result.activeLeaguesCount).toBe('string'); expect(typeof result.friendCount).toBe('string'); expect(typeof result.leagueStandings[0].position).toBe('string'); expect(typeof result.leagueStandings[0].points).toBe('string'); expect(typeof result.leagueStandings[0].totalDrivers).toBe('string'); }); it('should all handle missing data gracefully', () => { const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 0, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); // All fields should have safe defaults expect(result.currentDriver.name).toBe(''); expect(result.currentDriver.avatarUrl).toBe(''); expect(result.currentDriver.country).toBe(''); expect(result.currentDriver.rating).toBe('0.0'); expect(result.currentDriver.rank).toBe('0'); expect(result.currentDriver.totalRaces).toBe('0'); expect(result.currentDriver.wins).toBe('0'); expect(result.currentDriver.podiums).toBe('0'); expect(result.currentDriver.consistency).toBe('0%'); expect(result.nextRace).toBeNull(); expect(result.upcomingRaces).toEqual([]); expect(result.leagueStandings).toEqual([]); expect(result.feedItems).toEqual([]); expect(result.friends).toEqual([]); expect(result.activeLeaguesCount).toBe('0'); expect(result.friendCount).toBe('0'); }); it('should all preserve ISO timestamps for serialization', () => { const now = new Date(); const futureDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); const feedTimestamp = new Date(now.getTime() - 30 * 60 * 1000); const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 1, nextRace: { id: 'race-1', track: 'Spa', car: 'Porsche', scheduledAt: futureDate.toISOString(), status: 'scheduled', isMyLeague: true, }, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 1, items: [ { id: 'feed-1', type: 'notification', headline: 'Test', timestamp: feedTimestamp.toISOString(), }, ], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); // All timestamps should be preserved as ISO strings expect(result.nextRace?.scheduledAt).toBe(futureDate.toISOString()); expect(result.feedItems[0].timestamp).toBe(feedTimestamp.toISOString()); }); it('should all handle boolean flags correctly', () => { const dashboardDTO: DashboardOverviewDTO = { myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [ { id: 'race-1', track: 'Spa', car: 'Porsche', scheduledAt: new Date().toISOString(), status: 'scheduled', isMyLeague: true, }, { id: 'race-2', track: 'Monza', car: 'Ferrari', scheduledAt: new Date().toISOString(), status: 'scheduled', isMyLeague: false, }, ], activeLeaguesCount: 1, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 0, items: [], }, friends: [], }; const result = DashboardViewDataBuilder.build(dashboardDTO); expect(result.upcomingRaces[0].isMyLeague).toBe(true); expect(result.upcomingRaces[1].isMyLeague).toBe(false); }); }); describe('data integrity', () => { it('should maintain data consistency across transformations', () => { const dashboardDTO: DashboardOverviewDTO = { currentDriver: { id: 'driver-123', name: 'John Doe', country: 'USA', rating: 1234.56, globalRank: 42, totalRaces: 150, wins: 25, podiums: 60, consistency: 85, }, myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [], activeLeaguesCount: 3, nextRace: null, recentResults: [], leagueStandingsSummaries: [], feedSummary: { notificationCount: 5, items: [], }, friends: [ { id: 'friend-1', name: 'Alice', country: 'UK' }, { id: 'friend-2', name: 'Bob', country: 'Germany' }, ], }; const result = DashboardViewDataBuilder.build(dashboardDTO); // Verify derived fields match their source data expect(result.friendCount).toBe(dashboardDTO.friends.length.toString()); expect(result.activeLeaguesCount).toBe(dashboardDTO.activeLeaguesCount.toString()); expect(result.hasFriends).toBe(dashboardDTO.friends.length > 0); expect(result.hasUpcomingRaces).toBe(dashboardDTO.upcomingRaces.length > 0); expect(result.hasLeagueStandings).toBe(dashboardDTO.leagueStandingsSummaries.length > 0); expect(result.hasFeedItems).toBe(dashboardDTO.feedSummary.items.length > 0); }); it('should handle complex real-world scenarios', () => { const now = new Date(); const race1Date = new Date(now.getTime() + 2 * 24 * 60 * 60 * 1000); const race2Date = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000); const feedTimestamp = new Date(now.getTime() - 60 * 60 * 1000); const dashboardDTO: DashboardOverviewDTO = { currentDriver: { id: 'driver-123', name: 'John Doe', country: 'USA', avatarUrl: 'https://example.com/avatar.jpg', rating: 2456.78, globalRank: 15, totalRaces: 250, wins: 45, podiums: 120, consistency: 92.5, }, myUpcomingRaces: [], otherUpcomingRaces: [], upcomingRaces: [ { id: 'race-1', leagueId: 'league-1', leagueName: 'Pro League', track: 'Spa', car: 'Porsche 911 GT3', scheduledAt: race1Date.toISOString(), status: 'scheduled', isMyLeague: true, }, { id: 'race-2', track: 'Monza', car: 'Ferrari 488 GT3', scheduledAt: race2Date.toISOString(), status: 'scheduled', isMyLeague: false, }, ], activeLeaguesCount: 2, nextRace: { id: 'race-1', leagueId: 'league-1', leagueName: 'Pro League', track: 'Spa', car: 'Porsche 911 GT3', scheduledAt: race1Date.toISOString(), status: 'scheduled', isMyLeague: true, }, recentResults: [], leagueStandingsSummaries: [ { leagueId: 'league-1', leagueName: 'Pro League', position: 3, totalDrivers: 100, points: 2450, }, { leagueId: 'league-2', leagueName: 'Rookie League', position: 1, totalDrivers: 50, points: 1800, }, ], feedSummary: { notificationCount: 3, items: [ { id: 'feed-1', type: 'race_result', headline: 'Race completed', body: 'You finished 3rd in the Pro League race', timestamp: feedTimestamp.toISOString(), ctaLabel: 'View Results', ctaHref: '/races/123', }, { id: 'feed-2', type: 'league_update', headline: 'League standings updated', body: 'You moved up 2 positions', timestamp: feedTimestamp.toISOString(), }, ], }, friends: [ { id: 'friend-1', name: 'Alice', country: 'UK', avatarUrl: 'https://example.com/alice.jpg' }, { id: 'friend-2', name: 'Bob', country: 'Germany' }, { id: 'friend-3', name: 'Charlie', country: 'France', avatarUrl: 'https://example.com/charlie.jpg' }, ], }; const result = DashboardViewDataBuilder.build(dashboardDTO); // Verify all transformations expect(result.currentDriver.name).toBe('John Doe'); expect(result.currentDriver.rating).toBe('2,457'); expect(result.currentDriver.rank).toBe('15'); expect(result.currentDriver.totalRaces).toBe('250'); expect(result.currentDriver.wins).toBe('45'); expect(result.currentDriver.podiums).toBe('120'); expect(result.currentDriver.consistency).toBe('92.5%'); expect(result.nextRace).not.toBeNull(); expect(result.nextRace?.id).toBe('race-1'); expect(result.nextRace?.track).toBe('Spa'); expect(result.nextRace?.isMyLeague).toBe(true); expect(result.upcomingRaces).toHaveLength(2); expect(result.upcomingRaces[0].isMyLeague).toBe(true); expect(result.upcomingRaces[1].isMyLeague).toBe(false); expect(result.leagueStandings).toHaveLength(2); expect(result.leagueStandings[0].position).toBe('#3'); expect(result.leagueStandings[0].points).toBe('2450'); expect(result.leagueStandings[1].position).toBe('#1'); expect(result.leagueStandings[1].points).toBe('1800'); expect(result.feedItems).toHaveLength(2); expect(result.feedItems[0].type).toBe('race_result'); expect(result.feedItems[0].ctaLabel).toBe('View Results'); expect(result.feedItems[1].type).toBe('league_update'); expect(result.feedItems[1].ctaLabel).toBeUndefined(); expect(result.friends).toHaveLength(3); expect(result.friends[0].avatarUrl).toBe('https://example.com/alice.jpg'); expect(result.friends[1].avatarUrl).toBe(''); expect(result.friends[2].avatarUrl).toBe('https://example.com/charlie.jpg'); expect(result.activeLeaguesCount).toBe('2'); expect(result.friendCount).toBe('3'); expect(result.hasUpcomingRaces).toBe(true); expect(result.hasLeagueStandings).toBe(true); expect(result.hasFeedItems).toBe(true); expect(result.hasFriends).toBe(true); }); }); });