84 lines
3.5 KiB
TypeScript
84 lines
3.5 KiB
TypeScript
import React from 'react';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
user: { uid: 'owner', isAnonymous: false } as { uid: string; isAnonymous: boolean } | null,
|
|
auth: vi.fn(),
|
|
context: vi.fn(),
|
|
rides: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('firebase/auth', () => ({
|
|
onAuthStateChanged: mocks.auth,
|
|
}));
|
|
vi.mock('../lib/firebase-client', () => ({ firebaseAuth: {} }));
|
|
vi.mock('next/navigation', () => ({
|
|
useRouter: () => ({ push: vi.fn() }),
|
|
useSearchParams: () => new URLSearchParams(),
|
|
}));
|
|
vi.mock('../lib/club-data', () => ({
|
|
fetchFounderClubContext: mocks.context,
|
|
fetchClubRides: mocks.rides,
|
|
sanitizePortalError: () => 'Could not load club stats.',
|
|
}));
|
|
vi.mock('../components/founder-portal/founder-portal-shell', () => ({
|
|
FounderPortalShell: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
|
}));
|
|
|
|
import { FounderStatsPage } from '../components/founder-portal/founder-stats-page';
|
|
|
|
const context = {
|
|
club: { id: 'canonical-club', slug: 'club-slug', name: 'Riders Club', memberCount: 12 },
|
|
currentUser: { uid: 'owner', role: 'owner' },
|
|
permissions: { role: 'owner', canManageSettings: true },
|
|
isDemoFixture: false,
|
|
};
|
|
|
|
describe('Founder Stats', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mocks.user = { uid: 'owner', isAnonymous: false };
|
|
mocks.auth.mockImplementation((_auth, deliver) => { deliver(mocks.user); return vi.fn(); });
|
|
mocks.context.mockResolvedValue(context);
|
|
mocks.rides.mockResolvedValue({
|
|
upcomingRides: [
|
|
{ id: 'r1', title: 'Saturday Ride', status: 'scheduled', confirmedCount: 3 },
|
|
{ id: 'r2', title: 'Sunday Ride', status: 'assembling', confirmedCount: 2 },
|
|
],
|
|
pastRides: [
|
|
{ id: 'r3', title: 'Last Week Ride', status: 'completed', confirmedCount: 4 },
|
|
{ id: 'r4', title: 'Rainout', status: 'canceled', confirmedCount: 1 },
|
|
{ id: 'r5', title: 'Legacy Ride', status: null, confirmedCount: 1 },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('derives labeled stats from the authorized club and bounded ride lists', async () => {
|
|
render(<FounderStatsPage clubId="club-slug" />);
|
|
await screen.findByText('Last Week Ride');
|
|
expect(mocks.context).toHaveBeenCalledWith('club-slug', 'owner', { allowDemoPreview: false });
|
|
expect(mocks.rides).toHaveBeenCalledWith('canonical-club', 'owner', { allowDemoPreview: false });
|
|
expect(screen.getByText('Active members').closest('.glass-card')?.textContent).toContain('12');
|
|
expect(screen.getByText('Confirmed ride seats').closest('.glass-card')?.textContent).toContain('5');
|
|
expect(screen.getByRole('link', { name: 'Manage rides' }).getAttribute('href')).toBe('/clubs/club-slug/manage/rides');
|
|
expect(screen.getAllByText(/up to 30/i).length).toBeGreaterThan(0);
|
|
expect(screen.getByText('Unclassified').parentElement?.textContent).toContain('1');
|
|
});
|
|
|
|
it('does not query club data when signed out', async () => {
|
|
mocks.user = null;
|
|
render(<FounderStatsPage clubId="club-slug" />);
|
|
await screen.findByText('Authentication Required');
|
|
expect(mocks.context).not.toHaveBeenCalled();
|
|
expect(mocks.rides).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not query rides for ordinary members', async () => {
|
|
mocks.context.mockResolvedValue({ ...context, permissions: { role: 'member', canManageSettings: false } });
|
|
render(<FounderStatsPage clubId="club-slug" />);
|
|
await screen.findByText('Management Access Restricted');
|
|
expect(mocks.rides).not.toHaveBeenCalled();
|
|
});
|
|
});
|