Skip to content

fix(e2e): make visual-regression editor captures deterministic#2192

Merged
ascorbic merged 1 commit into
mainfrom
fix/visual-regression-determinism
Jul 22, 2026
Merged

fix(e2e): make visual-regression editor captures deterministic#2192
ascorbic merged 1 commit into
mainfrom
fix/visual-regression-determinism

Conversation

@ascorbic

Copy link
Copy Markdown
Collaborator

What does this PR do?

Makes the visual-regression suite deterministic on the editor screens and stops the measure job from flagging drift on flaky/infra failures.

Two independent fixes:

  1. Spec: blur before capture. content-editor and content-new mount a TipTap toolbar whose buttons reflect the editor's focus/active state. Whether the editor grabs focus during hydration is a race, so a run could capture a focused (highlighted) toolbar button or an unfocused one, producing spurious diffs. stabilize() now blurs the active element before awaiting fonts, so the capture depends on the rendered page rather than who won the focus race.

  2. Workflow: decide drift from git status, not the exit code. The measure job regenerates baselines with --update-snapshots after a failing diff, then decided "drift" from whether the candidates directory was non-empty. But cp -r always copies the committed baselines (which always exist), so that check flagged drift on any non-zero exit, including a flaky render that regenerated byte-identical baselines. The decision is now git status --porcelain on the snapshots dir: a modified/untracked PNG is real drift (including the bootstrap case, where a new screen has no committed baseline yet); a clean tree after regeneration is treated as flake or infra failure, keeps the check green, and emits a warning.

Root cause was confirmed from PR #2185's measure artifact (run 29865573115, head 86d30b1): the run-1 actual had a highlighted toolbar button while the --update-snapshots candidate was byte-identical to the committed baseline.

Follow-up to #2147 (suite + CI) and #2165 (baselines).

Type of change

  • Bug fix

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable)
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8

Screenshots / test output

The determinism fix cannot be meaningfully validated on macOS (Linux baselines differ), so verification happens on this PR's own measure runs. Residual fallback if flakiness persists on those two screens: mask the editor toolbar region for content-editor/content-new.

Blur the active element before capturing so a hydration focus race on the
TipTap toolbar cannot leave a button highlighted in some runs and not
others, which produced spurious diffs on the content-editor and content-new
screens.

Also decide visual drift from git status of the snapshots directory rather
than from a non-zero Playwright exit. A flaky render can fail the first-pass
diff yet regenerate byte-identical baselines under --update-snapshots; the
old candidate-emptiness check always saw the committed baselines and flagged
drift regardless. Now a clean tree after regeneration is treated as flake or
infra failure and keeps the check green with a warning.
Copilot AI review requested due to automatic review settings July 22, 2026 10:52
@changeset-bot

changeset-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f1aa36f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground f1aa36f Jul 22 2026, 09:45 AM

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache f1aa36f Jul 22 2026, 09:45 AM

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-do f1aa36f Jul 22 2026, 09:45 AM

@pkg-pr-new

pkg-pr-new Bot commented Jul 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2192

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2192

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2192

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2192

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2192

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2192

emdash

npm i https://pkg.pr.new/emdash@2192

create-emdash

npm i https://pkg.pr.new/create-emdash@2192

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2192

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2192

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2192

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2192

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2192

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2192

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2192

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2192

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2192

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2192

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2192

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2192

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2192

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2192

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2192

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2192

commit: f1aa36f

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right fix for a real problem. The visual suite's editor captures were nondeterministic because of a focus race during hydration, and the measure job was mislabeling flaky/infra exits as drift because cp -r always produced a non-empty candidates dir. Blurring the active element before capture is a pragmatic, low-cost way to stabilize the toolbar state, and switching the drift decision from exit code to git status --porcelain correctly distinguishes changed/new baselines from byte-identical regeneration.

What I checked:

  • The diff, the full visual.yml workflow, and the related visual-report.yml / visual-apply.yml workflows.
  • The full e2e/tests/visual-regression.spec.ts spec and the .gitignore rules for snapshot files.
  • AGENTS.md conventions that could apply to this e2e/CI-only change (changesets, comments, query counts, localization, RTL, SQL, authorization). No violations there; no published package is touched, so no changeset is needed.

Headline concern: the implementation intentionally treats any non-zero visual-suite exit with a git-clean snapshots tree as flake or infra. That is correct for the documented flaky-render case, but it also swallows genuine test/setup failures that prevent screenshots from being generated at all (e.g., a JS error or navigation timeout on one screen). In those cases the regeneration also leaves the tree clean, so the measure job exits green and the report workflow clears/op-noops the sticky comment, leaving no artifact that a maintainer can inspect. Because the visual job is explicitly advisory this is a trade-off, not a hard blocker, but it's worth adding a non-drift artifact path that preserves the failure signal.

