Skip to content

Commit a06be2b

Browse files
authored
Merge pull request #115 from PMDevSolutions/111-implement-cross-browser-screenshot-baseline-storage-rfc-0002
feat: cross-browser screenshot baseline storage (RFC 0002)
2 parents 234f346 + cdddd21 commit a06be2b

36 files changed

Lines changed: 4019 additions & 81 deletions

.claude/commands/build-from-figma.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -278,21 +278,20 @@ Generate and run end-to-end tests appropriate to the app type.
278278

279279
## Phase 7: Cross-Browser Verification (Non-Blocking)
280280

281-
Capture screenshots in Firefox and WebKit, compare against Chromium baseline.
281+
Diff Firefox and WebKit screenshots against their committed baselines with
282+
provenance verification (RFC 0002).
282283

283284
```bash
284285
# Only for web apps and PWAs (Chrome extensions are Chromium-only)
285-
./scripts/cross-browser-test.sh firefox http://localhost:3000
286-
./scripts/cross-browser-test.sh webkit http://localhost:3000
287-
288-
node scripts/visual-diff.js --batch \
289-
.claude/visual-qa/screenshots/firefox \
290-
.claude/visual-qa/screenshots/chromium \
291-
--output-dir .claude/visual-qa/diffs/firefox-vs-chromium \
292-
--threshold 0.03
286+
./scripts/cross-browser-baseline.sh compare http://localhost:3000 --json
293287
```
294288

295-
Cross-browser differences are logged in the build report but do NOT block the pipeline.
289+
If no cross-browser baselines are committed yet the compare skips with a
290+
capture hint (`./scripts/cross-browser-baseline.sh capture <url>` in the
291+
pinned Playwright container, then commit `.claude/visual-qa/baselines/`).
292+
293+
Cross-browser differences are logged in the build report but do NOT block the
294+
pipeline (`visualBaselines.blocking: false`).
296295

297296
## Phase 8: Quality Gate
298297

.claude/pipeline.config.json

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,41 @@
313313
"browsers": ["chromium"],
314314
"reportFile": "regression-report.md"
315315
},
316+
"visualBaselines": {
317+
"enabled": true,
318+
"backend": "commit",
319+
"storage": "git",
320+
"baselineDir": ".claude/visual-qa/baselines",
321+
"screenshotDir": ".claude/visual-qa/screenshots/cross-browser",
322+
"diffDir": ".claude/visual-qa/diffs/cross-browser",
323+
"browsers": ["chromium", "firefox", "webkit"],
324+
"routes": ["/"],
325+
"breakpoints": {
326+
"mobile": 375,
327+
"desktop": 1440
328+
},
329+
"threshold": 0.03,
330+
"blocking": false,
331+
"reportFile": "cross-browser-report.md",
332+
"capture": {
333+
"mode": "container",
334+
"image": "mcr.microsoft.com/playwright:v1.61.1-noble",
335+
"waitAfterLoadMs": 1500,
336+
"fullPage": true
337+
},
338+
"provenance": {
339+
"manifest": ".claude/visual-qa/baselines/manifest.json",
340+
"policy": "warn"
341+
},
342+
"ciArtifact": {
343+
"compareAgainst": "last-green-main",
344+
"retentionDays": 30
345+
},
346+
"service": {
347+
"provider": "chromatic",
348+
"projectTokenEnv": "CHROMATIC_PROJECT_TOKEN"
349+
}
350+
},
316351
"deadCode": {
317352
"enabled": true,
318353
"tool": "knip",
@@ -472,9 +507,9 @@
472507
},
473508
"cross-browser": {
474509
"depends": ["component-build"],
475-
"resources": [],
510+
"resources": [{ "name": "port:dev-server", "mode": "shared" }],
476511
"blocking": false,
477-
"description": "Firefox and WebKit screenshot comparison"
512+
"description": "Firefox and WebKit baseline comparison (cross-browser-baseline.sh compare)"
478513
},
479514
"quality-gate": {
480515
"depends": ["component-build"],

.claude/pipeline.config.schema.json

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,78 @@
362362
}
363363
},
364364

