@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.
@@ -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