Skip to content

Commit 2eccb4b

Browse files
anth-volkclaude
andcommitted
Implement dashboard from plan
Virginia tax and benefit reform calculator with household and statewide impact analysis for CTC, EITC, and income tax rate reforms at federal and state levels. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0cf050d commit 2eccb4b

20 files changed

+2043
-677
lines changed

__tests__/formatters.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { describe, it, expect } from 'vitest'
2+
import {
3+
formatCurrency,
4+
formatCurrencySigned,
5+
formatPercent,
6+
formatPercentagePoints,
7+
formatCompact,
8+
tickCurrency,
9+
tickPercent,
10+
} from '../lib/formatters'
11+
12+
describe('formatCurrency', () => {
13+
it('formats positive amounts', () => {
14+
expect(formatCurrency(50000)).toBe('$50,000')
15+
})
16+
17+
it('formats negative amounts with sign before dollar sign', () => {
18+
// Intl.NumberFormat produces "-$100" not "$-100"
19+
const result = formatCurrency(-100)
20+
expect(result).toMatch(/^-\$100$/)
21+
})
22+
23+
it('formats zero', () => {
24+
expect(formatCurrency(0)).toBe('$0')
25+
})
26+
})
27+
28+
describe('formatCurrencySigned', () => {
29+
it('adds + sign for positive values', () => {
30+
expect(formatCurrencySigned(2500)).toBe('+$2,500')
31+
})
32+
33+
it('keeps - sign for negative values', () => {
34+
expect(formatCurrencySigned(-1000)).toMatch(/^-\$1,000$/)
35+
})
36+
37+
it('formats zero without sign', () => {
38+
expect(formatCurrencySigned(0)).toBe('$0')
39+
})
40+
})
41+
42+
describe('formatPercent', () => {
43+
it('formats decimal as percent', () => {
44+
expect(formatPercent(0.22)).toBe('22.0%')
45+
})
46+
47+
it('formats zero', () => {
48+
expect(formatPercent(0)).toBe('0.0%')
49+
})
50+
})
51+
52+
describe('formatPercentagePoints', () => {
53+
it('formats positive change', () => {
54+
expect(formatPercentagePoints(0.5)).toBe('+0.50 pp')
55+
})
56+
57+
it('formats negative change', () => {
58+
expect(formatPercentagePoints(-1.2)).toBe('-1.20 pp')
59+
})
60+
})
61+
62+
describe('formatCompact', () => {
63+
it('formats millions', () => {
64+
const result = formatCompact(3500000)
65+
expect(result).toMatch(/3\.5M/)
66+
})
67+
68+
it('formats thousands', () => {
69+
const result = formatCompact(15000)
70+
expect(result).toMatch(/15K/)
71+
})
72+
})
73+
74+
describe('tickCurrency', () => {
75+
it('formats small values', () => {
76+
expect(tickCurrency(5000)).toBe('$5,000')
77+
})
78+
79+
it('uses compact format for millions', () => {
80+
const result = tickCurrency(2000000)
81+
expect(result).toMatch(/\$2(\.0)?M/)
82+
})
83+
})
84+
85+
describe('tickPercent', () => {
86+
it('formats decimal as percent', () => {
87+
expect(tickPercent(0.32)).toBe('32.0%')
88+
})
89+
})

__tests__/page.test.tsx

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,136 @@
1-
import { describe, it, expect } from 'vitest'
2-
import { render, screen } from '@testing-library/react'
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
import { render, screen, fireEvent } from '@testing-library/react'
33
import Home from '../app/page'
44

