omgkit 2.1.1 → 2.2.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/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,64 +1,1009 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vitest
|
|
3
|
-
description: Vitest testing
|
|
3
|
+
description: Vitest testing for TypeScript/JavaScript with mocking, coverage, snapshot testing, and Vue/React component testing
|
|
4
|
+
category: testing
|
|
5
|
+
triggers:
|
|
6
|
+
- vitest
|
|
7
|
+
- javascript testing
|
|
8
|
+
- typescript testing
|
|
9
|
+
- unit testing js
|
|
10
|
+
- vue testing
|
|
11
|
+
- react testing
|
|
4
12
|
---
|
|
5
13
|
|
|
6
|
-
# Vitest
|
|
14
|
+
# Vitest
|
|
15
|
+
|
|
16
|
+
Enterprise-grade **JavaScript/TypeScript testing framework** following industry best practices. This skill covers unit testing, mocking, snapshot testing, component testing, coverage reports, and CI integration patterns used by top engineering teams.
|
|
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
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// vitest.config.ts
|
|
36
|
+
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
|
+
|
|
41
|
+
export default defineConfig({
|
|
42
|
+
plugins: [vue(), react()],
|
|
43
|
+
test: {
|
|
44
|
+
globals: true,
|
|
45
|
+
environment: "jsdom",
|
|
46
|
+
include: ["**/*.{test,spec}.{js,ts,jsx,tsx}"],
|
|
47
|
+
exclude: ["**/node_modules/**", "**/dist/**", "**/e2e/**"],
|
|
48
|
+
setupFiles: ["./tests/setup.ts"],
|
|
49
|
+
coverage: {
|
|
50
|
+
provider: "v8",
|
|
51
|
+
reporter: ["text", "json", "html", "lcov"],
|
|
52
|
+
reportsDirectory: "./coverage",
|
|
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"),
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
```
|
|
7
97
|
|
|
8
|
-
## Basic Tests
|
|
9
98
|
```typescript
|
|
10
|
-
|
|
99
|
+
// tests/setup.ts
|
|
100
|
+
import { beforeAll, afterAll, afterEach, vi } from "vitest";
|
|
101
|
+
import { cleanup } from "@testing-library/vue";
|
|
102
|
+
import "@testing-library/jest-dom/vitest";
|
|
103
|
+
|
|
104
|
+
// Reset mocks after each test
|
|
105
|
+
afterEach(() => {
|
|
106
|
+
vi.clearAllMocks();
|
|
107
|
+
vi.resetAllMocks();
|
|
108
|
+
cleanup();
|
|
109
|
+
});
|
|
11
110
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
111
|
+
// Global setup
|
|
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
|
+
})),
|
|
15
126
|
});
|
|
16
127
|
|
|
17
|
-
|
|
18
|
-
|
|
128
|
+
// Mock IntersectionObserver
|
|
129
|
+
const mockIntersectionObserver = vi.fn();
|
|
130
|
+
mockIntersectionObserver.mockReturnValue({
|
|
131
|
+
observe: vi.fn(),
|
|
132
|
+
unobserve: vi.fn(),
|
|
133
|
+
disconnect: vi.fn(),
|
|
19
134
|
});
|
|
135
|
+
window.IntersectionObserver = mockIntersectionObserver;
|
|
136
|
+
|
|
137
|
+
// Mock ResizeObserver
|
|
138
|
+
window.ResizeObserver = vi.fn().mockImplementation(() => ({
|
|
139
|
+
observe: vi.fn(),
|
|
140
|
+
unobserve: vi.fn(),
|
|
141
|
+
disconnect: vi.fn(),
|
|
142
|
+
}));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Global teardown
|
|
146
|
+
afterAll(() => {
|
|
147
|
+
vi.restoreAllMocks();
|
|
20
148
|
});
|
|
21
149
|
```
|
|
22
150
|
|
|
23
|
-
|
|
151
|
+
### 2. Unit Testing Patterns
|
|
152
|
+
|
|
24
153
|
```typescript
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
154
|
+
// tests/unit/utils/string.test.ts
|
|
155
|
+
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
|
+
|
|
303
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe("retry", () => {
|
|
308
|
+
it("retries failed operations", async () => {
|
|
309
|
+
const fn = vi
|
|
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
|
+
});
|
|
350
|
+
});
|
|
28
351
|
});
|
|
29
352
|
```
|
|
30
353
|
|
|
31
|
-
|
|
354
|
+
### 3. Mocking Patterns
|
|
355
|
+
|
|
32
356
|
```typescript
|
|
33
|
-
|
|
357
|
+
// tests/unit/services/user.test.ts
|
|
358
|
+
import { describe, it, expect, vi, beforeEach, Mock } from "vitest";
|
|
359
|
+
import { UserService } from "@/services/user";
|
|
360
|
+
import { apiClient } from "@/lib/api";
|
|
361
|
+
import { cache } from "@/lib/cache";
|
|
362
|
+
|
|
363
|
+
// Mock modules
|
|
364
|
+
vi.mock("@/lib/api", () => ({
|
|
365
|
+
apiClient: {
|
|
366
|
+
get: vi.fn(),
|
|
367
|
+
post: vi.fn(),
|
|
368
|
+
put: vi.fn(),
|
|
369
|
+
delete: vi.fn(),
|
|
370
|
+
},
|
|
371
|
+
}));
|
|
34
372
|
|
|
35
|
-
vi.mock(
|
|
36
|
-
|
|
373
|
+
vi.mock("@/lib/cache", () => ({
|
|
374
|
+
cache: {
|
|
375
|
+
get: vi.fn(),
|
|
376
|
+
set: vi.fn(),
|
|
377
|
+
delete: vi.fn(),
|
|
378
|
+
},
|
|
37
379
|
}));
|
|
38
380
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
381
|
+
describe("UserService", () => {
|
|
382
|
+
let userService: UserService;
|
|
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
|
+
});
|
|
513
|
+
|
|
514
|
+
result.current.refetch();
|
|
515
|
+
|
|
516
|
+
await waitFor(() => {
|
|
517
|
+
expect(result.current.data).toEqual(mockData2);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
521
|
+
});
|
|
42
522
|
});
|
|
43
523
|
```
|
|
44
524
|
|
|
45
|
-
|
|
525
|
+
### 4. Vue Component Testing
|
|
526
|
+
|
|
46
527
|
```typescript
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
528
|
+
// tests/components/Button.test.ts
|
|
529
|
+
import { describe, it, expect, vi } from "vitest";
|
|
530
|
+
import { mount } from "@vue/test-utils";
|
|
531
|
+
import Button from "@/components/Button.vue";
|
|
532
|
+
|
|
533
|
+
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
|
+
it("emits click event", async () => {
|
|
555
|
+
const wrapper = mount(Button, {
|
|
556
|
+
slots: { default: "Click" },
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
await wrapper.trigger("click");
|
|
560
|
+
|
|
561
|
+
expect(wrapper.emitted("click")).toHaveLength(1);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("is disabled when loading", () => {
|
|
565
|
+
const wrapper = mount(Button, {
|
|
566
|
+
props: { loading: true },
|
|
567
|
+
slots: { default: "Submit" },
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
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
|
+
});
|
|
56
679
|
});
|
|
57
680
|
```
|
|
58
681
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
vitest
|
|
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
|
+
});
|
|
821
|
+
});
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
### 6. React Component Testing
|
|
825
|
+
|
|
826
|
+
```typescript
|
|
827
|
+
// tests/components/react/Counter.test.tsx
|
|
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 }));
|
|
900
|
+
|
|
901
|
+
expect(await screen.findByText(/name is required/i)).toBeInTheDocument();
|
|
902
|
+
expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
|
|
903
|
+
expect(mockSubmit).not.toHaveBeenCalled();
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it("validates email format", async () => {
|
|
907
|
+
const user = userEvent.setup();
|
|
908
|
+
render(<ContactForm onSubmit={mockSubmit} />);
|
|
909
|
+
|
|
910
|
+
await user.type(screen.getByLabelText(/email/i), "invalid-email");
|
|
911
|
+
await user.tab();
|
|
912
|
+
|
|
913
|
+
expect(await screen.findByText(/invalid email/i)).toBeInTheDocument();
|
|
914
|
+
});
|
|
915
|
+
});
|
|
64
916
|
```
|
|
917
|
+
|
|
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
|
+
## Best Practices
|
|
976
|
+
|
|
977
|
+
### Do's
|
|
978
|
+
|
|
979
|
+
- Use descriptive test names
|
|
980
|
+
- Test behavior, not implementation
|
|
981
|
+
- Keep tests focused and isolated
|
|
982
|
+
- Use test.each for multiple similar cases
|
|
983
|
+
- Mock external dependencies
|
|
984
|
+
- Write tests alongside code
|
|
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
|
|
1002
|
+
|
|
1003
|
+
## References
|
|
1004
|
+
|
|
1005
|
+
- [Vitest Documentation](https://vitest.dev/)
|
|
1006
|
+
- [Vue Test Utils](https://test-utils.vuejs.org/)
|
|
1007
|
+
- [Testing Library](https://testing-library.com/)
|
|
1008
|
+
- [Vitest UI](https://vitest.dev/guide/ui.html)
|
|
1009
|
+
- [Coverage Documentation](https://vitest.dev/guide/coverage.html)
|