Skip to content

Commit 87925f0

Browse files
committed
added tests and docs about frontend testing
1 parent 1d0eca6 commit 87925f0

File tree

14 files changed

+4020
-0
lines changed

14 files changed

+4020
-0
lines changed

docs/testing/frontend-testing.md

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
# Frontend testing
2+
3+
The frontend uses Vitest for unit and integration tests, with Playwright for end-to-end scenarios. Tests live alongside the source code in `__tests__` directories, following the same structure as the components they verify. The setup uses jsdom for DOM simulation and @testing-library/svelte for component rendering, giving you a realistic browser-like environment without the overhead of spinning up actual browsers for every test run.
4+
5+
## Quick start
6+
7+
Run all unit tests from the frontend directory:
8+
9+
```bash
10+
cd frontend
11+
npm run test
12+
```
13+
14+
For continuous development with watch mode:
15+
16+
```bash
17+
npm run test:watch
18+
```
19+
20+
Run with coverage report:
21+
22+
```bash
23+
npm run test:coverage
24+
```
25+
26+
End-to-end tests require the full stack running locally:
27+
28+
```bash
29+
npm run test:e2e
30+
```
31+
32+
## Test structure
33+
34+
Tests follow a consistent directory layout that mirrors the source code. Each testable module has a `__tests__` folder next to it containing the corresponding test files:
35+
36+
```
37+
src/
38+
├── stores/
39+
│ ├── auth.ts
40+
│ ├── theme.ts
41+
│ ├── toastStore.ts
42+
│ ├── notificationStore.ts
43+
│ ├── errorStore.ts
44+
│ └── __tests__/
45+
│ ├── auth.test.ts
46+
│ ├── theme.test.ts
47+
│ ├── toastStore.test.ts
48+
│ ├── notificationStore.test.ts
49+
│ └── errorStore.test.ts
50+
├── lib/
51+
│ ├── auth-init.ts
52+
│ ├── settings-cache.ts
53+
│ ├── user-settings.ts
54+
│ └── __tests__/
55+
│ ├── auth-init.test.ts
56+
│ ├── settings-cache.test.ts
57+
│ └── user-settings.test.ts
58+
├── utils/
59+
│ ├── meta.ts
60+
│ └── __tests__/
61+
│ └── meta.test.ts
62+
├── components/
63+
│ ├── Spinner.svelte
64+
│ ├── ErrorDisplay.svelte
65+
│ ├── Footer.svelte
66+
│ ├── ToastContainer.svelte
67+
│ └── __tests__/
68+
│ ├── Spinner.test.ts
69+
│ ├── ErrorDisplay.test.ts
70+
│ ├── Footer.test.ts
71+
│ └── ToastContainer.test.ts
72+
└── e2e/
73+
├── auth.spec.ts
74+
└── theme.spec.ts
75+
```
76+
77+
## What gets tested
78+
79+
The test suite covers several layers of the application, from pure logic to rendered components.
80+
81+
Stores handle reactive state management. Tests verify initial values, state transitions, persistence to localStorage, and subscription behavior. The auth store tests, for example, check login/logout flows, token verification with caching, and graceful handling of network errors with offline-first fallbacks.
82+
83+
Library utilities deal with initialization, caching, and API interactions. The auth-init tests verify the startup sequence that restores persisted sessions, validates tokens with the backend, and handles edge cases like expired or corrupted localStorage data. Settings cache tests ensure proper TTL expiration and nested object updates.
84+
85+
Component tests render Svelte components in jsdom and verify their DOM output, props handling, and user interactions. The Spinner tests check that size and color props produce the expected CSS classes. ErrorDisplay tests verify that network errors show user-friendly messages without exposing raw error details. ToastContainer tests confirm that toasts appear, animate, and disappear on schedule.
86+
87+
E2E tests run in Playwright against the real application. They exercise full user flows like registration, login, theme switching, and protected route access.
88+
89+
## Configuration
90+
91+
Vitest configuration lives in `vitest.config.ts`:
92+
93+
```typescript
94+
export default defineConfig({
95+
plugins: [svelte({ compilerOptions: { runes: true } }), svelteTesting()],
96+
test: {
97+
environment: 'jsdom',
98+
setupFiles: ['./vitest.setup.ts'],
99+
include: ['src/**/*.{test,spec}.{js,ts}'],
100+
globals: true,
101+
coverage: {
102+
provider: 'v8',
103+
include: ['src/**/*.{ts,svelte}'],
104+
exclude: ['src/lib/api/**', 'src/**/*.test.ts'],
105+
},
106+
},
107+
});
108+
```
109+
110+
The setup file (`vitest.setup.ts`) provides browser API mocks that jsdom lacks:
111+
112+
```typescript
113+
// localStorage and sessionStorage mocks
114+
vi.stubGlobal('localStorage', localStorageMock);
115+
vi.stubGlobal('sessionStorage', sessionStorageMock);
116+
117+
// matchMedia for theme detection
118+
vi.stubGlobal('matchMedia', vi.fn().mockImplementation(query => ({
119+
matches: false,
120+
media: query,
121+
addEventListener: vi.fn(),
122+
removeEventListener: vi.fn(),
123+
})));
124+
125+
// ResizeObserver and IntersectionObserver for layout-dependent components
126+
vi.stubGlobal('ResizeObserver', vi.fn().mockImplementation(() => ({
127+
observe: vi.fn(),
128+
unobserve: vi.fn(),
129+
disconnect: vi.fn(),
130+
})));
131+
```
132+
133+
Playwright configuration in `playwright.config.ts` sets up browser testing:
134+
135+
```typescript
136+
export default defineConfig({
137+
testDir: './e2e',
138+
timeout: 10000,
139+
use: {
140+
baseURL: 'https://localhost:5001',
141+
screenshot: 'only-on-failure',
142+
trace: 'on',
143+
},
144+
});
145+
```
146+
147+
## Writing component tests
148+
149+
Component tests use @testing-library/svelte to render components and query the DOM. The library encourages testing from the user's perspective—query by role, label, or text rather than implementation details like CSS classes or component internals.
150+
151+
A typical component test renders the component, queries for elements, and asserts on the output:
152+
153+
```typescript
154+
import { render, screen } from '@testing-library/svelte';
155+
import Spinner from '../Spinner.svelte';
156+
157+
it('renders with accessible label', () => {
158+
render(Spinner);
159+
expect(screen.getByLabelText('Loading')).toBeInTheDocument();
160+
});
161+
162+
it('applies size prop', () => {
163+
render(Spinner, { props: { size: 'large' } });
164+
const svg = screen.getByRole('status');
165+
expect(svg.classList.contains('h-8')).toBe(true);
166+
});
167+
```
168+
169+
For components with user interactions, use `@testing-library/user-event`:
170+
171+
```typescript
172+
import userEvent from '@testing-library/user-event';
173+
174+
it('calls reload on button click', async () => {
175+
const user = userEvent.setup();
176+
render(ErrorDisplay, { props: { error: 'Something broke' } });
177+
178+
await user.click(screen.getByRole('button', { name: /Reload/i }));
179+
expect(window.location.reload).toHaveBeenCalled();
180+
});
181+
```
182+
183+
Svelte 5 components using transitions need the Web Animations API mocked:
184+
185+
```typescript
186+
Element.prototype.animate = vi.fn().mockImplementation(() => ({
187+
onfinish: null,
188+
cancel: vi.fn(),
189+
finish: vi.fn(),
190+
}));
191+
```
192+
193+
## Testing stores
194+
195+
Svelte stores are plain JavaScript, so they test without any special setup. Import the store, call its methods, and check the current value with `get()`:
196+
197+
```typescript
198+
import { get } from 'svelte/store';
199+
import { toasts, addToast, removeToast } from '../toastStore';
200+
201+
beforeEach(() => {
202+
toasts.set([]);
203+
});
204+
205+
it('adds toast with correct type', () => {
206+
addToast('Success!', 'success');
207+
const current = get(toasts);
208+
expect(current[0].type).toBe('success');
209+
});
210+
```
211+
212+
For stores that persist to localStorage, mock the storage API and use `vi.resetModules()` to get fresh module state between tests:
213+
214+
```typescript
215+
beforeEach(async () => {
216+
vi.mocked(localStorage.getItem).mockReturnValue(null);
217+
vi.resetModules();
218+
});
219+
220+
it('restores from localStorage', async () => {
221+
localStorage.getItem.mockReturnValue(JSON.stringify({ theme: 'dark' }));
222+
const { theme } = await import('../theme');
223+
expect(get(theme)).toBe('dark');
224+
});
225+
```
226+
227+
## Mocking API calls
228+
229+
API functions are mocked at the module level using `vi.mock()`. Define mock functions at the top of the file, then configure their return values per test:
230+
231+
```typescript
232+
const mockLoginApi = vi.fn();
233+
vi.mock('../../lib/api', () => ({
234+
loginApiV1AuthLoginPost: (...args) => mockLoginApi(...args),
235+
}));
236+
237+
beforeEach(() => {
238+
mockLoginApi.mockReset();
239+
});
240+
241+
it('handles successful login', async () => {
242+
mockLoginApi.mockResolvedValue({
243+
data: { username: 'testuser', role: 'user', csrf_token: 'token' },
244+
error: null,
245+
});
246+
247+
const { login, isAuthenticated } = await import('../auth');
248+
await login('testuser', 'password');
249+
250+
expect(get(isAuthenticated)).toBe(true);
251+
});
252+
```
253+
254+
## CI integration
255+
256+
The frontend CI workflow runs tests as part of the build process. Unit tests run first, and if they pass, e2e tests run against the built application. Coverage reports go to Codecov for tracking.
257+
258+
```yaml
259+
- name: Run unit tests
260+
run: npm run test:coverage
261+
262+
- name: Run e2e tests
263+
run: npm run test:e2e
264+
```
265+
266+
Tests timeout after 5 minutes for unit tests and 10 minutes for e2e. If you're adding slow tests, consider whether they belong in the e2e suite rather than unit tests.
267+
268+
## Troubleshooting
269+
270+
When tests fail with "Cannot read properties of undefined (reading 'matches')", you're missing the matchMedia mock. Add it to your test file or ensure vitest.setup.ts is loading correctly.
271+
272+
Svelte transition errors like "element.animate is not a function" mean you need to mock the Web Animations API. Add the animate mock before rendering components that use `fly`, `fade`, or other transitions.
273+
274+
Timing issues with fake timers and async components usually mean you're mixing `vi.useFakeTimers()` with `waitFor()`. Either use real timers for that test or manually advance time with `vi.advanceTimersByTimeAsync()`.
275+
276+
Store tests that bleed state between runs need `vi.resetModules()` in beforeEach. This clears the module cache so each test gets fresh store instances.

0 commit comments

Comments
 (0)