claude-toolkit 0.1.12 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +3 -0
- package/core/skills/ct-testing-patterns/SKILL.md +37 -1
- package/docs/README.md +3 -0
- package/docs/best-practices/testing/README.md +84 -0
- package/docs/best-practices/testing/playwright-e2e.md +649 -0
- package/docs/best-practices/testing/storybook-interaction.md +445 -0
- package/docs/best-practices/testing/vitest-unit.md +451 -0
- package/docs/stacks/playwright-patterns.md +179 -0
- package/docs/stacks/storybook-patterns.md +160 -0
- package/docs/stacks/vite-vitest-patterns.md +485 -0
- package/package.json +1 -1
- package/stacks/playwright/skills/ct-playwright-patterns/SKILL.md +168 -0
- package/stacks/playwright/stack.json +39 -0
- package/stacks/solidjs/stack.json +7 -1
- package/stacks/storybook/skills/ct-storybook-patterns/SKILL.md +149 -0
- package/stacks/storybook/stack.json +46 -0
- package/stacks/vite/skills/ct-vite-vitest-patterns/SKILL.md +492 -0
- package/stacks/vite/stack.json +66 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# Vitest Unit Testing
|
|
2
|
+
|
|
3
|
+
> Sources: [Vitest 4.0 Announcement](https://voidzero.dev/posts/announcing-vitest-4), [Vitest 4.1 Blog](https://main.vitest.dev/blog/vitest-4-1), [Vitest Docs](https://vitest.dev/), [SolidJS Testing Docs](https://docs.solidjs.com/guides/testing), [@solidjs/testing-library](https://github.com/solidjs/solid-testing-library)
|
|
4
|
+
|
|
5
|
+
Unit tests form the base of the testing pyramid. They are fast, isolated, and cover pure logic, reactive primitives, and component behavior in a JSDOM environment.
|
|
6
|
+
|
|
7
|
+
## AAA Pattern
|
|
8
|
+
|
|
9
|
+
Every test follows **Arrange-Act-Assert**. Keep each phase visually distinct:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
it("should format currency correctly", () => {
|
|
13
|
+
// Arrange
|
|
14
|
+
const amount = 1234.5;
|
|
15
|
+
|
|
16
|
+
// Act
|
|
17
|
+
const result = formatCurrency(amount, "NZD");
|
|
18
|
+
|
|
19
|
+
// Assert
|
|
20
|
+
expect(result).toBe("$1,234.50");
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For simple tests, the phases can be implicit. For complex tests, add blank lines between them.
|
|
25
|
+
|
|
26
|
+
## Naming Conventions
|
|
27
|
+
|
|
28
|
+
- **Files**: `*.test.ts` / `*.test.tsx`, co-located with source or in `tests/`
|
|
29
|
+
- **Describe blocks**: Name the unit under test (`describe("formatCurrency", ...)`)
|
|
30
|
+
- **It blocks**: Describe behavior, not implementation ("should return loading state when resource is pending", not "should set isLoading to true")
|
|
31
|
+
|
|
32
|
+
## Testing SolidJS Components
|
|
33
|
+
|
|
34
|
+
### render() Takes a Callback
|
|
35
|
+
|
|
36
|
+
This is different from React Testing Library. SolidJS needs the callback form to establish a reactive root:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { render, screen } from "@solidjs/testing-library";
|
|
40
|
+
import { Button } from "../src/components/Button";
|
|
41
|
+
|
|
42
|
+
it("should render button text", () => {
|
|
43
|
+
render(() => <Button label="Click me" />);
|
|
44
|
+
expect(screen.getByRole("button")).toHaveTextContent("Click me");
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Testing Signals with renderHook
|
|
49
|
+
|
|
50
|
+
For hooks/composables that return reactive state:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { renderHook } from "@solidjs/testing-library";
|
|
54
|
+
import { useCounter } from "../src/hooks/useCounter";
|
|
55
|
+
|
|
56
|
+
it("should increment counter", () => {
|
|
57
|
+
const { result } = renderHook(useCounter);
|
|
58
|
+
expect(result.count).toBe(0);
|
|
59
|
+
result.increment();
|
|
60
|
+
expect(result.count).toBe(1);
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Testing Signals with createRoot
|
|
65
|
+
|
|
66
|
+
For primitives that don't need component context, `createRoot` is sufficient and faster:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { createRoot, createSignal, createMemo } from "solid-js";
|
|
70
|
+
|
|
71
|
+
it("should compute derived value", () => {
|
|
72
|
+
createRoot((dispose) => {
|
|
73
|
+
const [count, setCount] = createSignal(0);
|
|
74
|
+
const doubled = createMemo(() => count() * 2);
|
|
75
|
+
|
|
76
|
+
expect(doubled()).toBe(0);
|
|
77
|
+
setCount(5);
|
|
78
|
+
expect(doubled()).toBe(10);
|
|
79
|
+
|
|
80
|
+
dispose();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Testing Effects with testEffect
|
|
86
|
+
|
|
87
|
+
Effects are async in SolidJS. The `testEffect` utility provides a `done` callback for async assertion:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { testEffect } from "@solidjs/testing-library";
|
|
91
|
+
import { createSignal, createEffect } from "solid-js";
|
|
92
|
+
|
|
93
|
+
it("should react to signal changes", () => {
|
|
94
|
+
const [value, setValue] = createSignal(0);
|
|
95
|
+
|
|
96
|
+
return testEffect((done) =>
|
|
97
|
+
createEffect((run: number = 0) => {
|
|
98
|
+
if (run === 0) {
|
|
99
|
+
expect(value()).toBe(0);
|
|
100
|
+
setValue(1);
|
|
101
|
+
} else if (run === 1) {
|
|
102
|
+
expect(value()).toBe(1);
|
|
103
|
+
done();
|
|
104
|
+
}
|
|
105
|
+
return run + 1;
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Testing Resources (createResource)
|
|
112
|
+
|
|
113
|
+
Resources trigger Suspense. Wrap in `<Suspense>` and assert loading/data/error states:
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import { render, screen } from "@solidjs/testing-library";
|
|
117
|
+
import { Suspense } from "solid-js";
|
|
118
|
+
|
|
119
|
+
it("should show loading then data", async () => {
|
|
120
|
+
render(() => (
|
|
121
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
122
|
+
<MyResourceComponent />
|
|
123
|
+
</Suspense>
|
|
124
|
+
));
|
|
125
|
+
|
|
126
|
+
expect(screen.getByText("Loading...")).toBeInTheDocument();
|
|
127
|
+
expect(await screen.findByText("Data loaded")).toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Mocking Strategies
|
|
132
|
+
|
|
133
|
+
### Decision Framework
|
|
134
|
+
|
|
135
|
+
| Scenario | Tool |
|
|
136
|
+
|---|---|
|
|
137
|
+
| Replace entire module | `vi.mock("module")` |
|
|
138
|
+
| Spy on a specific method | `vi.spyOn(obj, "method")` |
|
|
139
|
+
| Network requests | MSW (`msw/node`) |
|
|
140
|
+
| Vary mock per test | `vi.hoisted` + `mockReturnValue` |
|
|
141
|
+
| Non-hoisted dynamic mock | `vi.doMock` |
|
|
142
|
+
|
|
143
|
+
### vi.mock with vi.hoisted (Recommended Pattern)
|
|
144
|
+
|
|
145
|
+
`vi.hoisted` lets you declare mock references that are accessible inside `vi.mock` (which is hoisted to the top of the file):
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const { mockFetch } = vi.hoisted(() => ({
|
|
149
|
+
mockFetch: vi.fn(),
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
vi.mock("../src/api/client", () => ({
|
|
153
|
+
fetchData: mockFetch,
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
it("should handle API error", () => {
|
|
157
|
+
mockFetch.mockRejectedValue(new Error("Network error"));
|
|
158
|
+
// ... test
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### vi.spyOn for Partial Mocking
|
|
163
|
+
|
|
164
|
+
When you only need to intercept one method while keeping the rest real:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import * as utils from "../src/utils";
|
|
168
|
+
|
|
169
|
+
it("should call logger", () => {
|
|
170
|
+
const spy = vi.spyOn(utils, "log");
|
|
171
|
+
doSomething();
|
|
172
|
+
expect(spy).toHaveBeenCalledWith("action completed");
|
|
173
|
+
spy.mockRestore();
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### MSW for Network Mocking
|
|
178
|
+
|
|
179
|
+
Mock Service Worker intercepts at the network level, giving realistic integration tests:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { setupServer } from "msw/node";
|
|
183
|
+
import { http, HttpResponse } from "msw";
|
|
184
|
+
|
|
185
|
+
const server = setupServer(
|
|
186
|
+
http.get("/api/user", () => HttpResponse.json({ name: "Alice" }))
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
beforeAll(() => server.listen());
|
|
190
|
+
afterEach(() => server.resetHandlers());
|
|
191
|
+
afterAll(() => server.close());
|
|
192
|
+
|
|
193
|
+
it("should display user name", async () => {
|
|
194
|
+
render(() => <UserProfile />);
|
|
195
|
+
expect(await screen.findByText("Alice")).toBeInTheDocument();
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### mockThrow / mockThrowOnce (Vitest 4.1+)
|
|
200
|
+
|
|
201
|
+
Concise error path testing without wrapping in functions:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
mockFn.mockThrow(new Error("failed"));
|
|
205
|
+
mockFn.mockThrowOnce(new Error("first call fails"));
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Type-Safe Mocks
|
|
209
|
+
|
|
210
|
+
Use `vi.mocked()` for typed access to mocked functions:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { fetchUser } from "../src/api";
|
|
214
|
+
vi.mock("../src/api");
|
|
215
|
+
|
|
216
|
+
const mockedFetchUser = vi.mocked(fetchUser);
|
|
217
|
+
mockedFetchUser.mockResolvedValue({ name: "Alice" });
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Snapshot Testing
|
|
221
|
+
|
|
222
|
+
### When to Use
|
|
223
|
+
|
|
224
|
+
- Serialized objects, API response shapes, configuration objects
|
|
225
|
+
- Small component output (buttons, cards, form fields)
|
|
226
|
+
|
|
227
|
+
### When NOT to Use
|
|
228
|
+
|
|
229
|
+
- Large pages or complex component trees
|
|
230
|
+
- Frequently changing UI
|
|
231
|
+
- Dynamic content (timestamps, IDs, random values)
|
|
232
|
+
|
|
233
|
+
### Prefer Inline Snapshots
|
|
234
|
+
|
|
235
|
+
Keeps expected output visible in the test file:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
expect(formatUser(user)).toMatchInlineSnapshot(`
|
|
239
|
+
{
|
|
240
|
+
"name": "Alice",
|
|
241
|
+
"role": "admin",
|
|
242
|
+
}
|
|
243
|
+
`);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Handle Dynamic Values
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
expect(result).toMatchObject({
|
|
250
|
+
id: expect.any(String),
|
|
251
|
+
createdAt: expect.any(Date),
|
|
252
|
+
name: "Alice",
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Snapshot Hygiene
|
|
257
|
+
|
|
258
|
+
- Review snapshot diffs as carefully as code diffs in PRs
|
|
259
|
+
- Run `vitest --update` only intentionally, never blindly
|
|
260
|
+
- CI should fail on obsolete snapshots
|
|
261
|
+
|
|
262
|
+
## Type Testing
|
|
263
|
+
|
|
264
|
+
Vitest has built-in type testing (since 2.1). Verify your types work correctly alongside runtime:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import { expectTypeOf } from "vitest";
|
|
268
|
+
|
|
269
|
+
it("should return correct type", () => {
|
|
270
|
+
const result = parseConfig(rawConfig);
|
|
271
|
+
expectTypeOf(result).toEqualTypeOf<AppConfig>();
|
|
272
|
+
expectTypeOf(result.port).toBeNumber();
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Coverage
|
|
277
|
+
|
|
278
|
+
### Use V8 Provider
|
|
279
|
+
|
|
280
|
+
Since Vitest 3.2, V8 coverage uses AST-based remapping that matches Istanbul accuracy with ~10% overhead vs Istanbul's ~300%. Always use V8:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// vite.config.ts
|
|
284
|
+
test: {
|
|
285
|
+
coverage: {
|
|
286
|
+
provider: "v8",
|
|
287
|
+
include: ["src/**/*.{ts,tsx}"],
|
|
288
|
+
exclude: [
|
|
289
|
+
"src/**/*.css.ts", // vanilla-extract style files
|
|
290
|
+
"src/**/index.ts", // barrel exports
|
|
291
|
+
"src/**/*.d.ts", // type declarations
|
|
292
|
+
"src/locales/**", // i18n generated files
|
|
293
|
+
],
|
|
294
|
+
reporter: ["text", "html", "lcov"],
|
|
295
|
+
thresholds: {
|
|
296
|
+
statements: 80,
|
|
297
|
+
branches: 70,
|
|
298
|
+
functions: 80,
|
|
299
|
+
lines: 80,
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Coverage Recommendations
|
|
306
|
+
|
|
307
|
+
- Start with moderate thresholds (70-80%) and ratchet up as coverage grows
|
|
308
|
+
- Focus on **branch coverage** -- it catches more real bugs than line coverage
|
|
309
|
+
- Use per-glob overrides for critical paths (e.g., `src/utils/**` at 95%)
|
|
310
|
+
- Use `/* v8 ignore start */` / `/* v8 ignore stop */` for intentionally uncovered code
|
|
311
|
+
|
|
312
|
+
## Vitest 4.x Features
|
|
313
|
+
|
|
314
|
+
### Test Tags
|
|
315
|
+
|
|
316
|
+
Define tags with per-tag configuration. Useful for separating fast/slow tests:
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
// vite.config.ts
|
|
320
|
+
test: {
|
|
321
|
+
tags: {
|
|
322
|
+
slow: { timeout: 30_000 },
|
|
323
|
+
flaky: { retry: 3 },
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
it.tags(["slow"])("should process large dataset", async () => {
|
|
330
|
+
// ...
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Builder-Pattern Fixtures
|
|
335
|
+
|
|
336
|
+
The idiomatic way to share setup in Vitest 4.x. TypeScript infers fixture types automatically:
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
const myTest = test
|
|
340
|
+
.extend({ config: { timeout: 5000 } })
|
|
341
|
+
.extend({
|
|
342
|
+
server: async ({ config }, { onCleanup }) => {
|
|
343
|
+
const srv = await startServer(config);
|
|
344
|
+
onCleanup(() => srv.close());
|
|
345
|
+
return srv;
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
myTest("should connect to server", ({ server }) => {
|
|
350
|
+
expect(server.isRunning()).toBe(true);
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Detect Async Leaks
|
|
355
|
+
|
|
356
|
+
Reports leaked async resources with source locations. Enable while debugging:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
test: {
|
|
360
|
+
detectAsyncLeaks: true, // Disable in CI (adds overhead)
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Performance
|
|
365
|
+
|
|
366
|
+
### Pool Configuration
|
|
367
|
+
|
|
368
|
+
| Pool | Best For |
|
|
369
|
+
|---|---|
|
|
370
|
+
| `threads` (default) | General use, SolidJS + JSDOM tests |
|
|
371
|
+
| `forks` | Better isolation, more overhead |
|
|
372
|
+
| `vmThreads` | VM contexts with worker parallelism |
|
|
373
|
+
|
|
374
|
+
### Sharding for CI
|
|
375
|
+
|
|
376
|
+
Split tests across CI machines:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
# Machine 1
|
|
380
|
+
vitest run --shard=1/3 --reporter=blob
|
|
381
|
+
|
|
382
|
+
# Machine 2
|
|
383
|
+
vitest run --shard=2/3 --reporter=blob
|
|
384
|
+
|
|
385
|
+
# Machine 3
|
|
386
|
+
vitest run --shard=3/3 --reporter=blob
|
|
387
|
+
|
|
388
|
+
# Merge
|
|
389
|
+
vitest merge-reports
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Other Tips
|
|
393
|
+
|
|
394
|
+
- Use `test.concurrent` for independent async tests
|
|
395
|
+
- Keep test files small and focused (Vitest parallelizes at the file level)
|
|
396
|
+
- Avoid heavy setup in `beforeAll` that could be lazy-loaded
|
|
397
|
+
- Use `--no-isolate` only for pure function test files, never for component tests
|
|
398
|
+
|
|
399
|
+
## Recommended Config
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
// vite.config.ts (test section)
|
|
403
|
+
test: {
|
|
404
|
+
environment: "jsdom",
|
|
405
|
+
globals: true,
|
|
406
|
+
setupFiles: ["./tests/setup.ts"],
|
|
407
|
+
include: ["src/**/*.test.{ts,tsx}", "tests/**/*.test.{ts,tsx}"],
|
|
408
|
+
restoreMocks: true,
|
|
409
|
+
coverage: {
|
|
410
|
+
provider: "v8",
|
|
411
|
+
include: ["src/**/*.{ts,tsx}"],
|
|
412
|
+
exclude: [
|
|
413
|
+
"src/**/*.css.ts",
|
|
414
|
+
"src/**/index.ts",
|
|
415
|
+
"src/**/*.d.ts",
|
|
416
|
+
"src/locales/**",
|
|
417
|
+
],
|
|
418
|
+
reporter: ["text", "html", "lcov"],
|
|
419
|
+
thresholds: {
|
|
420
|
+
statements: 80,
|
|
421
|
+
branches: 70,
|
|
422
|
+
functions: 80,
|
|
423
|
+
lines: 80,
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
// tests/setup.ts
|
|
431
|
+
import "@testing-library/jest-dom/vitest";
|
|
432
|
+
|
|
433
|
+
afterEach(() => {
|
|
434
|
+
vi.clearAllTimers();
|
|
435
|
+
});
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## Anti-Patterns
|
|
439
|
+
|
|
440
|
+
| Anti-Pattern | Fix |
|
|
441
|
+
|---|---|
|
|
442
|
+
| Testing implementation details (internal state, private methods) | Test behavior and public API only |
|
|
443
|
+
| Large snapshots (100+ lines) | Break into small focused snapshots or use explicit assertions |
|
|
444
|
+
| Blind `--update` on snapshot failures | Review each diff individually |
|
|
445
|
+
| `vi.mock` without cleanup | Enable `restoreMocks: true` in config |
|
|
446
|
+
| Shared mutable state across tests | Reset state in `beforeEach` or use fixtures |
|
|
447
|
+
| Testing framework code (that SolidJS reactivity works) | Test your logic, not the framework |
|
|
448
|
+
| `setTimeout` in tests for async waits | Use `waitFor`, `findBy*`, or `vi.advanceTimersByTime` |
|
|
449
|
+
| Over-mocking (mocking everything) | Mock only external boundaries (network, timers, platform APIs) |
|
|
450
|
+
| Destructuring SolidJS props in tests | Same rule as production -- never destructure props |
|
|
451
|
+
| Using `as any` in test code | If you need it, the API under test is wrong |
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Playwright E2E Patterns
|
|
2
|
+
|
|
3
|
+
> Playwright E2E testing with Page Objects, fixtures, auth, network mocking, and CI/CD.
|
|
4
|
+
|
|
5
|
+
**Type:** Stack Skill (requires `playwright` stack)
|
|
6
|
+
**Source:** [`stacks/playwright/skills/ct-playwright-patterns/SKILL.md`](../../stacks/playwright/skills/ct-playwright-patterns/SKILL.md)
|
|
7
|
+
**Directory Mappings:** `e2e/`, `playwright.config.ts`
|
|
8
|
+
**File Extensions:** `.spec.ts`
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
E2E tests sit at the top of the testing pyramid. They exercise full user journeys in real browsers. Write fewer, make each one count.
|
|
13
|
+
|
|
14
|
+
## Project Structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
e2e/
|
|
18
|
+
fixtures/ # Custom Playwright fixtures
|
|
19
|
+
index.ts # Extended test with page objects
|
|
20
|
+
pages/ # Page Object classes
|
|
21
|
+
components/ # Component Object classes (reusable fragments)
|
|
22
|
+
specs/ # Test files organized by feature
|
|
23
|
+
helpers/ # Utilities (data factories, API helpers)
|
|
24
|
+
.auth/ # storageState files (gitignored)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Page Object Model + Fixtures
|
|
28
|
+
|
|
29
|
+
Combine Page Objects with custom fixtures. Tests receive pre-built page objects:
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// e2e/pages/LoginPage.ts
|
|
33
|
+
export class LoginPage {
|
|
34
|
+
private readonly emailInput: Locator;
|
|
35
|
+
private readonly submitButton: Locator;
|
|
36
|
+
|
|
37
|
+
constructor(private readonly page: Page) {
|
|
38
|
+
this.emailInput = page.getByLabel("Email");
|
|
39
|
+
this.submitButton = page.getByRole("button", { name: "Sign in" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async goto() { await this.page.goto("/login"); }
|
|
43
|
+
async login(email: string, password: string) {
|
|
44
|
+
await this.emailInput.fill(email);
|
|
45
|
+
await this.submitButton.click();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// e2e/fixtures/index.ts
|
|
50
|
+
export const test = base.extend<Fixtures>({
|
|
51
|
+
loginPage: async ({ page }, use) => { await use(new LoginPage(page)); },
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Fixtures are lazy (created on demand) and composable (can depend on each other).
|
|
56
|
+
|
|
57
|
+
## Authentication
|
|
58
|
+
|
|
59
|
+
Use project dependencies with `storageState` to authenticate once and reuse:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// e2e/auth.setup.ts
|
|
63
|
+
setup("authenticate", async ({ page }) => {
|
|
64
|
+
await page.goto("/login");
|
|
65
|
+
await page.getByLabel("Email").fill("test@example.com");
|
|
66
|
+
await page.getByRole("button", { name: "Sign in" }).click();
|
|
67
|
+
await page.waitForURL("/dashboard");
|
|
68
|
+
await page.context().storageState({ path: "e2e/.auth/user.json" });
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Config: `dependencies: ["setup"]` + `storageState: "e2e/.auth/user.json"`. Add `e2e/.auth/` to `.gitignore`.
|
|
73
|
+
|
|
74
|
+
For multi-role testing, create separate setup files per role.
|
|
75
|
+
|
|
76
|
+
## Locator Priority
|
|
77
|
+
|
|
78
|
+
1. `getByRole()` -- accessibility semantics, survives refactors
|
|
79
|
+
2. `getByLabel()` -- form elements
|
|
80
|
+
3. `getByPlaceholder()` -- input hints
|
|
81
|
+
4. `getByText()` -- visible text
|
|
82
|
+
5. `getByTestId()` -- stable but less semantic
|
|
83
|
+
6. CSS/XPath -- **avoid**
|
|
84
|
+
|
|
85
|
+
## Web-First Assertions
|
|
86
|
+
|
|
87
|
+
Always use auto-retrying assertions:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// GOOD
|
|
91
|
+
await expect(page.getByRole("alert")).toBeVisible();
|
|
92
|
+
await expect(page).toHaveURL("/dashboard");
|
|
93
|
+
|
|
94
|
+
// BAD -- checks once, no retry
|
|
95
|
+
expect(await page.getByRole("alert").isVisible()).toBeTruthy();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Key assertions: `toBeVisible()`, `toBeEnabled()`, `toHaveText()`, `toHaveURL()`, `toHaveCount()`, `toHaveScreenshot()`, `toMatchAriaSnapshot()`.
|
|
99
|
+
|
|
100
|
+
## Network Mocking
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// Mock API
|
|
104
|
+
await page.route("**/api/users", async (route) => {
|
|
105
|
+
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([{ name: "Alice" }]) });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Block analytics
|
|
109
|
+
await page.route("**/analytics/**", (route) => route.abort());
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Mock ~80% of API calls for speed; keep ~20% hitting real endpoints. Always call `route.continue()`, `route.fulfill()`, or `route.abort()`. Use `page.unrouteAll()` in cleanup.
|
|
113
|
+
|
|
114
|
+
## WebSocket Mocking (v1.53+)
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
await page.routeWebSocket("wss://example.com/ws", (ws) => {
|
|
118
|
+
ws.onMessage((message) => { ws.send("mocked response"); });
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Accessibility Testing
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import AxeBuilder from "@axe-core/playwright";
|
|
126
|
+
|
|
127
|
+
test("WCAG 2.1 AA", async ({ page }) => {
|
|
128
|
+
await page.goto("/");
|
|
129
|
+
const results = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21aa"]).analyze();
|
|
130
|
+
expect(results.violations).toEqual([]);
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Aria snapshots (v1.52+): `await expect(nav).toMatchAriaSnapshot(...)`.
|
|
135
|
+
|
|
136
|
+
## Parallelization
|
|
137
|
+
|
|
138
|
+
- File-level parallel (default) -- start here
|
|
139
|
+
- In-file: `test.describe.configure({ mode: "parallel" })`
|
|
140
|
+
- Full: `fullyParallel: true` in config
|
|
141
|
+
|
|
142
|
+
Each worker gets its own `BrowserContext` (isolated cookies/storage). Use `mode: "serial"` sparingly.
|
|
143
|
+
|
|
144
|
+
## CI/CD
|
|
145
|
+
|
|
146
|
+
- Use official Playwright Docker image (`mcr.microsoft.com/playwright:v1.58.0-noble`)
|
|
147
|
+
- Upload reports + traces as artifacts
|
|
148
|
+
- Use `--shard` for suites exceeding 5 minutes
|
|
149
|
+
- `retries: process.env.CI ? 2 : 0`
|
|
150
|
+
- Traces: `trace: "on-first-retry"`
|
|
151
|
+
|
|
152
|
+
## Flaky Test Prevention
|
|
153
|
+
|
|
154
|
+
| Cause | Fix |
|
|
155
|
+
| ---------------------- | ------------------------------------------------ |
|
|
156
|
+
| `waitForTimeout()` | Web-first assertions or `waitForResponse` |
|
|
157
|
+
| Brittle selectors | `getByRole`, `getByLabel`, `getByTestId` |
|
|
158
|
+
| Shared state | Fresh `BrowserContext` per test |
|
|
159
|
+
| External API flakiness | Mock with `page.route()` |
|
|
160
|
+
| Animation timing | `--force-prefers-reduced-motion` |
|
|
161
|
+
| Missing `await` | #1 source of false-passing tests. Lint for it. |
|
|
162
|
+
|
|
163
|
+
## Anti-Patterns
|
|
164
|
+
|
|
165
|
+
1. **Using `waitForTimeout()`** -- Use web-first assertions or explicit wait conditions.
|
|
166
|
+
2. **Fragile CSS/XPath selectors** -- Use role-based and semantic locators.
|
|
167
|
+
3. **Using `{ force: true }`** -- Masks real actionability problems.
|
|
168
|
+
4. **Missing `await` on assertions** -- #1 source of false-passing tests.
|
|
169
|
+
5. **Tests without assertions** -- A test that clicks without asserting is just a script.
|
|
170
|
+
6. **Using Playwright for unit tests** -- Use Vitest for pure logic.
|
|
171
|
+
7. **Committing raw codegen output** -- Always refactor into Page Objects.
|
|
172
|
+
8. **Not cleaning up route handlers** -- Use `page.unrouteAll()`.
|
|
173
|
+
|
|
174
|
+
## See Also
|
|
175
|
+
|
|
176
|
+
- [ct-testing-patterns](../skills/testing-patterns.md) -- Framework-agnostic TDD practices
|
|
177
|
+
- [Playwright E2E Best Practices](../best-practices/testing/playwright-e2e.md) -- SolidJS-specific E2E patterns
|
|
178
|
+
- [ct-vite-vitest-patterns](vite-vitest-patterns.md) -- Unit testing layer
|
|
179
|
+
- [ct-storybook-patterns](storybook-patterns.md) -- Interaction testing layer
|