365+
"visualBaselines": {
366+
"type": "object",
367+
"additionalProperties": false,
368+
"description": "Cross-browser (firefox/webkit) screenshot baseline storage per RFC 0002: pluggable backend, deterministic containerized capture, and per-baseline provenance. Same-browser chromium regression stays in regressionTesting.",
369+
"properties": {
370+
"enabled": { "type": "boolean" },
371+
"backend": {
372+
"type": "string",
373+
"enum": ["commit", "ci-artifact", "service"],
374+
"description": "Where baselines live: committed to the repo (default), fetched from the last-green-main CI artifact, or delegated to a SaaS provider."
375+
},
376+
"storage": {
377+
"type": "string",
378+
"enum": ["git", "lfs"],
379+
"description": "commit backend only: plain git-tracked PNGs, or Git LFS for large baseline sets (see scripts/setup-baseline-lfs.sh)."
380+
},
381+
"baselineDir": { "type": "string", "description": "Baseline root, shared with regressionTesting; per-engine subdirectories." },
382+
"screenshotDir": { "type": "string", "description": "Transient current-capture directory (gitignored)." },
383+
"diffDir": { "type": "string", "description": "Transient diff-image directory (gitignored)." },
384+
"browsers": { "$ref": "#/$defs/browserList" },
385+
"routes": { "type": "array", "items": { "type": "string" } },
386+
"breakpoints": { "$ref": "#/$defs/breakpointMap" },
387+
"threshold": { "$ref": "#/$defs/percentageRatio", "description": "Cross-engine mismatch tolerance. Must equal e2e.crossBrowserDiffThreshold (structural check)." },
388+
"blocking": { "type": "boolean", "description": "When true, compare failures exit non-zero (Phase B teeth). Keep false until determinism is proven." },
389+
"reportFile": { "type": "string" },
390+
"capture": {
391+
"type": "object",
392+
"additionalProperties": false,
393+
"properties": {
394+
"mode": {
395+
"type": "string",
396+
"enum": ["container", "local"],
397+
"description": "container (required for trustworthy firefox/webkit baselines) runs capture in the pinned Playwright image; local captures are recorded with host=local and flagged by provenance."
398+
},
399+
"image": { "type": "string", "description": "Pinned Playwright container image (authoritative; scripts warn on drift vs the resolved @playwright/test version)." },
400+
"waitAfterLoadMs": { "type": "integer", "minimum": 0 },
401+
"fullPage": { "type": "boolean" }
402+
}
403+
},
404+
"provenance": {
405+
"type": "object",
406+
"additionalProperties": false,
407+
"properties": {
408+
"manifest": { "type": "string", "description": "Committed provenance manifest path (engine, Playwright version, image, sha256, gitSha, host per baseline)." },
409+
"policy": {
410+
"type": "string",
411+
"enum": ["warn", "enforce"],
412+
"description": "warn: provenance mismatches are reported but baselines are still diffed. enforce: flagged baselines are excluded from diffing and counted as failures."
413+
}
414+
}
415+
},
416+
"ciArtifact": {
417+
"type": "object",
418+
"additionalProperties": false,
419+
"description": "backend=ci-artifact only.",
420+
"properties": {
421+
"compareAgainst": { "type": "string", "enum": ["last-green-main"] },
422+
"retentionDays": { "type": "integer", "minimum": 1 }
423+
}
424+
},
425+
"service": {
426+
"type": "object",
427+
"additionalProperties": false,
428+
"description": "backend=service only. The provider owns capture, diffing, and review; opt-in, requires a token in CI secrets.",
429+
"properties": {
430+
"provider": { "type": "string", "enum": ["chromatic", "percy"] },
431+
"projectTokenEnv": { "type": "string", "description": "Name of the environment variable holding the provider project token." }
432+
}
433+
}
434+
}
435+
},
436+
365437
"deadCode": {
366438
"type": "object",
367439
"additionalProperties": false,

.claude/skills/figma-to-react-workflow/SKILL.md

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -480,22 +480,28 @@ For each page in build-spec:
480480

481481
### Step 3.2.5: Cross-Browser Verification
482482

483-
After Chromium passes the visual diff loop, verify in other browsers:
483+
After Chromium passes the visual diff loop, diff Firefox and WebKit against
484+
their committed baselines (RFC 0002):
484485

485486
```bash
486-
# Capture screenshots in Firefox and WebKit
487-
./scripts/cross-browser-test.sh firefox http://localhost:3000
488-
./scripts/cross-browser-test.sh webkit http://localhost:3000
489-
490-
# Compare against Chromium baseline (higher threshold for browser differences)
491-
node scripts/visual-diff.js --batch \
492-
.claude/visual-qa/screenshots/firefox \
493-
.claude/visual-qa/screenshots/chromium \
494-
--output-dir .claude/visual-qa/diffs/firefox-vs-chromium \
495-
--threshold 0.03
487+
# Compare current firefox/webkit screenshots against committed baselines
488+
# (threshold from visualBaselines.threshold, 0.03 for cross-engine tolerance)
489+
./scripts/cross-browser-baseline.sh compare http://localhost:3000 --json
496490
```
497491

498-
Cross-browser results are logged in the build report but are **not blocking** by default.
492+
If no cross-browser baselines exist yet, the compare skips with a hint —
493+
capture them deterministically first and commit the result:
494+
495+
```bash
496+
./scripts/cross-browser-baseline.sh capture http://localhost:3000 # pinned Playwright container
497+
git add .claude/visual-qa/baselines && git commit -m "test: add cross-browser baselines"
498+
```
499+
500+
Each baseline's provenance (engine, Playwright version, container image,
501+
sha256, host) is verified from `baselines/manifest.json` before diffing, so
502+
stale or locally-captured baselines are flagged instead of producing false
503+
pixel diffs. Results are logged in the build report but are **not blocking**
504+
by default (`visualBaselines.blocking`).
499505

