235 lines
7.0 KiB
TypeScript
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);
|
|
});
|
|
});
|