|
| 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. |
0 commit comments