@gallopsystems/agent-skills 1.0.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/README.md +137 -0
- package/package.json +26 -0
- package/plugins/doctl/.claude-plugin/plugin.json +8 -0
- package/plugins/doctl/skills/doctl/SKILL.md +93 -0
- package/plugins/kysely-postgres/.claude-plugin/plugin.json +8 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/SKILL.md +1101 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/aggregations.ts +167 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/ctes.ts +165 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/expressions.ts +272 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/joins.ts +206 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/json-arrays.ts +398 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/mutations.ts +199 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/orderby-pagination.ts +117 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/relations.ts +176 -0
- package/plugins/kysely-postgres/skills/kysely-postgres/references/select-where.ts +146 -0
- package/plugins/linear/.claude-plugin/plugin.json +8 -0
- package/plugins/linear/skills/linear/SKILL.md +1040 -0
- package/plugins/linear/skills/linear/bin/linear.mjs +1228 -0
- package/plugins/linear/skills/linear/tech-stack.md +273 -0
- package/plugins/nitro-testing/.claude-plugin/plugin.json +8 -0
- package/plugins/nitro-testing/skills/nitro-testing/SKILL.md +497 -0
- package/plugins/nitro-testing/skills/nitro-testing/async-testing.md +270 -0
- package/plugins/nitro-testing/skills/nitro-testing/ci-setup.md +226 -0
- package/plugins/nitro-testing/skills/nitro-testing/examples/global-setup.ts +90 -0
- package/plugins/nitro-testing/skills/nitro-testing/examples/handler.test.ts +167 -0
- package/plugins/nitro-testing/skills/nitro-testing/examples/setup.ts +29 -0
- package/plugins/nitro-testing/skills/nitro-testing/examples/test-utils-index.ts +297 -0
- package/plugins/nitro-testing/skills/nitro-testing/examples/vitest.config.ts +42 -0
- package/plugins/nitro-testing/skills/nitro-testing/factories.md +278 -0
- package/plugins/nitro-testing/skills/nitro-testing/frontend-testing.md +512 -0
- package/plugins/nitro-testing/skills/nitro-testing/test-utils.md +262 -0
- package/plugins/nitro-testing/skills/nitro-testing/transaction-rollback.md +183 -0
- package/plugins/nitro-testing/skills/nitro-testing/vitest-config.md +236 -0
- package/plugins/nuxt-nitro-api/.claude-plugin/plugin.json +8 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/SKILL.md +260 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/auth-patterns.md +228 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/composables-utils.md +174 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/deep-linking.md +190 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/auth-middleware.ts +32 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/auth-utils.ts +51 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/deep-link-page.vue +61 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/service-util.ts +63 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/sse-endpoint.ts +59 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/validation-endpoint.ts +38 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/fetch-patterns.md +178 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/nitro-tasks.md +243 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/page-structure.md +162 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/server-services.md +238 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/sse.md +221 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/ssr-client.md +166 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/validation.md +131 -0
- package/scripts/link-skills.mjs +252 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
# Frontend Testing with @nuxt/test-utils
|
|
2
|
+
|
|
3
|
+
Comprehensive guide to testing Vue components, pages, and utilities in Nuxt 3 applications.
|
|
4
|
+
|
|
5
|
+
## Dependencies
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn add -D @nuxt/test-utils @vue/test-utils happy-dom vitest @vitest/coverage-v8
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
| Package | Purpose |
|
|
12
|
+
|---------|---------|
|
|
13
|
+
| `@nuxt/test-utils` | Nuxt-aware testing utilities (`mountSuspended`, `mockNuxtImport`, `registerEndpoint`) |
|
|
14
|
+
| `@vue/test-utils` | Vue component testing (wrapper API) |
|
|
15
|
+
| `happy-dom` | Lightweight DOM implementation |
|
|
16
|
+
| `vitest` | Test runner |
|
|
17
|
+
|
|
18
|
+
## Vitest Configuration
|
|
19
|
+
|
|
20
|
+
Separate frontend and backend tests using an environment variable:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// vitest.config.ts
|
|
24
|
+
import { defineConfig } from "vitest/config";
|
|
25
|
+
import { defineVitestConfig } from "@nuxt/test-utils/config";
|
|
26
|
+
import path from "path";
|
|
27
|
+
|
|
28
|
+
const isNuxtEnv = process.env.VITEST_ENV === "nuxt";
|
|
29
|
+
|
|
30
|
+
export default isNuxtEnv
|
|
31
|
+
? defineVitestConfig({
|
|
32
|
+
test: {
|
|
33
|
+
environment: "nuxt",
|
|
34
|
+
globals: true,
|
|
35
|
+
include: [
|
|
36
|
+
"components/**/*.test.ts",
|
|
37
|
+
"pages/**/*.test.ts",
|
|
38
|
+
"utils/**/*.test.ts",
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
: defineConfig({
|
|
43
|
+
test: {
|
|
44
|
+
globals: true,
|
|
45
|
+
environment: "node",
|
|
46
|
+
include: ["server/**/*.test.ts"],
|
|
47
|
+
globalSetup: ["./server/test-utils/global-setup.ts"],
|
|
48
|
+
setupFiles: ["./server/test-utils/setup.ts"],
|
|
49
|
+
coverage: {
|
|
50
|
+
provider: "v8",
|
|
51
|
+
reporter: ["text", "json", "html"],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
resolve: {
|
|
55
|
+
alias: {
|
|
56
|
+
"~": path.resolve(__dirname),
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Nuxt Test Configuration
|
|
63
|
+
|
|
64
|
+
Create a minimal Nuxt config for testing:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// nuxt.config.test.ts
|
|
68
|
+
export default defineNuxtConfig({
|
|
69
|
+
modules: [
|
|
70
|
+
"@primevue/nuxt-module", // Include UI libraries your components need
|
|
71
|
+
],
|
|
72
|
+
ssr: false, // Simplifies component testing
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Package.json Scripts
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"scripts": {
|
|
81
|
+
"test": "vitest",
|
|
82
|
+
"test:run": "vitest run",
|
|
83
|
+
"test:coverage": "vitest run --coverage",
|
|
84
|
+
"test:frontend": "VITEST_ENV=nuxt vitest",
|
|
85
|
+
"test:frontend:run": "VITEST_ENV=nuxt vitest run"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Core Testing Patterns
|
|
91
|
+
|
|
92
|
+
### Basic Component Test
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { describe, it, expect } from "vitest";
|
|
96
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
97
|
+
import ProjectCard from "./ProjectCard.vue";
|
|
98
|
+
|
|
99
|
+
describe("ProjectCard", () => {
|
|
100
|
+
it("renders project information", async () => {
|
|
101
|
+
const wrapper = await mountSuspended(ProjectCard, {
|
|
102
|
+
props: {
|
|
103
|
+
project: {
|
|
104
|
+
id: 1,
|
|
105
|
+
name: "Website Redesign",
|
|
106
|
+
status: "active",
|
|
107
|
+
start_date: "2025-01-15",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(wrapper.exists()).toBe(true);
|
|
113
|
+
expect(wrapper.text()).toContain("Website Redesign");
|
|
114
|
+
expect(wrapper.text()).toContain("active");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("applies correct CSS class for status", async () => {
|
|
118
|
+
const wrapper = await mountSuspended(ProjectCard, {
|
|
119
|
+
props: {
|
|
120
|
+
project: { id: 1, name: "Test", status: "completed" },
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(wrapper.html()).toMatch(/completed/);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Testing with Mocked Composables
|
|
130
|
+
|
|
131
|
+
Mock composables **before** mounting:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { describe, it, expect } from "vitest";
|
|
135
|
+
import { mountSuspended, mockNuxtImport } from "@nuxt/test-utils/runtime";
|
|
136
|
+
import AddressDisplay from "./AddressDisplay.vue";
|
|
137
|
+
|
|
138
|
+
// Mock must be defined before any mountSuspended calls
|
|
139
|
+
mockNuxtImport("useAddress", () => {
|
|
140
|
+
return () => ({
|
|
141
|
+
getDisplayAddress: (project: any) => {
|
|
142
|
+
return project.address || project.address_details?.formatted_address || "No address";
|
|
143
|
+
},
|
|
144
|
+
formatAddress: (address: any) => `${address.city}, ${address.state}`,
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
mockNuxtImport("useUserSession", () => {
|
|
149
|
+
return () => ({
|
|
150
|
+
user: { id: 1, name: "Test User", email: "test@example.com" },
|
|
151
|
+
loggedIn: true,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("AddressDisplay", () => {
|
|
156
|
+
it("shows formatted address", async () => {
|
|
157
|
+
const wrapper = await mountSuspended(AddressDisplay, {
|
|
158
|
+
props: {
|
|
159
|
+
project: { address: "123 Main St, New York, NY" },
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(wrapper.text()).toContain("123 Main St");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Testing with Mocked API Endpoints
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
172
|
+
import { mountSuspended, registerEndpoint } from "@nuxt/test-utils/runtime";
|
|
173
|
+
import ProjectsList from "./ProjectsList.vue";
|
|
174
|
+
|
|
175
|
+
describe("ProjectsList", () => {
|
|
176
|
+
beforeEach(() => {
|
|
177
|
+
// Register endpoints before each test
|
|
178
|
+
registerEndpoint("/api/projects", {
|
|
179
|
+
method: "GET",
|
|
180
|
+
handler: () => [
|
|
181
|
+
{ id: 1, name: "Project Alpha", status: "active" },
|
|
182
|
+
{ id: 2, name: "Project Beta", status: "completed" },
|
|
183
|
+
{ id: 3, name: "Project Gamma", status: "active" },
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("renders list of projects", async () => {
|
|
189
|
+
const wrapper = await mountSuspended(ProjectsList);
|
|
190
|
+
|
|
191
|
+
expect(wrapper.text()).toContain("Project Alpha");
|
|
192
|
+
expect(wrapper.text()).toContain("Project Beta");
|
|
193
|
+
expect(wrapper.text()).toContain("Project Gamma");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("filters active projects", async () => {
|
|
197
|
+
registerEndpoint("/api/projects", {
|
|
198
|
+
method: "GET",
|
|
199
|
+
handler: (event) => {
|
|
200
|
+
const status = new URL(event.node.req.url!, "http://localhost").searchParams.get("status");
|
|
201
|
+
const projects = [
|
|
202
|
+
{ id: 1, name: "Project Alpha", status: "active" },
|
|
203
|
+
{ id: 2, name: "Project Beta", status: "completed" },
|
|
204
|
+
];
|
|
205
|
+
return status ? projects.filter((p) => p.status === status) : projects;
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const wrapper = await mountSuspended(ProjectsList, {
|
|
210
|
+
props: { filterStatus: "active" },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(wrapper.text()).toContain("Project Alpha");
|
|
214
|
+
expect(wrapper.text()).not.toContain("Project Beta");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Testing Pages with Route Parameters
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { describe, it, expect } from "vitest";
|
|
223
|
+
import { mountSuspended, registerEndpoint } from "@nuxt/test-utils/runtime";
|
|
224
|
+
import TaskDetailPage from "./[id].vue";
|
|
225
|
+
|
|
226
|
+
describe("Task Detail Page", () => {
|
|
227
|
+
it("loads and displays task details", async () => {
|
|
228
|
+
registerEndpoint("/api/tasks/456", {
|
|
229
|
+
method: "GET",
|
|
230
|
+
handler: () => ({
|
|
231
|
+
id: 456,
|
|
232
|
+
name: "Implement feature X",
|
|
233
|
+
status: "in_progress",
|
|
234
|
+
description: "Detailed description here",
|
|
235
|
+
assignee: { id: 1, name: "John Doe" },
|
|
236
|
+
}),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const wrapper = await mountSuspended(TaskDetailPage, {
|
|
240
|
+
route: {
|
|
241
|
+
params: { id: "456" },
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(wrapper.text()).toContain("Implement feature X");
|
|
246
|
+
expect(wrapper.text()).toContain("in_progress");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("handles missing task", async () => {
|
|
250
|
+
registerEndpoint("/api/tasks/999", {
|
|
251
|
+
method: "GET",
|
|
252
|
+
handler: () => {
|
|
253
|
+
throw createError({ statusCode: 404, message: "Task not found" });
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const wrapper = await mountSuspended(TaskDetailPage, {
|
|
258
|
+
route: {
|
|
259
|
+
params: { id: "999" },
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
expect(wrapper.text()).toContain("not found");
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Testing with UI Libraries (PrimeVue)
|
|
269
|
+
|
|
270
|
+
Stub complex components and register required services:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
import { describe, it, expect } from "vitest";
|
|
274
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
275
|
+
import ToastService from "primevue/toastservice";
|
|
276
|
+
import ConfirmationService from "primevue/confirmationservice";
|
|
277
|
+
import ProjectForm from "./ProjectForm.vue";
|
|
278
|
+
|
|
279
|
+
describe("ProjectForm", () => {
|
|
280
|
+
it("renders form fields", async () => {
|
|
281
|
+
const wrapper = await mountSuspended(ProjectForm, {
|
|
282
|
+
props: {
|
|
283
|
+
project: { id: 1, name: "", status: "draft" },
|
|
284
|
+
},
|
|
285
|
+
global: {
|
|
286
|
+
plugins: [ToastService, ConfirmationService],
|
|
287
|
+
stubs: {
|
|
288
|
+
// Stub complex PrimeVue components
|
|
289
|
+
DataTable: true,
|
|
290
|
+
Column: true,
|
|
291
|
+
Dialog: true,
|
|
292
|
+
Calendar: true,
|
|
293
|
+
Dropdown: true,
|
|
294
|
+
// Keep simple components
|
|
295
|
+
InputText: false,
|
|
296
|
+
Button: false,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
expect(wrapper.exists()).toBe(true);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Testing User Interactions
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import { describe, it, expect } from "vitest";
|
|
310
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
311
|
+
import { nextTick } from "vue";
|
|
312
|
+
import Counter from "./Counter.vue";
|
|
313
|
+
|
|
314
|
+
describe("Counter", () => {
|
|
315
|
+
it("increments count on button click", async () => {
|
|
316
|
+
const wrapper = await mountSuspended(Counter);
|
|
317
|
+
|
|
318
|
+
expect(wrapper.text()).toContain("Count: 0");
|
|
319
|
+
|
|
320
|
+
await wrapper.find("button.increment").trigger("click");
|
|
321
|
+
await nextTick();
|
|
322
|
+
|
|
323
|
+
expect(wrapper.text()).toContain("Count: 1");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("emits event on submit", async () => {
|
|
327
|
+
const wrapper = await mountSuspended(Counter);
|
|
328
|
+
|
|
329
|
+
await wrapper.find("button.submit").trigger("click");
|
|
330
|
+
|
|
331
|
+
expect(wrapper.emitted("submit")).toBeTruthy();
|
|
332
|
+
expect(wrapper.emitted("submit")![0]).toEqual([{ count: 0 }]);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Testing Utility Functions
|
|
338
|
+
|
|
339
|
+
Pure utilities don't need Nuxt context:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
343
|
+
import { formatDate, parseDate, isValidDate } from "./dates";
|
|
344
|
+
|
|
345
|
+
describe("date utilities", () => {
|
|
346
|
+
const originalTZ = process.env.TZ;
|
|
347
|
+
|
|
348
|
+
afterEach(() => {
|
|
349
|
+
process.env.TZ = originalTZ;
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe("formatDate", () => {
|
|
353
|
+
it("formats ISO date string", () => {
|
|
354
|
+
expect(formatDate("2025-01-15")).toBe("Jan 15, 2025");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("handles different timezones correctly", () => {
|
|
358
|
+
// Test that date-only strings don't shift across timezone boundaries
|
|
359
|
+
process.env.TZ = "America/New_York";
|
|
360
|
+
expect(formatDate("2025-01-15")).toBe("Jan 15, 2025");
|
|
361
|
+
|
|
362
|
+
process.env.TZ = "America/Los_Angeles";
|
|
363
|
+
expect(formatDate("2025-01-15")).toBe("Jan 15, 2025");
|
|
364
|
+
|
|
365
|
+
process.env.TZ = "Europe/London";
|
|
366
|
+
expect(formatDate("2025-01-15")).toBe("Jan 15, 2025");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("returns empty string for null/undefined", () => {
|
|
370
|
+
expect(formatDate(null)).toBe("");
|
|
371
|
+
expect(formatDate(undefined)).toBe("");
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe("isValidDate", () => {
|
|
376
|
+
it("validates correct dates", () => {
|
|
377
|
+
expect(isValidDate("2025-01-15")).toBe(true);
|
|
378
|
+
expect(isValidDate("2025-12-31")).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("rejects invalid dates", () => {
|
|
382
|
+
expect(isValidDate("not-a-date")).toBe(false);
|
|
383
|
+
expect(isValidDate("2025-13-01")).toBe(false);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Wrapper API Reference
|
|
390
|
+
|
|
391
|
+
Common methods on the wrapper returned by `mountSuspended`:
|
|
392
|
+
|
|
393
|
+
| Method | Description |
|
|
394
|
+
|--------|-------------|
|
|
395
|
+
| `wrapper.text()` | All text content (rendered) |
|
|
396
|
+
| `wrapper.html()` | Full HTML output |
|
|
397
|
+
| `wrapper.exists()` | Check if component mounted |
|
|
398
|
+
| `wrapper.props()` | Get all props |
|
|
399
|
+
| `wrapper.props('name')` | Get specific prop |
|
|
400
|
+
| `wrapper.emitted()` | All emitted events |
|
|
401
|
+
| `wrapper.emitted('click')` | Specific event emissions |
|
|
402
|
+
| `wrapper.find(selector)` | Find single element |
|
|
403
|
+
| `wrapper.findAll(selector)` | Find all matching elements |
|
|
404
|
+
| `wrapper.trigger('click')` | Trigger DOM event |
|
|
405
|
+
| `wrapper.setValue(value)` | Set input value |
|
|
406
|
+
|
|
407
|
+
## Common Gotchas
|
|
408
|
+
|
|
409
|
+
### 1. Always Use `mountSuspended`
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
// WRONG - doesn't handle async setup or Nuxt context
|
|
413
|
+
import { mount } from "@vue/test-utils";
|
|
414
|
+
const wrapper = mount(MyComponent);
|
|
415
|
+
|
|
416
|
+
// RIGHT - handles Suspense, async setup, and Nuxt context
|
|
417
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
418
|
+
const wrapper = await mountSuspended(MyComponent);
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### 2. Mock Before Mount
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
// WRONG - mock after mount has no effect
|
|
425
|
+
const wrapper = await mountSuspended(MyComponent);
|
|
426
|
+
mockNuxtImport("useAuth", () => () => ({ user: null }));
|
|
427
|
+
|
|
428
|
+
// RIGHT - mock before mount
|
|
429
|
+
mockNuxtImport("useAuth", () => () => ({ user: null }));
|
|
430
|
+
const wrapper = await mountSuspended(MyComponent);
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### 3. Register Endpoints Before Mount
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
// WRONG - component fetches before endpoint exists
|
|
437
|
+
const wrapper = await mountSuspended(DataComponent);
|
|
438
|
+
registerEndpoint("/api/data", { handler: () => [] });
|
|
439
|
+
|
|
440
|
+
// RIGHT - endpoint ready before component mounts
|
|
441
|
+
registerEndpoint("/api/data", { handler: () => [] });
|
|
442
|
+
const wrapper = await mountSuspended(DataComponent);
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### 4. Await All Async Operations
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
// WRONG - assertion runs before DOM updates
|
|
449
|
+
wrapper.find("button").trigger("click");
|
|
450
|
+
expect(wrapper.text()).toContain("Updated");
|
|
451
|
+
|
|
452
|
+
// RIGHT - wait for Vue to process updates
|
|
453
|
+
await wrapper.find("button").trigger("click");
|
|
454
|
+
await nextTick();
|
|
455
|
+
expect(wrapper.text()).toContain("Updated");
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### 5. Use `wrapper.text()` Over Specific Selectors
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
// FRAGILE - breaks if component structure changes
|
|
462
|
+
expect(wrapper.find(".project-name span").text()).toBe("My Project");
|
|
463
|
+
|
|
464
|
+
// ROBUST - checks rendered output regardless of structure
|
|
465
|
+
expect(wrapper.text()).toContain("My Project");
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### 6. Date Testing Across Timezones
|
|
469
|
+
|
|
470
|
+
When testing date formatting, test multiple timezones to catch off-by-one-day bugs:
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
const timezones = ["America/New_York", "America/Los_Angeles", "Europe/London", "Asia/Tokyo"];
|
|
474
|
+
|
|
475
|
+
for (const tz of timezones) {
|
|
476
|
+
it(`formats date correctly in ${tz}`, () => {
|
|
477
|
+
process.env.TZ = tz;
|
|
478
|
+
// Date string "2025-01-15" should always display as Jan 15
|
|
479
|
+
// regardless of timezone
|
|
480
|
+
expect(formatDate("2025-01-15")).toBe("Jan 15, 2025");
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
## File Organization
|
|
486
|
+
|
|
487
|
+
Co-locate tests with source files:
|
|
488
|
+
|
|
489
|
+
```
|
|
490
|
+
components/
|
|
491
|
+
projects/
|
|
492
|
+
ProjectCard.vue
|
|
493
|
+
ProjectCard.test.ts # Component test
|
|
494
|
+
ProjectsList.vue
|
|
495
|
+
ProjectsList.test.ts
|
|
496
|
+
pages/
|
|
497
|
+
projects/
|
|
498
|
+
index.vue
|
|
499
|
+
index.test.ts # Page test
|
|
500
|
+
[id].vue
|
|
501
|
+
[id].test.ts
|
|
502
|
+
utils/
|
|
503
|
+
dates.ts
|
|
504
|
+
dates.test.ts # Utility test
|
|
505
|
+
formatting.ts
|
|
506
|
+
formatting.test.ts
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Benefits:
|
|
510
|
+
- Easy to find tests for any file
|
|
511
|
+
- Tests move with components during refactoring
|
|
512
|
+
- Clear coverage visibility
|