Skip to content

feat: make the failure message legible - #264

Merged
pftg merged 1 commit into
masterfrom
feat/legible-failure-message
Aug 24, 2026
Merged

feat: make the failure message legible#264
pftg merged 1 commit into
masterfrom
feat/legible-failure-message

Conversation

@pftg

@pftg pftg commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Two people ran the product and could not read its most important output.

Before (real run, test/fixtures/summary_line_case.rb, real git baselines)

  1) Failure:
SummaryLineCase#test_changed [test/fixtures/summary_line_case.rb:53]:
Screenshot does not match for 'changed': ({"area_size":100.0,"region":[20.0,15.0,30.0,25.0]})
/Users/me/app/screenshots/changed.png
/Users/me/app/screenshots/changed.base.diff.png
/Users/me/app/screenshots/changed.diff.png
/Users/me/app/screenshots/changed.heatmap.diff.png
/Users/me/app/test/fixtures/summary_line_case.rb:53:in 'block (2 levels) in <class:SummaryLineCase>'
...18 more minitest frames...

region is four bare numbers. area_size has no denominator. The four paths are
unlabelled and sit against the backtrace, so they read as stack noise -- and the
baseline (.base.png) is not among them: the file listed first is the capture that
just overwrote it.

After (same run)

  1) Failure:
SummaryLineCase#test_changed [test/fixtures/summary_line_case.rb:53]:
Screenshot does not match for 'changed': the change spans 100 of 6400 px (1.56% of the 80x80 image)
  changed region: [20.0,15.0,30.0,25.0] (left,top,right,bottom edges)
  judged against: no tolerance thresholds configured (any difference fails)
  baseline:           screenshots/changed.base.png
  actual:             screenshots/changed.png
  baseline annotated: screenshots/changed.base.diff.png
  actual annotated:   screenshots/changed.diff.png
  heatmap:            screenshots/changed.heatmap.diff.png
/Users/me/app/test/fixtures/summary_line_case.rb:53:in 'block (2 levels) in <class:SummaryLineCase>'
...

With a tolerance set, the metric block also carries its unit:

  difference level: 0.00765625 (0.77% of the image area)
  judged against: tolerance 0.001, color_distance_limit 20

