claude-toolkit 0.1.12 → 0.1.20

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.
@@ -0,0 +1,649 @@
1
+ # Playwright E2E Testing
2
+
3
+ > Sources: [Playwright Release Notes](https://playwright.dev/docs/release-notes), [Playwright Best Practices](https://playwright.dev/docs/best-practices), [Playwright Docs](https://playwright.dev/docs/intro), [TestDino Guides](https://testdino.com/blog/playwright-best-practices/)
4
+
5
+ E2E tests sit at the top of the testing pyramid. They exercise full user journeys in real browsers -- navigation, authentication, API integration, and cross-browser rendering. Write fewer of them, but make each one count.
6
+
7
+ ## Project Structure
8
+
9
+ ```
10
+ e2e/
11
+ fixtures/ # Custom Playwright fixtures
12
+ index.ts # Extended test with page objects
13
+ pages/ # Page Object classes
14
+ LoginPage.ts
15
+ DashboardPage.ts
16
+ components/ # Component Object classes (reusable fragments)
17
+ Header.ts
18
+ Modal.ts
19
+ specs/ # Test files organized by feature
20
+ auth.spec.ts
21
+ dashboard.spec.ts
22
+ helpers/ # Utilities (data factories, API helpers)
23
+ .auth/ # storageState files (gitignored)
24
+ ```
25
+
26
+ ## Page Object Model + Fixtures
27
+
28
+ Combine Page Objects with Playwright's custom fixtures. Tests receive pre-built page objects without manual instantiation:
29
+
30
+ ### Page Object
31
+
32
+ ```typescript
33
+ // e2e/pages/LoginPage.ts
34
+ import type { Page, Locator } from "@playwright/test";
35
+ import { expect } from "@playwright/test";
36
+
37
+ export class LoginPage {
38
+ private readonly emailInput: Locator;
39
+ private readonly passwordInput: Locator;
40
+ private readonly submitButton: Locator;
41
+
42
+ constructor(private readonly page: Page) {
43
+ this.emailInput = page.getByLabel("Email");
44
+ this.passwordInput = page.getByLabel("Password");
45
+ this.submitButton = page.getByRole("button", { name: "Sign in" });
46
+ }
47
+
48
+ async goto() {
49
+ await this.page.goto("/login");
50
+ }
51
+
52
+ async login(email: string, password: string) {
53
+ await this.emailInput.fill(email);
54
+ await this.passwordInput.fill(password);
55
+ await this.submitButton.click();
56
+ }
57
+
58
+ async expectVisible() {
59
+ await expect(this.emailInput).toBeVisible();
60
+ }
61
+ }
62
+ ```
63
+
64
+ ### Custom Fixtures
65
+
66
+ ```typescript
67
+ // e2e/fixtures/index.ts
68
+ import { test as base } from "@playwright/test";
69
+ import { LoginPage } from "../pages/LoginPage";
70
+ import { DashboardPage } from "../pages/DashboardPage";
71
+
72
+ interface Fixtures {
73
+ loginPage: LoginPage;
74
+ dashboardPage: DashboardPage;
75
+ }
76
+
77
+ export const test = base.extend<Fixtures>({
78
+ loginPage: async ({ page }, use) => {
79
+ await use(new LoginPage(page));
80
+ },
81
+ dashboardPage: async ({ page }, use) => {
82
+ await use(new DashboardPage(page));
83
+ },
84
+ });
85
+
86
+ export { expect } from "@playwright/test";
87
+ ```
88
+
89
+ ### Using in Tests
90
+
91
+ ```typescript
92
+ // e2e/specs/auth.spec.ts
93
+ import { test, expect } from "../fixtures";
94
+
95
+ test("should login successfully", async ({ loginPage, dashboardPage, page }) => {
96
+ await loginPage.goto();
97
+ await loginPage.login("user@example.com", "password");
98
+ await expect(page).toHaveURL("/dashboard");
99
+ await dashboardPage.expectWelcomeMessage("user@example.com");
100
+ });
101
+ ```
102
+
103
+ Fixtures are **lazy** (only created when requested) and **composable** (can depend on each other).
104
+
105
+ ## Authentication Handling
106
+
107
+ Use **project dependencies** with `storageState` to authenticate once and reuse across all tests:
108
+
109
+ ### Setup File
110
+
111
+ ```typescript
112
+ // e2e/auth.setup.ts
113
+ import { test as setup, expect } from "@playwright/test";
114
+
115
+ const authFile = "e2e/.auth/user.json";
116
+
117
+ setup("authenticate", async ({ page }) => {
118
+ await page.goto("/login");
119
+ await page.getByLabel("Email").fill("test@example.com");
120
+ await page.getByLabel("Password").fill("password");
121
+ await page.getByRole("button", { name: "Sign in" }).click();
122
+ await page.waitForURL("/dashboard");
123
+ await page.context().storageState({ path: authFile });
124
+ });
125
+ ```
126
+
127
+ ### Config
128
+
129
+ ```typescript
130
+ // playwright.config.ts (projects section)
131
+ projects: [
132
+ { name: "setup", testMatch: /.*\.setup\.ts/ },
133
+ {
134
+ name: "chromium",
135
+ use: {
136
+ ...devices["Desktop Chrome"],
137
+ storageState: "e2e/.auth/user.json",
138
+ },
139
+ dependencies: ["setup"],
140
+ },
141
+ ],
142
+ ```
143
+
144
+ ### Multi-Role Testing
145
+
146
+ Separate setup files for different roles:
147
+
148
+ ```typescript
149
+ // e2e/admin.setup.ts -> e2e/.auth/admin.json
150
+ // e2e/user.setup.ts -> e2e/.auth/user.json
151
+
152
+ projects: [
153
+ { name: "admin-setup", testMatch: /admin\.setup\.ts/ },
154
+ { name: "user-setup", testMatch: /user\.setup\.ts/ },
155
+ {
156
+ name: "admin-tests",
157
+ use: { storageState: "e2e/.auth/admin.json" },
158
+ dependencies: ["admin-setup"],
159
+ },
160
+ {
161
+ name: "user-tests",
162
+ use: { storageState: "e2e/.auth/user.json" },
163
+ dependencies: ["user-setup"],
164
+ },
165
+ ],
166
+ ```
167
+
168
+ Add `e2e/.auth/` to `.gitignore`. Session tokens expire -- the setup re-runs each CI pipeline.
169
+
170
+ ## Network Mocking
171
+
172
+ ### Route Interception
173
+
174
+ ```typescript
175
+ // Mock API response
176
+ await page.route("**/api/users", async (route) => {
177
+ await route.fulfill({
178
+ status: 200,
179
+ contentType: "application/json",
180
+ body: JSON.stringify([{ name: "Alice" }]),
181
+ });
182
+ });
183
+
184
+ // Modify a real response
185
+ await page.route("**/api/config", async (route) => {
186
+ const response = await route.fetch();
187
+ const body = await response.json();
188
+ body.featureFlag = true;
189
+ await route.fulfill({ response, body: JSON.stringify(body) });
190
+ });
191
+
192
+ // Block requests (analytics, tracking)
193
+ await page.route("**/analytics/**", (route) => route.abort());
194
+ ```
195
+
196
+ ### Protobuf Responses
197
+
198
+ Since this project uses Protobuf, fulfill with encoded buffers:
199
+
200
+ ```typescript
201
+ import { UserResponse } from "../src/generated/proto";
202
+
203
+ await page.route("**/api/user", async (route) => {
204
+ const encoded = UserResponse.encode({ name: "Alice" }).finish();
205
+ await route.fulfill({
206
+ status: 200,
207
+ contentType: "application/octet-stream",
208
+ body: Buffer.from(encoded),
209
+ });
210
+ });
211
+ ```
212
+
213
+ ### HAR Recording/Playback
214
+
215
+ For deterministic replay of complex API interactions:
216
+
217
+ ```typescript
218
+ // Record
219
+ await page.routeFromHAR("e2e/fixtures/api.har", { update: true });
220
+
221
+ // Playback
222
+ await page.routeFromHAR("e2e/fixtures/api.har");
223
+ ```
224
+
225
+ ### WebSocket Mocking (v1.53+)
226
+
227
+ ```typescript
228
+ await page.routeWebSocket("wss://example.com/ws", (ws) => {
229
+ ws.onMessage((message) => {
230
+ ws.send("mocked response");
231
+ });
232
+ });
233
+ ```
234
+
235
+ ### API Testing Without a Browser
236
+
237
+ ```typescript
238
+ test("API health check", async ({ request }) => {
239
+ const response = await request.get("/api/health");
240
+ expect(response.ok()).toBeTruthy();
241
+ });
242
+ ```
243
+
244
+ **Best practice:** Mock ~80% of API calls for speed; keep ~20% hitting real endpoints for integration confidence. Always call `route.continue()`, `route.fulfill()`, or `route.abort()` -- never leave a handler hanging. Use `page.unrouteAll()` in cleanup.
245
+
246
+ ## Assertions
247
+
248
+ ### Web-First Assertions (Always Prefer These)
249
+
250
+ Web-first assertions auto-retry until the condition is met or timeout:
251
+
252
+ ```typescript
253
+ // GOOD -- auto-retrying
254
+ await expect(page.getByRole("alert")).toBeVisible();
255
+ await expect(page.getByRole("heading")).toHaveText("Dashboard");
256
+ await expect(page).toHaveURL("/dashboard");
257
+ await expect(page).toHaveTitle(/Dashboard/);
258
+ await expect(page.getByTestId("count")).toHaveText("5");
259
+
260
+ // BAD -- checks once, no retry
261
+ expect(await page.getByRole("alert").isVisible()).toBeTruthy();
262
+ ```
263
+
264
+ ### Key Web-First Assertions
265
+
266
+ | Assertion | Purpose |
267
+ |---|---|
268
+ | `toBeVisible()` / `toBeHidden()` | Element visibility |
269
+ | `toBeEnabled()` / `toBeDisabled()` | Form element state |
270
+ | `toHaveText()` / `toContainText()` | Text content |
271
+ | `toHaveURL()` / `toHaveTitle()` | Page navigation |
272
+ | `toHaveCount()` | List length |
273
+ | `toHaveScreenshot()` | Visual comparison |
274
+ | `toMatchAriaSnapshot()` | Accessibility structure |
275
+
276
+ ### Soft Assertions
277
+
278
+ Continue test after failure, report all at end:
279
+
280
+ ```typescript
281
+ await expect.soft(page.getByTestId("status")).toHaveText("Active");
282
+ await expect.soft(page.getByTestId("count")).toHaveText("5");
283
+ ```
284
+
285
+ ### Custom Timeout
286
+
287
+ ```typescript
288
+ await expect(page.getByRole("alert")).toBeVisible({ timeout: 10_000 });
289
+ ```
290
+
291
+ ## Locator Priority
292
+
293
+ Use the most resilient locator available:
294
+
295
+ 1. **`getByRole()`** -- accessibility semantics, survives refactors
296
+ 2. **`getByLabel()`** -- form elements by associated label
297
+ 3. **`getByPlaceholder()`** -- input hints
298
+ 4. **`getByText()`** -- visible text content
299
+ 5. **`getByTestId()`** -- explicit contract, stable but less semantic
300
+ 6. **CSS/XPath** -- **avoid** unless no alternative
301
+
302
+ ## Test Isolation and Parallelization
303
+
304
+ ### Parallelism Levels
305
+
306
+ | Level | How | When |
307
+ |---|---|---|
308
+ | File-level (default) | Files run in parallel, tests within a file run sequentially | Start here |
309
+ | In-file | `test.describe.configure({ mode: "parallel" })` | Independent tests within a describe |
310
+ | Full | `fullyParallel: true` in config | All tests properly isolated |
311
+
312
+ Each worker gets its own browser instance and `BrowserContext` -- cookies, localStorage, and sessionStorage are isolated automatically.
313
+
314
+ ### Worker Tuning
315
+
316
+ ```typescript
317
+ workers: process.env.CI ? 4 : undefined, // undefined = half CPU cores locally
318
+ fullyParallel: true,
319
+ retries: process.env.CI ? 2 : 0,
320
+ ```
321
+
322
+ Use `test.describe.configure({ mode: "serial" })` sparingly -- only for flows where one test depends on state from another.
323
+
324
+ ## Visual Comparison Testing
325
+
326
+ Built-in, no plugins needed:
327
+
328
+ ```typescript
329
+ // Full page
330
+ await expect(page).toHaveScreenshot("homepage.png");
331
+
332
+ // Element-level
333
+ await expect(page.getByTestId("sidebar")).toHaveScreenshot("sidebar.png");
334
+
335
+ // Mask dynamic content
336
+ await expect(page).toHaveScreenshot("dashboard.png", {
337
+ mask: [page.getByTestId("timestamp"), page.getByTestId("avatar")],
338
+ });
339
+ ```
340
+
341
+ ### Best Practices
342
+
343
+ - Run visual tests in CI with a fixed Docker image (avoid font rendering differences)
344
+ - Use `--update-snapshots` only when UI changes are intentional
345
+ - Commit baseline images and review in PRs like code
346
+ - On failure, Playwright generates baseline, actual, and diff images
347
+
348
+ ## Accessibility Testing
349
+
350
+ Integrate `@axe-core/playwright` for automated WCAG checks:
351
+
352
+ ```typescript
353
+ import AxeBuilder from "@axe-core/playwright";
354
+
355
+ test("homepage meets WCAG 2.1 AA", async ({ page }) => {
356
+ await page.goto("/");
357
+
358
+ const results = await new AxeBuilder({ page })
359
+ .withTags(["wcag2a", "wcag2aa", "wcag21aa"])
360
+ .analyze();
361
+
362
+ expect(results.violations).toEqual([]);
363
+ });
364
+ ```
365
+
366
+ ### Aria Snapshots (v1.52+)
367
+
368
+ Assert accessibility tree structure:
369
+
370
+ ```typescript
371
+ await expect(page.getByRole("navigation")).toMatchAriaSnapshot(`
372
+ - navigation:
373
+ - link "Home" /url: "/"
374
+ - link "About" /url: "/about"
375
+ `);
376
+ ```
377
+
378
+ ## Mobile and Responsive Testing
379
+
380
+ Use built-in device descriptors:
381
+
382
+ ```typescript
383
+ projects: [
384
+ { name: "Desktop Chrome", use: { ...devices["Desktop Chrome"] } },
385
+ { name: "Mobile Chrome", use: { ...devices["Pixel 7"] } },
386
+ { name: "Mobile Safari", use: { ...devices["iPhone 14"] } },
387
+ { name: "Tablet", use: { ...devices["iPad Pro 11"] } },
388
+ ],
389
+ ```
390
+
391
+ Custom viewport for responsive breakpoints:
392
+
393
+ ```typescript
394
+ {
395
+ name: "Mobile",
396
+ use: {
397
+ viewport: { width: 375, height: 812 },
398
+ hasTouch: true,
399
+ isMobile: true,
400
+ },
401
+ }
402
+ ```
403
+
404
+ **Limitation:** Playwright emulates devices inside desktop browser engines. Sufficient for responsive layout testing but cannot catch real mobile browser rendering bugs.
405
+
406
+ ## i18n Testing
407
+
408
+ Run tests against both locales using Playwright projects:
409
+
410
+ ```typescript
411
+ projects: [
412
+ {
413
+ name: "en",
414
+ use: { ...devices["Desktop Chrome"], locale: "en-NZ" },
415
+ },
416
+ {
417
+ name: "fr",
418
+ use: { ...devices["Desktop Chrome"], locale: "fr-FR" },
419
+ },
420
+ ],
421
+ ```
422
+
423
+ Verify typesafe-i18n strings render correctly in both locales.
424
+
425
+ ## Trace Viewer and Debugging
426
+
427
+ ### Trace Config
428
+
429
+ ```typescript
430
+ use: {
431
+ trace: "on-first-retry", // Capture traces only on failure retries
432
+ },
433
+ ```
434
+
435
+ ### Viewing Traces
436
+
437
+ ```bash
438
+ # Local CLI
439
+ bunx playwright show-trace test-results/test-name/trace.zip
440
+
441
+ # Browser-based (upload to trace.playwright.dev)
442
+ ```
443
+
444
+ Traces contain: DOM snapshots at every step, film strip, network requests, console logs, action timings, source code mapping.
445
+
446
+ ### Timeline View (v1.58)
447
+
448
+ The Speedboard tab in HTML reports shows where time is spent across tests -- identifies slow steps and bottlenecks.
449
+
450
+ ### Local Debugging
451
+
452
+ - `--ui` flag for interactive UI Mode with time-travel debugging
453
+ - `--debug` flag for step-through with Playwright Inspector
454
+ - VS Code extension for running/debugging individual tests
455
+
456
+ ## CI/CD Integration (GitHub Actions)
457
+
458
+ ```yaml
459
+ # .github/workflows/e2e.yml
460
+ name: E2E Tests
461
+ on:
462
+ push:
463
+ branches: [main]
464
+ pull_request:
465
+ branches: [main]
466
+
467
+ jobs:
468
+ e2e:
469
+ timeout-minutes: 30
470
+ runs-on: ubuntu-latest
471
+ container:
472
+ image: mcr.microsoft.com/playwright:v1.58.0-noble
473
+ steps:
474
+ - uses: actions/checkout@v4
475
+
476
+ - uses: oven-sh/setup-bun@v2
477
+ with:
478
+ bun-version: latest
479
+
480
+ - run: bun install --frozen-lockfile
481
+ - run: bun run build
482
+
483
+ - run: bunx playwright test
484
+ env:
485
+ BASE_URL: http://localhost:4173
486
+
487
+ - uses: actions/upload-artifact@v4
488
+ if: ${{ !cancelled() }}
489
+ with:
490
+ name: playwright-report
491
+ path: playwright-report/
492
+ retention-days: 14
493
+
494
+ - uses: actions/upload-artifact@v4
495
+ if: failure()
496
+ with:
497
+ name: test-traces
498
+ path: test-results/
499
+ retention-days: 7
500
+ ```
501
+
502
+ ### Sharding for Large Suites
503
+
504
+ ```yaml
505
+ strategy:
506
+ fail-fast: false
507
+ matrix:
508
+ shard: [1/4, 2/4, 3/4, 4/4]
509
+ steps:
510
+ - run: bunx playwright test --shard=${{ matrix.shard }}
511
+ ```
512
+
513
+ Then merge reports with `bunx playwright merge-reports`.
514
+
515
+ ### Best Practices
516
+
517
+ - Always use the official Playwright Docker image (has all required system libraries)
518
+ - Upload artifacts (reports + traces) unconditionally or on failure
519
+ - Run smoke tests on every commit; full suite as pre-merge gate
520
+ - Use `--shard` for suites exceeding 5 minutes
521
+
522
+ ## Performance Measurement
523
+
524
+ Playwright is not a load testing tool, but it can measure client-side performance:
525
+
526
+ ```typescript
527
+ test("page load performance", async ({ page }) => {
528
+ await page.goto("/");
529
+
530
+ const perfTiming = await page.evaluate(() => {
531
+ const nav = performance.getEntriesByType("navigation")[0] as PerformanceNavigationTiming;
532
+ return {
533
+ ttfb: nav.responseStart - nav.requestStart,
534
+ domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
535
+ loadComplete: nav.loadEventEnd - nav.startTime,
536
+ };
537
+ });
538
+
539
+ expect(perfTiming.ttfb).toBeLessThan(500);
540
+ expect(perfTiming.domContentLoaded).toBeLessThan(2000);
541
+ });
542
+ ```
543
+
544
+ For load testing at scale, pair with Artillery (`artillery-engine-playwright`).
545
+
546
+ ## Test Generation
547
+
548
+ ### Codegen
549
+
550
+ ```bash
551
+ bunx playwright codegen http://localhost:5173
552
+ ```
553
+
554
+ Records browser interactions and generates test code with role-based locators. As of v1.51, generates automatic `toBeVisible()` assertions.
555
+
556
+ **Always refactor raw codegen output into Page Objects before committing.**
557
+
558
+ ### AI-Assisted (v1.56+)
559
+
560
+ Playwright Test Agents provide planner, generator, and healer loops for AI-assisted test authoring. Use as a starting point, then review and refine.
561
+
562
+ ## Recommended Config
563
+
564
+ ```typescript
565
+ // playwright.config.ts
566
+ import { defineConfig, devices } from "@playwright/test";
567
+
568
+ export default defineConfig({
569
+ testDir: "./e2e/specs",
570
+ outputDir: "./test-results",
571
+ fullyParallel: true,
572
+ forbidOnly: !!process.env.CI,
573
+ retries: process.env.CI ? 2 : 0,
574
+ workers: process.env.CI ? 4 : undefined,
575
+ reporter: process.env.CI
576
+ ? [["blob"], ["github"]]
577
+ : [["html", { open: "never" }]],
578
+
579
+ use: {
580
+ baseURL: process.env.BASE_URL ?? "http://localhost:5173",
581
+ trace: "on-first-retry",
582
+ screenshot: "only-on-failure",
583
+ video: "retain-on-failure",
584
+ },
585
+
586
+ projects: [
587
+ { name: "setup", testMatch: /.*\.setup\.ts/ },
588
+ {
589
+ name: "chromium",
590
+ use: { ...devices["Desktop Chrome"], storageState: "e2e/.auth/user.json" },
591
+ dependencies: ["setup"],
592
+ },
593
+ {
594
+ name: "firefox",
595
+ use: { ...devices["Desktop Firefox"], storageState: "e2e/.auth/user.json" },
596
+ dependencies: ["setup"],
597
+ },
598
+ {
599
+ name: "webkit",
600
+ use: { ...devices["Desktop Safari"], storageState: "e2e/.auth/user.json" },
601
+ dependencies: ["setup"],
602
+ },
603
+ {
604
+ name: "mobile-chrome",
605
+ use: { ...devices["Pixel 7"], storageState: "e2e/.auth/user.json" },
606
+ dependencies: ["setup"],
607
+ },
608
+ {
609
+ name: "mobile-safari",
610
+ use: { ...devices["iPhone 14"], storageState: "e2e/.auth/user.json" },
611
+ dependencies: ["setup"],
612
+ },
613
+ ],
614
+
615
+ webServer: {
616
+ command: "bun run build && bun run preview",
617
+ port: 4173,
618
+ reuseExistingServer: !process.env.CI,
619
+ },
620
+ });
621
+ ```
622
+
623
+ ## Flaky Test Prevention
624
+
625
+ | Cause | Fix |
626
+ |---|---|
627
+ | `waitForTimeout()` / `page.waitForTimeout(5000)` | Replace with web-first assertions or `waitForResponse` |
628
+ | Brittle selectors (`div > span:nth-child(3)`) | Use `getByRole`, `getByLabel`, `getByTestId` |
629
+ | Shared state between tests | Isolate with fresh `BrowserContext` per test |
630
+ | External API flakiness | Mock network calls with `page.route()` |
631
+ | Animation timing | Use `--force-prefers-reduced-motion` or `* { animation: none !important }` |
632
+ | Race conditions | Use `waitForResponse()`, `waitForURL()`, or `waitForLoadState()` before asserting |
633
+ | Missing `await` | The #1 source of false-passing tests. Lint for it. |
634
+
635
+ ## Anti-Patterns
636
+
637
+ | Anti-Pattern | Fix |
638
+ |---|---|
639
+ | Using `waitForTimeout()` | Use web-first assertions or explicit wait conditions |
640
+ | Fragile CSS/XPath selectors | Use role-based and semantic locators |
641
+ | Creating new browser instances per test | Let Playwright manage `BrowserContext` isolation |
642
+ | Using `{ force: true }` | Masks real actionability problems (covered, disabled, invisible) |
643
+ | Missing `await` on assertions | The #1 source of false-passing tests |
644
+ | Tests without assertions | A test that clicks through without asserting is just a script |
645
+ | Redundant `page.reload()` calls | Playwright auto-waits; reload only when testing refresh behavior |
646
+ | Using Playwright for unit tests | Use Vitest for pure logic; Playwright is for browser-dependent behavior |
647
+ | Committing raw codegen output | Always refactor into Page Objects before committing |
648
+ | Not cleaning up route handlers | Use `page.unrouteAll()` to prevent mock leakage |
649
+ | Ignoring test isolation | Shared cookies/localStorage between tests causes cascading failures |