500506
### Step 3.3: E2E Tests
501507

@@ -686,7 +692,8 @@ This skill works with:
686692
- **scripts/visual-diff.js** — Pixel-level screenshot comparison with region analysis
687693
- **scripts/verify-tokens.sh** — Token integrity enforcement in quality gate
688694
- **scripts/verify-test-coverage.sh** — Test existence verification (TDD gate)
689-
- **scripts/cross-browser-test.sh** — Multi-browser screenshot capture
695+
- **scripts/cross-browser-baseline.sh** — Cross-browser baseline capture + provenance-verified comparison (RFC 0002)
696+
- **scripts/cross-browser-test.sh** — Ad-hoc multi-browser screenshot capture (no comparison)
690697
- **scripts/generate-stories.sh** — AST-based Storybook story + MDX generation (Phase 4.5)
691698
- **.claude/pipeline.config.json** — Thresholds, iteration limits, app type config
692699

.claude/skills/parallel-orchestration/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ function hasResourceConflict(candidatePhase, runningPhases):
145145
| `visual-diff` | **Agent:** invoke visual-qa-verification skill with pixel-diff loop (max iterations from `iterationLoop.maxVisualIterations`) |
146146
| `dark-mode` | **Bash:** `./scripts/check-dark-mode.sh http://localhost:3000` |
147147
| `e2e-tests` | **Agent:** invoke e2e-test-generator skill with flows from build-spec.json |
148-
| `cross-browser` | **Bash:** `./scripts/cross-browser-test.sh firefox http://localhost:3000 && ./scripts/cross-browser-test.sh webkit http://localhost:3000` |
148+
| `cross-browser` | **Bash:** `./scripts/cross-browser-baseline.sh compare http://localhost:3000 --json` (diffs firefox/webkit against committed baselines with provenance checks — RFC 0002; skips with a capture hint when no baselines exist) |
149149
| `quality-gate` | **Parallel sub-dispatch:** run all `qualityGateSubtasks` concurrently (see below) |
150150
| `responsive` | **Bash:** `./scripts/check-responsive.sh http://localhost:3000` |
151151
| `report` | **Agent:** generate `build-report.md` in `.claude/visual-qa/` from all collected phase results, timings, and diff images |