The spec change itself looks correct: blur() is called only on a non-body HTMLElement, and it runs after the freeze CSS and before waiting for fonts, so the capture reflects a consistent unfocused state.

Comment on lines +111 to +115
# or infra and keep the check green with a warning.
if [ -z "$(git status --porcelain -- "$SNAP_DIR")" ]; then
echo "false" > visual-out/has-drift
echo "has_drift=false" >> "$GITHUB_OUTPUT"
echo "::warning::Visual suite failed without producing candidate baselines (likely an infra failure, not a UI diff)."
else
echo "true" > visual-out/has-drift
echo "has_drift=true" >> "$GITHUB_OUTPUT"
echo "Visual drift detected — candidates and diff images staged for the Report workflow."
echo "::warning::Visual suite exited non-zero but regenerated baselines are identical to the committed ones (flaky render or infra failure, not a UI diff)."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The git-clean branch now treats every non-zero exit without baseline changes as flake/infra and exits 0. That correctly handles the documented flaky-render case, but it also makes the measure job green for genuine failures that prevent screenshots from being generated in the first place (e.g., a JS error or navigation timeout on a screen, which also leaves the snapshots tree unchanged). The ::warning:: is visible in the job log, but the report workflow will delete or skip the sticky comment, so there's no durable signal for maintainers.

Consider preserving test-results/ (logs, traces, or at least the Playwright output) in the artifact on this path, so a failure that isn't drift can still be investigated without re-running the suite.

Suggested change
# or infra and keep the check green with a warning.
if [ -z "$(git status --porcelain -- "$SNAP_DIR")" ]; then
echo "false" > visual-out/has-drift
echo "has_drift=false" >> "$GITHUB_OUTPUT"
echo "::warning::Visual suite failed without producing candidate baselines (likely an infra failure, not a UI diff)."
else
echo "true" > visual-out/has-drift
echo "has_drift=true" >> "$GITHUB_OUTPUT"
echo "Visual drift detected — candidates and diff images staged for the Report workflow."
echo "::warning::Visual suite exited non-zero but regenerated baselines are identical to the committed ones (flaky render or infra failure, not a UI diff)."
if [ -z "$(git status --porcelain -- "$SNAP_DIR")" ]; then
echo "false" > visual-out/has-drift
echo "has_drift=false" >> "$GITHUB_OUTPUT"
echo "::warning::Visual suite exited non-zero but regenerated baselines are identical to the committed ones (flaky render or infra failure, not a UI diff)."
# Keep failure artifacts around even though we aren't reporting drift,
# so setup/screenshot-prevention failures remain inspectable.
cp -r test-results visual-out/test-results 2>/dev/null || true
exit 0
fi

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jul 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to reduce flakiness in the admin visual-regression pipeline by making editor screenshots deterministic and by changing the CI “drift” decision logic to rely on actual snapshot file changes rather than a Playwright exit code.

Changes:

  • Updates the visual-regression spec to blur the active element as part of the stabilization sequence to avoid focus-dependent toolbar rendering differences.
  • Updates the Visual Regression measure workflow to detect drift via git status --porcelain on the snapshots directory after --update-snapshots, treating a clean tree as flake/infra instead of drift.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
e2e/tests/visual-regression.spec.ts Adjusts stabilization steps to reduce focus-state nondeterminism before screenshots.
.github/workflows/visual.yml Changes drift detection to be based on actual snapshot diffs (git status) rather than non-zero test exit codes.
Comments suppressed due to low confidence (1)

e2e/tests/visual-regression.spec.ts:128

  • The focus blur happens before awaiting document.fonts.ready, which leaves a window where the editor can re-focus itself (e.g. a post-hydration effect) while fonts are still loading. That defeats the determinism goal because the screenshot can still capture a focused/active toolbar state on slow font loads.

Move the blur to after fonts have settled (right before the screenshot), or blur both before and after the font wait to close the race.

	// Drop focus before capturing. The editor pages mount a TipTap toolbar
	// whose buttons reflect the editor's focus/active state; whether the editor
	// grabs focus during hydration is a race, so a run can capture a focused
	// (highlighted) toolbar button or an unfocused one. Blurring makes the
	// capture depend on the rendered page, not on who won the focus race.
	await admin.page.evaluate(() => {
		const active = document.activeElement;
		if (active instanceof HTMLElement && active !== document.body) {
			active.blur();
		}
	});
	// Await font loading without returning the FontFaceSet that
	// document.fonts.ready fulfils with -- Playwright cannot serialize it.
	await admin.page.evaluate(async () => {
		await document.fonts.ready;
	});

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@ascorbic
ascorbic merged commit 6379873 into main Jul 22, 2026
53 checks passed
@ascorbic
ascorbic deleted the fix/visual-regression-determinism branch July 22, 2026 11:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci cla: signed review/awaiting-author Reviewed; waiting on the author to respond size/S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants