view models
This commit is contained in:
234
apps/website/lib/view-models/LeagueDetailPageViewModel.test.ts
Normal file
234
apps/website/lib/view-models/LeagueDetailPageViewModel.test.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user