resolve todos in website
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import type { DriverDTO } from '@core/racing/application/dto/DriverDTO';
|
||||
import type { DriverDTO } from '@/lib/types/generated/DriverDTO';
|
||||
|
||||
export interface DriverIdentityProps {
|
||||
driver: DriverDTO;
|
||||
|
||||
41
apps/website/components/leagues/LeagueHeader.test.tsx
Normal file
41
apps/website/components/leagues/LeagueHeader.test.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import LeagueHeader from './LeagueHeader';
|
||||
|
||||
describe('LeagueHeader', () => {
|
||||
it('renders league name, description and sponsor', () => {
|
||||
render(
|
||||
<LeagueHeader
|
||||
leagueId="league-1"
|
||||
leagueName="Test League"
|
||||
description="A fun test league"
|
||||
ownerId="owner-1"
|
||||
ownerName="Owner Name"
|
||||
mainSponsor={{
|
||||
name: 'Test Sponsor',
|
||||
websiteUrl: 'https://example.com',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test League')).toBeInTheDocument();
|
||||
expect(screen.getByText('A fun test league')).toBeInTheDocument();
|
||||
expect(screen.getByText('by')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Sponsor')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders without description or sponsor', () => {
|
||||
render(
|
||||
<LeagueHeader
|
||||
leagueId="league-2"
|
||||
leagueName="League Without Details"
|
||||
ownerId="owner-2"
|
||||
ownerName="Owner 2"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('League Without Details')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -2,12 +2,7 @@
|
||||
|
||||
import MembershipStatus from '@/components/leagues/MembershipStatus';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type { DriverDTO } from '@core/racing/application/dto/DriverDTO';
|
||||
import { EntityMappers } from '@core/racing/application/mappers/EntityMappers';
|
||||
|
||||
// TODO EntityMapper is legacy. Must use ´useServices` hook.
|
||||
|
||||
// Main sponsor info for "by XYZ" display
|
||||
interface MainSponsorInfo {
|
||||
@@ -35,30 +30,6 @@ export default function LeagueHeader({
|
||||
const imageService = getImageService();
|
||||
const logoUrl = imageService.getLeagueLogo(leagueId);
|
||||
|
||||
const [ownerDriver, setOwnerDriver] = useState<DriverDTO | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
async function loadOwner() {
|
||||
try {
|
||||
const driverRepo = getDriverRepository();
|
||||
const entity = await driverRepo.findById(ownerId);
|
||||
if (!entity || !isMounted) return;
|
||||
setOwnerDriver(EntityMappers.toDriverDTO(entity));
|
||||
} catch (err) {
|
||||
console.error('Failed to load league owner for header:', err);
|
||||
}
|
||||
}
|
||||
|
||||
loadOwner();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [ownerId]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
{/* League header with logo - no cover image */}
|
||||
|
||||
129
apps/website/components/leagues/LeagueMembers.test.tsx
Normal file
129
apps/website/components/leagues/LeagueMembers.test.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import LeagueMembers from './LeagueMembers';
|
||||
import type { DriverDTO } from '@/lib/types/generated/DriverDTO';
|
||||
|
||||
// Stub global driver stats helper used by LeagueMembers sorting/rendering
|
||||
(globalThis as any).getDriverStats = (driverId: string) => ({
|
||||
driverId,
|
||||
rating: driverId === 'driver-1' ? 2500 : 2000,
|
||||
overallRank: driverId === 'driver-1' ? 1 : 2,
|
||||
wins: driverId === 'driver-1' ? 10 : 5,
|
||||
});
|
||||
|
||||
// Mock effective driver id so we can assert the "(You)" label
|
||||
vi.mock('@/hooks/useEffectiveDriverId', () => {
|
||||
return {
|
||||
useEffectiveDriverId: () => 'driver-1',
|
||||
};
|
||||
});
|
||||
|
||||
// Mock services hook to inject stub leagueMembershipService and driverService
|
||||
const mockFetchLeagueMemberships = vi.fn<[], Promise<any[]>>();
|
||||
const mockGetLeagueMembers = vi.fn<(leagueId: string) => any[]>();
|
||||
const mockFindByIds = vi.fn<(ids: string[]) => Promise<DriverDTO[]>>();
|
||||
|
||||
vi.mock('@/lib/services/ServiceProvider', () => {
|
||||
return {
|
||||
useServices: () => ({
|
||||
leagueMembershipService: {
|
||||
fetchLeagueMemberships: mockFetchLeagueMemberships,
|
||||
getLeagueMembers: mockGetLeagueMembers,
|
||||
},
|
||||
driverService: {
|
||||
findByIds: mockFindByIds,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('LeagueMembers', () => {
|
||||
beforeEach(() => {
|
||||
mockFetchLeagueMemberships.mockReset();
|
||||
mockGetLeagueMembers.mockReset();
|
||||
mockFindByIds.mockReset();
|
||||
});
|
||||
|
||||
it('loads memberships via services and renders driver rows', async () => {
|
||||
const leagueId = 'league-1';
|
||||
|
||||
const memberships = [
|
||||
{
|
||||
id: 'm1',
|
||||
leagueId,
|
||||
driverId: 'driver-1',
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
joinedAt: '2024-01-01T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
leagueId,
|
||||
driverId: 'driver-2',
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
joinedAt: '2024-01-02T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
|
||||
const drivers: DriverDTO[] = [
|
||||
{
|
||||
id: 'driver-1',
|
||||
iracingId: 'ir-1',
|
||||
name: 'Driver One',
|
||||
country: 'DE',
|
||||
},
|
||||
{
|
||||
id: 'driver-2',
|
||||
iracingId: 'ir-2',
|
||||
name: 'Driver Two',
|
||||
country: 'US',
|
||||
},
|
||||
];
|
||||
|
||||
mockFetchLeagueMemberships.mockResolvedValue(memberships);
|
||||
mockGetLeagueMembers.mockReturnValue(memberships);
|
||||
mockFindByIds.mockResolvedValue(drivers);
|
||||
|
||||
render(<LeagueMembers leagueId={leagueId} showActions={false} />);
|
||||
|
||||
// Loading state first
|
||||
expect(screen.getByText('Loading members...')).toBeInTheDocument();
|
||||
|
||||
// Wait for data to be rendered
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading members...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Services should have been called with expected arguments
|
||||
expect(mockFetchLeagueMemberships).toHaveBeenCalledWith(leagueId);
|
||||
expect(mockGetLeagueMembers).toHaveBeenCalledWith(leagueId);
|
||||
expect(mockFindByIds).toHaveBeenCalledTimes(1);
|
||||
expect(mockFindByIds).toHaveBeenCalledWith(['driver-1', 'driver-2']);
|
||||
|
||||
// Driver rows should be rendered using DTO names
|
||||
expect(screen.getByText('Driver One')).toBeInTheDocument();
|
||||
expect(screen.getByText('Driver Two')).toBeInTheDocument();
|
||||
|
||||
// Current user marker should appear for effective driver id
|
||||
expect(screen.getByText('(You)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles empty membership list gracefully', async () => {
|
||||
const leagueId = 'league-empty';
|
||||
|
||||
mockFetchLeagueMemberships.mockResolvedValue([]);
|
||||
mockGetLeagueMembers.mockReturnValue([]);
|
||||
mockFindByIds.mockResolvedValue([]);
|
||||
|
||||
render(<LeagueMembers leagueId={leagueId} showActions={false} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading members...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText('No members found')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import DriverIdentity from '@/components/drivers/DriverIdentity';
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import {
|
||||
getLeagueMembers,
|
||||
type LeagueMembership,
|
||||
type MembershipRole,
|
||||
} from '@/lib/leagueMembership';
|
||||
import type { DriverDTO } from '@core/racing/application/dto/DriverDTO';
|
||||
import { EntityMappers } from '@core/racing/application/mappers/EntityMappers';
|
||||
import DriverIdentity from '../drivers/DriverIdentity';
|
||||
import { useEffectiveDriverId } from '../../hooks/useEffectiveDriverId';
|
||||
import { useServices } from '../../lib/services/ServiceProvider';
|
||||
import type { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership';
|
||||
import type { MembershipRole } from '@core/racing/domain/entities/MembershipRole';
|
||||
import type { DriverDTO } from '@/lib/types/generated/DriverDTO';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
// TODO EntityMapper is legacy. Must use ´useServices` hook.
|
||||
// Migrated to useServices-based website services; legacy EntityMapper removed.
|
||||
|
||||
interface LeagueMembersProps {
|
||||
leagueId: string;
|
||||
@@ -31,32 +28,33 @@ export default function LeagueMembers({
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [sortBy, setSortBy] = useState<'role' | 'name' | 'date' | 'rating' | 'points' | 'wins'>('rating');
|
||||
const currentDriverId = useEffectiveDriverId();
|
||||
const { leagueMembershipService, driverService } = useServices();
|
||||
|
||||
const loadMembers = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const membershipData = getLeagueMembers(leagueId);
|
||||
await leagueMembershipService.fetchLeagueMemberships(leagueId);
|
||||
const membershipData = leagueMembershipService.getLeagueMembers(leagueId);
|
||||
setMembers(membershipData);
|
||||
|
||||
const driverRepo = getDriverRepository();
|
||||
const driverEntities = await Promise.all(
|
||||
membershipData.map((m) => driverRepo.findById(m.driverId))
|
||||
);
|
||||
const driverDtos = driverEntities
|
||||
.map((driver) => (driver ? EntityMappers.toDriverDTO(driver) : null))
|
||||
.filter((dto): dto is DriverDTO => dto !== null);
|
||||
const uniqueDriverIds = Array.from(new Set(membershipData.map((m) => m.driverId)));
|
||||
if (uniqueDriverIds.length > 0) {
|
||||
const driverDtos = await driverService.findByIds(uniqueDriverIds);
|
||||
|
||||
const byId: Record<string, DriverDTO> = {};
|
||||
for (const dto of driverDtos) {
|
||||
byId[dto.id] = dto;
|
||||
const byId: Record<string, DriverDTO> = {};
|
||||
for (const dto of driverDtos) {
|
||||
byId[dto.id] = dto;
|
||||
}
|
||||
setDriversById(byId);
|
||||
} else {
|
||||
setDriversById({});
|
||||
}
|
||||
setDriversById(byId);
|
||||
} catch (error) {
|
||||
console.error('Failed to load members:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [leagueId]);
|
||||
}, [leagueId, leagueMembershipService, driverService]);
|
||||
|
||||
useEffect(() => {
|
||||
loadMembers();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import type { DriverDTO } from '@core/racing/application/dto/DriverDTO';
|
||||
import type { DriverDTO } from '@/lib/types/generated/DriverDTO';
|
||||
import DriverRating from '@/components/profile/DriverRatingPill';
|
||||
|
||||
export interface DriverSummaryPillProps {
|
||||
|
||||
120
apps/website/components/profile/UserPill.test.tsx
Normal file
120
apps/website/components/profile/UserPill.test.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import UserPill from './UserPill';
|
||||
import type { DriverDTO } from '@/lib/types/generated/DriverDTO';
|
||||
|
||||
// Mock useAuth to control session state
|
||||
vi.mock('@/lib/auth/AuthContext', () => {
|
||||
return {
|
||||
useAuth: () => mockedAuthValue,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock effective driver id hook
|
||||
vi.mock('@/hooks/useEffectiveDriverId', () => {
|
||||
return {
|
||||
useEffectiveDriverId: () => mockedDriverId,
|
||||
};
|
||||
});
|
||||
|
||||
// Mock services hook to inject stub driverService/mediaService
|
||||
const mockFindById = vi.fn<[], Promise<DriverDTO | null>>();
|
||||
const mockGetDriverAvatar = vi.fn<(driverId: string) => string>();
|
||||
|
||||
vi.mock('@/lib/services/ServiceProvider', () => {
|
||||
return {
|
||||
useServices: () => ({
|
||||
driverService: {
|
||||
findById: mockFindById,
|
||||
},
|
||||
mediaService: {
|
||||
getDriverAvatar: mockGetDriverAvatar,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
interface MockSessionUser {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface MockSession {
|
||||
user: MockSessionUser | null;
|
||||
}
|
||||
|
||||
let mockedAuthValue: { session: MockSession | null } = { session: null };
|
||||
let mockedDriverId: string | null = null;
|
||||
|
||||
// Provide global stats helpers used by UserPill's rating/rank computation
|
||||
// They are UI-level helpers, so a minimal stub is sufficient for these tests.
|
||||
(globalThis as any).getDriverStats = (driverId: string) => ({
|
||||
driverId,
|
||||
rating: 2000,
|
||||
overallRank: 10,
|
||||
wins: 5,
|
||||
});
|
||||
|
||||
(globalThis as any).getAllDriverRankings = () => [
|
||||
{ driverId: 'driver-1', rating: 2100 },
|
||||
{ driverId: 'driver-2', rating: 2000 },
|
||||
];
|
||||
|
||||
describe('UserPill', () => {
|
||||
beforeEach(() => {
|
||||
mockedAuthValue = { session: null };
|
||||
mockedDriverId = null;
|
||||
mockFindById.mockReset();
|
||||
mockGetDriverAvatar.mockReset();
|
||||
});
|
||||
|
||||
it('renders auth links when there is no session', () => {
|
||||
mockedAuthValue = { session: null };
|
||||
|
||||
const { container } = render(<UserPill />);
|
||||
|
||||
expect(screen.getByText('Sign In')).toBeInTheDocument();
|
||||
expect(screen.getByText('Get Started')).toBeInTheDocument();
|
||||
expect(mockFindById).not.toHaveBeenCalled();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('does not load driver when there is no primary driver id', async () => {
|
||||
mockedAuthValue = { session: { user: { id: 'user-1' } } };
|
||||
mockedDriverId = null;
|
||||
|
||||
const { container } = render(<UserPill />);
|
||||
|
||||
await waitFor(() => {
|
||||
// component should render nothing in this state
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
expect(mockFindById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('loads driver via driverService and uses mediaService avatar', async () => {
|
||||
const driver: DriverDTO = {
|
||||
id: 'driver-1',
|
||||
iracingId: 'ir-123',
|
||||
name: 'Test Driver',
|
||||
country: 'DE',
|
||||
};
|
||||
|
||||
mockedAuthValue = { session: { user: { id: 'user-1' } } };
|
||||
mockedDriverId = driver.id;
|
||||
|
||||
mockFindById.mockResolvedValue(driver);
|
||||
mockGetDriverAvatar.mockImplementation((driverId: string) => `/api/media/avatar/${driverId}`);
|
||||
|
||||
render(<UserPill />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Driver')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(mockFindById).toHaveBeenCalledWith('driver-1');
|
||||
expect(mockGetDriverAvatar).toHaveBeenCalledWith('driver-1');
|
||||
});
|
||||
});
|
||||
@@ -8,10 +8,8 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import DriverSummaryPill from '@/components/profile/DriverSummaryPill';
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
import type { DriverDTO } from '@core/racing/application/dto/DriverDTO';
|
||||
import { EntityMappers } from '@core/racing/application/mappers/EntityMappers';
|
||||
|
||||
// TODO EntityMapper is legacy. Must use ´useServices` hook.
|
||||
import type { DriverDTO } from '@/lib/types/generated/DriverDTO';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
|
||||
// Hook to detect sponsor mode
|
||||
function useSponsorMode(): boolean {
|
||||
@@ -84,6 +82,7 @@ function SponsorSummaryPill({
|
||||
|
||||
export default function UserPill() {
|
||||
const { session } = useAuth();
|
||||
const { driverService, mediaService } = useServices();
|
||||
const [driver, setDriver] = useState<DriverDTO | null>(null);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const isSponsorMode = useSponsorMode();
|
||||
@@ -103,19 +102,18 @@ export default function UserPill() {
|
||||
return;
|
||||
}
|
||||
|
||||
const repo = getDriverRepository();
|
||||
const entity = await repo.findById(primaryDriverId);
|
||||
const dto = await driverService.findById(primaryDriverId);
|
||||
if (!cancelled) {
|
||||
setDriver(EntityMappers.toDriverDTO(entity));
|
||||
setDriver(dto);
|
||||
}
|
||||
}
|
||||
|
||||
loadDriver();
|
||||
void loadDriver();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [primaryDriverId]);
|
||||
}, [primaryDriverId, driverService]);
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (!session?.user || !primaryDriverId || !driver) {
|
||||
@@ -153,7 +151,7 @@ export default function UserPill() {
|
||||
}
|
||||
}
|
||||
|
||||
const avatarSrc = getImageService().getDriverAvatar(primaryDriverId);
|
||||
const avatarSrc = mediaService.getDriverAvatar(primaryDriverId);
|
||||
|
||||
return {
|
||||
driver,
|
||||
@@ -161,7 +159,7 @@ export default function UserPill() {
|
||||
rating,
|
||||
rank,
|
||||
};
|
||||
}, [session, driver, primaryDriverId]);
|
||||
}, [session, driver, primaryDriverId, mediaService]);
|
||||
|
||||
// Close menu when clicking outside
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user