@assertrx/dino 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.
Files changed (89) hide show
  1. package/README.md +293 -0
  2. package/extensions/dino.ts +70 -0
  3. package/extensions/mabl-mcp.ts +306 -0
  4. package/extensions/playwright-mcp.ts +142 -0
  5. package/package.json +47 -0
  6. package/prompts/create-pr.md +110 -0
  7. package/prompts/generate-bdd-from-url.md +65 -0
  8. package/prompts/generate-pom-from-url-cli.md +48 -0
  9. package/prompts/generate-pom-from-url.md +45 -0
  10. package/prompts/generate-spec-from-feature.md +64 -0
  11. package/prompts/heal-playwright-failures.md +106 -0
  12. package/prompts/setup-playwright.md +81 -0
  13. package/scripts/start-dino.cmd +8 -0
  14. package/scripts/start-dino.mjs +226 -0
  15. package/scripts/start-dino.ps1 +9 -0
  16. package/scripts/start-dino.sh +10 -0
  17. package/skills/acli/SKILL.md +292 -0
  18. package/skills/bdd-gherkin/SKILL.md +552 -0
  19. package/skills/build-review-interface/SKILL.md +96 -0
  20. package/skills/error-analysis/SKILL.md +164 -0
  21. package/skills/eval-audit/SKILL.md +183 -0
  22. package/skills/evaluate-rag/SKILL.md +177 -0
  23. package/skills/generate-synthetic-data/SKILL.md +131 -0
  24. package/skills/mabl-mcp/SKILL.md +79 -0
  25. package/skills/playwright-cli/SKILL.md +246 -0
  26. package/skills/playwright-cli/advanced-workflows.md +679 -0
  27. package/skills/playwright-cli/core-commands.md +375 -0
  28. package/skills/playwright-cli/device-emulation.md +475 -0
  29. package/skills/playwright-cli/request-mocking.md +373 -0
  30. package/skills/playwright-cli/running-custom-code.md +553 -0
  31. package/skills/playwright-cli/screenshots-and-media.md +457 -0
  32. package/skills/playwright-cli/session-management.md +425 -0
  33. package/skills/playwright-cli/storage-and-auth.md +487 -0
  34. package/skills/playwright-cli/test-generation.md +336 -0
  35. package/skills/playwright-cli/tracing-and-debugging.md +501 -0
  36. package/skills/playwright-core/SKILL.md +101 -0
  37. package/skills/playwright-core/accessibility.md +1517 -0
  38. package/skills/playwright-core/angular.md +1086 -0
  39. package/skills/playwright-core/api-testing.md +1623 -0
  40. package/skills/playwright-core/assertions-and-waiting.md +703 -0
  41. package/skills/playwright-core/auth-flows.md +1081 -0
  42. package/skills/playwright-core/authentication.md +1409 -0
  43. package/skills/playwright-core/browser-apis.md +673 -0
  44. package/skills/playwright-core/browser-extensions.md +319 -0
  45. package/skills/playwright-core/canvas-and-webgl.md +494 -0
  46. package/skills/playwright-core/clock-and-time-mocking.md +427 -0
  47. package/skills/playwright-core/common-pitfalls.md +1318 -0
  48. package/skills/playwright-core/component-testing.md +1179 -0
  49. package/skills/playwright-core/configuration.md +731 -0
  50. package/skills/playwright-core/crud-testing.md +945 -0
  51. package/skills/playwright-core/debugging.md +827 -0
  52. package/skills/playwright-core/drag-and-drop.md +992 -0
  53. package/skills/playwright-core/electron-testing.md +622 -0
  54. package/skills/playwright-core/error-and-edge-cases.md +1181 -0
  55. package/skills/playwright-core/error-index.md +1903 -0
  56. package/skills/playwright-core/file-operations.md +749 -0
  57. package/skills/playwright-core/file-upload-download.md +1012 -0
  58. package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
  59. package/skills/playwright-core/flaky-tests.md +883 -0
  60. package/skills/playwright-core/forms-and-validation.md +1056 -0
  61. package/skills/playwright-core/i18n-and-localization.md +622 -0
  62. package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
  63. package/skills/playwright-core/locator-strategy.md +588 -0
  64. package/skills/playwright-core/locators.md +743 -0
  65. package/skills/playwright-core/mobile-and-responsive.md +1669 -0
  66. package/skills/playwright-core/multi-context-and-popups.md +581 -0
  67. package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
  68. package/skills/playwright-core/network-mocking.md +1395 -0
  69. package/skills/playwright-core/nextjs.md +1013 -0
  70. package/skills/playwright-core/performance-testing.md +608 -0
  71. package/skills/playwright-core/react.md +1082 -0
  72. package/skills/playwright-core/search-and-filter.md +1366 -0
  73. package/skills/playwright-core/security-testing.md +624 -0
  74. package/skills/playwright-core/service-workers-and-pwa.md +524 -0
  75. package/skills/playwright-core/test-architecture.md +569 -0
  76. package/skills/playwright-core/test-data-management.md +1209 -0
  77. package/skills/playwright-core/test-organization.md +947 -0
  78. package/skills/playwright-core/third-party-integrations.md +754 -0
  79. package/skills/playwright-core/trace-analysis.md +161 -0
  80. package/skills/playwright-core/visual-regression.md +1027 -0
  81. package/skills/playwright-core/vue.md +1162 -0
  82. package/skills/playwright-core/websockets-and-realtime.md +623 -0
  83. package/skills/playwright-core/when-to-mock.md +827 -0
  84. package/skills/playwright-pom/SKILL.md +94 -0
  85. package/skills/playwright-pom/page-object-model.md +921 -0
  86. package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
  87. package/skills/validate-evaluator/SKILL.md +215 -0
  88. package/skills/write-judge-prompt/SKILL.md +144 -0
  89. package/themes/dino.json +66 -0