.claude/skills/visual-qa-verification/SKILL.md

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -143,35 +143,28 @@ When the diff reports failing regions, use this mapping to prioritize fixes:
143143

144144
### Step 2.5: Cross-Browser Verification
145145

146-
After Chromium passes the visual diff loop, verify in Firefox and WebKit.
147-
148-
**Using Playwright MCP or cross-browser-test.sh:**
146+
After Chromium passes the visual diff loop, diff Firefox and WebKit against
147+
their committed same-engine baselines (RFC 0002):
149148

150149
```bash
151-
# Capture Firefox screenshots
152-
./scripts/cross-browser-test.sh firefox http://localhost:3000
153-
154-
# Capture WebKit screenshots
155-
./scripts/cross-browser-test.sh webkit http://localhost:3000
156-
157-
# Compare Firefox against Chromium baseline (not Figma)
158-
node scripts/visual-diff.js --batch \
159-
.claude/visual-qa/screenshots/firefox \
160-
.claude/visual-qa/screenshots/chromium \
161-
--output-dir .claude/visual-qa/diffs/firefox-vs-chromium \
162-
--threshold 0.03
163-
164-
# Compare WebKit against Chromium baseline
165-
node scripts/visual-diff.js --batch \
166-
.claude/visual-qa/screenshots/webkit \
167-
.claude/visual-qa/screenshots/chromium \
168-
--output-dir .claude/visual-qa/diffs/webkit-vs-chromium \
169-
--threshold 0.03
150+
# Compare current firefox/webkit screenshots against committed baselines,
151+
# with per-baseline provenance verification from baselines/manifest.json
152+
./scripts/cross-browser-baseline.sh compare http://localhost:3000 --json
153+
154+
# First time (no baselines committed yet): capture deterministically in the
155+
# pinned Playwright container, then commit
156+
./scripts/cross-browser-baseline.sh capture http://localhost:3000
157+
git add .claude/visual-qa/baselines
158+
159+
# Provenance-only check (no dev server needed)
160+
./scripts/cross-browser-baseline.sh verify
170161
```
171162

172-
**Cross-browser threshold:** Use a slightly higher threshold (3% vs 2%) since browser rendering engines have legitimate differences in anti-aliasing, font rendering, and sub-pixel positioning.
163+
For ad-hoc engine captures without comparison, `./scripts/cross-browser-test.sh <browser> <url>` still works.
164+
165+
**Cross-browser threshold:** `visualBaselines.threshold` (3% vs the 2% same-browser threshold) since browser rendering engines have legitimate differences in anti-aliasing, font rendering, and sub-pixel positioning.
173166

174-
**Cross-browser failures are NOT blocking** by default (configurable in `pipeline.config.json` `qualityGate.crossBrowserScreenshotsRequired`). They are reported in the build report for manual review.
167+
**Cross-browser failures are NOT blocking** by default (`visualBaselines.blocking`, and `qualityGate.crossBrowserScreenshotsRequired` for the gate). They are reported in the build report for manual review. Compares run outside the pinned container are marked *advisory* by the provenance check.
175168

176169
**Common cross-browser differences to ignore:**
177170
- Font rendering (especially on macOS WebKit vs Windows Chromium)
@@ -349,7 +342,8 @@ This skill works with:
349342
- **Playwright MCP** -- Cross-browser screenshots (Firefox, WebKit) and automated interaction testing
350343
- **Figma MCP** -- Source design screenshots for comparison (`get_screenshot`, `get_design_context`)
351344
- **scripts/visual-diff.js** -- Pixel-level screenshot comparison with region analysis
352-
- **scripts/cross-browser-test.sh** -- Multi-browser screenshot capture
345+
- **scripts/cross-browser-baseline.sh** -- Cross-browser baseline capture + provenance-verified comparison (RFC 0002)
346+
- **scripts/cross-browser-test.sh** -- Ad-hoc multi-browser screenshot capture (no comparison)
353347
- **.claude/pipeline.config.json** -- Thresholds, iteration limits, breakpoint configuration
354348

355349
## Verification Report Template

0 commit comments

Comments
 (0)