omgkit 2.1.0 → 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.
Files changed (56) hide show
  1. package/package.json +1 -1
  2. package/plugin/skills/SKILL_STANDARDS.md +743 -0
  3. package/plugin/skills/databases/mongodb/SKILL.md +797 -28
  4. package/plugin/skills/databases/postgresql/SKILL.md +494 -18
  5. package/plugin/skills/databases/prisma/SKILL.md +776 -30
  6. package/plugin/skills/databases/redis/SKILL.md +885 -25
  7. package/plugin/skills/devops/aws/SKILL.md +686 -28
  8. package/plugin/skills/devops/docker/SKILL.md +466 -18
  9. package/plugin/skills/devops/github-actions/SKILL.md +684 -29
  10. package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
  11. package/plugin/skills/frameworks/django/SKILL.md +920 -20
  12. package/plugin/skills/frameworks/express/SKILL.md +1361 -35
  13. package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
  14. package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
  15. package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
  16. package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
  17. package/plugin/skills/frameworks/rails/SKILL.md +594 -28
  18. package/plugin/skills/frameworks/react/SKILL.md +1006 -32
  19. package/plugin/skills/frameworks/spring/SKILL.md +528 -35
  20. package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
  21. package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
  22. package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
  23. package/plugin/skills/frontend/responsive/SKILL.md +847 -21
  24. package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
  25. package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
  26. package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
  27. package/plugin/skills/languages/javascript/SKILL.md +935 -31
  28. package/plugin/skills/languages/python/SKILL.md +489 -25
  29. package/plugin/skills/languages/typescript/SKILL.md +379 -30
  30. package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
  31. package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
  32. package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
  33. package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
  34. package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
  35. package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
  36. package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
  37. package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
  38. package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
  39. package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
  40. package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
  41. package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
  42. package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
  43. package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
  44. package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
  45. package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
  46. package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
  47. package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
  48. package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
  49. package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
  50. package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
  51. package/plugin/skills/security/better-auth/SKILL.md +1065 -28
  52. package/plugin/skills/security/oauth/SKILL.md +968 -31
  53. package/plugin/skills/security/owasp/SKILL.md +894 -33
  54. package/plugin/skills/testing/playwright/SKILL.md +764 -38
  55. package/plugin/skills/testing/pytest/SKILL.md +873 -36
  56. package/plugin/skills/testing/vitest/SKILL.md +980 -35
@@ -1,64 +1,1009 @@
1
1
  ---
2
2
  name: vitest
3
- description: Vitest testing. Use for JavaScript/TypeScript unit tests.
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 Skill
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
- import { describe, it, expect } from 'vitest';
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
- describe('add', () => {
13
- it('adds two numbers', () => {
14
- expect(add(1, 2)).toBe(3);
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
- it('handles negative numbers', () => {
18
- expect(add(-1, 1)).toBe(0);
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
- ## Async Tests
151
+ ### 2. Unit Testing Patterns
152
+
24
153
  ```typescript
25
- it('fetches user', async () => {
26
- const user = await fetchUser('123');
27
- expect(user.email).toBe('test@example.com');
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
- ## Mocking
354
+ ### 3. Mocking Patterns
355
+
32
356
  ```typescript
33
- import { vi } from 'vitest';
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('./api', () => ({
36
- fetchData: vi.fn().mockResolvedValue({ data: 'test' }),
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
- it('calls API', async () => {
40
- const result = await getData();
41
- expect(fetchData).toHaveBeenCalled();
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
- ## Setup
525
+ ### 4. Vue Component Testing
526
+
46
527
  ```typescript
47
- // vitest.config.ts
48
- export default defineConfig({
49
- test: {
50
- globals: true,
51
- environment: 'jsdom',
52
- coverage: {
53
- provider: 'v8',
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
- ## Run
60
- ```bash
61
- vitest
62
- vitest --coverage
63
- vitest --ui
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)