@@ -0,0 +1,1179 @@
1
+ # Component Testing
2
+
3
+ > **When to use**: When you need to test UI components in isolation — verifying rendering, interactions, and behavior without spinning up your full application. Ideal for design systems, shared component libraries, and complex interactive widgets.
4
+ > **Prerequisites**: [core/configuration.md](configuration.md), [core/fixtures-and-hooks.md](fixtures-and-hooks.md)
5
+
6
+ ## Quick Reference
7
+
8
+ ```typescript
9
+ // Install for your framework:
10
+ // npm init playwright@latest -- --ct (interactive)
11
+ // npm install -D @playwright/experimental-ct-react
12
+ // npm install -D @playwright/experimental-ct-vue
13
+ // npm install -D @playwright/experimental-ct-svelte
14
+
15
+ // Mount a component, interact, assert:
16
+ import { test, expect } from '@playwright/experimental-ct-react';
17
+ import { Button } from './Button';
18
+
19
+ test('button renders and responds to click', async ({ mount }) => {
20
+ let clicked = false;
21
+ const component = await mount(
22
+ <Button label="Save" onClick={() => { clicked = true; }} />
23
+ );
24
+ await expect(component).toContainText('Save');
25
+ await component.click();
26
+ expect(clicked).toBe(true);
27
+ });
28
+ ```
29
+
30
+ ## Patterns
31
+
32
+ ### 1. Setup and Configuration
33
+
34
+ **Use when**: Starting component testing in an existing Playwright project.
35
+ **Avoid when**: You only need full E2E tests against a running application — component testing adds build complexity that is not justified for pure integration tests.
36
+
37
+ Component testing uses a separate config file (`playwright-ct.config.ts`) and a dedicated `playwright/index.html` entry point. Playwright bundles your component with Vite under the hood.
38
+
39
+ **TypeScript**
40
+ ```typescript
41
+ // playwright-ct.config.ts
42
+ import { defineConfig, devices } from '@playwright/experimental-ct-react';
43
+
44
+ export default defineConfig({
45
+ testDir: './src',
46
+ testMatch: '**/*.ct.tsx',
47
+ use: {
48
+ ctPort: 3100,
49
+ // Vite config for component bundling
50
+ ctViteConfig: {
51
+ resolve: {
52
+ alias: {
53
+ '@': '/src',
54
+ },
55
+ },
56
+ },
57
+ },
58
+ projects: [
59
+ {
60
+ name: 'chromium',
61
+ use: { ...devices['Desktop Chrome'] },
62
+ },
63
+ {
64
+ name: 'firefox',
65
+ use: { ...devices['Desktop Firefox'] },
66
+ },
67
+ {
68
+ name: 'webkit',
69
+ use: { ...devices['Desktop Safari'] },
70
+ },
71
+ ],
72
+ });
73
+ ```
74
+
75
+ ```html
76
+ <!-- playwright/index.html — entry point for component tests -->
77
+ <!DOCTYPE html>
78
+ <html lang="en">
79
+ <head>
80
+ <meta charset="UTF-8" />
81
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
82
+ <title>Component Tests</title>
83
+ </head>
84
+ <body>
85
+ <div id="root"></div>
86
+ <script type="module" src="./index.ts"></script>
87
+ </body>
88
+ </html>
89
+ ```
90
+
91
+ ```typescript
92
+ // playwright/index.ts — global styles and providers for all component tests
93
+ import '../src/styles/globals.css';
94
+ ```
95
+
96
+ **JavaScript**
97
+ ```javascript
98
+ // playwright-ct.config.js
99
+ const { defineConfig, devices } = require('@playwright/experimental-ct-react');
100
+
101
+ module.exports = defineConfig({
102
+ testDir: './src',
103
+ testMatch: '**/*.ct.jsx',
104
+ use: {
105
+ ctPort: 3100,
106
+ ctViteConfig: {
107
+ resolve: {
108
+ alias: {
109
+ '@': '/src',
110
+ },
111
+ },
112
+ },
113
+ },
114
+ projects: [
115
+ {
116
+ name: 'chromium',
117
+ use: { ...devices['Desktop Chrome'] },
118
+ },
119
+ {
120
+ name: 'firefox',
121
+ use: { ...devices['Desktop Firefox'] },
122
+ },
123
+ {
124
+ name: 'webkit',
125
+ use: { ...devices['Desktop Safari'] },
126
+ },
127
+ ],
128
+ });
129
+ ```
130
+
131
+ Run component tests with:
132
+ ```bash
133
+ npx playwright test -c playwright-ct.config.ts
134
+ ```
135
+
136
+ ### 2. Mounting Components
137
+
138
+ **Use when**: You need to render a component in a real browser with full DOM, CSS, and event handling.
139
+ **Avoid when**: The component is trivial (a pure function that returns a string) — use a unit test instead.
140
+
141
+ The `mount()` fixture renders your component into a real browser page. It returns a `Locator` pointed at the mounted component root.
142
+
143
+ **TypeScript**
144
+ ```typescript
145
+ import { test, expect } from '@playwright/experimental-ct-react';
146
+ import { Card } from './Card';
147
+
148
+ test('mount a component with props', async ({ mount }) => {
149
+ const component = await mount(
150
+ <Card title="Welcome" description="Get started with Playwright" />
151
+ );
152
+
153
+ await expect(component.getByRole('heading', { name: 'Welcome' })).toBeVisible();
154
+ await expect(component.getByText('Get started with Playwright')).toBeVisible();
155
+ });
156
+
157
+ test('mount with children', async ({ mount }) => {
158
+ const component = await mount(
159
+ <Card title="Actions">
160
+ <button>Click me</button>
161
+ </Card>
162
+ );
163
+
164
+ await expect(component.getByRole('button', { name: 'Click me' })).toBeVisible();
165
+ });
166
+
167
+ test('update props after mount', async ({ mount }) => {
168
+ const component = await mount(<Card title="Initial" description="First" />);
169
+ await expect(component.getByRole('heading', { name: 'Initial' })).toBeVisible();
170
+
171
+ // Re-render with new props
172
+ await component.update(<Card title="Updated" description="Second" />);
173
+ await expect(component.getByRole('heading', { name: 'Updated' })).toBeVisible();
174
+ });
175
+ ```
176
+
177
+ **JavaScript**
178
+ ```javascript
179
+ const { test, expect } = require('@playwright/experimental-ct-react');
180
+ const { Card } = require('./Card');
181
+
182
+ test('mount a component with props', async ({ mount }) => {
183
+ const component = await mount(
184
+ <Card title="Welcome" description="Get started with Playwright" />
185
+ );
186
+
187
+ await expect(component.getByRole('heading', { name: 'Welcome' })).toBeVisible();
188
+ await expect(component.getByText('Get started with Playwright')).toBeVisible();
189
+ });
190
+
191
+ test('mount with children', async ({ mount }) => {
192
+ const component = await mount(
193
+ <Card title="Actions">
194
+ <button>Click me</button>
195
+ </Card>
196
+ );
197
+
198
+ await expect(component.getByRole('button', { name: 'Click me' })).toBeVisible();
199
+ });
200
+
201
+ test('update props after mount', async ({ mount }) => {
202
+ const component = await mount(<Card title="Initial" description="First" />);
203
+ await expect(component.getByRole('heading', { name: 'Initial' })).toBeVisible();
204
+
205
+ await component.update(<Card title="Updated" description="Second" />);
206
+ await expect(component.getByRole('heading', { name: 'Updated' })).toBeVisible();
207
+ });
208
+ ```
209
+
210
+ ### 3. Testing Interactions
211
+
212
+ **Use when**: The component has clickable elements, form inputs, keyboard handling, or hover states.
213
+ **Avoid when**: You are testing browser-level behavior (navigation, cookies) — use E2E tests for that.
214
+
215
+ Component test interactions use the same Playwright locator API as E2E tests. The `mount()` return value is a `Locator`, so all standard methods work.
216
+
217
+ **TypeScript**
218
+ ```typescript
219
+ import { test, expect } from '@playwright/experimental-ct-react';
220
+ import { Counter } from './Counter';
221
+ import { SearchInput } from './SearchInput';
222
+ import { Dropdown } from './Dropdown';
223
+
224
+ test('click interactions', async ({ mount }) => {
225
+ const component = await mount(<Counter initialCount={0} />);
226
+
227
+ await component.getByRole('button', { name: 'Increment' }).click();
228
+ await component.getByRole('button', { name: 'Increment' }).click();
229
+ await expect(component.getByText('Count: 2')).toBeVisible();
230
+
231
+ await component.getByRole('button', { name: 'Decrement' }).click();
232
+ await expect(component.getByText('Count: 1')).toBeVisible();
233
+ });
234
+
235
+ test('typing interactions', async ({ mount }) => {
236
+ const component = await mount(<SearchInput placeholder="Search..." />);
237
+
238
+ const input = component.getByRole('textbox', { name: 'Search' });
239
+ await input.fill('playwright');
240
+ await expect(component.getByText('Showing results for: playwright')).toBeVisible();
241
+
242
+ // Clear and type again
243
+ await input.clear();
244
+ await input.fill('testing');
245
+ await expect(component.getByText('Showing results for: testing')).toBeVisible();
246
+ });
247
+
248
+ test('keyboard interactions', async ({ mount }) => {
249
+ const component = await mount(<SearchInput placeholder="Search..." />);
250
+
251
+ const input = component.getByRole('textbox', { name: 'Search' });
252
+ await input.fill('playwright');
253
+ await input.press('Enter');
254
+ await expect(component.getByText('Searched: playwright')).toBeVisible();
255
+ });
256
+
257
+ test('select from dropdown', async ({ mount }) => {
258
+ const component = await mount(
259
+ <Dropdown
260
+ label="Color"
261
+ options={['Red', 'Green', 'Blue']}
262
+ />
263
+ );
264
+
265
+ await component.getByRole('combobox', { name: 'Color' }).click();
266
+ await component.getByRole('option', { name: 'Green' }).click();
267
+ await expect(component.getByText('Selected: Green')).toBeVisible();
268
+ });
269
+ ```
270
+
271
+ **JavaScript**
272
+ ```javascript
273
+ const { test, expect } = require('@playwright/experimental-ct-react');
274
+ const { Counter } = require('./Counter');
275
+ const { SearchInput } = require('./SearchInput');
276
+ const { Dropdown } = require('./Dropdown');
277
+
278
+ test('click interactions', async ({ mount }) => {
279
+ const component = await mount(<Counter initialCount={0} />);
280
+
281
+ await component.getByRole('button', { name: 'Increment' }).click();
282
+ await component.getByRole('button', { name: 'Increment' }).click();
283
+ await expect(component.getByText('Count: 2')).toBeVisible();
284
+
285
+ await component.getByRole('button', { name: 'Decrement' }).click();
286
+ await expect(component.getByText('Count: 1')).toBeVisible();
287
+ });
288
+
289
+ test('typing interactions', async ({ mount }) => {
290
+ const component = await mount(<SearchInput placeholder="Search..." />);
291
+
292
+ const input = component.getByRole('textbox', { name: 'Search' });
293
+ await input.fill('playwright');
294
+ await expect(component.getByText('Showing results for: playwright')).toBeVisible();
295
+
296
+ await input.clear();
297
+ await input.fill('testing');
298
+ await expect(component.getByText('Showing results for: testing')).toBeVisible();
299
+ });
300
+
301
+ test('keyboard interactions', async ({ mount }) => {
302
+ const component = await mount(<SearchInput placeholder="Search..." />);
303
+
304
+ const input = component.getByRole('textbox', { name: 'Search' });
305
+ await input.fill('playwright');
306
+ await input.press('Enter');
307
+ await expect(component.getByText('Searched: playwright')).toBeVisible();
308
+ });
309
+
310
+ test('select from dropdown', async ({ mount }) => {
311
+ const component = await mount(
312
+ <Dropdown label="Color" options={['Red', 'Green', 'Blue']} />
313
+ );
314
+
315
+ await component.getByRole('combobox', { name: 'Color' }).click();
316
+ await component.getByRole('option', { name: 'Green' }).click();
317
+ await expect(component.getByText('Selected: Green')).toBeVisible();
318
+ });
319
+ ```
320
+
321
+ ### 4. Testing Props
322
+
323
+ **Use when**: You need to verify a component renders correctly with different prop combinations — states, variants, edge cases.
324
+ **Avoid when**: The prop differences are purely visual with no DOM change — use visual regression instead.
325
+
326
+ **TypeScript**
327
+ ```typescript
328
+ import { test, expect } from '@playwright/experimental-ct-react';
329
+ import { Alert } from './Alert';
330
+ import { Badge } from './Badge';
331
+ import { Avatar } from './Avatar';
332
+
333
+ test('alert renders different severity levels', async ({ mount }) => {
334
+ const success = await mount(<Alert severity="success" message="Saved!" />);
335
+ await expect(success.getByRole('alert')).toContainText('Saved!');
336
+ await expect(success.getByRole('alert')).toHaveAttribute('data-severity', 'success');
337
+
338
+ const error = await mount(<Alert severity="error" message="Failed to save" />);
339
+ await expect(error.getByRole('alert')).toContainText('Failed to save');
340
+ await expect(error.getByRole('alert')).toHaveAttribute('data-severity', 'error');
341
+ });
342
+
343
+ test('badge renders count and caps at 99+', async ({ mount }) => {
344
+ const low = await mount(<Badge count={5} />);
345
+ await expect(low).toContainText('5');
346
+
347
+ const high = await mount(<Badge count={150} />);
348
+ await expect(high).toContainText('99+');
349
+
350
+ const zero = await mount(<Badge count={0} />);
351
+ await expect(zero).toBeHidden();
352
+ });
353
+
354
+ test('avatar shows initials when no image provided', async ({ mount }) => {
355
+ const withImage = await mount(
356
+ <Avatar src="/photo.jpg" name="Jane Doe" />
357
+ );
358
+ await expect(withImage.getByRole('img', { name: 'Jane Doe' })).toBeVisible();
359
+
360
+ const withoutImage = await mount(<Avatar name="Jane Doe" />);
361
+ await expect(withoutImage.getByText('JD')).toBeVisible();
362
+ await expect(withoutImage.getByRole('img')).toHaveCount(0);
363
+ });
364
+
365
+ test('disabled button is not interactive', async ({ mount }) => {
366
+ const component = await mount(
367
+ <button disabled>Submit</button>
368
+ );
369
+ await expect(component.getByRole('button', { name: 'Submit' })).toBeDisabled();
370
+ });
371
+ ```
372
+
373
+ **JavaScript**
374
+ ```javascript
375
+ const { test, expect } = require('@playwright/experimental-ct-react');
376
+ const { Alert } = require('./Alert');
377
+ const { Badge } = require('./Badge');
378
+ const { Avatar } = require('./Avatar');
379
+
380
+ test('alert renders different severity levels', async ({ mount }) => {
381
+ const success = await mount(<Alert severity="success" message="Saved!" />);
382
+ await expect(success.getByRole('alert')).toContainText('Saved!');
383
+ await expect(success.getByRole('alert')).toHaveAttribute('data-severity', 'success');
384
+
385
+ const error = await mount(<Alert severity="error" message="Failed to save" />);
386
+ await expect(error.getByRole('alert')).toContainText('Failed to save');
387
+ await expect(error.getByRole('alert')).toHaveAttribute('data-severity', 'error');
388
+ });
389
+
390
+ test('badge renders count and caps at 99+', async ({ mount }) => {
391
+ const low = await mount(<Badge count={5} />);
392
+ await expect(low).toContainText('5');
393
+
394
+ const high = await mount(<Badge count={150} />);
395
+ await expect(high).toContainText('99+');
396
+
397
+ const zero = await mount(<Badge count={0} />);
398
+ await expect(zero).toBeHidden();
399
+ });
400
+
401
+ test('avatar shows initials when no image provided', async ({ mount }) => {
402
+ const withImage = await mount(<Avatar src="/photo.jpg" name="Jane Doe" />);
403
+ await expect(withImage.getByRole('img', { name: 'Jane Doe' })).toBeVisible();
404
+
405
+ const withoutImage = await mount(<Avatar name="Jane Doe" />);
406
+ await expect(withoutImage.getByText('JD')).toBeVisible();
407
+ await expect(withoutImage.getByRole('img')).toHaveCount(0);
408
+ });
409
+
410
+ test('disabled button is not interactive', async ({ mount }) => {
411
+ const component = await mount(<button disabled>Submit</button>);
412
+ await expect(component.getByRole('button', { name: 'Submit' })).toBeDisabled();
413
+ });
414
+ ```
415
+
416
+ ### 5. Testing Events
417
+
418
+ **Use when**: A component emits events or calls callback props — form submissions, toggle changes, custom events.
419
+ **Avoid when**: You only care that something renders — use a prop/snapshot test instead.
420
+
421
+ Capture events by passing callback props to `mount()`. Use closures or arrays to collect values for assertion.
422
+
423
+ **TypeScript**
424
+ ```typescript
425
+ import { test, expect } from '@playwright/experimental-ct-react';
426
+ import { Toggle } from './Toggle';
427
+ import { ContactForm } from './ContactForm';
428
+ import { TagInput } from './TagInput';
429
+
430
+ test('toggle fires onChange with new value', async ({ mount }) => {
431
+ const events: boolean[] = [];
432
+ const component = await mount(
433
+ <Toggle
434
+ label="Dark mode"
435
+ onChange={(checked: boolean) => { events.push(checked); }}
436
+ />
437
+ );
438
+
439
+ await component.getByRole('switch', { name: 'Dark mode' }).click();
440
+ expect(events).toEqual([true]);
441
+
442
+ await component.getByRole('switch', { name: 'Dark mode' }).click();
443
+ expect(events).toEqual([true, false]);
444
+ });
445
+
446
+ test('form calls onSubmit with field values', async ({ mount }) => {
447
+ let submittedData: Record<string, string> | null = null;
448
+ const component = await mount(
449
+ <ContactForm
450
+ onSubmit={(data: Record<string, string>) => { submittedData = data; }}
451
+ />
452
+ );
453
+
454
+ await component.getByLabel('Name').fill('Jane Doe');
455
+ await component.getByLabel('Email').fill('jane@example.com');
456
+ await component.getByLabel('Message').fill('Hello!');
457
+ await component.getByRole('button', { name: 'Send' }).click();
458
+
459
+ expect(submittedData).toEqual({
460
+ name: 'Jane Doe',
461
+ email: 'jane@example.com',
462
+ message: 'Hello!',
463
+ });
464
+ });
465
+
466
+ test('tag input fires onTagAdd and onTagRemove', async ({ mount }) => {
467
+ const added: string[] = [];
468
+ const removed: string[] = [];
469
+ const component = await mount(
470
+ <TagInput
471
+ onTagAdd={(tag: string) => { added.push(tag); }}
472
+ onTagRemove={(tag: string) => { removed.push(tag); }}
473
+ />
474
+ );
475
+
476
+ const input = component.getByRole('textbox');
477
+ await input.fill('playwright');
478
+ await input.press('Enter');
479
+ expect(added).toEqual(['playwright']);
480
+
481
+ // Remove the tag
482
+ await component.getByRole('button', { name: 'Remove playwright' }).click();
483
+ expect(removed).toEqual(['playwright']);
484
+ });
485
+ ```
486
+
487
+ **JavaScript**
488
+ ```javascript
489
+ const { test, expect } = require('@playwright/experimental-ct-react');
490
+ const { Toggle } = require('./Toggle');
491
+ const { ContactForm } = require('./ContactForm');
492
+ const { TagInput } = require('./TagInput');
493
+
494
+ test('toggle fires onChange with new value', async ({ mount }) => {
495
+ const events = [];
496
+ const component = await mount(
497
+ <Toggle
498
+ label="Dark mode"
499
+ onChange={(checked) => { events.push(checked); }}
500
+ />
501
+ );
502
+
503
+ await component.getByRole('switch', { name: 'Dark mode' }).click();
504
+ expect(events).toEqual([true]);
505
+
506
+ await component.getByRole('switch', { name: 'Dark mode' }).click();
507
+ expect(events).toEqual([true, false]);
508
+ });
509
+
510
+ test('form calls onSubmit with field values', async ({ mount }) => {
511
+ let submittedData = null;
512
+ const component = await mount(
513
+ <ContactForm
514
+ onSubmit={(data) => { submittedData = data; }}
515
+ />
516
+ );
517
+
518
+ await component.getByLabel('Name').fill('Jane Doe');
519
+ await component.getByLabel('Email').fill('jane@example.com');
520
+ await component.getByLabel('Message').fill('Hello!');
521
+ await component.getByRole('button', { name: 'Send' }).click();
522
+
523
+ expect(submittedData).toEqual({
524
+ name: 'Jane Doe',
525
+ email: 'jane@example.com',
526
+ message: 'Hello!',
527
+ });
528
+ });
529
+
530
+ test('tag input fires onTagAdd and onTagRemove', async ({ mount }) => {
531
+ const added = [];
532
+ const removed = [];
533
+ const component = await mount(
534
+ <TagInput
535
+ onTagAdd={(tag) => { added.push(tag); }}
536
+ onTagRemove={(tag) => { removed.push(tag); }}
537
+ />
538
+ );
539
+
540
+ const input = component.getByRole('textbox');
541
+ await input.fill('playwright');
542
+ await input.press('Enter');
543
+ expect(added).toEqual(['playwright']);
544
+
545
+ await component.getByRole('button', { name: 'Remove playwright' }).click();
546
+ expect(removed).toEqual(['playwright']);
547
+ });
548
+ ```
549
+
550
+ ### 6. Testing Slots and Children
551
+
552
+ **Use when**: Your component accepts children, named slots (Vue), or render props — layout components, wrappers, modals.
553
+ **Avoid when**: The component has no slot/children API.
554
+
555
+ **TypeScript**
556
+ ```typescript
557
+ import { test, expect } from '@playwright/experimental-ct-react';
558
+ import { Modal } from './Modal';
559
+ import { Accordion } from './Accordion';
560
+ import { Layout } from './Layout';
561
+
562
+ test('modal renders children in the dialog', async ({ mount }) => {
563
+ const component = await mount(
564
+ <Modal open={true} title="Confirm">
565
+ <p>Are you sure you want to delete this item?</p>
566
+ <button>Delete</button>
567
+ <button>Cancel</button>
568
+ </Modal>
569
+ );
570
+
571
+ await expect(component.getByRole('dialog', { name: 'Confirm' })).toBeVisible();
572
+ await expect(component.getByText('Are you sure you want to delete this item?')).toBeVisible();
573
+ await expect(component.getByRole('button', { name: 'Delete' })).toBeVisible();
574
+ await expect(component.getByRole('button', { name: 'Cancel' })).toBeVisible();
575
+ });
576
+
577
+ test('accordion renders multiple sections', async ({ mount }) => {
578
+ const component = await mount(
579
+ <Accordion>
580
+ <Accordion.Item title="Section 1">Content for section 1</Accordion.Item>
581
+ <Accordion.Item title="Section 2">Content for section 2</Accordion.Item>
582
+ </Accordion>
583
+ );
584
+
585
+ // First section collapsed by default
586
+ await expect(component.getByText('Content for section 1')).toBeHidden();
587
+
588
+ // Expand first section
589
+ await component.getByRole('button', { name: 'Section 1' }).click();
590
+ await expect(component.getByText('Content for section 1')).toBeVisible();
591
+
592
+ // Second section remains collapsed
593
+ await expect(component.getByText('Content for section 2')).toBeHidden();
594
+ });
595
+
596
+ test('layout component renders header and body slots', async ({ mount }) => {
597
+ const component = await mount(
598
+ <Layout
599
+ header={<h1>Dashboard</h1>}
600
+ sidebar={<nav><a href="/settings">Settings</a></nav>}
601
+ >
602
+ <p>Main content goes here</p>
603
+ </Layout>
604
+ );
605
+
606
+ await expect(component.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
607
+ await expect(component.getByRole('link', { name: 'Settings' })).toBeVisible();
608
+ await expect(component.getByText('Main content goes here')).toBeVisible();
609
+ });
610
+ ```
611
+
612
+ **JavaScript**
613
+ ```javascript
614
+ const { test, expect } = require('@playwright/experimental-ct-react');
615
+ const { Modal } = require('./Modal');
616
+ const { Accordion } = require('./Accordion');
617
+ const { Layout } = require('./Layout');
618
+
619
+ test('modal renders children in the dialog', async ({ mount }) => {
620
+ const component = await mount(
621
+ <Modal open={true} title="Confirm">
622
+ <p>Are you sure you want to delete this item?</p>
623
+ <button>Delete</button>
624
+ <button>Cancel</button>
625
+ </Modal>
626
+ );
627
+
628
+ await expect(component.getByRole('dialog', { name: 'Confirm' })).toBeVisible();
629
+ await expect(component.getByText('Are you sure you want to delete this item?')).toBeVisible();
630
+ await expect(component.getByRole('button', { name: 'Delete' })).toBeVisible();
631
+ await expect(component.getByRole('button', { name: 'Cancel' })).toBeVisible();
632
+ });
633
+
634
+ test('accordion renders multiple sections', async ({ mount }) => {
635
+ const component = await mount(
636
+ <Accordion>
637
+ <Accordion.Item title="Section 1">Content for section 1</Accordion.Item>
638
+ <Accordion.Item title="Section 2">Content for section 2</Accordion.Item>
639
+ </Accordion>
640
+ );
641
+
642
+ await expect(component.getByText('Content for section 1')).toBeHidden();
643
+ await component.getByRole('button', { name: 'Section 1' }).click();
644
+ await expect(component.getByText('Content for section 1')).toBeVisible();
645
+ await expect(component.getByText('Content for section 2')).toBeHidden();
646
+ });
647
+
648
+ test('layout component renders header and body slots', async ({ mount }) => {
649
+ const component = await mount(
650
+ <Layout
651
+ header={<h1>Dashboard</h1>}
652
+ sidebar={<nav><a href="/settings">Settings</a></nav>}
653
+ >
654
+ <p>Main content goes here</p>
655
+ </Layout>
656
+ );
657
+
658
+ await expect(component.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
659
+ await expect(component.getByRole('link', { name: 'Settings' })).toBeVisible();
660
+ await expect(component.getByText('Main content goes here')).toBeVisible();
661
+ });
662
+ ```
663
+
664
+ **Vue Slots Example (TypeScript)**
665
+ ```typescript
666
+ import { test, expect } from '@playwright/experimental-ct-vue';
667
+ import Card from './Card.vue';
668
+
669
+ test('vue named slots', async ({ mount }) => {
670
+ const component = await mount(Card, {
671
+ props: { title: 'My Card' },
672
+ slots: {
673
+ default: '<p>Card body content</p>',
674
+ footer: '<button>Save</button>',
675
+ },
676
+ });
677
+
678
+ await expect(component.getByText('Card body content')).toBeVisible();
679
+ await expect(component.getByRole('button', { name: 'Save' })).toBeVisible();
680
+ });
681
+ ```
682
+
683
+ ### 7. Providing Context (Wrappers and Providers)
684
+
685
+ **Use when**: Your components depend on React context, Vue provide/inject, or global state (theme, auth, i18n, store).
686
+ **Avoid when**: The component has no context dependencies — do not wrap unnecessarily.
687
+
688
+ Use the `playwright/index.ts` file to register global wrappers, or wrap per-test using a wrapper component.
689
+
690
+ **TypeScript**
691
+ ```typescript
692
+ // playwright/index.tsx — global wrapper for ALL component tests
693
+ import '../src/styles/globals.css';
694
+ import { ThemeProvider } from '../src/providers/ThemeProvider';
695
+ import { IntlProvider } from '../src/providers/IntlProvider';
696
+
697
+ // beforeMount runs before every component is mounted
698
+ // Use it to wrap all components with global providers
699
+ import { beforeMount } from '@playwright/experimental-ct-react/hooks';
700
+
701
+ beforeMount(async ({ App }) => {
702
+ return (
703
+ <IntlProvider locale="en">
704
+ <ThemeProvider theme="light">
705
+ <App />
706
+ </ThemeProvider>
707
+ </IntlProvider>
708
+ );
709
+ });
710
+ ```
711
+
712
+ ```typescript
713
+ // Per-test provider wrapping — for tests that need specific context
714
+ import { test, expect } from '@playwright/experimental-ct-react';
715
+ import { UserProfile } from './UserProfile';
716
+ import { AuthContext } from '../contexts/AuthContext';
717
+
718
+ // Create a test wrapper component
719
+ function AuthWrapper({ children, user }: { children: React.ReactNode; user: any }) {
720
+ return (
721
+ <AuthContext.Provider value={{ user, isAuthenticated: true }}>
722
+ {children}
723
+ </AuthContext.Provider>
724
+ );
725
+ }
726
+
727
+ test('profile shows authenticated user info', async ({ mount }) => {
728
+ const user = { name: 'Jane Doe', email: 'jane@example.com', role: 'admin' };
729
+
730
+ const component = await mount(
731
+ <AuthWrapper user={user}>
732
+ <UserProfile />
733
+ </AuthWrapper>
734
+ );
735
+
736
+ await expect(component.getByText('Jane Doe')).toBeVisible();
737
+ await expect(component.getByText('admin')).toBeVisible();
738
+ });
739
+ ```
740
+
741
+ ```typescript
742
+ // Redux/Zustand store wrapping
743
+ import { test, expect } from '@playwright/experimental-ct-react';
744
+ import { Provider } from 'react-redux';
745
+ import { configureStore } from '@reduxjs/toolkit';
746
+ import { cartReducer } from '../store/cartSlice';
747
+ import { CartSummary } from './CartSummary';
748
+
749
+ test('cart summary shows item count from store', async ({ mount }) => {
750
+ const store = configureStore({
751
+ reducer: { cart: cartReducer },
752
+ preloadedState: {
753
+ cart: {
754
+ items: [
755
+ { id: '1', name: 'Widget', quantity: 2, price: 9.99 },
756
+ { id: '2', name: 'Gadget', quantity: 1, price: 24.99 },
757
+ ],
758
+ },
759
+ },
760
+ });
761
+
762
+ const component = await mount(
763
+ <Provider store={store}>
764
+ <CartSummary />
765
+ </Provider>
766
+ );
767
+
768
+ await expect(component.getByText('3 items')).toBeVisible();
769
+ await expect(component.getByText('$44.97')).toBeVisible();
770
+ });
771
+ ```
772
+
773
+ **JavaScript**
774
+ ```javascript
775
+ // playwright/index.jsx — global wrapper
776
+ import '../src/styles/globals.css';
777
+ import { ThemeProvider } from '../src/providers/ThemeProvider';
778
+ import { IntlProvider } from '../src/providers/IntlProvider';
779
+ import { beforeMount } from '@playwright/experimental-ct-react/hooks';
780
+
781
+ beforeMount(async ({ App }) => {
782
+ return (
783
+ <IntlProvider locale="en">
784
+ <ThemeProvider theme="light">
785
+ <App />
786
+ </ThemeProvider>
787
+ </IntlProvider>
788
+ );
789
+ });
790
+ ```
791
+
792
+ ```javascript
793
+ // Per-test store wrapping
794
+ const { test, expect } = require('@playwright/experimental-ct-react');
795
+ const { Provider } = require('react-redux');
796
+ const { configureStore } = require('@reduxjs/toolkit');
797
+ const { cartReducer } = require('../store/cartSlice');
798
+ const { CartSummary } = require('./CartSummary');
799
+
800
+ test('cart summary shows item count from store', async ({ mount }) => {
801
+ const store = configureStore({
802
+ reducer: { cart: cartReducer },
803
+ preloadedState: {
804
+ cart: {
805
+ items: [
806
+ { id: '1', name: 'Widget', quantity: 2, price: 9.99 },
807
+ { id: '2', name: 'Gadget', quantity: 1, price: 24.99 },
808
+ ],
809
+ },
810
+ },
811
+ });
812
+
813
+ const component = await mount(
814
+ <Provider store={store}>
815
+ <CartSummary />
816
+ </Provider>
817
+ );
818
+
819
+ await expect(component.getByText('3 items')).toBeVisible();
820
+ await expect(component.getByText('$44.97')).toBeVisible();
821
+ });
822
+ ```
823
+
824
+ ### 8. Mocking Imports
825
+
826
+ **Use when**: A component imports modules that should not run in tests — API clients, analytics, heavy third-party libraries.
827
+ **Avoid when**: You can provide the dependency via props or context instead — explicit injection is always better than import mocking.
828
+
829
+ Use the `beforeMount` hook in `playwright/index.ts` to intercept and replace modules.
830
+
831
+ **TypeScript**
832
+ ```typescript
833
+ // playwright/index.tsx — mock modules globally
834
+ import { beforeMount } from '@playwright/experimental-ct-react/hooks';
835
+
836
+ beforeMount(async ({ hooksConfig }) => {
837
+ // hooksConfig is passed from individual tests via mount options
838
+ if (hooksConfig?.mockApi) {
839
+ // Mock the API module before the component loads
840
+ const apiModule = await import('../src/api/client');
841
+ apiModule.fetchUser = async () => hooksConfig.mockUser;
842
+ apiModule.fetchProducts = async () => hooksConfig.mockProducts;
843
+ }
844
+ });
845
+ ```
846
+
847
+ ```typescript
848
+ // UserDashboard.ct.tsx
849
+ import { test, expect } from '@playwright/experimental-ct-react';
850
+ import { UserDashboard } from './UserDashboard';
851
+
852
+ test('dashboard renders with mocked API data', async ({ mount }) => {
853
+ const component = await mount(<UserDashboard />, {
854
+ hooksConfig: {
855
+ mockApi: true,
856
+ mockUser: { name: 'Jane Doe', email: 'jane@example.com' },
857
+ mockProducts: [
858
+ { id: '1', name: 'Widget', price: 9.99 },
859
+ { id: '2', name: 'Gadget', price: 24.99 },
860
+ ],
861
+ },
862
+ });
863
+
864
+ await expect(component.getByText('Jane Doe')).toBeVisible();
865
+ await expect(component.getByRole('listitem')).toHaveCount(2);
866
+ });
867
+ ```
868
+
869
+ ```typescript
870
+ // Alternative: mock at the network level using page.route()
871
+ import { test, expect } from '@playwright/experimental-ct-react';
872
+ import { ProductList } from './ProductList';
873
+
874
+ test('product list with network-level mocking', async ({ mount, page }) => {
875
+ // Intercept fetch/XHR calls made by the component
876
+ await page.route('**/api/products', (route) =>
877
+ route.fulfill({
878
+ json: [
879
+ { id: '1', name: 'Widget', price: 9.99 },
880
+ { id: '2', name: 'Gadget', price: 24.99 },
881
+ ],
882
+ })
883
+ );
884
+
885
+ const component = await mount(<ProductList />);
886
+ await expect(component.getByRole('listitem')).toHaveCount(2);
887
+ await expect(component.getByText('Widget')).toBeVisible();
888
+ });
889
+ ```
890
+
891
+ **JavaScript**
892
+ ```javascript
893
+ // playwright/index.jsx — mock modules globally
894
+ const { beforeMount } = require('@playwright/experimental-ct-react/hooks');
895
+
896
+ beforeMount(async ({ hooksConfig }) => {
897
+ if (hooksConfig?.mockApi) {
898
+ const apiModule = await import('../src/api/client');
899
+ apiModule.fetchUser = async () => hooksConfig.mockUser;
900
+ apiModule.fetchProducts = async () => hooksConfig.mockProducts;
901
+ }
902
+ });
903
+ ```
904
+
905
+ ```javascript
906
+ // Alternative: network-level mocking
907
+ const { test, expect } = require('@playwright/experimental-ct-react');
908
+ const { ProductList } = require('./ProductList');
909
+
910
+ test('product list with network-level mocking', async ({ mount, page }) => {
911
+ await page.route('**/api/products', (route) =>
912
+ route.fulfill({
913
+ json: [
914
+ { id: '1', name: 'Widget', price: 9.99 },
915
+ { id: '2', name: 'Gadget', price: 24.99 },
916
+ ],
917
+ })
918
+ );
919
+
920
+ const component = await mount(<ProductList />);
921
+ await expect(component.getByRole('listitem')).toHaveCount(2);
922
+ await expect(component.getByText('Widget')).toBeVisible();
923
+ });
924
+ ```
925
+
926
+ ### 9. Visual Component Testing
927
+
928
+ **Use when**: You need pixel-level verification of component appearance — design system components, theme variants, responsive states.
929
+ **Avoid when**: The component is purely functional with no meaningful visual output.
930
+
931
+ Component tests support `toHaveScreenshot()` just like E2E tests. This is powerful for testing individual component states without needing a full application.
932
+
933
+ **TypeScript**
934
+ ```typescript
935
+ import { test, expect } from '@playwright/experimental-ct-react';
936
+ import { Button } from './Button';
937
+ import { Card } from './Card';
938
+
939
+ test('button visual variants', async ({ mount }) => {
940
+ const primary = await mount(<Button variant="primary">Save</Button>);
941
+ await expect(primary).toHaveScreenshot('button-primary.png');
942
+
943
+ const secondary = await mount(<Button variant="secondary">Cancel</Button>);
944
+ await expect(secondary).toHaveScreenshot('button-secondary.png');
945
+
946
+ const danger = await mount(<Button variant="danger">Delete</Button>);
947
+ await expect(danger).toHaveScreenshot('button-danger.png');
948
+ });
949
+
950
+ test('button states', async ({ mount }) => {
951
+ const component = await mount(<Button variant="primary">Save</Button>);
952
+
953
+ // Default state
954
+ await expect(component).toHaveScreenshot('button-default.png');
955
+
956
+ // Hover state
957
+ await component.hover();
958
+ await expect(component).toHaveScreenshot('button-hover.png');
959
+
960
+ // Focus state
961
+ await component.focus();
962
+ await expect(component).toHaveScreenshot('button-focus.png');
963
+ });
964
+
965
+ test('card renders consistently', async ({ mount }) => {
966
+ const component = await mount(
967
+ <Card title="Product" description="A great product for testing.">
968
+ <Button variant="primary">Buy now</Button>
969
+ </Card>
970
+ );
971
+
972
+ await expect(component).toHaveScreenshot('card-with-button.png', {
973
+ maxDiffPixelRatio: 0.01,
974
+ });
975
+ });
976
+
977
+ test('responsive component at different widths', async ({ mount, page }) => {
978
+ const component = await mount(<Card title="Responsive" description="Adapts to width" />);
979
+
980
+ await page.setViewportSize({ width: 1200, height: 800 });
981
+ await expect(component).toHaveScreenshot('card-desktop.png');
982
+
983
+ await page.setViewportSize({ width: 375, height: 667 });
984
+ await expect(component).toHaveScreenshot('card-mobile.png');
985
+ });
986
+ ```
987
+
988
+ **JavaScript**
989
+ ```javascript
990
+ const { test, expect } = require('@playwright/experimental-ct-react');
991
+ const { Button } = require('./Button');
992
+ const { Card } = require('./Card');
993
+
994
+ test('button visual variants', async ({ mount }) => {
995
+ const primary = await mount(<Button variant="primary">Save</Button>);
996
+ await expect(primary).toHaveScreenshot('button-primary.png');
997
+
998
+ const secondary = await mount(<Button variant="secondary">Cancel</Button>);
999
+ await expect(secondary).toHaveScreenshot('button-secondary.png');
1000
+
1001
+ const danger = await mount(<Button variant="danger">Delete</Button>);
1002
+ await expect(danger).toHaveScreenshot('button-danger.png');
1003
+ });
1004
+
1005
+ test('button states', async ({ mount }) => {
1006
+ const component = await mount(<Button variant="primary">Save</Button>);
1007
+
1008
+ await expect(component).toHaveScreenshot('button-default.png');
1009
+
1010
+ await component.hover();
1011
+ await expect(component).toHaveScreenshot('button-hover.png');
1012
+
1013
+ await component.focus();
1014
+ await expect(component).toHaveScreenshot('button-focus.png');
1015
+ });
1016
+
1017
+ test('responsive component at different widths', async ({ mount, page }) => {
1018
+ const component = await mount(<Card title="Responsive" description="Adapts to width" />);
1019
+
1020
+ await page.setViewportSize({ width: 1200, height: 800 });
1021
+ await expect(component).toHaveScreenshot('card-desktop.png');
1022
+
1023
+ await page.setViewportSize({ width: 375, height: 667 });
1024
+ await expect(component).toHaveScreenshot('card-mobile.png');
1025
+ });
1026
+ ```
1027
+
1028
+ ### 10. Component Test vs E2E Test
1029
+
1030
+ **Use when**: Deciding whether to write a component test, an E2E test, or both for a piece of UI.
1031
+ **Avoid when**: You already have a clear testing strategy for your project.
1032
+
1033
+ The core distinction: component tests verify **how a component behaves in isolation**, while E2E tests verify **how the whole system works together**. They complement each other.
1034
+
1035
+ **TypeScript — Component test (isolated behavior)**
1036
+ ```typescript
1037
+ // Button.ct.tsx — tests the Button component in isolation
1038
+ import { test, expect } from '@playwright/experimental-ct-react';
1039
+ import { Button } from './Button';
1040
+
1041
+ test('button shows loading spinner when loading prop is true', async ({ mount }) => {
1042
+ const component = await mount(<Button loading={true}>Save</Button>);
1043
+
1044
+ await expect(component.getByRole('button', { name: 'Save' })).toBeDisabled();
1045
+ await expect(component.getByRole('progressbar')).toBeVisible();
1046
+ await expect(component.getByText('Save')).toBeVisible();
1047
+ });
1048
+
1049
+ test('button calls onClick when clicked', async ({ mount }) => {
1050
+ let clicked = false;
1051
+ const component = await mount(
1052
+ <Button onClick={() => { clicked = true; }}>Save</Button>
1053
+ );
1054
+
1055
+ await component.click();
1056
+ expect(clicked).toBe(true);
1057
+ });
1058
+ ```
1059
+
1060
+ ```typescript
1061
+ // checkout.spec.ts — E2E test verifying the full flow
1062
+ import { test, expect } from '@playwright/test';
1063
+
1064
+ test('complete checkout flow', async ({ page }) => {
1065
+ await page.goto('/products');
1066
+ await page.getByRole('button', { name: 'Add to cart' }).first().click();
1067
+ await page.getByRole('link', { name: 'Cart' }).click();
1068
+ await page.getByRole('button', { name: 'Checkout' }).click();
1069
+
1070
+ // Fill shipping form
1071
+ await page.getByLabel('Address').fill('123 Main St');
1072
+ await page.getByLabel('City').fill('Springfield');
1073
+ await page.getByRole('button', { name: 'Continue to payment' }).click();
1074
+
1075
+ // The Save button's loading state is tested in the component test.
1076
+ // Here we test the real user flow end-to-end.
1077
+ await page.getByRole('button', { name: 'Place order' }).click();
1078
+ await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
1079
+ });
1080
+ ```
1081
+
1082
+ **JavaScript — Component test (isolated behavior)**
1083
+ ```javascript
1084
+ // Button.ct.jsx
1085
+ const { test, expect } = require('@playwright/experimental-ct-react');
1086
+ const { Button } = require('./Button');
1087
+
1088
+ test('button shows loading spinner when loading prop is true', async ({ mount }) => {
1089
+ const component = await mount(<Button loading={true}>Save</Button>);
1090
+
1091
+ await expect(component.getByRole('button', { name: 'Save' })).toBeDisabled();
1092
+ await expect(component.getByRole('progressbar')).toBeVisible();
1093
+ });
1094
+
1095
+ test('button calls onClick when clicked', async ({ mount }) => {
1096
+ let clicked = false;
1097
+ const component = await mount(
1098
+ <Button onClick={() => { clicked = true; }}>Save</Button>
1099
+ );
1100
+
1101
+ await component.click();
1102
+ expect(clicked).toBe(true);
1103
+ });
1104
+ ```
1105
+
1106
+ ```javascript
1107
+ // checkout.spec.js — E2E test
1108
+ const { test, expect } = require('@playwright/test');
1109
+
1110
+ test('complete checkout flow', async ({ page }) => {
1111
+ await page.goto('/products');
1112
+ await page.getByRole('button', { name: 'Add to cart' }).first().click();
1113
+ await page.getByRole('link', { name: 'Cart' }).click();
1114
+ await page.getByRole('button', { name: 'Checkout' }).click();
1115
+
1116
+ await page.getByLabel('Address').fill('123 Main St');
1117
+ await page.getByLabel('City').fill('Springfield');
1118
+ await page.getByRole('button', { name: 'Continue to payment' }).click();
1119
+
1120
+ await page.getByRole('button', { name: 'Place order' }).click();
1121
+ await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
1122
+ });
1123
+ ```
1124
+
1125
+ ## Decision Guide
1126
+
1127
+ | UI Element | Component Test | E2E Test | Unit Test |
1128
+ |---|---|---|---|
1129
+ | **Button** (variants, states, loading) | Yes — test all visual variants, disabled state, loading state, click handlers | Only as part of a larger flow | No — needs real DOM for styling and accessibility |
1130
+ | **Form field** (validation, masking) | Yes — test validation messages, input masking, error states in isolation | Yes — test the full form submission flow with backend | Validate-only logic (regex, format functions) |
1131
+ | **Modal/Dialog** (open, close, content) | Yes — test open/close behavior, focus trap, content rendering | Yes — test the trigger flow that opens the modal | No — needs real DOM |
1132
+ | **Data table** (sorting, filtering, pagination) | Yes — test sort, filter, pagination with mock data | Yes — test with real API data and URL sync | Pure sort/filter logic on arrays |
1133
+ | **Navigation/Menu** | Partially — test dropdown behavior, active states | Yes — test actual route changes and page loads | No |
1134
+ | **Full page** (dashboard, settings) | No — too much context required; defeats isolation purpose | Yes — this is what E2E tests are for | No |
1135
+ | **Layout** (sidebar, header, grid) | Yes — test responsive behavior, slot rendering | Only if layout affects user flows (e.g., mobile nav) | No |
1136
+ | **Chart/Graph** | Yes — visual regression of rendered output | Only if charts are part of a critical flow | Data transformation logic only |
1137
+ | **Toast/Notification** | Yes — test appearance, auto-dismiss, action buttons | Yes — test that real actions trigger correct toasts | No |
1138
+ | **Design system primitives** | Yes — this is the primary use case for component testing | No — not needed for primitives | No |
1139
+
1140
+ **Rule of thumb**: Component tests for **behavior and appearance in isolation**. E2E tests for **user journeys across multiple components and pages**. Unit tests for **pure logic with no DOM**.
1141
+
1142
+ ## Anti-Patterns
1143
+
1144
+ | Don't Do This | Problem | Do This Instead |
1145
+ |---|---|---|
1146
+ | Check internal state (`component.state.count`) | Couples test to implementation; breaks on any refactor | Assert on visible output: `expect(component.getByText('Count: 5')).toBeVisible()` |
1147
+ | Mount an entire page in a component test | Requires too many providers, mock data, and context; slow; defeats isolation purpose | Use E2E tests for full pages; component test only the individual widgets |
1148
+ | Skip required providers/context | Component crashes with "Cannot read property of undefined" or "useContext must be inside Provider" | Wrap with required providers in `playwright/index.tsx` or per-test wrapper |
1149
+ | Test framework internals (React lifecycle, Vue watchers) | You are testing the framework, not your code; these are already tested by React/Vue | Test user-visible behavior: what renders, what happens on click |
1150
+ | Mount and immediately screenshot without waiting | Screenshot captures loading/transition state | `await expect(component.getByText('...')).toBeVisible()` before screenshot |
1151
+ | Duplicate E2E coverage in component tests | Same behavior tested twice adds maintenance cost with no new confidence | Component tests: isolated behavior. E2E tests: integrated flows. Overlap only at critical boundaries |
1152
+ | Test CSS class names or inline styles | Implementation detail; breaks on any styling refactor | Use `toHaveScreenshot()` for visual verification or `toBeVisible()`/`toBeHidden()` for behavior |
1153
+ | Create one giant test file per component | Hard to debug, slow feedback loop, poor test isolation | One test file per component, grouped by behavior with `test.describe()` |
1154
+ | Pass mock data that does not match real API shape | Tests pass but component breaks with real data | Define shared TypeScript types or use a factory function for consistent test data |
1155
+ | Use `page.goto()` in component tests | Component tests do not navigate — `mount()` renders directly | Use `mount(<Component />)` only; use `page.goto()` in E2E tests |
1156
+
1157
+ ## Troubleshooting
1158
+
1159
+ | Symptom | Cause | Fix |
1160
+ |---|---|---|
1161
+ | `Cannot find module '@playwright/experimental-ct-react'` | Package not installed | `npm install -D @playwright/experimental-ct-react` (or `-vue`, `-svelte`) |
1162
+ | Component renders blank | Missing CSS imports or provider wrappers | Add global styles to `playwright/index.ts` and wrap with required providers |
1163
+ | `Error: No tests found` | Test file does not match `testMatch` pattern in `playwright-ct.config.ts` | Ensure files use the configured suffix (e.g., `*.ct.tsx`) and `testDir` is correct |
1164
+ | `Cannot use JSX` in test file | TypeScript/Vite not configured for JSX | Ensure test files use `.ct.tsx`/`.ct.jsx` extension and `tsconfig.json` has `"jsx": "react-jsx"` |
1165
+ | `useContext returns undefined` | Component depends on a context provider that was not wrapped | Add the provider in `playwright/index.tsx` via `beforeMount` hook or wrap per-test |
1166
+ | `hooksConfig` values are undefined | The `playwright/index.ts` hooks file is not set up or not reading `hooksConfig` | Ensure `beforeMount` destructures `{ hooksConfig }` and the file is at `playwright/index.ts` |
1167
+ | Screenshots differ between CI and local | Different OS renders fonts differently | Run screenshot tests in Docker or use `maxDiffPixelRatio` tolerance; generate baselines in CI |
1168
+ | Component test is slow (>5s per test) | Mounting a large component tree with many providers or importing heavy modules | Reduce provider scope; mock heavy imports; use `page.route()` instead of real API calls |
1169
+ | `mount()` returns but component is not visible | Component renders off-screen or with `display: none` by default | Check CSS and props; use `await expect(component).toBeVisible()` to debug |
1170
+ | `Error: page.route is not available` | Using `page` fixture without requesting it | Destructure `{ mount, page }` in the test function signature |
1171
+
1172
+ ## Related
1173
+
1174
+ - [core/fixtures-and-hooks.md](fixtures-and-hooks.md) — fixtures work inside component tests the same way
1175
+ - [core/visual-regression.md](visual-regression.md) — screenshot comparison patterns applicable to component tests
1176
+ - [core/network-mocking.md](network-mocking.md) — `page.route()` works inside component tests for mocking API calls
1177
+ - [core/test-architecture.md](test-architecture.md) — when to use component vs E2E vs API tests
1178
+ - [core/react.md](react.md) — React-specific component testing setup
1179
+ - [core/vue.md](vue.md) — Vue-specific component testing with slots and provide/inject