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:
33
client/src/__tests__/App.test.jsx
Normal file
33
client/src/__tests__/App.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
73
client/src/__tests__/ThemeContext.test.jsx
Normal file
73
client/src/__tests__/ThemeContext.test.jsx
Normal 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
16
client/src/test/setup.js
Normal 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: () => {},
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user