@schalkneethling/calavera-skill-frontend-testing 0.2.0-next.1
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/LICENSE +21 -0
- package/calavera-artifact.json +11 -0
- package/package.json +24 -0
- package/payload/frontend-testing/SKILL.md +348 -0
- package/payload/frontend-testing/agents/openai.yaml +4 -0
- package/payload/frontend-testing/references/accessibility-testing.md +362 -0
- package/payload/frontend-testing/references/aria-snapshots.md +523 -0
- package/payload/frontend-testing/references/locator-strategies.md +295 -0
- package/payload/frontend-testing/references/visual-regression.md +519 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
# Visual Regression Testing
|
|
2
|
+
|
|
3
|
+
Visual regression testing (VRT) catches unintended visual changes by comparing screenshots against approved baselines.
|
|
4
|
+
|
|
5
|
+
## When to Use VRT
|
|
6
|
+
|
|
7
|
+
**Good candidates:**
|
|
8
|
+
|
|
9
|
+
- Design system components
|
|
10
|
+
- Critical landing pages
|
|
11
|
+
- Complex layouts (grids, dashboards)
|
|
12
|
+
- Components with CSS that changes frequently
|
|
13
|
+
- Cross-browser visual consistency
|
|
14
|
+
|
|
15
|
+
**Poor candidates:**
|
|
16
|
+
|
|
17
|
+
- Pages with frequently changing dynamic content
|
|
18
|
+
- Components with animations
|
|
19
|
+
- Time-dependent displays
|
|
20
|
+
- Pages with third-party content (ads, embeds)
|
|
21
|
+
|
|
22
|
+
## Playwright Visual Testing
|
|
23
|
+
|
|
24
|
+
### Basic Screenshot Comparison
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
import { test, expect } from "@playwright/test";
|
|
28
|
+
|
|
29
|
+
test("homepage looks correct", async ({ page }) => {
|
|
30
|
+
await page.goto("/");
|
|
31
|
+
|
|
32
|
+
await expect(page).toHaveScreenshot();
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
First run creates the baseline. Subsequent runs compare against it.
|
|
37
|
+
|
|
38
|
+
### Named Screenshots
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
test("hero section renders correctly", async ({ page }) => {
|
|
42
|
+
await page.goto("/");
|
|
43
|
+
|
|
44
|
+
await expect(page).toHaveScreenshot("homepage-hero.png");
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Element Screenshots
|
|
49
|
+
|
|
50
|
+
More stable than full-page screenshots:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
test("product card renders correctly", async ({ page }) => {
|
|
54
|
+
await page.goto("/products/123");
|
|
55
|
+
|
|
56
|
+
const productCard = page.getByTestId("product-card");
|
|
57
|
+
await expect(productCard).toHaveScreenshot("product-card.png");
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Full Page Screenshots
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
test("full page layout", async ({ page }) => {
|
|
65
|
+
await page.goto("/about");
|
|
66
|
+
|
|
67
|
+
await expect(page).toHaveScreenshot("about-page-full.png", {
|
|
68
|
+
fullPage: true,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Handling Instability
|
|
74
|
+
|
|
75
|
+
Visual tests fail due to timing, rendering, or environment differences. Stabilize tests before trusting results.
|
|
76
|
+
|
|
77
|
+
### Wait for Network and Fonts
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
test("page renders after loading", async ({ page }) => {
|
|
81
|
+
await page.goto("/");
|
|
82
|
+
|
|
83
|
+
// Wait for network to be idle
|
|
84
|
+
await page.waitForLoadState("networkidle");
|
|
85
|
+
|
|
86
|
+
// Wait for web fonts to load
|
|
87
|
+
await page.evaluate(() => document.fonts.ready);
|
|
88
|
+
|
|
89
|
+
await expect(page).toHaveScreenshot();
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Disable Animations
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
test("static screenshot", async ({ page }) => {
|
|
97
|
+
await page.goto("/");
|
|
98
|
+
await expect(page).toHaveScreenshot({ animations: "disabled" });
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`reducedMotion: "reduce"` only emulates the `prefers-reduced-motion` media feature; it does not
|
|
103
|
+
disable arbitrary animations. Use it separately when testing the reduced-motion experience.
|
|
104
|
+
|
|
105
|
+
Or in test:
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
test("static screenshot", async ({ page }) => {
|
|
109
|
+
await page.goto("/");
|
|
110
|
+
|
|
111
|
+
// Inject CSS to disable animations
|
|
112
|
+
await page.addStyleTag({
|
|
113
|
+
content: `
|
|
114
|
+
*, *::before, *::after {
|
|
115
|
+
animation-duration: 0s !important;
|
|
116
|
+
transition-duration: 0s !important;
|
|
117
|
+
}
|
|
118
|
+
`,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await expect(page).toHaveScreenshot();
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Mask Dynamic Content
|
|
126
|
+
|
|
127
|
+
Hide elements that change between runs:
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
test("page with masked dynamic content", async ({ page }) => {
|
|
131
|
+
await page.goto("/dashboard");
|
|
132
|
+
|
|
133
|
+
await expect(page).toHaveScreenshot({
|
|
134
|
+
mask: [
|
|
135
|
+
page.locator("[data-testid='current-time']"),
|
|
136
|
+
page.locator("[data-testid='user-avatar']"),
|
|
137
|
+
page.locator(".ad-container"),
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Fixed Viewport
|
|
144
|
+
|
|
145
|
+
Lock viewport size for consistent screenshots:
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
// playwright.config.js
|
|
149
|
+
export default defineConfig({
|
|
150
|
+
use: {
|
|
151
|
+
viewport: { width: 1280, height: 720 },
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Or per-test:
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
test("mobile view", async ({ page }) => {
|
|
160
|
+
await page.setViewportSize({ width: 375, height: 667 });
|
|
161
|
+
await page.goto("/");
|
|
162
|
+
await expect(page).toHaveScreenshot("homepage-mobile.png");
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Hide Scrollbars
|
|
167
|
+
|
|
168
|
+
Scrollbars vary by OS:
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
await page.addStyleTag({
|
|
172
|
+
content: `
|
|
173
|
+
::-webkit-scrollbar { display: none; }
|
|
174
|
+
* { scrollbar-width: none; }
|
|
175
|
+
`,
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Tolerance Settings
|
|
180
|
+
|
|
181
|
+
Allow minor pixel differences to reduce flakiness.
|
|
182
|
+
|
|
183
|
+
### Global Configuration
|
|
184
|
+
|
|
185
|
+
```javascript
|
|
186
|
+
// playwright.config.js
|
|
187
|
+
export default defineConfig({
|
|
188
|
+
expect: {
|
|
189
|
+
toHaveScreenshot: {
|
|
190
|
+
maxDiffPixels: 100, // Allow up to 100 different pixels
|
|
191
|
+
maxDiffPixelRatio: 0.01, // Or 1% of total pixels
|
|
192
|
+
threshold: 0.2, // Color difference threshold (0-1)
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Per-Screenshot Settings
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
await expect(page).toHaveScreenshot({
|
|
202
|
+
maxDiffPixels: 50,
|
|
203
|
+
threshold: 0.3,
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Threshold Guidelines
|
|
208
|
+
|
|
209
|
+
| Setting | Value | Use Case |
|
|
210
|
+
| ------------------- | ----- | ---------------------------- |
|
|
211
|
+
| `threshold` | 0.1 | Strict pixel matching |
|
|
212
|
+
| `threshold` | 0.2 | Standard tolerance (default) |
|
|
213
|
+
| `threshold` | 0.3 | Generous for anti-aliasing |
|
|
214
|
+
| `maxDiffPixels` | 50 | Small components |
|
|
215
|
+
| `maxDiffPixels` | 200 | Full pages |
|
|
216
|
+
| `maxDiffPixelRatio` | 0.01 | 1% tolerance |
|
|
217
|
+
|
|
218
|
+
## Updating Baselines
|
|
219
|
+
|
|
220
|
+
### When to Update
|
|
221
|
+
|
|
222
|
+
- Intentional design changes
|
|
223
|
+
- New features affecting layout
|
|
224
|
+
- Browser rendering engine updates
|
|
225
|
+
- After fixing visual bugs
|
|
226
|
+
|
|
227
|
+
### Update Command
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
npx playwright test --update-snapshots
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Never update blindly.** Review changes before committing:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# See what changed
|
|
237
|
+
git diff --stat
|
|
238
|
+
# Review visual diffs in the Playwright report or configured snapshot directory
|
|
239
|
+
npx playwright show-report
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Workflow
|
|
243
|
+
|
|
244
|
+
1. Test fails due to visual difference
|
|
245
|
+
2. Open test report, review diff image
|
|
246
|
+
3. If change is intentional:
|
|
247
|
+
- Update baseline with `--update-snapshots`
|
|
248
|
+
- Commit new baseline
|
|
249
|
+
4. If change is unintentional:
|
|
250
|
+
- Fix the code
|
|
251
|
+
- Run test again
|
|
252
|
+
|
|
253
|
+
## Cross-Browser Testing
|
|
254
|
+
|
|
255
|
+
Screenshots differ between browsers due to rendering differences.
|
|
256
|
+
|
|
257
|
+
### Browser-Specific Baselines
|
|
258
|
+
|
|
259
|
+
Playwright automatically creates separate baselines per browser:
|
|
260
|
+
|
|
261
|
+
```text
|
|
262
|
+
example.spec.ts-snapshots/
|
|
263
|
+
├── homepage-chromium-linux.png
|
|
264
|
+
├── homepage-firefox-linux.png
|
|
265
|
+
└── homepage-webkit-linux.png
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Testing Specific Browsers
|
|
269
|
+
|
|
270
|
+
```javascript
|
|
271
|
+
// playwright.config.js
|
|
272
|
+
export default defineConfig({
|
|
273
|
+
projects: [
|
|
274
|
+
{
|
|
275
|
+
name: "chromium",
|
|
276
|
+
use: { ...devices["Desktop Chrome"] },
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: "firefox",
|
|
280
|
+
use: { ...devices["Desktop Firefox"] },
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: "webkit",
|
|
284
|
+
use: { ...devices["Desktop Safari"] },
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### OS Differences
|
|
291
|
+
|
|
292
|
+
Font rendering varies by OS. Options:
|
|
293
|
+
|
|
294
|
+
- Run visual tests in CI only (consistent environment)
|
|
295
|
+
- Use Docker with consistent fonts
|
|
296
|
+
- Generate baselines in CI, not locally
|
|
297
|
+
|
|
298
|
+
## CI/CD Configuration
|
|
299
|
+
|
|
300
|
+
When adding GitHub Actions examples or workflow files, declare explicit least-privilege
|
|
301
|
+
`GITHUB_TOKEN` permissions. Use a restrictive top-level default such as `contents: read`, then add
|
|
302
|
+
job-level permissions only for jobs that need to push commits, update pull requests, publish pages,
|
|
303
|
+
or write other repository resources.
|
|
304
|
+
|
|
305
|
+
Pin every third-party or GitHub-owned action to a full-length commit SHA. Do not commit tag-based
|
|
306
|
+
refs such as `@v6` or `@v7`; use those only as temporary input to a pinning tool, then keep the
|
|
307
|
+
version as a comment beside the SHA.
|
|
308
|
+
|
|
309
|
+
When a job checks out the repository, set `persist-credentials: false` on `actions/checkout`.
|
|
310
|
+
Artifact-producing jobs should not keep the repository token persisted for later test, build, or
|
|
311
|
+
upload steps.
|
|
312
|
+
|
|
313
|
+
### GitHub Actions Example
|
|
314
|
+
|
|
315
|
+
```yaml
|
|
316
|
+
on:
|
|
317
|
+
workflow_dispatch:
|
|
318
|
+
push:
|
|
319
|
+
branches: [main]
|
|
320
|
+
|
|
321
|
+
permissions:
|
|
322
|
+
contents: read
|
|
323
|
+
|
|
324
|
+
jobs:
|
|
325
|
+
visual-tests:
|
|
326
|
+
permissions:
|
|
327
|
+
contents: read
|
|
328
|
+
steps:
|
|
329
|
+
- name: Checkout
|
|
330
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 - re-resolve before use
|
|
331
|
+
with:
|
|
332
|
+
persist-credentials: false
|
|
333
|
+
|
|
334
|
+
- name: Run visual tests
|
|
335
|
+
run: npx playwright test --project=chromium
|
|
336
|
+
|
|
337
|
+
- name: Upload diff artifacts on failure
|
|
338
|
+
if: failure()
|
|
339
|
+
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 - re-resolve before use
|
|
340
|
+
with:
|
|
341
|
+
name: visual-diff
|
|
342
|
+
path: test-results/
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Generate Baselines in CI
|
|
346
|
+
|
|
347
|
+
For consistent baselines, generate in CI:
|
|
348
|
+
|
|
349
|
+
```yaml
|
|
350
|
+
permissions:
|
|
351
|
+
contents: read
|
|
352
|
+
|
|
353
|
+
jobs:
|
|
354
|
+
update-baselines:
|
|
355
|
+
permissions:
|
|
356
|
+
contents: write
|
|
357
|
+
steps:
|
|
358
|
+
- name: Checkout
|
|
359
|
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 - re-resolve before use
|
|
360
|
+
with:
|
|
361
|
+
persist-credentials: false
|
|
362
|
+
|
|
363
|
+
- name: Update baselines
|
|
364
|
+
run: npx playwright test --update-snapshots
|
|
365
|
+
|
|
366
|
+
- name: Commit baselines
|
|
367
|
+
env:
|
|
368
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
369
|
+
run: |
|
|
370
|
+
git config user.name "CI Bot"
|
|
371
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
372
|
+
git add "**/*.png"
|
|
373
|
+
if ! git diff --cached --quiet; then
|
|
374
|
+
git commit -m "Update visual baselines"
|
|
375
|
+
git -c http.https://github.com/.extraheader="AUTHORIZATION: bearer $GITHUB_TOKEN" push origin HEAD:refs/heads/${GITHUB_REF_NAME}
|
|
376
|
+
fi
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Organizing Screenshots
|
|
380
|
+
|
|
381
|
+
### Directory Structure
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
tests/
|
|
385
|
+
├── visual/
|
|
386
|
+
│ ├── homepage.spec.ts
|
|
387
|
+
│ └── components/
|
|
388
|
+
│ ├── button.spec.ts
|
|
389
|
+
│ └── card.spec.ts
|
|
390
|
+
└── visual.spec.ts-snapshots/
|
|
391
|
+
├── homepage-chromium.png
|
|
392
|
+
└── button-default-chromium.png
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Custom Path Template
|
|
396
|
+
|
|
397
|
+
```javascript
|
|
398
|
+
// playwright.config.js
|
|
399
|
+
export default defineConfig({
|
|
400
|
+
snapshotPathTemplate: "{testDir}/__snapshots__/{projectName}/{testFilePath}/{arg}{ext}",
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Reporting and Review
|
|
405
|
+
|
|
406
|
+
### Test Report
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
npx playwright show-report
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Shows:
|
|
413
|
+
|
|
414
|
+
- Expected (baseline) image
|
|
415
|
+
- Actual (current) image
|
|
416
|
+
- Diff highlighting changed pixels
|
|
417
|
+
|
|
418
|
+
### Artifacts on Failure
|
|
419
|
+
|
|
420
|
+
Configure to save all artifacts:
|
|
421
|
+
|
|
422
|
+
```javascript
|
|
423
|
+
// playwright.config.js
|
|
424
|
+
export default defineConfig({
|
|
425
|
+
use: {
|
|
426
|
+
trace: "on-first-retry",
|
|
427
|
+
screenshot: "only-on-failure",
|
|
428
|
+
},
|
|
429
|
+
outputDir: "test-results/",
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Anti-Patterns
|
|
434
|
+
|
|
435
|
+
### Removing Platform Suffix
|
|
436
|
+
|
|
437
|
+
```javascript
|
|
438
|
+
// BAD: Removes platform/browser suffix from snapshot names
|
|
439
|
+
test.beforeEach(async ({}, testInfo) => {
|
|
440
|
+
testInfo.snapshotSuffix = "";
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Results in single baseline used across all platforms:
|
|
444
|
+
// button.png ← Used for Chrome/Mac, Firefox/Linux, Safari/Windows...
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
This seems appealing for "cleaner" snapshot names, but it breaks visual testing. Per [Playwright documentation](https://playwright.dev/docs/test-snapshots):
|
|
448
|
+
|
|
449
|
+
> "Different snapshots are needed for different browsers and platforms due to variations in rendering and fonts."
|
|
450
|
+
|
|
451
|
+
**Why it fails:**
|
|
452
|
+
|
|
453
|
+
- Font rendering differs between macOS, Windows, and Linux
|
|
454
|
+
- Anti-aliasing algorithms vary by OS and browser
|
|
455
|
+
- Subpixel rendering produces different results
|
|
456
|
+
- You'll get constant false positives or false negatives
|
|
457
|
+
|
|
458
|
+
**Keep the default naming:**
|
|
459
|
+
|
|
460
|
+
```
|
|
461
|
+
button-chromium-darwin.png
|
|
462
|
+
button-chromium-linux.png
|
|
463
|
+
button-firefox-linux.png
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Yes, it's more files. But each baseline accurately represents that specific environment.
|
|
467
|
+
|
|
468
|
+
### Too Many Full-Page Screenshots
|
|
469
|
+
|
|
470
|
+
```javascript
|
|
471
|
+
// BAD: Full page with dynamic content
|
|
472
|
+
await expect(page).toHaveScreenshot({ fullPage: true });
|
|
473
|
+
|
|
474
|
+
// GOOD: Target stable regions
|
|
475
|
+
const header = page.getByRole("banner");
|
|
476
|
+
await expect(header).toHaveScreenshot("header.png");
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### No Stabilization
|
|
480
|
+
|
|
481
|
+
```javascript
|
|
482
|
+
// BAD: Screenshot immediately
|
|
483
|
+
await page.goto("/");
|
|
484
|
+
await expect(page).toHaveScreenshot();
|
|
485
|
+
|
|
486
|
+
// GOOD: Wait for stable state
|
|
487
|
+
await page.goto("/");
|
|
488
|
+
await page.waitForLoadState("networkidle");
|
|
489
|
+
await page.evaluate(() => document.fonts.ready);
|
|
490
|
+
await expect(page).toHaveScreenshot();
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Overly Strict Tolerance
|
|
494
|
+
|
|
495
|
+
```javascript
|
|
496
|
+
// BAD: Zero tolerance causes flaky tests
|
|
497
|
+
await expect(page).toHaveScreenshot({ maxDiffPixels: 0 });
|
|
498
|
+
|
|
499
|
+
// GOOD: Allow minor rendering differences
|
|
500
|
+
await expect(page).toHaveScreenshot({ maxDiffPixels: 50 });
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Updating Without Review
|
|
504
|
+
|
|
505
|
+
```bash
|
|
506
|
+
# BAD: Blindly update all baselines
|
|
507
|
+
npx playwright test --update-snapshots
|
|
508
|
+
|
|
509
|
+
# GOOD: Review changes first
|
|
510
|
+
npx playwright test
|
|
511
|
+
npx playwright show-report
|
|
512
|
+
# Review diffs, then update if intentional
|
|
513
|
+
npx playwright test --update-snapshots
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## References
|
|
517
|
+
|
|
518
|
+
- [Playwright Visual Comparisons](https://playwright.dev/docs/test-snapshots)
|
|
519
|
+
- [Pixelmatch](https://github.com/mapbox/pixelmatch) — Image comparison library used by Playwright
|