What changed

  • Labels and order. Baseline first, then the actual capture, then what we drew
    on them. A label is printed only when that file is on disk -- chunky_png
    produces no diff mask, so it gets no heatmap line (the old message printed one
    regardless; same family as The new-screenshot error prints a path that will never exist in CI #260).
  • Numbers mean something. area_size gets a denominator and a percentage of
    total pixels plus the image dimensions. region is stated as edge coordinates --
    verified in Region#to_edge_coordinates, not guessed. difference_level is the
    changed share of the image area, so it is printed with that unit and only when a
    tolerance made us compute it.
  • The threshold is restated (VCR-style), or the message says plainly that none
    was configured and any difference fails.
  • Relative paths (to SnapDiff.config.root), absolute when the file is outside
    it. Chosen because most terminals resolve relative paths against the working
    directory, so they stay click-through-able while being short enough to read --
    and short paths visually separate the artifact block from the absolute backtrace
    frames below it, which was half the "reads as noise" complaint.

Not changed

Pass/fail behaviour, exit codes, and the end-of-run summary line
([snap_diff] 1 verified, 1 changed, 0 new) are untouched -- the after-run above
still prints it verbatim.

Evidence

  • Seven guard tests in test/unit/reporters/default_test.rb, each mutation-checked:
    the denominator, the region interpretation, the difference-level unit, the
    threshold line, the labels, the on-disk filter, and both directions of the
    relative/absolute path rule. Every mutation went red; each was restored by a
    targeted edit and confirmed green.
  • Surfacing checked under Minitest and RSpec with real runs in fresh
    processes (RSpec indents the block under Failure/Error: and it holds up).
    Cucumber re-raises the same ExpectationNotMet string with no message handling
    of its own.
  • rake test:unit (617 runs), rake test:canonical (512 runs), standardrb lib test
    all green.

Expected test churn

Two expectations in test/unit/dsl_test.rb pinned the old wording verbatim and are
updated -- a commissioned wording change, not a behaviour break. One of them was
asserting a heatmap.diff.png path that chunky_png never writes.

Summary by Sourcery

Improve screenshot mismatch failure messages so their metrics, thresholds, and diagnostic artifacts are immediately understandable.

New Features:

  • Make screenshot mismatch failures easier to interpret with contextual metrics, threshold information, and clearly labeled artifact paths.

Bug Fixes:

  • Avoid listing screenshot artifacts that were not created and prevent unreadable or misleading failure details.

Enhancements:

  • Report changed area as pixels and image percentage, clarify region coordinates and difference-level units, and use concise relative paths where possible.

Tests:

  • Expand reporter coverage for metrics, thresholds, artifact labeling and filtering, and relative versus absolute paths.

A differing screenshot printed a JSON blob and four unlabelled absolute
paths. Nothing said whether `region` was [x,y,w,h] or edge coordinates,
`area_size` and `difference_level` had no denominator or unit, and the
paths -- pressed against ~20 lines of minitest/activesupport backtrace --
read as noise. The one path a reader wants most, the committed baseline
`.base.png`, was not among them: the file listed first was the capture
that had just overwritten it.

Now, from live state only:

  Screenshot does not match for 'changed': the change spans 100 of 6400 px (1.56% of the 80x80 image)
    changed region: [20.0,15.0,30.0,25.0] (left,top,right,bottom edges)
    difference level: 0.00765625 (0.77% of the image area)
    judged against: tolerance 0.001
    baseline:           screenshots/changed.base.png
    actual:             screenshots/changed.png
    baseline annotated: screenshots/changed.base.diff.png
    actual annotated:   screenshots/changed.diff.png
    heatmap:            screenshots/changed.heatmap.diff.png

- every artifact is labelled, baseline first, and a label is printed only
  when that file is on disk (chunky_png writes no heatmap; issue #260 is
  the message naming a file CI never wrote)
- paths are relative to SnapDiff.config.root, absolute when outside it
- `judged against` restates the thresholds that were actually applied, or
  says plainly that none were configured

Pass/fail behaviour and exit codes are untouched; so is the end-of-run
summary line. The two DSL expectations that pinned the old wording are
updated -- and one of them was asserting a heatmap path that chunky_png
never writes.

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

Sorry @pftg, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 52 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ff09382-1007-4b77-8512-63cb431bea44

📥 Commits

Reviewing files that changed from the base of the PR and between f9ef044 and 95b6ae1.

📒 Files selected for processing (3)
  • lib/snap_diff/reporters/default.rb
  • test/unit/dsl_test.rb
  • test/unit/reporters/default_test.rb

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors the default screenshot comparison failure message to be structured, explanatory, and artifact-aware, including denominated metrics, threshold restatement, and labeled, relative artifact paths, with tests updated and expanded to lock in the new behavior.

Sequence diagram for structured screenshot failure reporting

sequenceDiagram
    participant Comparison
    participant DefaultReporter
    participant Driver
    participant SnapDiffConfig
    participant Filesystem

    Comparison->>DefaultReporter: build_error_message()
    DefaultReporter->>Driver: dimension(comparison.base_image)
    Driver-->>DefaultReporter: width, height
    DefaultReporter->>Comparison: difference.region_area_size
    DefaultReporter->>Comparison: difference.coordinates
    DefaultReporter->>Comparison: difference.ratio
    DefaultReporter->>Comparison: difference.options
    DefaultReporter->>Comparison: difference.meta
    DefaultReporter->>SnapDiffConfig: config.root
    DefaultReporter->>Filesystem: path.exist? for each artifact
    Filesystem-->>DefaultReporter: on-disk artifact paths
    DefaultReporter-->>Comparison: headline, metric_lines, artifact_lines
Loading

File-Level Changes

Change Details Files
Rework failure message generation to produce a structured, human-readable summary with denominated metrics and threshold information.
  • Replace the old JSON dump plus raw paths with a composed message built from a headline, metric lines, and artifact lines.
  • Compute total pixels and changed area to express the difference both as absolute pixels and percentage of the image, including dimensions.
  • Include explicit interpretation of the region coordinates and conditionally print difference level and max distances with units.
lib/snap_diff/reporters/default.rb
Add artifact labeling and on-disk filtering with relative/absolute path logic for comparison outputs.
  • Define ordered artifact labels mapping to underlying path methods for baseline, actual, annotated images, and heatmap.
  • Filter artifacts to only those whose files actually exist before printing, and align labels for readability.
  • Implement display_path to show paths relative to SnapDiff.config.root when inside it, and absolute otherwise.
lib/snap_diff/reporters/default.rb
Introduce threshold listing logic and supporting constants for failure messages.
  • Add THRESHOLDS constant to define the order and names of configurable limits relevant to comparisons.
  • Implement thresholds helper to render applied threshold values or a clear message when none are configured.
  • Ensure the metric block always includes a "judged against" line summarizing configuration context.
lib/snap_diff/reporters/default.rb
Update DSL tests to expect the new structured failure message format and relative artifact paths.
  • Adjust assert_image_not_changed expectations to match the new headline, metric, and artifact lines.
  • Change expected paths to be relative to SnapDiff.config.root and omit non-existent heatmap artifacts.
  • Preserve test intent that behavior is unchanged aside from message wording and artifact listing correctness.
test/unit/dsl_test.rb
Expand reporter tests to cover message structure, numeric interpretations, thresholds, artifact labeling, and path rules.
  • Replace previous metric presence checks with assertions against the new message fields and absence of leaked Ruby object signatures.
  • Add tests verifying denominators for area and region interpretation, and that difference_level is expressed with its unit.
  • Add tests ensuring thresholds are restated, artifacts are labeled and filtered by existence, and paths obey relative/absolute rules.
  • Introduce helper methods to set SnapDiff.config.root and to build comparisons with configurable options.
test/unit/reporters/default_test.rb

Possibly linked issues

  • #issue: PR implements issue items 3 and 4 by improving failure diagnostics, artifact labels, thresholds, and relative paths.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@pftg
pftg merged commit 88c20ea into master Aug 24, 2026
8 checks passed
@pftg
pftg deleted the feat/legible-failure-message branch August 24, 2026 08:45
pftg added a commit that referenced this pull request Aug 24, 2026
The v2.0.0 section was written before #250, #253, #254, #255, #256, #261,
#263, #264, #266 and #267 landed, and three of its claims had gone false:

- "Known limitations: fork-based parallel tests produce no HTML report ...
  Fixed in 2.1" -- fixed in 2.0 by #266. Reproduced both sides here:
  1.15.1 + `parallelize(workers: 2, threshold: 0)` writes NO report and
  prints no summary line; master writes one merged report and
  `4 verified, 4 changed, 0 new`.
- "a suite whose only contact with the v1 API is
  `require \"capybara_screenshot_diff/minitest\"` + `include ...Assertions`
  still prints nothing" -- #263 made the require doors warn. That exact
  setup now prints the migration notice; verified in a scratch project.
- "Two removals 2.0 cannot warn about ... `driver:` as a setting" -- #263
  made both the setting writer and the per-screenshot key warn. Verified:
  `Capybara::Screenshot::Diff.driver = :vips` prints the removal line with
  a call site.

And the silent-by-design constant list repeated the shape of the beta2
`defined?` mistake: it listed "Os, Region" inside a run of
`Capybara::Screenshot::Diff::` names. Probed on master --
`defined?(Capybara::Screenshot::Diff::Os)` and
`defined?(Capybara::Screenshot::Diff::Region)` are both nil. The real
names are `Capybara::Screenshot::Os` and the top-level `Region`, neither
of which existed under `::Diff` in 1.15.1 either. Fully qualified now, and
`::Comparison` added to match docs/UPGRADING.md.

New material, every claim checked against the code or a live run:

- a "why upgrade" section for the four green-suite-testing-nothing bugs
  (#255, #256, #254, #266), plus the unfollowable CI message (#267) and
  the fail_if_new precedence change
- before/after transcripts of the failure message (#264), taken from the
  same page rendered on 1.15.1 and on master
- the summary line (#261), with the fact that it comes from the HTML
  reporter and needs its one-line require -- an omission that would have
  read as a missing feature
- the #250 / #253 perf table, attributed to its harness, with columns
  labelled before/after rather than 1.x/2.0
- the libvips fix is stated as guarded on libvips 8.15+, so a reader on an
  older libvips knows the bug is still theirs

Install snippets stay pinned to 2.0.0.beta3 on purpose: `~> 2.0` resolves
to nothing on rubygems today. docs/RELEASE_PREP.md already carries a
precise step to swap all five (its grep finds exactly those five), and
gains one line so the record-modes placeholder in the entry cannot ship
unfilled.

`rake test:unit` 651 runs / 0 failures, `standardrb lib test` clean.
@pftg pftg mentioned this pull request Aug 24, 2026
pftg added a commit that referenced this pull request Aug 24, 2026
beta3 fixed the canonical entry points and shipped almost none of the behaviour.
beta4 is the prerelease the 2.0.0 entry actually describes: the four green-suite
bugs (#254 #255 #256 #258), the accept workflow (#259), the legible failure
message (#264), and the deprecation warnings that make 2.1's removals visible
(#246 #263).

- `lib/snap_diff/version.rb` -> 2.0.0.beta4. Nothing else holds a version; the
  gemspec, the legacy version file and the mirror gemspec all read it. Verified
  with the release workflow's own guard command:
  `ruby -I lib -r capybara/screenshot/diff/version -e "puts Capybara::Screenshot::Diff::VERSION"`
  => 2.0.0.beta4
- CHANGELOG: a `[v2.0.0.beta4]` section written as the delta from beta3, plus the
  record-modes PLACEHOLDER filled from #259 now that it has shipped.
  `grep -n PLACEHOLDER CHANGELOG.md` returns nothing.
- Install snippets moved beta3 -> beta4 in README, docs/UPGRADING.md and
  docs/migration-guide.md. They stay PINNED: `~> 2.0` resolves to nothing while
  only prereleases exist, so unpinning belongs to 2.0.0 final, not here.

The notes name the #272 caveat explicitly. Removing `skip_area`'s implicit
stabilization wait (10.012 s -> 0.009 s measured) means a selector not yet in
the DOM now yields no mask, silently, where it previously resolved after the
wait. #277's run-level tally shipped in the same beta as the replacement signal,
and the notes say so rather than leaving it to be discovered.

Gates: `rake test:unit` 720 runs / 2124 assertions / 0 failures / 0 skips under
CI=true on 4.0.6, `standardrb lib test` clean over 161 files, and
`gem build` produces capybara-screenshot-diff-2.0.0.beta4.gem (93 files, 13 docs,
RELEASE_PREP correctly excluded).
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.

1 participant