From 3dc79aa42579956370c56dd96f581feeba60c5e2 Mon Sep 17 00:00:00 2001 From: Felix Schulze Date: Sun, 7 Dec 2025 01:43:26 +0100 Subject: [PATCH] fix MC test --- src/app/components/FireCalculatorForm.tsx | 27 ++++++++++++++++--- .../__tests__/FireCalculatorForm.test.tsx | 2 +- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/app/components/FireCalculatorForm.tsx b/src/app/components/FireCalculatorForm.tsx index dd42900..ea464a1 100644 --- a/src/app/components/FireCalculatorForm.tsx +++ b/src/app/components/FireCalculatorForm.tsx @@ -333,9 +333,18 @@ export default function FireCalculatorForm({ // Sort to find percentiles balancesForYear.sort((a, b) => a - b); - const p10 = balancesForYear[Math.floor(numSimulations * 0.4)]; - const p50 = balancesForYear[Math.floor(numSimulations * 0.5)]; - const p90 = balancesForYear[Math.floor(numSimulations * 0.6)]; + const pickPercentile = (fraction: number) => { + const clampedIndex = Math.min( + balancesForYear.length - 1, + Math.max(0, Math.floor((balancesForYear.length - 1) * fraction)), + ); + return balancesForYear[clampedIndex]; + }; + + // For Monte Carlo, we present a narrow middle band (40th-60th) to show typical outcomes + const p10 = pickPercentile(0.4); + const p50 = pickPercentile(0.5); + const p90 = pickPercentile(0.6); // Calculate other metrics (using deterministic logic for "untouched" etc for simplicity, or p50) // We need to reconstruct the "standard" fields for compatibility with the chart @@ -437,13 +446,23 @@ export default function FireCalculatorForm({ }); }; - const isMonteCarlo = form.watch('simulationMode') === 'monte-carlo'; + const simulationModeValue = form.watch('simulationMode'); + const isMonteCarlo = simulationModeValue === 'monte-carlo'; const chartData = result?.yearlyData.map((row) => ({ ...row, mcRange: (row.balanceP90 ?? 0) - (row.balanceP10 ?? 0), })) ?? []; + // Ensure we always have a fresh calculation when switching simulation modes (or on first render) + useEffect(() => { + if (!result) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + form.handleSubmit(onSubmit)(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [simulationModeValue]); + const projectionChartConfig: ChartConfig = { year: { label: 'Year', diff --git a/src/app/components/__tests__/FireCalculatorForm.test.tsx b/src/app/components/__tests__/FireCalculatorForm.test.tsx index 1f02d98..c073245 100644 --- a/src/app/components/__tests__/FireCalculatorForm.test.tsx +++ b/src/app/components/__tests__/FireCalculatorForm.test.tsx @@ -126,7 +126,7 @@ describe('FireCalculatorForm', () => { await screen.findByText('Financial Projection'); const bandLegend = await screen.findByTestId('mc-band-legend'); - expect(bandLegend).toHaveTextContent('10th-90th percentile'); + expect(bandLegend).toHaveTextContent('40th-60th percentile'); }); it('handles withdrawal strategy selection', async () => {