Files
fire/src/app/components/__tests__/FireCalculatorForm.test.tsx
Felix Schulze 3dc79aa425
All checks were successful
Lint / Lint and Typecheck (push) Successful in 41s
fix MC test
2025-12-07 01:43:26 +01:00

147 lines
4.7 KiB
TypeScript

import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import FireCalculatorForm from '../FireCalculatorForm';
import { describe, it, expect, vi, beforeAll } from 'vitest';
// Mocking ResizeObserver
class ResizeObserver {
observe() {
/* noop */
}
unobserve() {
/* noop */
}
disconnect() {
/* noop */
}
}
global.ResizeObserver = ResizeObserver;
// Fix for Radix UI pointer capture error in JSDOM
beforeAll(() => {
window.HTMLElement.prototype.hasPointerCapture = vi.fn();
window.HTMLElement.prototype.setPointerCapture = vi.fn();
window.HTMLElement.prototype.releasePointerCapture = vi.fn();
window.HTMLElement.prototype.scrollIntoView = vi.fn();
});
// Mock Recharts ResponsiveContainer
vi.mock('recharts', async () => {
const originalModule = await vi.importActual('recharts');
return {
...originalModule,
ResponsiveContainer: ({ children }: { children: React.ReactNode }) => (
<div style={{ width: '500px', height: '300px' }}>{children}</div>
),
};
});
// Mock next/navigation
vi.mock('next/navigation', () => ({
useSearchParams: () => new URLSearchParams(),
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
prefetch: vi.fn(),
}),
usePathname: () => '/',
}));
describe('FireCalculatorForm', () => {
it('renders the form with default values', () => {
render(<FireCalculatorForm />);
expect(screen.getByText('FIRE Calculator')).toBeInTheDocument();
expect(screen.getByRole('spinbutton', { name: /Starting Capital/i })).toHaveValue(50000);
expect(screen.getByRole('spinbutton', { name: /Monthly Savings/i })).toHaveValue(1500);
expect(screen.getByRole('spinbutton', { name: /Current Age/i })).toHaveValue(25);
});
it('calculates and displays results when submitted', async () => {
const user = userEvent.setup();
render(<FireCalculatorForm />);
const calculateButton = screen.getByRole('button', { name: /Calculate/i });
await user.click(calculateButton);
await waitFor(() => {
expect(screen.getByText('Financial Projection')).toBeInTheDocument();
expect(screen.getByText('FIRE Number')).toBeInTheDocument();
});
});
it('allows changing inputs', async () => {
render(<FireCalculatorForm />);
const savingsInput = screen.getByRole('spinbutton', { name: /Monthly Savings/i });
fireEvent.change(savingsInput, { target: { value: '2000' } });
await waitFor(() => {
expect(savingsInput).toHaveValue(2000);
});
});
it('validates inputs', async () => {
const user = userEvent.setup();
render(<FireCalculatorForm />);
const ageInput = screen.getByRole('spinbutton', { name: /Current Age/i });
// Use fireEvent to set invalid value directly
fireEvent.change(ageInput, { target: { value: '-5' } });
const calculateButton = screen.getByRole('button', { name: /Calculate/i });
await user.click(calculateButton);
// Look for error message text
expect(await screen.findByText(/Age must be at least 1/i)).toBeInTheDocument();
});
it('toggles Monte Carlo simulation mode', async () => {
const user = userEvent.setup();
render(<FireCalculatorForm />);
// Select Trigger
const modeTrigger = screen.getByRole('combobox', { name: /Simulation Mode/i });
await user.click(modeTrigger);
// Select Monte Carlo from dropdown
const monteCarloOption = await screen.findByRole('option', { name: /Monte Carlo/i });
await user.click(monteCarloOption);
// Verify Volatility input appears
expect(await screen.findByRole('spinbutton', { name: /Market Volatility/i })).toBeInTheDocument();
});
it('shows Monte Carlo percentile bounds on the chart', async () => {
const user = userEvent.setup();
render(<FireCalculatorForm />);
const modeTrigger = screen.getByRole('combobox', { name: /Simulation Mode/i });
await user.click(modeTrigger);
const monteCarloOption = await screen.findByRole('option', { name: /Monte Carlo/i });
await user.click(monteCarloOption);
await screen.findByText('Financial Projection');
const bandLegend = await screen.findByTestId('mc-band-legend');
expect(bandLegend).toHaveTextContent('40th-60th percentile');
});
it('handles withdrawal strategy selection', async () => {
const user = userEvent.setup();
render(<FireCalculatorForm />);
const strategyTrigger = screen.getByRole('combobox', { name: /Withdrawal Strategy/i });
await user.click(strategyTrigger);
const percentageOption = await screen.findByRole('option', { name: /Percentage of Portfolio/i });
await user.click(percentageOption);
expect(
await screen.findByRole('spinbutton', { name: /Withdrawal Percentage/i }),
).toBeInTheDocument();
});
});