Files
gridpilot.gg/apps/website/lib/view-models/LeagueDetailPageViewModel.test.ts
2025-12-20 00:31:31 +01:00

235 lines
7.0 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { LeagueDetailPageViewModel, type SponsorInfo } from './LeagueDetailPageViewModel';
import type { LeagueWithCapacityDTO } from '../types/generated/LeagueWithCapacityDTO';
import type { LeagueStatsDTO } from '../types/generated/LeagueStatsDTO';
import type { GetDriverOutputDTO } from '../types/generated/GetDriverOutputDTO';
import type { LeagueMembershipsDTO } from '../types/generated/LeagueMembershipsDTO';
import type { LeagueScheduleDTO } from '../types/generated/LeagueScheduleDTO';
import { RaceViewModel } from './RaceViewModel';
describe('LeagueDetailPageViewModel', () => {
const league: LeagueWithCapacityDTO = {
id: 'league-1',
name: 'Pro Series',
// extra fields used by view-model but not in generated type yet
description: 'Top tier competition',
ownerId: 'owner-1',
createdAt: '2025-01-01T00:00:00Z',
maxDrivers: 40,
socialLinks: {
discordUrl: 'https://discord.gg/example',
youtubeUrl: 'https://youtube.com/example',
websiteUrl: 'https://example.com',
},
} as any;
const owner: GetDriverOutputDTO = {
id: 'owner-1',
iracingId: '100',
name: 'Owner Driver',
country: 'US',
joinedAt: '2024-01-01T00:00:00Z',
};
const drivers: GetDriverOutputDTO[] = [
owner,
{
id: 'driver-2',
iracingId: '101',
name: 'Second Driver',
country: 'DE',
joinedAt: '2024-02-01T00:00:00Z',
},
];
const memberships: LeagueMembershipsDTO = {
memberships: [
{
driverId: 'owner-1',
role: 'owner',
status: 'active',
joinedAt: '2024-01-01T00:00:00Z',
},
{
driverId: 'driver-2',
role: 'admin',
status: 'active',
joinedAt: '2024-02-01T00:00:00Z',
},
{
driverId: 'driver-3',
role: 'steward',
status: 'inactive',
joinedAt: '2024-03-01T00:00:00Z',
},
],
} as any;
const schedule: LeagueScheduleDTO = {
races: [
{ id: 'race-1', name: 'Round 1', date: '2025-01-10T20:00:00Z' },
{ id: 'race-2', name: 'Round 2', date: '2025-01-17T20:00:00Z' },
],
} as any;
const allRaces: RaceViewModel[] = schedule.races.map(r => new RaceViewModel({ ...r, views: 12000, status: 'completed' }));
const leagueStats: LeagueStatsDTO = {
totalMembers: 2,
totalRaces: 10,
averageRating: 3_200,
// view-model also expects averageSOF and completedRaces
averageSOF: 3200,
completedRaces: 4,
} as any;
const sponsors: SponsorInfo[] = [
{ id: 's1', name: 'Main Sponsor', tier: 'main', logoUrl: '/logo-main.png', tagline: 'Primary partner' },
{ id: 's2', name: 'Secondary Sponsor', tier: 'secondary', logoUrl: '/logo-sec.png' },
];
it('maps core league, owner, drivers, memberships and races', () => {
const vm = new LeagueDetailPageViewModel(
league,
owner,
null,
drivers,
memberships,
allRaces,
leagueStats,
sponsors,
);
expect(vm.id).toBe(league.id);
expect(vm.name).toBe(league.name);
expect(vm.description).toBe(league.description);
expect(vm.ownerId).toBe(league.ownerId);
expect(vm.settings.maxDrivers).toBe(league.maxDrivers);
expect(vm.socialLinks?.discordUrl).toBe(league.socialLinks?.discordUrl);
expect(vm.owner).toEqual(owner);
expect(vm.scoringConfig).toBeNull();
expect(vm.drivers).toHaveLength(drivers.length);
expect(vm.memberships).toHaveLength(memberships.memberships.length);
expect(vm.allRaces).toHaveLength(allRaces.length);
expect(vm.runningRaces.every(r => r.status === 'running')).toBe(true);
});
it('computes sponsor insights from member count and stats', () => {
const vm = new LeagueDetailPageViewModel(
league,
owner,
null,
drivers,
memberships,
allRaces,
leagueStats,
sponsors,
);
const memberCount = memberships.memberships.length;
const mainSponsorTaken = sponsors.some(s => s.tier === 'main');
const secondaryTaken = sponsors.filter(s => s.tier === 'secondary').length;
expect(vm.sponsorInsights.avgViewsPerRace).toBe(5400 + memberCount * 50);
expect(vm.sponsorInsights.totalImpressions).toBe(45000 + memberCount * 500);
expect(vm.sponsorInsights.engagementRate).toBe((3.5 + memberCount / 50).toFixed(1));
expect(vm.sponsorInsights.estimatedReach).toBe(memberCount * 150);
expect(vm.sponsorInsights.mainSponsorAvailable).toBe(!mainSponsorTaken);
expect(vm.sponsorInsights.secondarySlotsAvailable).toBe(Math.max(0, 2 - secondaryTaken));
expect(vm.sponsorInsights.mainSponsorPrice).toBe(800 + Math.floor(memberCount * 10));
expect(vm.sponsorInsights.secondaryPrice).toBe(250 + Math.floor(memberCount * 3));
expect(vm.sponsorInsights.discordMembers).toBe(memberCount * 3);
expect(vm.sponsorInsights.monthlyActivity).toBe(Math.min(100, 40 + (leagueStats as any).completedRaces * 2));
});
it('derives sponsor tier and trustScore from averageSOF and completedRaces', () => {
const highSofStats = { ...leagueStats, averageSOF: 3500 } as any;
const vmHigh = new LeagueDetailPageViewModel(
league,
owner,
null,
drivers,
memberships,
allRaces,
highSofStats,
sponsors,
);
expect(vmHigh.sponsorInsights.tier).toBe('premium');
const midSofStats = { ...leagueStats, averageSOF: 2500 } as any;
const vmMid = new LeagueDetailPageViewModel(
league,
owner,
null,
drivers,
memberships,
allRaces,
midSofStats,
sponsors,
);
expect(vmMid.sponsorInsights.tier).toBe('standard');
const lowSofStats = { ...leagueStats, averageSOF: 1500 } as any;
const vmLow = new LeagueDetailPageViewModel(
league,
owner,
null,
drivers,
memberships,
allRaces,
lowSofStats,
sponsors,
);
expect(vmLow.sponsorInsights.tier).toBe('starter');
expect(vmHigh.sponsorInsights.trustScore).toBe(
Math.min(100, 60 + memberships.memberships.length + (leagueStats as any).completedRaces),
);
});
it('builds owner, admin and steward summaries from memberships', () => {
const vm = new LeagueDetailPageViewModel(
league,
owner,
null,
drivers,
memberships,
allRaces,
leagueStats,
sponsors,
);
expect(vm.ownerSummary?.driver.id).toBe('owner-1');
expect(vm.ownerSummary?.rating).toBeNull();
expect(vm.ownerSummary?.rank).toBeNull();
expect(vm.adminSummaries.length).toBeGreaterThanOrEqual(1);
expect(vm.adminSummaries[0].driver.id).toBe('driver-2');
expect(Array.isArray(vm.stewardSummaries)).toBe(true);
});
it('returns falsey sponsor/membership helpers by default', () => {
const vm = new LeagueDetailPageViewModel(
league,
owner,
null,
drivers,
memberships,
allRaces,
leagueStats,
sponsors,
);
expect(vm.isSponsorMode).toBe(false);
expect(vm.currentUserMembership).toBeNull();
expect(vm.canEndRaces).toBe(false);
});
});