Skip to content

Commit cf4c390

Browse files
authored
Merge pull request #297 from SableClient/feat/testing-setup
Add Vitest testing infrastructure
2 parents 931c13f + b2696f4 commit cf4c390

25 files changed

Lines changed: 2404 additions & 3 deletions

.changeset/feat-testing-setup.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: patch
3+
---
4+
5+
Add Vitest testing infrastructure with example tests and contributor documentation

.github/workflows/quality-checks.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,24 @@ jobs:
7979
- name: Run Knip
8080
run: pnpm run knip
8181

82+
tests:
83+
name: Tests
84+
runs-on: ubuntu-latest
85+
if: github.head_ref != 'release'
86+
permissions:
87+
contents: read
88+
steps:
89+
- name: Checkout repository
90+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
91+
with:
92+
persist-credentials: false
93+
94+
- name: Setup app
95+
uses: ./.github/actions/setup
96+
97+
- name: Run tests
98+
run: pnpm run test:run
99+
82100
build:
83101
name: Build
84102
runs-on: ubuntu-latest

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
experiment
22
dist
3+
coverage
34
node_modules
45
devAssets
56

CONTRIBUTING.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,15 @@ It is not always possible to phrase every change in such a manner, but it is des
3737

3838
Also, we use [ESLint](https://eslint.org/) for clean and stylistically consistent code syntax, so make sure your pull request follow it.
3939

40-
**Pull requests are not merged unless all quality checks are passing.** At minimum, `format`, `lint`, `typecheck`, and `knip` must all be green before a pull request can be merged. Run these locally before opening or updating a pull request:
40+
**Pull requests are not merged unless all quality checks are passing.** At minimum, `format`, `lint`, `typecheck`, `knip`, and `tests` must all be green before a pull request can be merged. Run these locally before opening or updating a pull request:
4141

4242
- `pnpm run fmt:check`
4343
- `pnpm run lint`
4444
- `pnpm run typecheck`
4545
- `pnpm run knip`
46+
- `pnpm run test:run`
47+
48+
If your change touches logic with testable behaviour, please include tests. See [docs/TESTING.md](./docs/TESTING.md) for a guide on how to write them.
4649

4750
## Restrictions on Generative AI Usage
4851

docs/TESTING.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Testing Guide
2+
3+
Sable uses [Vitest](https://vitest.dev/) as its test runner and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) for component tests. Tests run in a [jsdom](https://github.com/jsdom/jsdom) environment and coverage is collected via V8.
4+
5+
---
6+
7+
## Running tests
8+
9+
```sh
10+
# Watch mode — reruns affected tests on file save (recommended during development)
11+
pnpm test
12+
13+
# Single run — equivalent to what CI runs
14+
pnpm test:run
15+
16+
# With browser UI (interactive results viewer)
17+
pnpm test:ui
18+
19+
# With coverage report
20+
pnpm test:coverage
21+
```
22+
23+
Coverage reports are written to `coverage/`. Open `coverage/index.html` in your browser for the full HTML report.
24+
25+
---
26+
27+
## Writing tests
28+
29+
### Where to put test files
30+
31+
Place test files next to the source file they cover, with a `.test.ts` or `.test.tsx` suffix:
32+
33+
```
34+
src/app/utils/colorMXID.ts
35+
src/app/utils/colorMXID.test.ts
36+
37+
src/app/features/room/RoomTimeline.tsx
38+
src/app/features/room/RoomTimeline.test.tsx
39+
```
40+
41+
### Testing plain utility functions
42+
43+
For pure functions in `src/app/utils/`, no special setup is needed — just import and assert:
44+
45+
```ts
46+
import { describe, it, expect } from 'vitest';
47+
import { bytesToSize } from './common';
48+
49+
describe('bytesToSize', () => {
50+
it('converts bytes to KB', () => {
51+
expect(bytesToSize(1500)).toBe('1.5 KB');
52+
});
53+
});
54+
```
55+
56+
### Testing React components
57+
58+
Use `@testing-library/react` to render components inside the jsdom environment. Query by accessible role/text rather than CSS classes or implementation details:
59+
60+
```tsx
61+
import { describe, it, expect } from 'vitest';
62+
import { render, screen } from '@testing-library/react';
63+
import userEvent from '@testing-library/user-event';
64+
import { MyButton } from './MyButton';
65+
66+
describe('MyButton', () => {
67+
it('calls onClick when pressed', async () => {
68+
const user = userEvent.setup();
69+
const onClick = vi.fn();
70+
71+
render(<MyButton onClick={onClick}>Click me</MyButton>);
72+
73+
await user.click(screen.getByRole('button', { name: 'Click me' }));
74+
75+
expect(onClick).toHaveBeenCalledOnce();
76+
});
77+
});
78+
```
79+
80+
### Testing hooks
81+
82+
Use `renderHook` from `@testing-library/react`:
83+
84+
```ts
85+
import { describe, it, expect } from 'vitest';
86+
import { renderHook, act } from '@testing-library/react';
87+
import { useMyHook } from './useMyHook';
88+
89+
describe('useMyHook', () => {
90+
it('returns the expected initial value', () => {
91+
const { result } = renderHook(() => useMyHook());
92+
expect(result.current.value).toBe(0);
93+
});
94+
});
95+
```
96+
97+
### Mocking
98+
99+
Vitest has Jest-compatible mocking APIs:
100+
101+
```ts
102+
import { vi } from 'vitest';
103+
104+
// Mock a module
105+
vi.mock('./someModule', () => ({ doThing: vi.fn(() => 'mocked') }));
106+
107+
// Spy on a method
108+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
109+
110+
// Restore after test
111+
afterEach(() => vi.restoreAllMocks());
112+
```
113+
114+
### Path aliases
115+
116+
All the project's path aliases work inside tests — you can import using `$utils/`, `$components/`, `$features/`, etc., just like in application code.
117+
118+
---
119+
120+
## What to test
121+
122+
Not every file needs tests. Focus on logic that would be painful to debug when broken:
123+
124+
| Worth testing | Less valuable |
125+
| ----------------------------------------------- | ---------------------------------------------- |
126+
| Pure utility functions (`src/app/utils/`) | Purely presentational components with no logic |
127+
| Custom hooks with non-trivial state transitions | Thin wrappers around third-party APIs |
128+
| State atoms and reducers | Generated or declarative config |
129+
| Data transformation / formatting functions | |
130+
131+
When you fix a bug, consider adding a regression test that would have caught it — the description in the test is useful documentation.
132+
133+
---
134+
135+
## CI
136+
137+
`pnpm test:run` is part of the required quality checks and runs on every pull request alongside `lint`, `typecheck`, and `knip`. A PR with failing tests cannot be merged.

knip.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"ignoreDependencies": [
99
"buffer",
1010
"@element-hq/element-call-embedded",
11-
"@matrix-org/matrix-sdk-crypto-wasm"
11+
"@matrix-org/matrix-sdk-crypto-wasm",
12+
"@testing-library/user-event"
1213
],
1314
"ignoreBinaries": ["knope"],
1415
"rules": {

package.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
"fmt": "prettier --write .",
1818
"fmt:check": "prettier --check .",
1919
"typecheck": "tsc",
20+
"test": "vitest",
21+
"test:ui": "vitest --ui",
22+
"test:run": "vitest run",
23+
"test:coverage": "vitest run --coverage",
2024
"knip": "knip",
2125
"knope": "knope",
2226
"document-change": "knope document-change",
@@ -95,6 +99,9 @@
9599
"@eslint/js": "9.39.3",
96100
"@rollup/plugin-inject": "^5.0.5",
97101
"@rollup/plugin-wasm": "^6.2.2",
102+
"@testing-library/jest-dom": "^6.9.1",
103+
"@testing-library/react": "^16.3.2",
104+
"@testing-library/user-event": "^14.6.1",
98105
"@types/chroma-js": "^3.1.2",
99106
"@types/file-saver": "^2.0.7",
100107
"@types/is-hotkey": "^0.1.10",
@@ -106,12 +113,15 @@
106113
"@types/sanitize-html": "^2.16.0",
107114
"@types/ua-parser-js": "^0.7.39",
108115
"@vitejs/plugin-react": "^5.1.4",
116+
"@vitest/coverage-v8": "^4.1.0",
117+
"@vitest/ui": "^4.1.0",
109118
"buffer": "^6.0.3",
110119
"eslint": "9.39.3",
111120
"eslint-config-airbnb-extended": "3.0.1",
112121
"eslint-config-prettier": "10.1.8",
113122
"eslint-plugin-prettier": "5.5.5",
114123
"globals": "17.3.0",
124+
"jsdom": "^29.0.0",
115125
"knip": "5.85.0",
116126
"prettier": "3.8.1",
117127
"typescript": "^5.9.3",
@@ -121,6 +131,7 @@
121131
"vite-plugin-static-copy": "^3.2.0",
122132
"vite-plugin-svgr": "4.5.0",
123133
"vite-plugin-top-level-await": "^1.6.0",
134+
"vitest": "^4.1.0",
124135
"wrangler": "^4.70.0"
125136
}
126137
}

0 commit comments

Comments
 (0)