This commit is contained in:
2025-12-16 12:14:06 +01:00
parent 9a891ac8b3
commit 7d3393e1b9
90 changed files with 20 additions and 974 deletions

View File

@@ -1,102 +0,0 @@
import { describe, it, expect, vi } from 'vitest';
import React from 'react';
import { render, screen } from '@testing-library/react';
vi.mock('next/navigation', () => ({
usePathname: () => '/',
useRouter: () => ({
push: () => {},
replace: () => {},
prefetch: () => {},
}),
}));
vi.mock('next/link', () => {
const ActualLink = ({ href, children, ...rest }: any) => (
<a href={href} {...rest}>
{children}
</a>
);
return { default: ActualLink };
});
vi.mock('../../../apps/website/components/profile/UserPill', () => {
return {
__esModule: true,
default: function MockUserPill() {
return (
<div>
<a href="/auth/login">Sign In</a>
<a href="/auth/signup">Get Started</a>
<button type="button">Logout</button>
</div>
);
},
};
});
vi.mock('../../../apps/website/lib/auth/AuthContext', () => {
const React = require('react');
const AuthContext = React.createContext({
session: null,
loading: false,
login: () => {},
logout: async () => {},
refreshSession: async () => {},
});
const AuthProvider = ({ initialSession, children }: { initialSession?: any; children: React.ReactNode }) => (
<AuthContext.Provider value={{ session: initialSession, loading: false, login: () => {}, logout: async () => {}, refreshSession: async () => {} }}>{children}</AuthContext.Provider>
);
const useAuth = () => React.useContext(AuthContext);
return {
__esModule: true,
AuthProvider,
useAuth,
};
});
import { AuthProvider } from '../../../apps/website/lib/auth/AuthContext';
import { AlphaNav } from '../../../apps/website/components/alpha/AlphaNav';
describe('AlphaNav', () => {
it('hides Dashboard link and uses Home when unauthenticated', () => {
render(
<AuthProvider
initialSession={null}
>
<AlphaNav />
</AuthProvider>,
);
const dashboardLinks = screen.queryAllByText('Dashboard');
expect(dashboardLinks.length).toBe(0);
const homeLink = screen.getByText('Home');
expect(homeLink).toBeInTheDocument();
});
it('shows Dashboard link and hides Home when authenticated', () => {
render(
<AuthProvider
initialSession={{
user: { id: 'user-1', displayName: 'Test User' },
issuedAt: Date.now(),
expiresAt: Date.now() + 3600000,
token: 'fake-token',
}}
>
<AlphaNav />
</AuthProvider>,
);
const dashboard = screen.getByText('Dashboard');
expect(dashboard).toBeInTheDocument();
expect((dashboard as HTMLAnchorElement).getAttribute('href')).toBe('/dashboard');
const homeLink = screen.queryByText('Home');
expect(homeLink).toBeNull();
});
});

View File

@@ -1,123 +0,0 @@
import { describe, it, expect, vi } from 'vitest';
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
// Mock() Button component
vi.mock('../../../apps/website/components/ui/Button', () => ({
default: ({ onClick, children, className, title, variant }: any) => (
<button
onClick={onClick}
className={className}
title={title}
data-variant={variant}
data-testid="penalty-button"
>
{children}
</button>
),
}));
import InlinePenaltyButton from '../../../apps/website/components/races/InlinePenaltyButton';
describe('InlinePenaltyButton', () => {
const mockDriver = { id: 'driver-1', name: 'Test Driver' };
const mockOnPenaltyClick = vi.fn();
it('should not render when user is not admin', () => {
render(
<InlinePenaltyButton
driver={mockDriver}
onPenaltyClick={mockOnPenaltyClick}
isAdmin={false}
/>
);
const button = screen.queryByTestId('penalty-button');
expect(button).not.toBeInTheDocument();
});
it('should render when user is admin', () => {
render(
<InlinePenaltyButton
driver={mockDriver}
onPenaltyClick={mockOnPenaltyClick}
isAdmin={true}
/>
);
const button = screen.getByTestId('penalty-button');
expect(button).toBeInTheDocument();
expect(button).toHaveAttribute('title', 'Issue penalty to Test Driver');
expect(button).toHaveAttribute('data-variant', 'danger');
});
it('should call onPenaltyClick when button is clicked', () => {
render(
<InlinePenaltyButton
driver={mockDriver}
onPenaltyClick={mockOnPenaltyClick}
isAdmin={true}
/>
);
const button = screen.getByTestId('penalty-button');
fireEvent.click(button);
expect(mockOnPenaltyClick).toHaveBeenCalledTimes(1);
expect(mockOnPenaltyClick).toHaveBeenCalledWith(mockDriver);
});
it('should not crash when onPenaltyClick is not provided', () => {
render(
<InlinePenaltyButton
driver={mockDriver}
isAdmin={true}
/>
);
const button = screen.getByTestId('penalty-button');
// Should not crash when clicked without onPenaltyClick
expect(() => fireEvent.click(button)).not.toThrow();
});
it('should have proper button styling for spacing', () => {
render(
<InlinePenaltyButton
driver={mockDriver}
onPenaltyClick={mockOnPenaltyClick}
isAdmin={true}
/>
);
const button = screen.getByTestId('penalty-button');
// Check that button has proper spacing classes
expect(button).toHaveClass('p-1.5');
expect(button).toHaveClass('min-h-[32px]');
expect(button).toHaveClass('w-8');
expect(button).toHaveClass('h-8');
expect(button).toHaveClass('rounded-full');
expect(button).toHaveClass('flex');
expect(button).toHaveClass('items-center');
expect(button).toHaveClass('justify-center');
});
it('should render AlertTriangle icon with proper sizing', () => {
render(
<InlinePenaltyButton
driver={mockDriver}
onPenaltyClick={mockOnPenaltyClick}
isAdmin={true}
/>
);
const button = screen.getByTestId('penalty-button');
const icon = button.querySelector('svg');
expect(icon).toBeInTheDocument();
expect(icon).toHaveClass('w-4');
expect(icon).toHaveClass('h-4');
expect(icon).toHaveClass('flex-shrink-0');
});
});

