Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/capybara/screenshot/diff/reporters/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def build_error_for_different_dimensions

def build_error_message
[
"(#{difference.inspect})",
"(#{difference.to_h.to_json})",
image_path.to_path,
annotated_base_image_path.to_path,
annotated_image_path.to_path,
Expand Down
7 changes: 6 additions & 1 deletion lib/snap_diff/comparison_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,13 @@ def coordinates
region&.to_edge_coordinates
end

# One-line debugging summary with the difference metrics.
# (Error messages use #to_h — see Reporters::Default#build_error_message.)
def inspect
to_h.to_json
"#<#{self.class.name} different=#{different?} failed_by=#{failed_by.inspect} " \
"area_size=#{region_area_size} region=#{coordinates.inspect} " \
"difference_level=#{ratio.inspect} " \
"base=#{original_image_path} new=#{new_image_path}>"
end

def tolerable?
Expand Down
48 changes: 36 additions & 12 deletions lib/snap_diff/screenshot_assertion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,39 @@ def initialize(name, **args)
@args = args
end

# One-line debugging summary. Never triggers the (expensive, file-touching)
# comparison itself: an unprocessed comparison shows as "pending".
def inspect
return "#<#{self.class.name} #{name.inspect} (no comparison)>" unless compare

state = if compare.processed?
compare.difference.different? ? "different" : "matches"
else
"pending"
end
"#<#{self.class.name} #{name.inspect} #{state} new=#{compare.image_path} base=#{compare.base_image_path}>"
end

def validate
return unless compare

self.class.assert_image_not_changed(caller, name, compare)
if compare.different?
"Screenshot does not match for '#{name}': #{compare.error_message}\n#{caller.join("\n")}"
else
archive_baseline!
nil
end
end

# Commits the baseline after a passing comparison: the base image is
# moved over the actual image, so the captured screenshot becomes the
# recorded baseline again. This is the file-mutating half of the verify
# flow, kept explicit and separate from the pure "are they different?"
# question. Idempotent: a second call is a no-op.
def archive_baseline!
return unless compare && !compare.different? && compare.base_image_path.exist?

FileUtils.mv(compare.base_image_path, compare.image_path, force: true)
end

def validate!
Expand Down Expand Up @@ -50,18 +79,13 @@ def self.verify_screenshots!(screenshots)
# @param name [String] The name of the screenshot being verified.
# @param comparison [Object] The comparison object containing the result and details of the comparison.
# @return [String, nil] Returns an error message if the screenshot differs from the baseline, otherwise nil.
# @note This method is used internally to verify individual screenshots.
# @note Legacy entry point; delegates to the instance verify flow
# (pure question + explicit #archive_baseline! on pass).
def self.assert_image_not_changed(backtrace, name, comparison)
result = comparison.different?

# Cleanup after comparisons
if !result && comparison.base_image_path.exist?
FileUtils.mv(comparison.base_image_path, comparison.image_path, force: true)
end

return unless result

"Screenshot does not match for '#{name}': #{comparison.error_message}\n#{backtrace.join("\n")}"
assertion = new(name)
assertion.caller = backtrace
assertion.compare = comparison
assertion.validate
end
end

Expand Down
10 changes: 10 additions & 0 deletions test/unit/difference_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,15 @@ class DifferenceTest < ActiveSupport::TestCase
test "#failed? returns true when images have different dimensions" do
assert_predicate @difference, :failed?
end

test "#inspect is a one-line summary with the difference metrics" do
line = @difference.inspect

assert_includes line, "different=true"
assert_includes line, "failed_by="
assert_includes line, "area_size=0"
assert_includes line, "difference_level="
assert_not_includes line, "\n"
end
end
end
121 changes: 121 additions & 0 deletions test/unit/screenshot_assertion_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# frozen_string_literal: true

require "test_helper"
require "capybara_screenshot_diff"

module SnapDiff
# Pins the baseline-archiving side effect of the verify flow: when a
# comparison passes, the base image is moved over the actual image
# (the baseline is "committed" and the temp base copy disappears).
# These guards protect the file-level behavior while the mutation is
# extracted from the read path into an explicit archive step.
class ScreenshotAssertionTest < ActiveSupport::TestCase
include CapybaraScreenshotDiff::DSL
include CapybaraScreenshotDiff::DSLStub

test "#validate! archives the baseline when the comparison passes" do
comparison = make_comparison(:a, :a)
assertion = build_assertion(comparison)

assertion.validate!

assert_not comparison.base_image_path.exist?, "base image must be archived (moved over the actual image) on pass"
assert_predicate comparison.image_path, :exist?
end

test "#validate! keeps the baseline and raises when the comparison fails" do
comparison = make_comparison(:a, :b)
assertion = build_assertion(comparison)

assert_raises(CapybaraScreenshotDiff::ExpectationNotMet) { assertion.validate! }

assert comparison.base_image_path.exist?, "base image must be kept for the reporter on failure"
end

test "verify archives baselines of passing delayed assertions end-to-end" do
Capybara::Screenshot::Diff::Vcs.stub(:checkout_vcs, true) do
snap = create_snapshot_for(:a, :a)

assert_matches_screenshot(snap.full_name) # delayed by default
assert comparison_for(snap.full_name).base_image_path.exist?, "verify has not run yet: base image must still be present"

CapybaraScreenshotDiff.verify

assert_not snap.base_path.exist?, "verify must archive the baseline of a passing assertion"
assert_predicate snap.path, :exist?
end
end

test "verify keeps baselines of failing delayed assertions end-to-end" do
Capybara::Screenshot::Diff::Vcs.stub(:checkout_vcs, true) do
snap = create_snapshot_for(:a, :b)

assert_matches_screenshot(snap.full_name) # delayed by default

assert_raises(CapybaraScreenshotDiff::ExpectationNotMet) { CapybaraScreenshotDiff.verify }

assert snap.base_path.exist?, "base image must be kept for the reporter on failure"
end
end

test "#archive_baseline! moves the base image over the actual image and is idempotent" do
comparison = make_comparison(:a, :a)
assertion = build_assertion(comparison)

assertion.archive_baseline!

assert_not comparison.base_image_path.exist?
assert_predicate comparison.image_path, :exist?

assertion.archive_baseline! # second call is a no-op

assert_predicate comparison.image_path, :exist?
end

test "#archive_baseline! keeps the baseline when the comparison differs" do
comparison = make_comparison(:a, :b)
assertion = build_assertion(comparison)

assertion.archive_baseline!

assert comparison.base_image_path.exist?
end

test "#inspect is a one-line summary that does not run the comparison" do
comparison = make_comparison(:a, :b)
assertion = build_assertion(comparison, name: "widget")

line = assertion.inspect

assert_includes line, '"widget"'
assert_includes line, "pending"
assert_includes line, comparison.image_path.to_s
assert_includes line, comparison.base_image_path.to_s
assert_not_includes line, "\n"
assert_not comparison.processed?, "#inspect must not trigger the comparison"
end

test "#inspect shows the verified state after the comparison ran" do
comparison = make_comparison(:a, :b)
assertion = build_assertion(comparison)
assertion.validate

assert_includes assertion.inspect, "different"

assert_includes SnapDiff::ScreenshotAssertion.new("fresh").inspect, "no comparison"
end

private

def build_assertion(comparison, name: "name")
SnapDiff::ScreenshotAssertion.new(name).tap do |assertion|
assertion.compare = comparison
assertion.caller = ["my_test.rb:42"]
end
end

def comparison_for(name)
CapybaraScreenshotDiff.assertions.find { |assertion| assertion.name == name }.compare
end
end
end
Loading