Add unit testing infrastructure with Vitest

Set up Vitest for both server (Node + Supertest) and client (jsdom + React
Testing Library). Extract Express app into app.js for testability. Add example
tests covering bills validation, bills route CRUD, ThemeContext, and App nav
rendering. Update CLAUDE.md with testing docs and requirement to write tests
with features.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 21:03:29 -04:00
parent 10f8debdf5
commit e9f5a48f2d
15 changed files with 3851 additions and 40 deletions

1932
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,9 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"react": "^18.3.1",
@@ -13,7 +15,12 @@
"recharts": "^3.8.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@vitejs/plugin-react": "^4.3.0",
"vite": "^5.2.11"
"jsdom": "^29.0.0",
"vite": "^5.2.11",
"vitest": "^4.1.0"
}
}

View File

@@ -0,0 +1,33 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { ThemeProvider } from '../ThemeContext.jsx';
import App from '../App.jsx';
function renderApp(initialRoute = '/') {
return render(
<MemoryRouter initialEntries={[initialRoute]}>
<ThemeProvider>
<App />
</ThemeProvider>
</MemoryRouter>
);
}
describe('App', () => {
it('renders the navigation bar', () => {
renderApp();
expect(screen.getByText('Budget')).toBeInTheDocument();
expect(screen.getByText('Paychecks')).toBeInTheDocument();
expect(screen.getByText('Bills')).toBeInTheDocument();
expect(screen.getByText('Financing')).toBeInTheDocument();
expect(screen.getByText('Settings')).toBeInTheDocument();
});
it('renders theme toggle button', () => {
renderApp();
expect(screen.getByRole('button', { name: /toggle theme/i })).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,73 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider, useTheme } from '../ThemeContext.jsx';
function ThemeDisplay() {
const { theme, toggle } = useTheme();
return (
<div>
<span data-testid="theme">{theme}</span>
<button onClick={toggle}>Toggle</button>
</div>
);
}
describe('ThemeContext', () => {
beforeEach(() => {
localStorage.clear();
document.documentElement.removeAttribute('data-theme');
});
it('defaults to light theme when no preference', () => {
// jsdom defaults to no prefers-color-scheme match
render(
<ThemeProvider>
<ThemeDisplay />
</ThemeProvider>
);
expect(screen.getByTestId('theme').textContent).toBe('light');
});
it('reads stored theme from localStorage', () => {
localStorage.setItem('theme', 'dark');
render(
<ThemeProvider>
<ThemeDisplay />
</ThemeProvider>
);
expect(screen.getByTestId('theme').textContent).toBe('dark');
});
it('toggles theme on button click', async () => {
const user = userEvent.setup();
render(
<ThemeProvider>
<ThemeDisplay />
</ThemeProvider>
);
expect(screen.getByTestId('theme').textContent).toBe('light');
await user.click(screen.getByText('Toggle'));
expect(screen.getByTestId('theme').textContent).toBe('dark');
expect(localStorage.getItem('theme')).toBe('dark');
});
it('sets data-theme attribute on document', () => {
localStorage.setItem('theme', 'dark');
render(
<ThemeProvider>
<ThemeDisplay />
</ThemeProvider>
);
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
});
});

16
client/src/test/setup.js Normal file
View File

@@ -0,0 +1,16 @@
import '@testing-library/jest-dom';
// jsdom doesn't implement window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: (query) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => {},
}),
});

11
client/vitest.config.js Normal file
View File

@@ -0,0 +1,11 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.js',
},
});