claude-toolkit 0.1.12 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +3 -0
- package/core/skills/ct-testing-patterns/SKILL.md +37 -1
- package/docs/README.md +3 -0
- package/docs/best-practices/testing/README.md +84 -0
- package/docs/best-practices/testing/playwright-e2e.md +649 -0
- package/docs/best-practices/testing/storybook-interaction.md +455 -0
- package/docs/best-practices/testing/vitest-unit.md +451 -0
- package/docs/stacks/playwright-patterns.md +179 -0
- package/docs/stacks/storybook-patterns.md +177 -0
- package/docs/stacks/vite-vitest-patterns.md +485 -0
- package/package.json +1 -1
- package/stacks/playwright/skills/ct-playwright-patterns/SKILL.md +168 -0
- package/stacks/playwright/stack.json +39 -0
- package/stacks/solidjs/stack.json +7 -1
- package/stacks/storybook/skills/ct-storybook-patterns/SKILL.md +166 -0
- package/stacks/storybook/stack.json +46 -0
- package/stacks/vite/skills/ct-vite-vitest-patterns/SKILL.md +492 -0
- package/stacks/vite/stack.json +66 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Storybook Patterns
|
|
2
|
+
|
|
3
|
+
> Storybook interaction testing, CSF 3 stories, play functions, a11y, and visual regression for SolidJS components.
|
|
4
|
+
|
|
5
|
+
**Type:** Stack Skill (requires `storybook` stack)
|
|
6
|
+
**Source:** [`stacks/storybook/skills/ct-storybook-patterns/SKILL.md`](../../stacks/storybook/skills/ct-storybook-patterns/SKILL.md)
|
|
7
|
+
**Directory Mappings:** `.storybook/`, `src/**/*.stories.tsx`
|
|
8
|
+
**File Extensions:** `.stories.tsx`, `.stories.ts`
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Storybook is the middle layer of the testing pyramid: component interaction testing, visual regression, and accessibility auditing in a real browser.
|
|
13
|
+
|
|
14
|
+
## SolidJS Integration
|
|
15
|
+
|
|
16
|
+
Uses community packages: `storybook-solidjs` (renderer) + `storybook-solidjs-vite` (builder).
|
|
17
|
+
|
|
18
|
+
**Critical:** Always use `createJSXDecorator` for JSX decorators. Standard decorators cause duplicate DOM elements with SolidJS.
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { createJSXDecorator } from "storybook-solidjs";
|
|
22
|
+
|
|
23
|
+
const ThemeDecorator = createJSXDecorator((Story) => (
|
|
24
|
+
<ThemeProvider><Story /></ThemeProvider>
|
|
25
|
+
));
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## CSF 3 Stories
|
|
29
|
+
|
|
30
|
+
Use CSF 3 (not CSF Factories -- React-only as of Storybook 10.3).
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import type { Meta, StoryObj } from "storybook-solidjs";
|
|
34
|
+
|
|
35
|
+
const meta = {
|
|
36
|
+
title: "Components/Button",
|
|
37
|
+
component: Button,
|
|
38
|
+
tags: ["autodocs"],
|
|
39
|
+
} satisfies Meta<typeof Button>;
|
|
40
|
+
|
|
41
|
+
export default meta;
|
|
42
|
+
type Story = StoryObj<typeof meta>;
|
|
43
|
+
|
|
44
|
+
export const Primary: Story = {
|
|
45
|
+
args: { variant: "primary", children: "Click me" },
|
|
46
|
+
};
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Coverage Checklist
|
|
50
|
+
|
|
51
|
+
Every component needs stories for: all visual states (default, hover, focus, active, disabled, loading, error, empty), viewport sizes where layout differs, theme variants, edge cases (long text, missing data).
|
|
52
|
+
|
|
53
|
+
## Interaction Testing
|
|
54
|
+
|
|
55
|
+
Import everything from `@storybook/test` (instrumented versions). Never import from `@testing-library/dom` directly.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { expect, fn, userEvent, within } from "@storybook/test";
|
|
59
|
+
|
|
60
|
+
export const SubmitForm: Story = {
|
|
61
|
+
args: { onSubmit: fn() },
|
|
62
|
+
play: async ({ canvasElement, args, step }) => {
|
|
63
|
+
const canvas = within(canvasElement);
|
|
64
|
+
await step("Fill form", async () => {
|
|
65
|
+
await userEvent.type(canvas.getByLabelText("Email"), "user@example.com");
|
|
66
|
+
});
|
|
67
|
+
await step("Submit", async () => {
|
|
68
|
+
await userEvent.click(canvas.getByRole("button", { name: "Sign in" }));
|
|
69
|
+
});
|
|
70
|
+
await expect(args.onSubmit).toHaveBeenCalledOnce();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Key rules:**
|
|
76
|
+
1. Always `await` expect calls (enables Interactions panel logging).
|
|
77
|
+
2. Use `step()` to organize complex interactions.
|
|
78
|
+
3. Use `fn()` for spying (auto-restored between stories).
|
|
79
|
+
|
|
80
|
+
## Running in CI (Vitest Addon)
|
|
81
|
+
|
|
82
|
+
Use `@storybook/addon-vitest` (replaces old test-runner). No running Storybook instance needed.
|
|
83
|
+
|
|
84
|
+
Required dependencies: `@storybook/addon-vitest`, `@vitest/browser`, `@vitest/browser-playwright`, `@vitest/coverage-v8`.
|
|
85
|
+
|
|
86
|
+
Add as an inline project in your main Vite config using `test.projects`:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// vite.config.ts
|
|
90
|
+
import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
|
|
91
|
+
import { playwright } from "@vitest/browser-playwright";
|
|
92
|
+
|
|
93
|
+
export default defineConfig({
|
|
94
|
+
plugins: [/* ...app plugins */],
|
|
95
|
+
test: {
|
|
96
|
+
projects: [
|
|
97
|
+
{
|
|
98
|
+
extends: true,
|
|
99
|
+
test: { name: "unit", environment: "jsdom", setupFiles: ["./tests/setup.ts"] },
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
extends: true,
|
|
103
|
+
plugins: [storybookTest({ configDir: ".storybook" })],
|
|
104
|
+
test: {
|
|
105
|
+
name: "storybook",
|
|
106
|
+
browser: { enabled: true, headless: true, provider: playwright(), instances: [{ browser: "chromium" }] },
|
|
107
|
+
setupFiles: [".storybook/vitest.setup.ts"],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Accessibility
|
|
116
|
+
|
|
117
|
+
Built on axe-core. Set globally in `.storybook/preview.ts`:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
export default {
|
|
121
|
+
parameters: { a11y: { test: "error" } }, // "error" | "todo" | "off"
|
|
122
|
+
tags: ["a11y-test"],
|
|
123
|
+
};
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## MSW for API Components
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
// .storybook/preview.ts
|
|
130
|
+
import { initialize, mswLoader } from "msw-storybook-addon";
|
|
131
|
+
initialize();
|
|
132
|
+
export default { loaders: [mswLoader] };
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Per-story handlers via `parameters.msw.handlers`. Keep success handlers in shared `mocks/handlers.ts`, override with error/edge-case at story level.
|
|
136
|
+
|
|
137
|
+
## Module Mocking (Storybook 10)
|
|
138
|
+
|
|
139
|
+
`sb.mock` for internal module replacement. Register in `.storybook/preview.ts` only:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { sb } from "@storybook/test";
|
|
143
|
+
sb.mock("../src/api/client", () => ({ fetchUser: async () => ({ name: "Mock" }) }));
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
MSW for network-level; `sb.mock` for internal modules.
|
|
147
|
+
|
|
148
|
+
## Portable Stories
|
|
149
|
+
|
|
150
|
+
Reuse stories in Vitest with `composeStories`:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
import { composeStories } from "storybook-solidjs";
|
|
154
|
+
import * as stories from "./Button.stories";
|
|
155
|
+
const { Primary } = composeStories(stories);
|
|
156
|
+
|
|
157
|
+
test("renders primary", () => {
|
|
158
|
+
const { getByRole } = render(() => <Primary />);
|
|
159
|
+
expect(getByRole("button")).toBeInTheDocument();
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Anti-Patterns
|
|
164
|
+
|
|
165
|
+
1. **Destructuring SolidJS props in stories** -- Pass props via `args`; use `createJSXDecorator` for decorators.
|
|
166
|
+
2. **Importing from `@testing-library/dom`** -- Use `@storybook/test` for instrumented versions.
|
|
167
|
+
3. **Not awaiting `expect()` in play functions** -- Always `await expect(...)`.
|
|
168
|
+
4. **Skipping error/edge-case stories** -- Always include loading, error, empty, boundary states.
|
|
169
|
+
5. **Using `@storybook/test-runner`** -- Use `@storybook/addon-vitest` instead.
|
|
170
|
+
6. **Registering `sb.mock` in story files** -- Register in `.storybook/preview.ts` only.
|
|
171
|
+
|
|
172
|
+
## See Also
|
|
173
|
+
|
|
174
|
+
- [ct-testing-patterns](../skills/testing-patterns.md) -- Framework-agnostic TDD practices
|
|
175
|
+
- [Storybook Interaction Best Practices](../best-practices/testing/storybook-interaction.md) -- SolidJS-specific Storybook patterns
|
|
176
|
+
- [ct-vite-vitest-patterns](vite-vitest-patterns.md) -- Unit testing layer
|
|
177
|
+
- [ct-playwright-patterns](playwright-patterns.md) -- E2E testing layer
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
# Vite + Vitest Patterns
|
|
2
|
+
|
|
3
|
+
> Vite build tooling and Vitest unit/integration testing patterns.
|
|
4
|
+
|
|
5
|
+
**Type:** Stack Skill (requires `vite` stack)
|
|
6
|
+
**Source:** [`stacks/vite/skills/ct-vite-vitest-patterns/SKILL.md`](../../stacks/vite/skills/ct-vite-vitest-patterns/SKILL.md)
|
|
7
|
+
**Directory Mappings:** `vite.config.ts`, `vitest.config.ts`, `src/**/*.test.ts`, `tests/`
|
|
8
|
+
**File Extensions:** `.test.ts`, `.test.tsx`, `.spec.ts`, `.spec.tsx`
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Vite is the build tool; Vitest is its native test runner. They share a unified config -- aliases, plugins, and `define` values work identically in both. This is the primary advantage over separate build/test toolchains.
|
|
13
|
+
|
|
14
|
+
## Vite Configuration
|
|
15
|
+
|
|
16
|
+
### Config Structure
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { defineConfig } from 'vite'
|
|
20
|
+
|
|
21
|
+
export default defineConfig({
|
|
22
|
+
base: '/',
|
|
23
|
+
plugins: [],
|
|
24
|
+
envPrefix: 'VITE_',
|
|
25
|
+
|
|
26
|
+
resolve: {
|
|
27
|
+
alias: { '@': '/src' },
|
|
28
|
+
conditions: [],
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
css: {
|
|
32
|
+
transformer: 'postcss', // or 'lightningcss'
|
|
33
|
+
modules: {},
|
|
34
|
+
preprocessorOptions: {},
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
oxc: {}, // Oxc transformer (replaces esbuild in Vite 8)
|
|
38
|
+
|
|
39
|
+
server: {
|
|
40
|
+
port: 5173,
|
|
41
|
+
proxy: {},
|
|
42
|
+
warmup: { clientFiles: [] }, // pre-transform hot files for faster cold starts
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
build: {
|
|
46
|
+
target: 'baseline-widely-available',
|
|
47
|
+
outDir: 'dist',
|
|
48
|
+
sourcemap: false,
|
|
49
|
+
minify: 'oxc', // 'oxc' (default) | 'terser' | false
|
|
50
|
+
rolldownOptions: {}, // Rolldown bundler config (replaces rollupOptions in Vite 8)
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
ssr: {
|
|
54
|
+
target: 'node',
|
|
55
|
+
noExternal: [],
|
|
56
|
+
external: [],
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Conditional Config
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
export default defineConfig(({ command, mode, isSsrBuild }) => {
|
|
65
|
+
if (command === 'serve') return { /* dev-specific */ }
|
|
66
|
+
return { /* build-specific */ }
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Environment Variables
|
|
71
|
+
|
|
72
|
+
Loading order (later overrides earlier): `.env` > `.env.local` > `.env.[mode]` > `.env.[mode].local`.
|
|
73
|
+
|
|
74
|
+
Built-in `import.meta.env`: `MODE`, `BASE_URL`, `PROD`, `DEV`, `SSR`.
|
|
75
|
+
|
|
76
|
+
**Security:** Never put secrets in `VITE_*` variables -- they are embedded in client bundles.
|
|
77
|
+
|
|
78
|
+
TypeScript declarations (`src/vite-env.d.ts`):
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
/// <reference types="vite/client" />
|
|
82
|
+
|
|
83
|
+
interface ImportMetaEnv {
|
|
84
|
+
readonly VITE_API_URL: string
|
|
85
|
+
}
|
|
86
|
+
interface ImportMeta {
|
|
87
|
+
readonly env: ImportMetaEnv
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Plugin System
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
export default defineConfig({
|
|
95
|
+
plugins: [
|
|
96
|
+
solidPlugin(),
|
|
97
|
+
process.env.ANALYZE && visualizer(), // conditional (falsy ignored)
|
|
98
|
+
{ ...myPlugin(), enforce: 'pre' }, // before Vite core transforms
|
|
99
|
+
{ ...myPlugin(), apply: 'build' }, // build only
|
|
100
|
+
],
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Key hooks (execution order): `config` > `configResolved` > `configureServer` > `transformIndexHtml` > `resolveId` > `load` > `transform` > `handleHotUpdate`.
|
|
105
|
+
|
|
106
|
+
### Library Mode
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { resolve } from 'node:path'
|
|
110
|
+
|
|
111
|
+
export default defineConfig({
|
|
112
|
+
build: {
|
|
113
|
+
lib: {
|
|
114
|
+
entry: resolve(import.meta.dirname, 'lib/main.ts'),
|
|
115
|
+
name: 'MyLib',
|
|
116
|
+
fileName: 'my-lib',
|
|
117
|
+
formats: ['es', 'cjs'],
|
|
118
|
+
},
|
|
119
|
+
rolldownOptions: {
|
|
120
|
+
external: ['solid-js', 'solid-js/web'], // always externalize peer deps
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### SSR
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"build:client": "vite build --outDir dist/client",
|
|
131
|
+
"build:server": "vite build --outDir dist/server --ssr src/entry-server.ts"
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Use `middlewareMode: true` + `appType: 'custom'` for framework SSR integration.
|
|
136
|
+
|
|
137
|
+
### Build Performance
|
|
138
|
+
|
|
139
|
+
1. **Avoid barrel files** -- import directly from source modules, not re-export `index.ts`
|
|
140
|
+
2. **Use explicit extensions** -- `import './Component.tsx'` not `import './Component'`
|
|
141
|
+
3. **Warm up hot files** -- `server.warmup.clientFiles: ['./src/main.tsx']`
|
|
142
|
+
4. **Prefer native CSS** -- CSS nesting is supported natively; avoid Sass when possible
|
|
143
|
+
5. **Narrow resolve.extensions** -- remove extensions you don't use
|
|
144
|
+
|
|
145
|
+
## Vitest Configuration
|
|
146
|
+
|
|
147
|
+
### Standalone Config
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { defineConfig } from 'vitest/config'
|
|
151
|
+
|
|
152
|
+
export default defineConfig({
|
|
153
|
+
test: {
|
|
154
|
+
include: ['**/*.{test,spec}.{ts,tsx}'],
|
|
155
|
+
environment: 'node', // 'node' | 'jsdom' | 'happy-dom'
|
|
156
|
+
globals: false,
|
|
157
|
+
setupFiles: [],
|
|
158
|
+
testTimeout: 5000,
|
|
159
|
+
pool: 'forks', // 'forks' | 'threads' | 'vmThreads'
|
|
160
|
+
|
|
161
|
+
restoreMocks: true, // auto vi.restoreAllMocks() after each test
|
|
162
|
+
clearMocks: true,
|
|
163
|
+
|
|
164
|
+
coverage: {
|
|
165
|
+
provider: 'v8',
|
|
166
|
+
include: ['src/**/*.{ts,tsx}'],
|
|
167
|
+
exclude: ['**/*.test.*', '**/*.d.ts', 'src/types/**'],
|
|
168
|
+
reporter: ['text', 'html', 'lcov'],
|
|
169
|
+
thresholds: { lines: 80, branches: 80, functions: 80, statements: 80 },
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
snapshotSerializers: [],
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Integrated with Vite Config
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
/// <reference types="vitest/config" />
|
|
181
|
+
import { defineConfig } from 'vite'
|
|
182
|
+
|
|
183
|
+
export default defineConfig({
|
|
184
|
+
plugins: [solidPlugin()],
|
|
185
|
+
resolve: { alias: { '@': '/src' } }, // shared: works in both app and tests
|
|
186
|
+
test: {
|
|
187
|
+
environment: 'jsdom',
|
|
188
|
+
setupFiles: ['./tests/setup.ts'],
|
|
189
|
+
},
|
|
190
|
+
})
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Vite's `resolve.alias`, `plugins`, and `define` are automatically inherited by Vitest. Never duplicate them.
|
|
194
|
+
|
|
195
|
+
### Merging Separate Configs
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { defineConfig, mergeConfig } from 'vitest/config'
|
|
199
|
+
import viteConfig from './vite.config'
|
|
200
|
+
|
|
201
|
+
export default mergeConfig(viteConfig, defineConfig({
|
|
202
|
+
test: { environment: 'jsdom' },
|
|
203
|
+
}))
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Workspace / Projects (Vitest 3+)
|
|
207
|
+
|
|
208
|
+
Use `test.projects` for monorepos or mixed environments:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
export default defineConfig({
|
|
212
|
+
test: {
|
|
213
|
+
projects: [
|
|
214
|
+
'packages/*',
|
|
215
|
+
{
|
|
216
|
+
test: {
|
|
217
|
+
name: 'unit',
|
|
218
|
+
include: ['src/**/*.test.ts'],
|
|
219
|
+
environment: 'node',
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
test: {
|
|
224
|
+
name: 'components',
|
|
225
|
+
include: ['src/**/*.test.tsx'],
|
|
226
|
+
environment: 'jsdom',
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
})
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Mocking
|
|
235
|
+
|
|
236
|
+
### Mock Functions
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
const fn = vi.fn()
|
|
240
|
+
fn.mockReturnValue(42)
|
|
241
|
+
fn.mockResolvedValue({ data: [] })
|
|
242
|
+
fn.mockImplementation((x) => x * 2)
|
|
243
|
+
|
|
244
|
+
expect(fn).toHaveBeenCalledWith('arg')
|
|
245
|
+
expect(fn).toHaveBeenCalledTimes(1)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Module Mocking
|
|
249
|
+
|
|
250
|
+
`vi.mock` is hoisted above imports automatically:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
vi.mock('./api', () => ({
|
|
254
|
+
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Test' }),
|
|
255
|
+
}))
|
|
256
|
+
|
|
257
|
+
// Access original implementation inside mock
|
|
258
|
+
vi.mock('./utils', async (importOriginal) => {
|
|
259
|
+
const actual = await importOriginal<typeof import('./utils')>()
|
|
260
|
+
return { ...actual, format: vi.fn() }
|
|
261
|
+
})
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Hoisting caveat:** Variables defined before `vi.mock()` are not accessible inside the factory. Use `vi.hoisted()` for shared variables:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
const { mockFetch } = vi.hoisted(() => ({
|
|
268
|
+
mockFetch: vi.fn(),
|
|
269
|
+
}))
|
|
270
|
+
|
|
271
|
+
vi.mock('./api', () => ({ fetch: mockFetch }))
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Spying
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
278
|
+
expect(spy).toHaveBeenCalledWith('expected warning')
|
|
279
|
+
spy.mockRestore()
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Timer Mocking
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
vi.useFakeTimers()
|
|
286
|
+
vi.setSystemTime(new Date('2026-01-01'))
|
|
287
|
+
|
|
288
|
+
setTimeout(callback, 1000)
|
|
289
|
+
vi.advanceTimersByTime(1000)
|
|
290
|
+
expect(callback).toHaveBeenCalled()
|
|
291
|
+
|
|
292
|
+
vi.useRealTimers()
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Environment Stubs
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
vi.stubGlobal('__VERSION__', '1.0.0')
|
|
299
|
+
vi.stubEnv('VITE_API_URL', 'http://test.example.com')
|
|
300
|
+
|
|
301
|
+
afterEach(() => {
|
|
302
|
+
vi.unstubAllGlobals()
|
|
303
|
+
vi.unstubAllEnvs()
|
|
304
|
+
})
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Snapshot Testing
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// File snapshots (stored in __snapshots__/)
|
|
311
|
+
expect(result).toMatchSnapshot()
|
|
312
|
+
|
|
313
|
+
// Inline snapshots (written into test file by vitest --update)
|
|
314
|
+
expect(result).toMatchInlineSnapshot(`{ "id": 1 }`)
|
|
315
|
+
|
|
316
|
+
// Custom file path
|
|
317
|
+
expect(htmlOutput).toMatchFileSnapshot('./fixtures/expected.html')
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Keep inline snapshots under 10-15 lines. Use file snapshots for complex output. Snapshots catch regressions but don't verify correctness -- combine with explicit assertions on critical values.
|
|
321
|
+
|
|
322
|
+
## In-Source Testing
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// src/utils/math.ts
|
|
326
|
+
export function add(...args: number[]) {
|
|
327
|
+
return args.reduce((a, b) => a + b, 0)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (import.meta.vitest) {
|
|
331
|
+
const { it, expect } = import.meta.vitest
|
|
332
|
+
it('adds numbers', () => {
|
|
333
|
+
expect(add(1, 2, 3)).toBe(6)
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Config:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
export default defineConfig({
|
|
342
|
+
test: { includeSource: ['src/**/*.ts'] },
|
|
343
|
+
define: { 'import.meta.vitest': 'undefined' }, // tree-shake from production
|
|
344
|
+
})
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Best for small utility functions. Use separate test files for components and integration tests.
|
|
348
|
+
|
|
349
|
+
## Browser Mode (Vitest 4+, Stable)
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import { playwright } from '@vitest/browser-playwright'
|
|
353
|
+
|
|
354
|
+
export default defineConfig({
|
|
355
|
+
test: {
|
|
356
|
+
browser: {
|
|
357
|
+
enabled: true,
|
|
358
|
+
provider: playwright(),
|
|
359
|
+
instances: [{ browser: 'chromium' }],
|
|
360
|
+
headless: true,
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Providers: `@vitest/browser-playwright` (recommended), `@vitest/browser-webdriverio`.
|
|
367
|
+
|
|
368
|
+
Quick setup: `npx vitest init browser`
|
|
369
|
+
|
|
370
|
+
### Visual Regression
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
import { page } from 'vitest/browser'
|
|
374
|
+
|
|
375
|
+
it('matches visual baseline', async () => {
|
|
376
|
+
await page.goto('/component-demo')
|
|
377
|
+
await expect(page.getByRole('button')).toMatchScreenshot()
|
|
378
|
+
})
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Playwright Traces
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
browser: {
|
|
385
|
+
provider: playwright({ trace: 'on-first-retry' }),
|
|
386
|
+
// 'off' | 'on' | 'on-first-retry' | 'on-all-retries' | 'retain-on-failure'
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Browser Mode Limitations
|
|
391
|
+
|
|
392
|
+
- Cannot `vi.spyOn` module exports -- use `vi.mock('./module', { spy: true })` instead
|
|
393
|
+
- Thread-blocking APIs (`alert`, `confirm`, `prompt`) are auto-mocked
|
|
394
|
+
- Uses testing-library selectors: `getByRole`, `getByText`, `getByLabelText`
|
|
395
|
+
|
|
396
|
+
## Coverage
|
|
397
|
+
|
|
398
|
+
Install: `@vitest/coverage-v8` (default, fastest) or `@vitest/coverage-istanbul` (universal).
|
|
399
|
+
|
|
400
|
+
Run: `vitest --coverage`
|
|
401
|
+
|
|
402
|
+
Ignore comments:
|
|
403
|
+
|
|
404
|
+
- V8: `/* v8 ignore next */`
|
|
405
|
+
- Istanbul: `/* istanbul ignore next -- @preserve */`
|
|
406
|
+
|
|
407
|
+
Include `@preserve` to prevent Oxc minifier from stripping comments.
|
|
408
|
+
|
|
409
|
+
## Custom Matchers
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import { expect } from 'vitest'
|
|
413
|
+
|
|
414
|
+
expect.extend({
|
|
415
|
+
toBeWithinRange(received: number, floor: number, ceiling: number) {
|
|
416
|
+
const pass = received >= floor && received <= ceiling
|
|
417
|
+
return {
|
|
418
|
+
pass,
|
|
419
|
+
message: () => `expected ${received} to be within [${floor}, ${ceiling}]`,
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
})
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
TypeScript (`vitest.d.ts`):
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
import 'vitest'
|
|
429
|
+
|
|
430
|
+
declare module 'vitest' {
|
|
431
|
+
interface Matchers<T = any> {
|
|
432
|
+
toBeWithinRange(floor: number, ceiling: number): T
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Schema Validation (Vitest 4+)
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
import { z } from 'zod'
|
|
441
|
+
|
|
442
|
+
const UserSchema = z.object({ id: z.number(), name: z.string() })
|
|
443
|
+
expect(response.data).toSatisfy(expect.schemaMatching(UserSchema))
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Works with any Standard Schema v1 library (Zod, Valibot, ArkType).
|
|
447
|
+
|
|
448
|
+
## Vite 8 Migration
|
|
449
|
+
|
|
450
|
+
| Old (Vite 7-) | New (Vite 8) | Status |
|
|
451
|
+
| ---------------------------- | ---------------------------- | ------------------------------- |
|
|
452
|
+
| `build.rollupOptions` | `build.rolldownOptions` | Deprecated, compat layer exists |
|
|
453
|
+
| `optimizeDeps.esbuildOptions` | `optimizeDeps.rolldownOptions` | Deprecated |
|
|
454
|
+
| `esbuild` config | `oxc` config | Deprecated |
|
|
455
|
+
| `build.minify: 'esbuild'` | `build.minify: 'oxc'` | New default |
|
|
456
|
+
|
|
457
|
+
The compat layer auto-converts old keys. New code should use the new keys.
|
|
458
|
+
|
|
459
|
+
## Anti-Patterns
|
|
460
|
+
|
|
461
|
+
### Vite
|
|
462
|
+
|
|
463
|
+
1. **Barrel file sprawl** -- Re-exporting through `index.ts` forces transforming all modules. Import directly from source.
|
|
464
|
+
2. **Secrets in VITE_ variables** -- Embedded in client bundles. Use server-only env vars, proxy through API routes.
|
|
465
|
+
3. **Duplicating resolve config for tests** -- Vitest inherits Vite's aliases and plugins. Don't redeclare.
|
|
466
|
+
4. **Using `rollupOptions` in Vite 8** -- Works via compat but generates warnings. Use `rolldownOptions`.
|
|
467
|
+
5. **Not externalizing peer deps in library mode** -- Bundling framework deps causes duplicate instances.
|
|
468
|
+
|
|
469
|
+
### Vitest
|
|
470
|
+
|
|
471
|
+
1. **Over-mocking internal modules** -- Mock at boundaries (HTTP, filesystem, external APIs), not internal functions.
|
|
472
|
+
2. **Forgetting vi.mock is hoisted** -- Variables before `vi.mock()` aren't accessible in the factory. Use `vi.hoisted()`.
|
|
473
|
+
3. **Not restoring mocks** -- Set `restoreMocks: true` in config. Leaked mocks cause cascading failures.
|
|
474
|
+
4. **Using jsdom when node suffices** -- jsdom adds overhead. Use workspace projects to split: `node` for logic, `jsdom` for components.
|
|
475
|
+
5. **Large inline snapshots** -- Over 10-15 lines reduces readability. Use `toMatchFileSnapshot`.
|
|
476
|
+
6. **Snapshot-only testing** -- Snapshots catch regressions but don't verify correctness. Add explicit assertions.
|
|
477
|
+
7. **Global jsdom environment** -- Use `test.projects` to run DOM tests in jsdom and logic tests in node.
|
|
478
|
+
8. **Using `vi.spyOn` in browser mode** -- Use `vi.mock('./module', { spy: true })` instead.
|
|
479
|
+
|
|
480
|
+
## See Also
|
|
481
|
+
|
|
482
|
+
- [ct-testing-patterns](../skills/testing-patterns.md) -- Framework-agnostic TDD practices
|
|
483
|
+
- [Vitest Unit Testing Best Practices](../best-practices/testing/vitest-unit.md) -- SolidJS-specific Vitest patterns
|
|
484
|
+
- [ct-storybook-patterns](storybook-patterns.md) -- Interaction testing layer
|
|
485
|
+
- [ct-playwright-patterns](playwright-patterns.md) -- E2E testing layer
|