View File

@@ -1,34 +0,0 @@
import { describe, it, expect } from 'vitest';
/**
* Auth + caching behavior for RootLayout and Dashboard.
*
* These tests assert that:
* - RootLayout is marked dynamic so it re-evaluates cookies per request.
* - DashboardPage is also dynamic (no static caching of auth state).
*/
describe('RootLayout auth caching behavior', () => {
it('is configured as dynamic to avoid static auth caching', async () => {
const layoutModule = (await import(
'../../../../apps/website/app/layout',
)) as { dynamic?: string };
// Next.js dynamic routing flag
const dynamic = layoutModule.dynamic;
expect(dynamic).toBe('force-dynamic');
});
});
describe('Dashboard auth caching behavior', () => {
it('is configured as dynamic to evaluate auth per request', async () => {
const dashboardModule = (await import(
'../../../../apps/website/app/dashboard/page',
)) as { dynamic?: string };
const dynamic = dashboardModule.dynamic;
expect(dynamic).toBe('force-dynamic');
});
});

View File

@@ -1,124 +0,0 @@
import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
// --- Mocks for Next.js navigation ---
const useSearchParamsMock = vi.fn();
const useRouterMock = vi.fn();
const routerInstance = {
push: vi.fn(),
replace: vi.fn(),
prefetch: vi.fn(),
};
vi.mock('next/navigation', () => {
return {
useSearchParams: () => useSearchParamsMock(),
useRouter: () => {
return useRouterMock() ?? routerInstance;
},
};
});
// Minimal next/link mock to keep existing patterns consistent
vi.mock('next/link', () => {
const ActualLink = ({ href, children, ...rest }: any) => (
<a href={href} {...rest}>
{children}
</a>
);
return { default: ActualLink };
});
import CreateLeaguePage from '../../../../apps/website/app/leagues/create/page';
// Helper to build a searchParams-like object
function createSearchParams(stepValue: string | null) {
return {
get: (key: string) => {
if (key === 'step') {
return stepValue;
}
return null;
},
} as URLSearchParams;
}
describe('CreateLeaguePage - URL-bound wizard steps', () => {
beforeEach(() => {
useSearchParamsMock.mockReset();
useRouterMock.mockReset();
routerInstance.push.mockReset();
routerInstance.replace.mockReset();
});
it('defaults to basics step when step param is missing', () => {
useSearchParamsMock.mockReturnValue(createSearchParams(null));
render(<CreateLeaguePage />);
// Basics step title from the wizard
expect(screen.getByText('Name your league')).toBeInTheDocument();
});
it('treats invalid step value as basics', () => {
useSearchParamsMock.mockReturnValue(createSearchParams('invalid-step'));
render(<CreateLeaguePage />);
expect(screen.getByText('Name your league')).toBeInTheDocument();
});
it('mounts directly on scoring step when step=scoring', () => {
useSearchParamsMock.mockReturnValue(createSearchParams('scoring'));
render(<CreateLeaguePage />);
// Step 4 title in the wizard
expect(screen.getByText('Scoring & championships')).toBeInTheDocument();
});
it('renders a Continue button on the basics step that can trigger navigation when the form is valid', () => {
useSearchParamsMock.mockReturnValue(createSearchParams(null));
useRouterMock.mockReturnValue(routerInstance);
render(<CreateLeaguePage />);
const continueButton = screen.getByRole('button', { name: /continue/i });
// The underlying wizard only enables this button when the form is valid.
// This smoke-test just confirms the button is present and clickable without asserting navigation,
// leaving detailed navigation behavior to more focused integration tests.
fireEvent.click(continueButton);
});
it('clicking Back from schedule navigates to step=structure via router', () => {
useSearchParamsMock.mockReturnValue(createSearchParams('schedule'));
useRouterMock.mockReturnValue(routerInstance);
render(<CreateLeaguePage />);
const backButton = screen.getByRole('button', { name: /back/i });
fireEvent.click(backButton);
expect(routerInstance.push).toHaveBeenCalledTimes(1);
const call = routerInstance.push.mock.calls[0];
expect(call).toBeDefined();
const callArg = (call as [string])[0];
expect(callArg).toContain('/leagues/create');
expect(callArg).toContain('step=structure');
});
it('derives current step solely from URL so a "reload" keeps the same step', () => {
useSearchParamsMock.mockReturnValueOnce(createSearchParams('scoring'));
useSearchParamsMock.mockReturnValueOnce(createSearchParams('scoring'));
render(<CreateLeaguePage />);
expect(screen.getAllByText('Scoring & championships').length).toBeGreaterThanOrEqual(1);
// Simulate a logical reload by re-rendering with the same URL state
render(<CreateLeaguePage />);
expect(screen.getAllByText('Scoring & championships').length).toBeGreaterThanOrEqual(1);
});
});