omgkit 2.2.0 → 2.3.0
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/package.json +1 -1
- package/plugin/skills/databases/mongodb/SKILL.md +60 -776
- package/plugin/skills/databases/prisma/SKILL.md +53 -744
- package/plugin/skills/databases/redis/SKILL.md +53 -860
- package/plugin/skills/devops/aws/SKILL.md +68 -672
- package/plugin/skills/devops/github-actions/SKILL.md +54 -657
- package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +87 -853
- package/plugin/skills/frameworks/express/SKILL.md +95 -1301
- package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
- package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
- package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
- package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
- package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
- package/plugin/skills/frontend/responsive/SKILL.md +76 -799
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
- package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
- package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
- package/plugin/skills/languages/javascript/SKILL.md +106 -849
- package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
- package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
- package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
- package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
- package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
- package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
- package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
- package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
- package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
- package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
- package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
- package/plugin/skills/security/better-auth/SKILL.md +46 -1034
- package/plugin/skills/security/oauth/SKILL.md +80 -934
- package/plugin/skills/security/owasp/SKILL.md +78 -862
- package/plugin/skills/testing/playwright/SKILL.md +77 -700
- package/plugin/skills/testing/pytest/SKILL.md +73 -811
- package/plugin/skills/testing/vitest/SKILL.md +60 -920
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
- package/plugin/skills/SKILL_STANDARDS.md +0 -743
|
@@ -1,1009 +1,149 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: Vitest
|
|
4
|
-
category: testing
|
|
5
|
-
triggers:
|
|
6
|
-
- vitest
|
|
7
|
-
- javascript testing
|
|
8
|
-
- typescript testing
|
|
9
|
-
- unit testing js
|
|
10
|
-
- vue testing
|
|
11
|
-
- react testing
|
|
2
|
+
name: Testing with Vitest
|
|
3
|
+
description: Claude writes fast, reliable tests using Vitest for TypeScript/JavaScript projects. Use when writing unit tests, component tests, setting up mocks, snapshot testing, or configuring test coverage.
|
|
12
4
|
---
|
|
13
5
|
|
|
14
|
-
# Vitest
|
|
6
|
+
# Testing with Vitest
|
|
15
7
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
## Purpose
|
|
19
|
-
|
|
20
|
-
Build comprehensive JavaScript/TypeScript test suites:
|
|
21
|
-
|
|
22
|
-
- Write fast and reliable unit tests
|
|
23
|
-
- Mock modules, timers, and network requests
|
|
24
|
-
- Test Vue and React components
|
|
25
|
-
- Implement snapshot testing
|
|
26
|
-
- Generate coverage reports
|
|
27
|
-
- Run tests in parallel and watch mode
|
|
28
|
-
- Integrate with CI/CD pipelines
|
|
29
|
-
|
|
30
|
-
## Features
|
|
31
|
-
|
|
32
|
-
### 1. Configuration Setup
|
|
8
|
+
## Quick Start
|
|
33
9
|
|
|
34
10
|
```typescript
|
|
35
11
|
// vitest.config.ts
|
|
36
12
|
import { defineConfig } from "vitest/config";
|
|
37
|
-
import vue from "@vitejs/plugin-vue";
|
|
38
|
-
import react from "@vitejs/plugin-react";
|
|
39
|
-
import path from "path";
|
|
40
13
|
|
|
41
14
|
export default defineConfig({
|
|
42
|
-
plugins: [vue(), react()],
|
|
43
15
|
test: {
|
|
44
16
|
globals: true,
|
|
45
17
|
environment: "jsdom",
|
|
46
|
-
include: ["**/*.{test,spec}.{js,ts,jsx,tsx}"],
|
|
47
|
-
exclude: ["**/node_modules/**", "**/dist/**", "**/e2e/**"],
|
|
48
18
|
setupFiles: ["./tests/setup.ts"],
|
|
49
19
|
coverage: {
|
|
50
20
|
provider: "v8",
|
|
51
|
-
reporter: ["text", "
|
|
52
|
-
|
|
53
|
-
exclude: [
|
|
54
|
-
"node_modules/",
|
|
55
|
-
"tests/",
|
|
56
|
-
"**/*.d.ts",
|
|
57
|
-
"**/*.config.*",
|
|
58
|
-
"**/types/**",
|
|
59
|
-
],
|
|
60
|
-
thresholds: {
|
|
61
|
-
global: {
|
|
62
|
-
branches: 80,
|
|
63
|
-
functions: 80,
|
|
64
|
-
lines: 80,
|
|
65
|
-
statements: 80,
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
pool: "threads",
|
|
70
|
-
poolOptions: {
|
|
71
|
-
threads: {
|
|
72
|
-
singleThread: false,
|
|
73
|
-
maxThreads: 4,
|
|
74
|
-
minThreads: 1,
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
testTimeout: 10000,
|
|
78
|
-
hookTimeout: 10000,
|
|
79
|
-
reporters: ["default", "json", "junit"],
|
|
80
|
-
outputFile: {
|
|
81
|
-
json: "./test-results/results.json",
|
|
82
|
-
junit: "./test-results/junit.xml",
|
|
83
|
-
},
|
|
84
|
-
typecheck: {
|
|
85
|
-
enabled: true,
|
|
86
|
-
checker: "tsc",
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
resolve: {
|
|
90
|
-
alias: {
|
|
91
|
-
"@": path.resolve(__dirname, "./src"),
|
|
92
|
-
"@tests": path.resolve(__dirname, "./tests"),
|
|
21
|
+
reporter: ["text", "html"],
|
|
22
|
+
thresholds: { global: { branches: 80, functions: 80, lines: 80 } },
|
|
93
23
|
},
|
|
94
24
|
},
|
|
95
25
|
});
|
|
96
|
-
```
|
|
97
26
|
|
|
98
|
-
```typescript
|
|
99
27
|
// tests/setup.ts
|
|
100
|
-
import {
|
|
28
|
+
import { afterEach, vi } from "vitest";
|
|
101
29
|
import { cleanup } from "@testing-library/vue";
|
|
102
30
|
import "@testing-library/jest-dom/vitest";
|
|
103
31
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
vi.clearAllMocks();
|
|
107
|
-
vi.resetAllMocks();
|
|
108
|
-
cleanup();
|
|
109
|
-
});
|
|
32
|
+
afterEach(() => { vi.clearAllMocks(); cleanup(); });
|
|
33
|
+
```
|
|
110
34
|
|
|
111
|
-
|
|
112
|
-
beforeAll(() => {
|
|
113
|
-
// Mock window.matchMedia
|
|
114
|
-
Object.defineProperty(window, "matchMedia", {
|
|
115
|
-
writable: true,
|
|
116
|
-
value: vi.fn().mockImplementation((query) => ({
|
|
117
|
-
matches: false,
|
|
118
|
-
media: query,
|
|
119
|
-
onchange: null,
|
|
120
|
-
addListener: vi.fn(),
|
|
121
|
-
removeListener: vi.fn(),
|
|
122
|
-
addEventListener: vi.fn(),
|
|
123
|
-
removeEventListener: vi.fn(),
|
|
124
|
-
dispatchEvent: vi.fn(),
|
|
125
|
-
})),
|
|
126
|
-
});
|
|
35
|
+
## Features
|
|
127
36
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// Mock ResizeObserver
|
|
138
|
-
window.ResizeObserver = vi.fn().mockImplementation(() => ({
|
|
139
|
-
observe: vi.fn(),
|
|
140
|
-
unobserve: vi.fn(),
|
|
141
|
-
disconnect: vi.fn(),
|
|
142
|
-
}));
|
|
143
|
-
});
|
|
37
|
+
| Feature | Description | Reference |
|
|
38
|
+
|---------|-------------|-----------|
|
|
39
|
+
| Unit Testing | Fast isolated tests with describe/it/expect | [Vitest API](https://vitest.dev/api/) |
|
|
40
|
+
| Mocking | Module, function, and timer mocking with vi | [Mocking Guide](https://vitest.dev/guide/mocking.html) |
|
|
41
|
+
| Snapshot Testing | Component and data structure snapshots | [Snapshot Testing](https://vitest.dev/guide/snapshot.html) |
|
|
42
|
+
| Component Testing | Vue/React component testing with Testing Library | [Vue Test Utils](https://test-utils.vuejs.org/) |
|
|
43
|
+
| Coverage Reports | V8/Istanbul coverage with thresholds | [Coverage](https://vitest.dev/guide/coverage.html) |
|
|
44
|
+
| Parallel Execution | Multi-threaded test runner | [Test Runner](https://vitest.dev/guide/features.html) |
|
|
144
45
|
|
|
145
|
-
|
|
146
|
-
afterAll(() => {
|
|
147
|
-
vi.restoreAllMocks();
|
|
148
|
-
});
|
|
149
|
-
```
|
|
46
|
+
## Common Patterns
|
|
150
47
|
|
|
151
|
-
###
|
|
48
|
+
### Unit Testing with Parametrization
|
|
152
49
|
|
|
153
50
|
```typescript
|
|
154
|
-
// tests/unit/utils/string.test.ts
|
|
155
51
|
import { describe, it, expect, test } from "vitest";
|
|
156
|
-
import {
|
|
157
|
-
capitalize,
|
|
158
|
-
truncate,
|
|
159
|
-
slugify,
|
|
160
|
-
camelToKebab,
|
|
161
|
-
parseTemplate,
|
|
162
|
-
} from "@/utils/string";
|
|
163
|
-
|
|
164
|
-
describe("String Utilities", () => {
|
|
165
|
-
describe("capitalize", () => {
|
|
166
|
-
it("capitalizes the first letter", () => {
|
|
167
|
-
expect(capitalize("hello")).toBe("Hello");
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("handles empty strings", () => {
|
|
171
|
-
expect(capitalize("")).toBe("");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("handles single character", () => {
|
|
175
|
-
expect(capitalize("a")).toBe("A");
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("preserves rest of string case", () => {
|
|
179
|
-
expect(capitalize("hELLO")).toBe("HELLO");
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe("truncate", () => {
|
|
184
|
-
it("truncates long strings", () => {
|
|
185
|
-
expect(truncate("Hello World", 5)).toBe("Hello...");
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("returns original if shorter than limit", () => {
|
|
189
|
-
expect(truncate("Hi", 10)).toBe("Hi");
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("uses custom suffix", () => {
|
|
193
|
-
expect(truncate("Hello World", 5, "…")).toBe("Hello…");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test.each([
|
|
197
|
-
["Hello World", 5, "...", "Hello..."],
|
|
198
|
-
["Short", 10, "...", "Short"],
|
|
199
|
-
["Test", 4, "", "Test"],
|
|
200
|
-
["Testing", 4, "…", "Test…"],
|
|
201
|
-
])('truncate("%s", %d, "%s") returns "%s"', (str, len, suffix, expected) => {
|
|
202
|
-
expect(truncate(str, len, suffix)).toBe(expected);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
describe("slugify", () => {
|
|
207
|
-
it("converts to lowercase", () => {
|
|
208
|
-
expect(slugify("HELLO")).toBe("hello");
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("replaces spaces with hyphens", () => {
|
|
212
|
-
expect(slugify("Hello World")).toBe("hello-world");
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("removes special characters", () => {
|
|
216
|
-
expect(slugify("Hello! World?")).toBe("hello-world");
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it("handles multiple spaces", () => {
|
|
220
|
-
expect(slugify("Hello World")).toBe("hello-world");
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
describe("parseTemplate", () => {
|
|
225
|
-
it("replaces placeholders with values", () => {
|
|
226
|
-
const template = "Hello, {{name}}!";
|
|
227
|
-
const result = parseTemplate(template, { name: "World" });
|
|
228
|
-
expect(result).toBe("Hello, World!");
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it("handles multiple placeholders", () => {
|
|
232
|
-
const template = "{{greeting}}, {{name}}!";
|
|
233
|
-
const result = parseTemplate(template, {
|
|
234
|
-
greeting: "Hi",
|
|
235
|
-
name: "John",
|
|
236
|
-
});
|
|
237
|
-
expect(result).toBe("Hi, John!");
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("leaves unmatched placeholders", () => {
|
|
241
|
-
const template = "Hello, {{name}}!";
|
|
242
|
-
const result = parseTemplate(template, {});
|
|
243
|
-
expect(result).toBe("Hello, {{name}}!");
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// tests/unit/utils/async.test.ts
|
|
249
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
250
|
-
import { debounce, throttle, retry, timeout } from "@/utils/async";
|
|
251
|
-
|
|
252
|
-
describe("Async Utilities", () => {
|
|
253
|
-
beforeEach(() => {
|
|
254
|
-
vi.useFakeTimers();
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
afterEach(() => {
|
|
258
|
-
vi.useRealTimers();
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
describe("debounce", () => {
|
|
262
|
-
it("delays function execution", () => {
|
|
263
|
-
const fn = vi.fn();
|
|
264
|
-
const debouncedFn = debounce(fn, 100);
|
|
265
|
-
|
|
266
|
-
debouncedFn();
|
|
267
|
-
expect(fn).not.toHaveBeenCalled();
|
|
268
|
-
|
|
269
|
-
vi.advanceTimersByTime(100);
|
|
270
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("resets timer on subsequent calls", () => {
|
|
274
|
-
const fn = vi.fn();
|
|
275
|
-
const debouncedFn = debounce(fn, 100);
|
|
276
|
-
|
|
277
|
-
debouncedFn();
|
|
278
|
-
vi.advanceTimersByTime(50);
|
|
279
|
-
debouncedFn();
|
|
280
|
-
vi.advanceTimersByTime(50);
|
|
281
|
-
|
|
282
|
-
expect(fn).not.toHaveBeenCalled();
|
|
283
|
-
|
|
284
|
-
vi.advanceTimersByTime(50);
|
|
285
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
describe("throttle", () => {
|
|
290
|
-
it("limits function calls", () => {
|
|
291
|
-
const fn = vi.fn();
|
|
292
|
-
const throttledFn = throttle(fn, 100);
|
|
293
|
-
|
|
294
|
-
throttledFn();
|
|
295
|
-
throttledFn();
|
|
296
|
-
throttledFn();
|
|
297
|
-
|
|
298
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
299
|
-
|
|
300
|
-
vi.advanceTimersByTime(100);
|
|
301
|
-
throttledFn();
|
|
302
52
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
.fn()
|
|
311
|
-
.mockRejectedValueOnce(new Error("Fail 1"))
|
|
312
|
-
.mockRejectedValueOnce(new Error("Fail 2"))
|
|
313
|
-
.mockResolvedValueOnce("Success");
|
|
314
|
-
|
|
315
|
-
const result = await retry(fn, { maxAttempts: 3, delay: 100 });
|
|
316
|
-
|
|
317
|
-
expect(result).toBe("Success");
|
|
318
|
-
expect(fn).toHaveBeenCalledTimes(3);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it("throws after max attempts", async () => {
|
|
322
|
-
const fn = vi.fn().mockRejectedValue(new Error("Always fails"));
|
|
323
|
-
|
|
324
|
-
await expect(retry(fn, { maxAttempts: 3, delay: 10 })).rejects.toThrow(
|
|
325
|
-
"Always fails"
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
expect(fn).toHaveBeenCalledTimes(3);
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
describe("timeout", () => {
|
|
333
|
-
it("resolves if promise completes in time", async () => {
|
|
334
|
-
const promise = Promise.resolve("Success");
|
|
335
|
-
const result = await timeout(promise, 1000);
|
|
336
|
-
expect(result).toBe("Success");
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it("rejects if promise takes too long", async () => {
|
|
340
|
-
const slowPromise = new Promise((resolve) =>
|
|
341
|
-
setTimeout(() => resolve("Late"), 2000)
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
const timeoutPromise = timeout(slowPromise, 1000);
|
|
345
|
-
|
|
346
|
-
vi.advanceTimersByTime(1000);
|
|
347
|
-
|
|
348
|
-
await expect(timeoutPromise).rejects.toThrow("Operation timed out");
|
|
349
|
-
});
|
|
53
|
+
describe("String Utils", () => {
|
|
54
|
+
test.each([
|
|
55
|
+
["hello", 3, "hel..."],
|
|
56
|
+
["hi", 10, "hi"],
|
|
57
|
+
["test", 4, "test"],
|
|
58
|
+
])('truncate("%s", %d) returns "%s"', (str, len, expected) => {
|
|
59
|
+
expect(truncate(str, len)).toBe(expected);
|
|
350
60
|
});
|
|
351
61
|
});
|
|
352
62
|
```
|
|
353
63
|
|
|
354
|
-
###
|
|
64
|
+
### Mocking Modules and Services
|
|
355
65
|
|
|
356
66
|
```typescript
|
|
357
|
-
// tests/unit/services/user.test.ts
|
|
358
67
|
import { describe, it, expect, vi, beforeEach, Mock } from "vitest";
|
|
359
68
|
import { UserService } from "@/services/user";
|
|
360
69
|
import { apiClient } from "@/lib/api";
|
|
361
|
-
import { cache } from "@/lib/cache";
|
|
362
70
|
|
|
363
|
-
// Mock modules
|
|
364
71
|
vi.mock("@/lib/api", () => ({
|
|
365
|
-
apiClient: {
|
|
366
|
-
get: vi.fn(),
|
|
367
|
-
post: vi.fn(),
|
|
368
|
-
put: vi.fn(),
|
|
369
|
-
delete: vi.fn(),
|
|
370
|
-
},
|
|
371
|
-
}));
|
|
372
|
-
|
|
373
|
-
vi.mock("@/lib/cache", () => ({
|
|
374
|
-
cache: {
|
|
375
|
-
get: vi.fn(),
|
|
376
|
-
set: vi.fn(),
|
|
377
|
-
delete: vi.fn(),
|
|
378
|
-
},
|
|
72
|
+
apiClient: { get: vi.fn(), post: vi.fn() },
|
|
379
73
|
}));
|
|
380
74
|
|
|
381
75
|
describe("UserService", () => {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
beforeEach(() => {
|
|
385
|
-
vi.clearAllMocks();
|
|
386
|
-
userService = new UserService();
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
describe("getUser", () => {
|
|
390
|
-
it("fetches user from API", async () => {
|
|
391
|
-
const mockUser = { id: "1", name: "John", email: "john@example.com" };
|
|
392
|
-
(apiClient.get as Mock).mockResolvedValue({ data: mockUser });
|
|
393
|
-
(cache.get as Mock).mockReturnValue(null);
|
|
394
|
-
|
|
395
|
-
const user = await userService.getUser("1");
|
|
396
|
-
|
|
397
|
-
expect(apiClient.get).toHaveBeenCalledWith("/users/1");
|
|
398
|
-
expect(user).toEqual(mockUser);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it("returns cached user if available", async () => {
|
|
402
|
-
const cachedUser = { id: "1", name: "John", email: "john@example.com" };
|
|
403
|
-
(cache.get as Mock).mockReturnValue(cachedUser);
|
|
404
|
-
|
|
405
|
-
const user = await userService.getUser("1");
|
|
406
|
-
|
|
407
|
-
expect(cache.get).toHaveBeenCalledWith("user:1");
|
|
408
|
-
expect(apiClient.get).not.toHaveBeenCalled();
|
|
409
|
-
expect(user).toEqual(cachedUser);
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
it("caches fetched user", async () => {
|
|
413
|
-
const mockUser = { id: "1", name: "John", email: "john@example.com" };
|
|
414
|
-
(apiClient.get as Mock).mockResolvedValue({ data: mockUser });
|
|
415
|
-
(cache.get as Mock).mockReturnValue(null);
|
|
416
|
-
|
|
417
|
-
await userService.getUser("1");
|
|
418
|
-
|
|
419
|
-
expect(cache.set).toHaveBeenCalledWith("user:1", mockUser, {
|
|
420
|
-
ttl: 3600,
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
describe("createUser", () => {
|
|
426
|
-
it("creates user and invalidates cache", async () => {
|
|
427
|
-
const newUser = { name: "Jane", email: "jane@example.com" };
|
|
428
|
-
const createdUser = { id: "2", ...newUser };
|
|
429
|
-
(apiClient.post as Mock).mockResolvedValue({ data: createdUser });
|
|
430
|
-
|
|
431
|
-
const user = await userService.createUser(newUser);
|
|
432
|
-
|
|
433
|
-
expect(apiClient.post).toHaveBeenCalledWith("/users", newUser);
|
|
434
|
-
expect(cache.delete).toHaveBeenCalledWith("users:list");
|
|
435
|
-
expect(user).toEqual(createdUser);
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it("throws on validation error", async () => {
|
|
439
|
-
(apiClient.post as Mock).mockRejectedValue({
|
|
440
|
-
response: {
|
|
441
|
-
status: 400,
|
|
442
|
-
data: { errors: { email: "Invalid email" } },
|
|
443
|
-
},
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
await expect(
|
|
447
|
-
userService.createUser({ name: "Test", email: "invalid" })
|
|
448
|
-
).rejects.toMatchObject({
|
|
449
|
-
response: { status: 400 },
|
|
450
|
-
});
|
|
451
|
-
});
|
|
452
|
-
});
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
// tests/unit/hooks/useApi.test.ts
|
|
456
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
457
|
-
import { renderHook, waitFor } from "@testing-library/vue";
|
|
458
|
-
import { useApi } from "@/hooks/useApi";
|
|
459
|
-
|
|
460
|
-
// Mock fetch globally
|
|
461
|
-
const mockFetch = vi.fn();
|
|
462
|
-
global.fetch = mockFetch;
|
|
463
|
-
|
|
464
|
-
describe("useApi", () => {
|
|
465
|
-
beforeEach(() => {
|
|
466
|
-
mockFetch.mockReset();
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
it("fetches data successfully", async () => {
|
|
470
|
-
const mockData = { users: [{ id: 1, name: "John" }] };
|
|
471
|
-
mockFetch.mockResolvedValueOnce({
|
|
472
|
-
ok: true,
|
|
473
|
-
json: async () => mockData,
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
const { result } = renderHook(() => useApi("/api/users"));
|
|
477
|
-
|
|
478
|
-
expect(result.current.loading).toBe(true);
|
|
479
|
-
|
|
480
|
-
await waitFor(() => {
|
|
481
|
-
expect(result.current.loading).toBe(false);
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
expect(result.current.data).toEqual(mockData);
|
|
485
|
-
expect(result.current.error).toBeNull();
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
it("handles fetch errors", async () => {
|
|
489
|
-
mockFetch.mockRejectedValueOnce(new Error("Network error"));
|
|
490
|
-
|
|
491
|
-
const { result } = renderHook(() => useApi("/api/users"));
|
|
492
|
-
|
|
493
|
-
await waitFor(() => {
|
|
494
|
-
expect(result.current.loading).toBe(false);
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
expect(result.current.error).toBeInstanceOf(Error);
|
|
498
|
-
expect(result.current.data).toBeNull();
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
it("refetches on manual trigger", async () => {
|
|
502
|
-
const mockData1 = { count: 1 };
|
|
503
|
-
const mockData2 = { count: 2 };
|
|
504
|
-
mockFetch
|
|
505
|
-
.mockResolvedValueOnce({ ok: true, json: async () => mockData1 })
|
|
506
|
-
.mockResolvedValueOnce({ ok: true, json: async () => mockData2 });
|
|
507
|
-
|
|
508
|
-
const { result } = renderHook(() => useApi("/api/count"));
|
|
509
|
-
|
|
510
|
-
await waitFor(() => {
|
|
511
|
-
expect(result.current.data).toEqual(mockData1);
|
|
512
|
-
});
|
|
76
|
+
beforeEach(() => vi.clearAllMocks());
|
|
513
77
|
|
|
514
|
-
|
|
78
|
+
it("fetches user from API", async () => {
|
|
79
|
+
const mockUser = { id: "1", name: "John" };
|
|
80
|
+
(apiClient.get as Mock).mockResolvedValue({ data: mockUser });
|
|
515
81
|
|
|
516
|
-
await
|
|
517
|
-
expect(result.current.data).toEqual(mockData2);
|
|
518
|
-
});
|
|
82
|
+
const user = await new UserService().getUser("1");
|
|
519
83
|
|
|
520
|
-
expect(
|
|
84
|
+
expect(apiClient.get).toHaveBeenCalledWith("/users/1");
|
|
85
|
+
expect(user).toEqual(mockUser);
|
|
521
86
|
});
|
|
522
87
|
});
|
|
523
88
|
```
|
|
524
89
|
|
|
525
|
-
###
|
|
90
|
+
### Vue Component Testing
|
|
526
91
|
|
|
527
92
|
```typescript
|
|
528
|
-
// tests/components/Button.test.ts
|
|
529
93
|
import { describe, it, expect, vi } from "vitest";
|
|
530
94
|
import { mount } from "@vue/test-utils";
|
|
531
95
|
import Button from "@/components/Button.vue";
|
|
532
96
|
|
|
533
97
|
describe("Button", () => {
|
|
534
|
-
it("renders with default props", () => {
|
|
535
|
-
const wrapper = mount(Button, {
|
|
536
|
-
slots: {
|
|
537
|
-
default: "Click me",
|
|
538
|
-
},
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
expect(wrapper.text()).toContain("Click me");
|
|
542
|
-
expect(wrapper.classes()).toContain("btn-primary");
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
it("applies variant classes", () => {
|
|
546
|
-
const wrapper = mount(Button, {
|
|
547
|
-
props: { variant: "secondary" },
|
|
548
|
-
slots: { default: "Button" },
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
expect(wrapper.classes()).toContain("btn-secondary");
|
|
552
|
-
});
|
|
553
|
-
|
|
554
98
|
it("emits click event", async () => {
|
|
555
|
-
const wrapper = mount(Button, {
|
|
556
|
-
slots: { default: "Click" },
|
|
557
|
-
});
|
|
558
|
-
|
|
99
|
+
const wrapper = mount(Button, { slots: { default: "Click" } });
|
|
559
100
|
await wrapper.trigger("click");
|
|
560
|
-
|
|
561
101
|
expect(wrapper.emitted("click")).toHaveLength(1);
|
|
562
102
|
});
|
|
563
103
|
|
|
564
104
|
it("is disabled when loading", () => {
|
|
565
|
-
const wrapper = mount(Button, {
|
|
566
|
-
props: { loading: true },
|
|
567
|
-
slots: { default: "Submit" },
|
|
568
|
-
});
|
|
569
|
-
|
|
105
|
+
const wrapper = mount(Button, { props: { loading: true } });
|
|
570
106
|
expect(wrapper.attributes("disabled")).toBeDefined();
|
|
571
|
-
expect(wrapper.find(".spinner").exists()).toBe(true);
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
it("does not emit click when disabled", async () => {
|
|
575
|
-
const wrapper = mount(Button, {
|
|
576
|
-
props: { disabled: true },
|
|
577
|
-
slots: { default: "Click" },
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
await wrapper.trigger("click");
|
|
581
|
-
|
|
582
|
-
expect(wrapper.emitted("click")).toBeUndefined();
|
|
583
|
-
});
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
// tests/components/UserProfile.test.ts
|
|
587
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
588
|
-
import { mount, flushPromises } from "@vue/test-utils";
|
|
589
|
-
import { createPinia, setActivePinia } from "pinia";
|
|
590
|
-
import UserProfile from "@/components/UserProfile.vue";
|
|
591
|
-
import { useUserStore } from "@/stores/user";
|
|
592
|
-
|
|
593
|
-
describe("UserProfile", () => {
|
|
594
|
-
beforeEach(() => {
|
|
595
|
-
setActivePinia(createPinia());
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
it("displays user information", async () => {
|
|
599
|
-
const userStore = useUserStore();
|
|
600
|
-
userStore.user = {
|
|
601
|
-
id: "1",
|
|
602
|
-
name: "John Doe",
|
|
603
|
-
email: "john@example.com",
|
|
604
|
-
avatar: "https://example.com/avatar.jpg",
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
const wrapper = mount(UserProfile);
|
|
608
|
-
|
|
609
|
-
expect(wrapper.find("[data-testid='user-name']").text()).toBe("John Doe");
|
|
610
|
-
expect(wrapper.find("[data-testid='user-email']").text()).toBe(
|
|
611
|
-
"john@example.com"
|
|
612
|
-
);
|
|
613
|
-
expect(wrapper.find("img").attributes("src")).toBe(
|
|
614
|
-
"https://example.com/avatar.jpg"
|
|
615
|
-
);
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
it("shows loading state", () => {
|
|
619
|
-
const userStore = useUserStore();
|
|
620
|
-
userStore.loading = true;
|
|
621
|
-
|
|
622
|
-
const wrapper = mount(UserProfile);
|
|
623
|
-
|
|
624
|
-
expect(wrapper.find("[data-testid='loading']").exists()).toBe(true);
|
|
625
|
-
expect(wrapper.find("[data-testid='user-name']").exists()).toBe(false);
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
it("handles logout", async () => {
|
|
629
|
-
const userStore = useUserStore();
|
|
630
|
-
userStore.user = { id: "1", name: "John", email: "john@example.com" };
|
|
631
|
-
userStore.logout = vi.fn();
|
|
632
|
-
|
|
633
|
-
const wrapper = mount(UserProfile);
|
|
634
|
-
|
|
635
|
-
await wrapper.find("[data-testid='logout-btn']").trigger("click");
|
|
636
|
-
|
|
637
|
-
expect(userStore.logout).toHaveBeenCalled();
|
|
638
|
-
});
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
// tests/components/SearchInput.test.ts
|
|
642
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
643
|
-
import { mount } from "@vue/test-utils";
|
|
644
|
-
import SearchInput from "@/components/SearchInput.vue";
|
|
645
|
-
|
|
646
|
-
describe("SearchInput", () => {
|
|
647
|
-
beforeEach(() => {
|
|
648
|
-
vi.useFakeTimers();
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
afterEach(() => {
|
|
652
|
-
vi.useRealTimers();
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
it("debounces input", async () => {
|
|
656
|
-
const wrapper = mount(SearchInput, {
|
|
657
|
-
props: { debounce: 300 },
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
await wrapper.find("input").setValue("test");
|
|
661
|
-
|
|
662
|
-
expect(wrapper.emitted("search")).toBeUndefined();
|
|
663
|
-
|
|
664
|
-
vi.advanceTimersByTime(300);
|
|
665
|
-
|
|
666
|
-
expect(wrapper.emitted("search")).toHaveLength(1);
|
|
667
|
-
expect(wrapper.emitted("search")![0]).toEqual(["test"]);
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
it("clears input on clear button click", async () => {
|
|
671
|
-
const wrapper = mount(SearchInput);
|
|
672
|
-
|
|
673
|
-
await wrapper.find("input").setValue("test");
|
|
674
|
-
await wrapper.find("[data-testid='clear-btn']").trigger("click");
|
|
675
|
-
|
|
676
|
-
expect((wrapper.find("input").element as HTMLInputElement).value).toBe("");
|
|
677
|
-
expect(wrapper.emitted("clear")).toHaveLength(1);
|
|
678
|
-
});
|
|
679
|
-
});
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
### 5. Snapshot Testing
|
|
683
|
-
|
|
684
|
-
```typescript
|
|
685
|
-
// tests/snapshots/components.test.ts
|
|
686
|
-
import { describe, it, expect } from "vitest";
|
|
687
|
-
import { mount } from "@vue/test-utils";
|
|
688
|
-
import Card from "@/components/Card.vue";
|
|
689
|
-
import Badge from "@/components/Badge.vue";
|
|
690
|
-
import Alert from "@/components/Alert.vue";
|
|
691
|
-
|
|
692
|
-
describe("Component Snapshots", () => {
|
|
693
|
-
describe("Card", () => {
|
|
694
|
-
it("matches snapshot with default props", () => {
|
|
695
|
-
const wrapper = mount(Card, {
|
|
696
|
-
slots: {
|
|
697
|
-
header: "Card Title",
|
|
698
|
-
default: "Card content",
|
|
699
|
-
footer: "Card footer",
|
|
700
|
-
},
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
expect(wrapper.html()).toMatchSnapshot();
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
it("matches snapshot with custom props", () => {
|
|
707
|
-
const wrapper = mount(Card, {
|
|
708
|
-
props: {
|
|
709
|
-
variant: "outlined",
|
|
710
|
-
hoverable: true,
|
|
711
|
-
},
|
|
712
|
-
slots: {
|
|
713
|
-
default: "Content",
|
|
714
|
-
},
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
expect(wrapper.html()).toMatchSnapshot();
|
|
718
|
-
});
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
describe("Badge", () => {
|
|
722
|
-
it.each(["default", "primary", "success", "warning", "error"] as const)(
|
|
723
|
-
"matches snapshot for %s variant",
|
|
724
|
-
(variant) => {
|
|
725
|
-
const wrapper = mount(Badge, {
|
|
726
|
-
props: { variant },
|
|
727
|
-
slots: { default: "Badge" },
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
expect(wrapper.html()).toMatchSnapshot();
|
|
731
|
-
}
|
|
732
|
-
);
|
|
733
|
-
});
|
|
734
|
-
|
|
735
|
-
describe("Alert", () => {
|
|
736
|
-
it("matches snapshot with icon and close button", () => {
|
|
737
|
-
const wrapper = mount(Alert, {
|
|
738
|
-
props: {
|
|
739
|
-
type: "warning",
|
|
740
|
-
title: "Warning",
|
|
741
|
-
closable: true,
|
|
742
|
-
},
|
|
743
|
-
slots: {
|
|
744
|
-
default: "This is a warning message",
|
|
745
|
-
},
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
expect(wrapper.html()).toMatchSnapshot();
|
|
749
|
-
});
|
|
750
|
-
});
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
// tests/snapshots/data-structures.test.ts
|
|
754
|
-
import { describe, it, expect } from "vitest";
|
|
755
|
-
import { transformUserData, normalizeApiResponse } from "@/utils/transforms";
|
|
756
|
-
|
|
757
|
-
describe("Data Transform Snapshots", () => {
|
|
758
|
-
it("transforms user data correctly", () => {
|
|
759
|
-
const rawUser = {
|
|
760
|
-
id: "123",
|
|
761
|
-
first_name: "John",
|
|
762
|
-
last_name: "Doe",
|
|
763
|
-
email_address: "john.doe@example.com",
|
|
764
|
-
created_at: "2024-01-15T10:30:00Z",
|
|
765
|
-
roles: ["admin", "user"],
|
|
766
|
-
};
|
|
767
|
-
|
|
768
|
-
const transformed = transformUserData(rawUser);
|
|
769
|
-
|
|
770
|
-
expect(transformed).toMatchInlineSnapshot(`
|
|
771
|
-
{
|
|
772
|
-
"createdAt": 2024-01-15T10:30:00.000Z,
|
|
773
|
-
"email": "john.doe@example.com",
|
|
774
|
-
"fullName": "John Doe",
|
|
775
|
-
"id": "123",
|
|
776
|
-
"isAdmin": true,
|
|
777
|
-
"roles": [
|
|
778
|
-
"admin",
|
|
779
|
-
"user",
|
|
780
|
-
],
|
|
781
|
-
}
|
|
782
|
-
`);
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
it("normalizes API response", () => {
|
|
786
|
-
const apiResponse = {
|
|
787
|
-
data: [
|
|
788
|
-
{ id: 1, name: "Item 1" },
|
|
789
|
-
{ id: 2, name: "Item 2" },
|
|
790
|
-
],
|
|
791
|
-
meta: {
|
|
792
|
-
total: 100,
|
|
793
|
-
page: 1,
|
|
794
|
-
per_page: 10,
|
|
795
|
-
},
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
const normalized = normalizeApiResponse(apiResponse);
|
|
799
|
-
|
|
800
|
-
expect(normalized).toMatchInlineSnapshot(`
|
|
801
|
-
{
|
|
802
|
-
"items": [
|
|
803
|
-
{
|
|
804
|
-
"id": 1,
|
|
805
|
-
"name": "Item 1",
|
|
806
|
-
},
|
|
807
|
-
{
|
|
808
|
-
"id": 2,
|
|
809
|
-
"name": "Item 2",
|
|
810
|
-
},
|
|
811
|
-
],
|
|
812
|
-
"pagination": {
|
|
813
|
-
"currentPage": 1,
|
|
814
|
-
"perPage": 10,
|
|
815
|
-
"totalItems": 100,
|
|
816
|
-
"totalPages": 10,
|
|
817
|
-
},
|
|
818
|
-
}
|
|
819
|
-
`);
|
|
820
107
|
});
|
|
821
108
|
});
|
|
822
109
|
```
|
|
823
110
|
|
|
824
|
-
###
|
|
111
|
+
### Testing Async Operations with Timers
|
|
825
112
|
|
|
826
113
|
```typescript
|
|
827
|
-
|
|
828
|
-
import { describe, it, expect } from "vitest";
|
|
829
|
-
import { render, screen, fireEvent } from "@testing-library/react";
|
|
830
|
-
import Counter from "@/components/Counter";
|
|
831
|
-
|
|
832
|
-
describe("Counter", () => {
|
|
833
|
-
it("renders initial count", () => {
|
|
834
|
-
render(<Counter initialCount={5} />);
|
|
835
|
-
|
|
836
|
-
expect(screen.getByText("Count: 5")).toBeInTheDocument();
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
it("increments count on button click", async () => {
|
|
840
|
-
render(<Counter initialCount={0} />);
|
|
841
|
-
|
|
842
|
-
const incrementBtn = screen.getByRole("button", { name: /increment/i });
|
|
843
|
-
await fireEvent.click(incrementBtn);
|
|
844
|
-
|
|
845
|
-
expect(screen.getByText("Count: 1")).toBeInTheDocument();
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
it("decrements count on button click", async () => {
|
|
849
|
-
render(<Counter initialCount={5} />);
|
|
850
|
-
|
|
851
|
-
const decrementBtn = screen.getByRole("button", { name: /decrement/i });
|
|
852
|
-
await fireEvent.click(decrementBtn);
|
|
853
|
-
|
|
854
|
-
expect(screen.getByText("Count: 4")).toBeInTheDocument();
|
|
855
|
-
});
|
|
856
|
-
|
|
857
|
-
it("respects min value", async () => {
|
|
858
|
-
render(<Counter initialCount={0} min={0} />);
|
|
859
|
-
|
|
860
|
-
const decrementBtn = screen.getByRole("button", { name: /decrement/i });
|
|
861
|
-
await fireEvent.click(decrementBtn);
|
|
862
|
-
|
|
863
|
-
expect(screen.getByText("Count: 0")).toBeInTheDocument();
|
|
864
|
-
expect(decrementBtn).toBeDisabled();
|
|
865
|
-
});
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
// tests/components/react/Form.test.tsx
|
|
869
|
-
import { describe, it, expect, vi } from "vitest";
|
|
870
|
-
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
871
|
-
import userEvent from "@testing-library/user-event";
|
|
872
|
-
import ContactForm from "@/components/ContactForm";
|
|
873
|
-
|
|
874
|
-
describe("ContactForm", () => {
|
|
875
|
-
const mockSubmit = vi.fn();
|
|
876
|
-
|
|
877
|
-
it("submits form with valid data", async () => {
|
|
878
|
-
const user = userEvent.setup();
|
|
879
|
-
render(<ContactForm onSubmit={mockSubmit} />);
|
|
880
|
-
|
|
881
|
-
await user.type(screen.getByLabelText(/name/i), "John Doe");
|
|
882
|
-
await user.type(screen.getByLabelText(/email/i), "john@example.com");
|
|
883
|
-
await user.type(screen.getByLabelText(/message/i), "Hello!");
|
|
884
|
-
await user.click(screen.getByRole("button", { name: /submit/i }));
|
|
885
|
-
|
|
886
|
-
await waitFor(() => {
|
|
887
|
-
expect(mockSubmit).toHaveBeenCalledWith({
|
|
888
|
-
name: "John Doe",
|
|
889
|
-
email: "john@example.com",
|
|
890
|
-
message: "Hello!",
|
|
891
|
-
});
|
|
892
|
-
});
|
|
893
|
-
});
|
|
894
|
-
|
|
895
|
-
it("displays validation errors", async () => {
|
|
896
|
-
const user = userEvent.setup();
|
|
897
|
-
render(<ContactForm onSubmit={mockSubmit} />);
|
|
898
|
-
|
|
899
|
-
await user.click(screen.getByRole("button", { name: /submit/i }));
|
|
114
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
900
115
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
});
|
|
116
|
+
describe("Debounce", () => {
|
|
117
|
+
beforeEach(() => vi.useFakeTimers());
|
|
118
|
+
afterEach(() => vi.useRealTimers());
|
|
905
119
|
|
|
906
|
-
it("
|
|
907
|
-
const
|
|
908
|
-
|
|
120
|
+
it("delays function execution", () => {
|
|
121
|
+
const fn = vi.fn();
|
|
122
|
+
const debouncedFn = debounce(fn, 100);
|
|
909
123
|
|
|
910
|
-
|
|
911
|
-
|
|
124
|
+
debouncedFn();
|
|
125
|
+
expect(fn).not.toHaveBeenCalled();
|
|
912
126
|
|
|
913
|
-
|
|
127
|
+
vi.advanceTimersByTime(100);
|
|
128
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
914
129
|
});
|
|
915
130
|
});
|
|
916
131
|
```
|
|
917
132
|
|
|
918
|
-
## Use Cases
|
|
919
|
-
|
|
920
|
-
### CI/CD Integration
|
|
921
|
-
|
|
922
|
-
```yaml
|
|
923
|
-
# .github/workflows/test.yml
|
|
924
|
-
name: Tests
|
|
925
|
-
|
|
926
|
-
on: [push, pull_request]
|
|
927
|
-
|
|
928
|
-
jobs:
|
|
929
|
-
test:
|
|
930
|
-
runs-on: ubuntu-latest
|
|
931
|
-
|
|
932
|
-
steps:
|
|
933
|
-
- uses: actions/checkout@v4
|
|
934
|
-
|
|
935
|
-
- name: Setup Node.js
|
|
936
|
-
uses: actions/setup-node@v4
|
|
937
|
-
with:
|
|
938
|
-
node-version: "20"
|
|
939
|
-
cache: "npm"
|
|
940
|
-
|
|
941
|
-
- name: Install dependencies
|
|
942
|
-
run: npm ci
|
|
943
|
-
|
|
944
|
-
- name: Run type check
|
|
945
|
-
run: npm run type-check
|
|
946
|
-
|
|
947
|
-
- name: Run unit tests
|
|
948
|
-
run: npm run test:unit -- --coverage
|
|
949
|
-
|
|
950
|
-
- name: Run component tests
|
|
951
|
-
run: npm run test:components
|
|
952
|
-
|
|
953
|
-
- name: Upload coverage
|
|
954
|
-
uses: codecov/codecov-action@v4
|
|
955
|
-
with:
|
|
956
|
-
file: ./coverage/lcov.info
|
|
957
|
-
```
|
|
958
|
-
|
|
959
|
-
### Watch Mode Development
|
|
960
|
-
|
|
961
|
-
```json
|
|
962
|
-
// package.json
|
|
963
|
-
{
|
|
964
|
-
"scripts": {
|
|
965
|
-
"test": "vitest",
|
|
966
|
-
"test:unit": "vitest run",
|
|
967
|
-
"test:watch": "vitest watch",
|
|
968
|
-
"test:ui": "vitest --ui",
|
|
969
|
-
"test:coverage": "vitest run --coverage",
|
|
970
|
-
"test:changed": "vitest --changed HEAD~1"
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
```
|
|
974
|
-
|
|
975
133
|
## Best Practices
|
|
976
134
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
- Use appropriate matchers
|
|
986
|
-
- Group related tests with describe
|
|
987
|
-
- Run tests in CI/CD pipeline
|
|
988
|
-
- Maintain high coverage thresholds
|
|
989
|
-
|
|
990
|
-
### Don'ts
|
|
991
|
-
|
|
992
|
-
- Don't test framework internals
|
|
993
|
-
- Don't share state between tests
|
|
994
|
-
- Don't use arbitrary timeouts
|
|
995
|
-
- Don't over-mock
|
|
996
|
-
- Don't ignore flaky tests
|
|
997
|
-
- Don't test private methods directly
|
|
998
|
-
- Don't duplicate coverage
|
|
999
|
-
- Don't write tests after the fact only
|
|
1000
|
-
- Don't skip snapshot updates without review
|
|
1001
|
-
- Don't test third-party libraries
|
|
135
|
+
| Do | Avoid |
|
|
136
|
+
|----|-------|
|
|
137
|
+
| Use descriptive test names explaining behavior | Testing implementation details |
|
|
138
|
+
| Test behavior, not internal state | Sharing state between tests |
|
|
139
|
+
| Use test.each for multiple similar cases | Using arbitrary timeouts |
|
|
140
|
+
| Mock external dependencies | Over-mocking internal modules |
|
|
141
|
+
| Keep tests focused and isolated | Duplicating test coverage |
|
|
142
|
+
| Write tests alongside code | Ignoring flaky tests |
|
|
1002
143
|
|
|
1003
144
|
## References
|
|
1004
145
|
|
|
1005
146
|
- [Vitest Documentation](https://vitest.dev/)
|
|
1006
147
|
- [Vue Test Utils](https://test-utils.vuejs.org/)
|
|
1007
148
|
- [Testing Library](https://testing-library.com/)
|
|
1008
|
-
- [Vitest
|
|
1009
|
-
- [Coverage Documentation](https://vitest.dev/guide/coverage.html)
|
|
149
|
+
- [Vitest Coverage](https://vitest.dev/guide/coverage.html)
|