5+
// Mock the embedding module since tests run in jsdom without real window.location.hash
6+
vi.mock('../lib/embedding', () => ({
7+
getCountryFromHash: () => 'us',
8+
isEmbedded: () => false,
9+
updateHash: vi.fn(),
10+
getShareUrl: () => 'https://policyengine.org/us/va-reform-dashboard',
11+
}))
12+
13+
// Mock the API client to prevent actual network calls
14+
vi.mock('../lib/api/client', () => ({
15+
submitJob: vi.fn().mockRejectedValue(new Error('No API in tests')),
16+
pollStatus: vi.fn().mockRejectedValue(new Error('No API in tests')),
17+
}))
18+
519
describe('Home page', () => {
20+
beforeEach(() => {
21+
vi.clearAllMocks()
22+
})
23+
624
it('renders the dashboard title', () => {
725
render(<Home />)
826
expect(
927
screen.getByText('Virginia tax and benefit reform calculator'),
1028
).toBeDefined()
1129
})
1230

13-
it('renders household configuration section', () => {
14-
render(<Home />)
15-
expect(screen.getByText('Household configuration')).toBeDefined()
16-
})
17-
1831
it('renders both tab options', () => {
1932
render(<Home />)
2033
expect(screen.getByText('Household impact')).toBeDefined()
2134
expect(screen.getByText('Statewide impact')).toBeDefined()
2235
})
2336

24-
it('renders reform parameter sections', () => {
37+
it('renders all sidebar input group labels', () => {
2538
render(<Home />)
39+
expect(screen.getByText('Household configuration')).toBeDefined()
2640
expect(screen.getByText('Federal income tax')).toBeDefined()
2741
expect(screen.getByText('Federal Child Tax Credit')).toBeDefined()
2842
expect(screen.getByText('Federal EITC')).toBeDefined()
2943
expect(screen.getByText('Virginia income tax')).toBeDefined()
3044
expect(screen.getByText('Virginia EITC')).toBeDefined()
3145
})
46+
47+
it('renders filing status label and options', () => {
48+
render(<Home />)
49+
expect(screen.getByText('Filing status')).toBeDefined()
50+
// The select should have the three options available
51+
expect(screen.getByText('Single')).toBeDefined()
52+
})
53+
54+
it('shows spouse age field only when filing status is joint', () => {
55+
render(<Home />)
56+
// Initially single - no spouse field
57+
expect(screen.queryByText("Spouse's age")).toBeNull()
58+
59+
// Change to joint by finding and changing the select
60+
const selects = document.querySelectorAll('select')
61+
const filingStatusSelect = Array.from(selects).find((s) =>
62+
Array.from(s.options).some((o) => o.value === 'joint'),
63+
)
64+
expect(filingStatusSelect).toBeDefined()
65+
fireEvent.change(filingStatusSelect!, { target: { value: 'joint' } })
66+
expect(screen.getByText("Spouse's age")).toBeDefined()
67+
})
68+
69+
it('renders dependent age inputs based on count', () => {
70+
render(<Home />)
71+
// No dependent age fields initially
72+
expect(screen.queryByText('Dependent 1')).toBeNull()
73+
74+
// Find the number of dependents input
75+
const numDepsInput = document.querySelector(
76+
'input[type="number"][min="0"][max="10"]',
77+
) as HTMLInputElement
78+
expect(numDepsInput).toBeDefined()
79+
80+
// Set to 2 dependents
81+
fireEvent.change(numDepsInput, { target: { value: '2' } })
82+
expect(screen.getByText('Dependent 1')).toBeDefined()
83+
expect(screen.getByText('Dependent 2')).toBeDefined()
84+
})
85+
86+
it('renders all federal bracket rate sliders', () => {
87+
render(<Home />)
88+
expect(screen.getByText('10% bracket rate')).toBeDefined()
89+
expect(screen.getByText('12% bracket rate')).toBeDefined()
90+
expect(screen.getByText('22% bracket rate')).toBeDefined()
91+
expect(screen.getByText('24% bracket rate')).toBeDefined()
92+
expect(screen.getByText('32% bracket rate')).toBeDefined()
93+
expect(screen.getByText('35% bracket rate')).toBeDefined()
94+
expect(screen.getByText('37% bracket rate')).toBeDefined()
95+
})
96+
97+
it('renders CTC input fields', () => {
98+
render(<Home />)
99+
expect(screen.getByText('CTC amount per child')).toBeDefined()
100+
expect(screen.getByText('Phase-out threshold (single)')).toBeDefined()
101+
expect(screen.getByText('Phase-out threshold (joint)')).toBeDefined()
102+
expect(screen.getByText('Max refundable (ACTC) per child')).toBeDefined()
103+
expect(screen.getByText('Make fully refundable')).toBeDefined()
104+
})
105+
106+
it('renders EITC fields for all child counts', () => {
107+
render(<Home />)
108+
expect(screen.getByText('Max EITC (0 children)')).toBeDefined()
109+
expect(screen.getByText('Max EITC (1 child)')).toBeDefined()
110+
expect(screen.getByText('Max EITC (2 children)')).toBeDefined()
111+
expect(screen.getByText('Max EITC (3+ children)')).toBeDefined()
112+
})
113+
114+
it('renders VA tax rate sliders', () => {
115+
render(<Home />)
116+
expect(screen.getByText('Bracket 1 rate ($0-$3k)')).toBeDefined()
117+
expect(screen.getByText('Bracket 2 rate ($3k-$5k)')).toBeDefined()
118+
expect(screen.getByText('Bracket 3 rate ($5k-$17k)')).toBeDefined()
119+
expect(screen.getByText('Bracket 4 rate ($17k+)')).toBeDefined()
120+
})
121+
122+
it('renders VA EITC match rate slider', () => {
123+
render(<Home />)
124+
expect(screen.getByText('VA EITC match rate')).toBeDefined()
125+
})
126+
127+
it('renders VA standard deduction field', () => {
128+
render(<Home />)
129+
expect(screen.getByText('Standard deduction')).toBeDefined()
130+
})
131+
132+
it('renders PolicyEngine logo in header', () => {
133+
render(<Home />)
134+
expect(screen.getByAltText('PolicyEngine')).toBeDefined()
135+
})
32136
})

0 commit comments

Comments
 (0)