Skip to content

--browser webkit: shoot both sides in WebKit - #156

Merged
tmashara merged 4 commits into
mainfrom
claude/issue-150
Nov 5, 2026
Merged

tmashara merged 4 commits into
mainfrom
claude/issue-150

Conversation

@tmashara

@tmashara tmashara commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Closes #150

--browser webkit (or browser: "webkit" in the config) shoots both sides in Playwright's WebKit, for CSS that only Safari reads differently. The prefix check from #155 covers the issue's own case, which no WebKit pixel shows. This option covers the cases that do render differently. The new box-decoration-break fixture is one: an autofix drops -webkit-box-decoration-break, which gives pixel parity in Chromium and a diff in WebKit.

What changes

  • Capture. WebKit has no CDP, so grab() falls back to page.screenshot({ fullPage: true, clip }). Tiling at 16,384px is kept, so pageShot and captureTiled are unchanged. WebKit holds document.fonts.ready, which its screenshot waits on, until the load event; a stalled frame never fires that, so the page is window.stop()ped before the shot when it is still loading.
  • --computed. snapshotStates falls back to the plain walk, because forcing :hover/:focus-visible needs CDP. The run prints one line saying so. Rule coverage and the prefix check are plain JS and work as on Chromium.
  • Config. browser is optional in ResolvedConfig (like lottieFrame), so a Chromium run's config hash, and with it --rerun-failed, is unchanged. A WebKit run defaults to renderdiff-out-webkit and renderdiff.accepted.webkit.json, so its pixel hashes never prune Chromium's entries. merge refuses to mix engines. The cache key already includes the browser.
  • Action and CI. The Action gets a browser input (install, cache key, flag, accepted file name) and its self-test gains a WebKit step. CI installs chromium webkit.
  • Not done. GIFs aren't frozen in WebKit: there's no image-animation setting, so they show as unstable and unmeasured, which is documented. No isMobile, and no running both engines in one run.

Chromium stays byte-identical

Every WebKit branch is gated on the engine. I ran the whole corpus (60 routes × 2 widths) on main and on this branch, then main against main as a control. In both comparisons the only differences are the fixtures that vary per load by design (clock, bistable, unsettled, drifting) and the fixture server's port inside failed-asset URLs and error text. Every other PNG and results.json entry is byte-identical.

Tests

  • tests/visual/webkit.test.ts:
    • box-decoration-break: parity in Chromium, a diff in WebKit;
    • flex-vs-grid scores 100% and color doesn't;
    • tall is stitched to 18,000px;
    • stalled-frame is shot in normal time;
    • animated is unstable, not a diff;
    • --computed works without states and prints the note;
    • --accept writes the .webkit file and leaves Chromium's alone;
    • the default out dir is renderdiff-out-webkit.
  • Unit: --browser and config validation; merge refusing mixed engines.
  • typecheck, lint, format:check and npm test pass, as do the webkit, computed and guardrails visual files. Before the rebase, the controls, equivalence and accepted visual files passed too.

🤖 Generated with Claude Code

tmashara and others added 4 commits November 4, 2026 14:45
WebKit has no CDP, so a shot falls back to page.screenshot({ fullPage,
clip }), tiles kept, and --computed walks without forced states and says
so. WebKit holds fonts.ready, which its screenshot waits on, until the
load event, so a page a stalled frame keeps loading is stopped first.

browser is optional in ResolvedConfig, so a Chromium run's config hash
and capture path are unchanged. A webkit run writes renderdiff-out-webkit
and renderdiff.accepted.webkit.json by default, and merge refuses to mix
engines. The Action takes a browser input; CI installs WebKit.

Closes #150

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
window.stop() fails the page's open requests (a pending fetch rejects,
images break), so it is a last resort: the WebKit shot gets 5s, and only
past that is the load stopped and the shot retaken, with the side noted
as stopped in results.json and the row. GIF docs say "usually" caught as
unstable; the Action's browser input is documented as deciding over the
config; the rerun refusal names --browser; the missing-WebKit hint names
--with-deps. Tests trimmed per review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/engine.ts holds an Engine interface with a chromium and a webkit
implementation: output defaults, launch, grab, shot and forced states.
capture, run, probe and the CLI resolve it once with engineOf(cfg) and
pass it to pageShot, captureTiled and snapshotStates, so isWebKit, the
launch branch, browserOf and the out-dir and accepted-file ternaries go.

A stalled WebKit shot is no longer retaken: window.stop() lets the shot
already in flight finish. Its 5s now covers a whole tiled shot, not each
tile. The stopped flag is a local in capture, not a WeakSet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed asset

The 5s now spans a whole tiled shot, so a slow finished page was flagged
stopped. window.stop() now runs only while the load is open, and that is
the flag. A hung or rejected evaluate no longer costs a finished shot.

A stopped shot read as clean: broken() ignored it and the note printed
only with --progress. It now counts as broken, so it is never clean or
reused, shows under the row and in report.html, and evidence.html says
Not verified. Like a failed asset it leaves the exit code alone, and a
stopped right side isn't recorded for --cache-right.

ENGINES goes, for Object.keys(engines), and engine.ts loses comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tmashara
tmashara merged commit 5211bdd into main Nov 5, 2026
3 checks passed
@tmashara tmashara mentioned this pull request Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shoot in WebKit too: Safari-only regressions pass a Chromium-only run

1 participant