@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.
- package/README.md +293 -0
- package/extensions/dino.ts +70 -0
- package/extensions/mabl-mcp.ts +306 -0
- package/extensions/playwright-mcp.ts +142 -0
- package/package.json +47 -0
- package/prompts/create-pr.md +110 -0
- package/prompts/generate-bdd-from-url.md +65 -0
- package/prompts/generate-pom-from-url-cli.md +48 -0
- package/prompts/generate-pom-from-url.md +45 -0
- package/prompts/generate-spec-from-feature.md +64 -0
- package/prompts/heal-playwright-failures.md +106 -0
- package/prompts/setup-playwright.md +81 -0
- package/scripts/start-dino.cmd +8 -0
- package/scripts/start-dino.mjs +226 -0
- package/scripts/start-dino.ps1 +9 -0
- package/scripts/start-dino.sh +10 -0
- package/skills/acli/SKILL.md +292 -0
- package/skills/bdd-gherkin/SKILL.md +552 -0
- package/skills/build-review-interface/SKILL.md +96 -0
- package/skills/error-analysis/SKILL.md +164 -0
- package/skills/eval-audit/SKILL.md +183 -0
- package/skills/evaluate-rag/SKILL.md +177 -0
- package/skills/generate-synthetic-data/SKILL.md +131 -0
- package/skills/mabl-mcp/SKILL.md +79 -0
- package/skills/playwright-cli/SKILL.md +246 -0
- package/skills/playwright-cli/advanced-workflows.md +679 -0
- package/skills/playwright-cli/core-commands.md +375 -0
- package/skills/playwright-cli/device-emulation.md +475 -0
- package/skills/playwright-cli/request-mocking.md +373 -0
- package/skills/playwright-cli/running-custom-code.md +553 -0
- package/skills/playwright-cli/screenshots-and-media.md +457 -0
- package/skills/playwright-cli/session-management.md +425 -0
- package/skills/playwright-cli/storage-and-auth.md +487 -0
- package/skills/playwright-cli/test-generation.md +336 -0
- package/skills/playwright-cli/tracing-and-debugging.md +501 -0
- package/skills/playwright-core/SKILL.md +101 -0
- package/skills/playwright-core/accessibility.md +1517 -0
- package/skills/playwright-core/angular.md +1086 -0
- package/skills/playwright-core/api-testing.md +1623 -0
- package/skills/playwright-core/assertions-and-waiting.md +703 -0
- package/skills/playwright-core/auth-flows.md +1081 -0
- package/skills/playwright-core/authentication.md +1409 -0
- package/skills/playwright-core/browser-apis.md +673 -0
- package/skills/playwright-core/browser-extensions.md +319 -0
- package/skills/playwright-core/canvas-and-webgl.md +494 -0
- package/skills/playwright-core/clock-and-time-mocking.md +427 -0
- package/skills/playwright-core/common-pitfalls.md +1318 -0
- package/skills/playwright-core/component-testing.md +1179 -0
- package/skills/playwright-core/configuration.md +731 -0
- package/skills/playwright-core/crud-testing.md +945 -0
- package/skills/playwright-core/debugging.md +827 -0
- package/skills/playwright-core/drag-and-drop.md +992 -0
- package/skills/playwright-core/electron-testing.md +622 -0
- package/skills/playwright-core/error-and-edge-cases.md +1181 -0
- package/skills/playwright-core/error-index.md +1903 -0
- package/skills/playwright-core/file-operations.md +749 -0
- package/skills/playwright-core/file-upload-download.md +1012 -0
- package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
- package/skills/playwright-core/flaky-tests.md +883 -0
- package/skills/playwright-core/forms-and-validation.md +1056 -0
- package/skills/playwright-core/i18n-and-localization.md +622 -0
- package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
- package/skills/playwright-core/locator-strategy.md +588 -0
- package/skills/playwright-core/locators.md +743 -0
- package/skills/playwright-core/mobile-and-responsive.md +1669 -0
- package/skills/playwright-core/multi-context-and-popups.md +581 -0
- package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
- package/skills/playwright-core/network-mocking.md +1395 -0
- package/skills/playwright-core/nextjs.md +1013 -0
- package/skills/playwright-core/performance-testing.md +608 -0
- package/skills/playwright-core/react.md +1082 -0
- package/skills/playwright-core/search-and-filter.md +1366 -0
- package/skills/playwright-core/security-testing.md +624 -0
- package/skills/playwright-core/service-workers-and-pwa.md +524 -0
- package/skills/playwright-core/test-architecture.md +569 -0
- package/skills/playwright-core/test-data-management.md +1209 -0
- package/skills/playwright-core/test-organization.md +947 -0
- package/skills/playwright-core/third-party-integrations.md +754 -0
- package/skills/playwright-core/trace-analysis.md +161 -0
- package/skills/playwright-core/visual-regression.md +1027 -0
- package/skills/playwright-core/vue.md +1162 -0
- package/skills/playwright-core/websockets-and-realtime.md +623 -0
- package/skills/playwright-core/when-to-mock.md +827 -0
- package/skills/playwright-pom/SKILL.md +94 -0
- package/skills/playwright-pom/page-object-model.md +921 -0
- package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
- package/skills/validate-evaluator/SKILL.md +215 -0
- package/skills/write-judge-prompt/SKILL.md +144 -0
- 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
|