snap_diff-capybara 0.0.1 → 2.0.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +190 -0
  3. data/CODE_OF_CONDUCT.md +129 -0
  4. data/LICENSE.txt +21 -0
  5. data/Rakefile +65 -0
  6. data/capybara-screenshot-diff.gemspec +29 -0
  7. data/docs/RELEASE_PREP.md +44 -0
  8. data/docs/UPGRADING.md +702 -0
  9. data/docs/architecture.md +289 -0
  10. data/docs/ci-integration.md +238 -0
  11. data/docs/configuration.md +395 -0
  12. data/docs/docker-testing.md +24 -0
  13. data/docs/drivers.md +102 -0
  14. data/docs/framework-setup.md +87 -0
  15. data/docs/images/snap_diff_annotated.png +0 -0
  16. data/docs/images/snap_diff_web_ui.png +0 -0
  17. data/docs/migration-guide.md +286 -0
  18. data/docs/organization.md +204 -0
  19. data/docs/reporters.md +46 -0
  20. data/docs/thread_safety.md +112 -0
  21. data/gems.rb +39 -0
  22. data/lib/capybara/screenshot/diff/annotation_service.rb +7 -0
  23. data/lib/capybara/screenshot/diff/area_calculator.rb +7 -0
  24. data/lib/capybara/screenshot/diff/browser_helpers.rb +7 -0
  25. data/lib/capybara/screenshot/diff/config_legacy.rb +113 -0
  26. data/lib/capybara/screenshot/diff/cucumber.rb +3 -0
  27. data/lib/capybara/screenshot/diff/difference.rb +8 -0
  28. data/lib/capybara/screenshot/diff/drivers/base_driver.rb +9 -0
  29. data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +7 -0
  30. data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +7 -0
  31. data/lib/capybara/screenshot/diff/drivers.rb +9 -0
  32. data/lib/capybara/screenshot/diff/image_compare.rb +12 -0
  33. data/lib/capybara/screenshot/diff/image_preprocessor.rb +7 -0
  34. data/lib/capybara/screenshot/diff/os.rb +14 -0
  35. data/lib/capybara/screenshot/diff/region.rb +107 -0
  36. data/lib/capybara/screenshot/diff/reporters/default.rb +109 -0
  37. data/lib/capybara/screenshot/diff/screenshot_matcher.rb +7 -0
  38. data/lib/capybara/screenshot/diff/screenshoter.rb +7 -0
  39. data/lib/capybara/screenshot/diff/stable_screenshoter.rb +7 -0
  40. data/lib/capybara/screenshot/diff/utils.rb +7 -0
  41. data/lib/capybara/screenshot/diff/vcs.rb +7 -0
  42. data/lib/capybara/screenshot/diff/version.rb +15 -0
  43. data/lib/capybara/screenshot/diff.rb +3 -0
  44. data/lib/capybara-screenshot-diff.rb +3 -0
  45. data/lib/capybara_screenshot_diff/attempts_reporter.rb +7 -0
  46. data/lib/capybara_screenshot_diff/cucumber.rb +8 -0
  47. data/lib/capybara_screenshot_diff/dsl.rb +12 -0
  48. data/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb +8 -0
  49. data/lib/capybara_screenshot_diff/minitest.rb +14 -0
  50. data/lib/capybara_screenshot_diff/reporters/html.rb +7 -0
  51. data/lib/capybara_screenshot_diff/rspec.rb +8 -0
  52. data/lib/capybara_screenshot_diff/screenshot_assertion.rb +71 -0
  53. data/lib/capybara_screenshot_diff/screenshot_namer.rb +7 -0
  54. data/lib/capybara_screenshot_diff/snap.rb +7 -0
  55. data/lib/capybara_screenshot_diff/snap_manager.rb +7 -0
  56. data/lib/capybara_screenshot_diff/static.rb +9 -0
  57. data/lib/capybara_screenshot_diff.rb +50 -0
  58. data/lib/snap_diff/annotation_service.rb +84 -0
  59. data/lib/snap_diff/area_calculator.rb +56 -0
  60. data/lib/snap_diff/attempts_reporter.rb +51 -0
  61. data/lib/snap_diff/browser_helpers.rb +121 -0
  62. data/lib/snap_diff/capture/viewport.rb +40 -0
  63. data/lib/snap_diff/comparison.rb +238 -0
  64. data/lib/snap_diff/comparison_result.rb +104 -0
  65. data/lib/snap_diff/config.rb +78 -0
  66. data/lib/snap_diff/deprecation.rb +84 -0
  67. data/lib/snap_diff/driver.rb +37 -0
  68. data/lib/snap_diff/drivers/chunky_png_driver.rb +298 -0
  69. data/lib/snap_diff/drivers/vips_driver.rb +171 -0
  70. data/lib/snap_diff/drivers.rb +14 -0
  71. data/lib/snap_diff/dsl.rb +143 -0
  72. data/lib/snap_diff/error_with_filtered_backtrace.rb +32 -0
  73. data/lib/snap_diff/image_preprocessor.rb +68 -0
  74. data/lib/snap_diff/integrations/cucumber.rb +22 -0
  75. data/lib/snap_diff/integrations/minitest.rb +70 -0
  76. data/lib/snap_diff/integrations/rspec.rb +68 -0
  77. data/lib/snap_diff/legacy_shims.rb +99 -0
  78. data/lib/snap_diff/os.rb +17 -0
  79. data/lib/snap_diff/reporters/html.rb +143 -0
  80. data/lib/snap_diff/reporters/templates/report.html.erb +463 -0
  81. data/lib/snap_diff/reporting.rb +53 -0
  82. data/lib/snap_diff/screenshot_assertion.rb +143 -0
  83. data/lib/snap_diff/screenshot_matcher.rb +113 -0
  84. data/lib/snap_diff/screenshot_namer.rb +81 -0
  85. data/lib/snap_diff/screenshoter.rb +129 -0
  86. data/lib/snap_diff/snap.rb +66 -0
  87. data/lib/snap_diff/snap_manager.rb +125 -0
  88. data/lib/snap_diff/stable_screenshoter.rb +103 -0
  89. data/lib/snap_diff/static.rb +11 -0
  90. data/lib/snap_diff/utils.rb +38 -0
  91. data/lib/snap_diff/vcs.rb +35 -0
  92. data/lib/snap_diff/version.rb +5 -0
  93. data/lib/snap_diff.rb +65 -0
  94. metadata +160 -10
  95. data/README.md +0 -3
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "snap_diff/deprecation"
4
+ require "snap_diff/drivers"
5
+
6
+ # ADR-004 v2 step 6: const_missing-based forwarders for the pre-v2
7
+ # namespaces. Every old-name lookup below resolves -- lazily -- to the exact
8
+ # same object as its SnapDiff:: replacement (identity pinned by
9
+ # test/unit/namespace_forwarding_test.rb) and emits a deprecation warning,
10
+ # once per constant per process, silenceable via
11
+ # SnapDiff.silence_deprecations or SNAP_DIFF_SILENCE_DEPRECATIONS=1.
12
+ #
13
+ # Deliberately eager-and-silent exceptions (plain constants, defined by
14
+ # their own forwarder files, never warn):
15
+ #
16
+ # - Capybara::Screenshot::Os and CapybaraScreenshotDiff::DSL (and the
17
+ # unmapped CapybaraScreenshotDiff::Minitest::Assertions): advertised
18
+ # entry-point constants probed with Object.const_defined? by
19
+ # support_load_probe_test.rb -- const_defined? never triggers
20
+ # const_missing, so a lazy shim would break that contract.
21
+ # - Capybara::Screenshot::Diff::VERSION: the gemspec resolves it at build
22
+ # time; a lazy shim would make every `gem build` warn.
23
+ # - Drivers::ChunkyPNGDriver / Drivers::VipsDriver: real constants on the
24
+ # shared SnapDiff::Drivers module (the Drivers alias is same-object by
25
+ # contract), so const_missing can never fire for the leaf names;
26
+ # resolving them through the old path still warns for ...::Drivers.
27
+ module SnapDiff
28
+ # @api private
29
+ module LegacyShims
30
+ # Installs a warn-then-forward const_missing on +namespace+.
31
+ #
32
+ # @param namespace [Module] the old namespace to hook
33
+ # @param old_prefix [String] how the old constant path reads to a human
34
+ # @param mapping [Hash{Symbol => String}] old leaf name => new full name
35
+ def self.install(namespace, old_prefix, mapping)
36
+ namespace.define_singleton_method(:const_missing) do |name|
37
+ target = mapping[name]
38
+ return super(name) unless target
39
+
40
+ Deprecation.warn("#{old_prefix}::#{name}", target, category: :constant)
41
+ Object.const_get(target)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ module Capybara
48
+ module Screenshot
49
+ module Diff
50
+ end
51
+ end
52
+ end
53
+
54
+ module CapybaraScreenshotDiff
55
+ module Reporters
56
+ end
57
+ end
58
+
59
+ SnapDiff::LegacyShims.install(Capybara::Screenshot, "Capybara::Screenshot", {
60
+ BrowserHelpers: "SnapDiff::BrowserHelpers",
61
+ Screenshoter: "SnapDiff::Screenshoter"
62
+ }.freeze)
63
+
64
+ SnapDiff::LegacyShims.install(Capybara::Screenshot::Diff, "Capybara::Screenshot::Diff", {
65
+ Vcs: "SnapDiff::Vcs",
66
+ StableScreenshoter: "SnapDiff::StableScreenshoter",
67
+ ImagePreprocessor: "SnapDiff::ImagePreprocessor",
68
+ AreaCalculator: "SnapDiff::AreaCalculator",
69
+ AnnotationService: "SnapDiff::AnnotationService",
70
+ Utils: "SnapDiff::Utils",
71
+ ScreenshotMatcher: "SnapDiff::ScreenshotMatcher",
72
+ Drivers: "SnapDiff::Drivers",
73
+ ImageCompare: "SnapDiff::Comparison",
74
+ Difference: "SnapDiff::ComparisonResult"
75
+ }.freeze)
76
+
77
+ SnapDiff::LegacyShims.install(CapybaraScreenshotDiff, "CapybaraScreenshotDiff", {
78
+ RED_RGBA: "SnapDiff::RED_RGBA",
79
+ ORANGE_RGBA: "SnapDiff::ORANGE_RGBA",
80
+ SnapManager: "SnapDiff::SnapManager",
81
+ Snap: "SnapDiff::Snap",
82
+ ScreenshotNamer: "SnapDiff::ScreenshotNamer",
83
+ AttemptsReporter: "SnapDiff::AttemptsReporter",
84
+ BacktraceFilter: "SnapDiff::BacktraceFilter",
85
+ ErrorWithFilteredBacktrace: "SnapDiff::ErrorWithFilteredBacktrace",
86
+ ScreenshotAssertion: "SnapDiff::ScreenshotAssertion",
87
+ AssertionRegistry: "SnapDiff::AssertionRegistry"
88
+ }.freeze)
89
+
90
+ SnapDiff::LegacyShims.install(CapybaraScreenshotDiff::Reporters, "CapybaraScreenshotDiff::Reporters", {
91
+ HTML: "SnapDiff::Reporters::HTML"
92
+ }.freeze)
93
+
94
+ # BaseDriver dissolved into the SnapDiff::Driver mixin (v2 step 4); the
95
+ # Drivers alias is same-object, so the hook has to live on SnapDiff::Drivers
96
+ # itself. `class MyDriver < BaseDriver` becomes `include SnapDiff::Driver`.
97
+ SnapDiff::LegacyShims.install(SnapDiff::Drivers, "Capybara::Screenshot::Diff::Drivers", {
98
+ BaseDriver: "SnapDiff::Driver"
99
+ }.freeze)
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SnapDiff
4
+ module Os
5
+ ON_WINDOWS = !!(RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/)
6
+ ON_MAC = !!(RbConfig::CONFIG["host_os"] =~ /darwin/)
7
+ ON_LINUX = !!(RbConfig::CONFIG["host_os"] =~ /linux/)
8
+
9
+ def self.name
10
+ return "windows" if ON_WINDOWS
11
+ return "macos" if ON_MAC
12
+ return "linux" if ON_LINUX
13
+
14
+ "unknown"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "erb"
5
+ require "fileutils"
6
+ require "pathname"
7
+ require "json"
8
+ # The auto-registration block at the bottom of this file calls
9
+ # CapybaraScreenshotDiff.reporters, which needs the registry singleton
10
+ # machinery in the *old* capybara_screenshot_diff/screenshot_assertion.rb
11
+ # file (deliberately not moved -- see that file's own comment).
12
+ require "capybara_screenshot_diff/screenshot_assertion"
13
+ require "capybara/screenshot/diff/config_legacy"
14
+
15
+ module SnapDiff
16
+ module Reporters
17
+ class HTML
18
+ attr_reader :failures, :total
19
+
20
+ def initialize(output_path: nil, embed_images: false)
21
+ @explicit_output_path = output_path
22
+ @embed_images = embed_images
23
+ @failures = []
24
+ @total = 0
25
+ @finalized = false
26
+ @mutex = Mutex.new
27
+ end
28
+
29
+ def record(assertions)
30
+ return if @finalized
31
+
32
+ failures = []
33
+ total = 0
34
+
35
+ assertions.each do |assertion|
36
+ compare = assertion.compare
37
+ next unless compare
38
+
39
+ total += 1
40
+ next unless compare.difference&.different?
41
+
42
+ failures << failure_entry_for(assertion.name, compare)
43
+ rescue => e
44
+ warn "[snap_diff] Reporter skipped '#{assertion.name}': #{e.message}" if ENV["DEBUG"]
45
+ end
46
+
47
+ @mutex.synchronize do
48
+ return if @finalized
49
+ @total += total
50
+ @failures.concat(failures)
51
+ end
52
+ end
53
+
54
+ def finalize
55
+ @mutex.synchronize do
56
+ return if @finalized
57
+ return if failures.empty?
58
+
59
+ write_report
60
+ @finalized = true
61
+ output_path
62
+ end
63
+ end
64
+
65
+ def output_path
66
+ @output_path ||= Pathname.new(@explicit_output_path || self.class.default_output_path)
67
+ end
68
+
69
+ def passed = total - failures.size
70
+ def failed = failures.size
71
+
72
+ def summary
73
+ return if total.zero?
74
+
75
+ screenshots_label = (total == 1) ? "1 screenshot" : "#{total} screenshots"
76
+
77
+ if failures.empty?
78
+ "[snap_diff] #{screenshots_label} compared, no failures."
79
+ else
80
+ failures_label = (failures.size == 1) ? "1 failure" : "#{failures.size} failures"
81
+ "[snap_diff] #{screenshots_label} compared, #{failures_label}. Report: #{output_path}"
82
+ end
83
+ end
84
+
85
+ def render
86
+ ERB.new(File.read(self.class.template_path)).result(binding)
87
+ end
88
+
89
+ def self.template_path
90
+ File.expand_path("templates/report.html.erb", __dir__)
91
+ end
92
+
93
+ def self.default_output_path
94
+ root = Capybara::Screenshot.root || Pathname.pwd
95
+ root / Capybara::Screenshot.save_path / "snap_diff_report.html"
96
+ end
97
+
98
+ private
99
+
100
+ def failure_entry_for(name, compare)
101
+ difference = compare.difference
102
+ {
103
+ name: name,
104
+ original: resolve_image(compare.base_image_path),
105
+ new: resolve_image(compare.image_path),
106
+ base_diff: resolve_image(compare.reporter.annotated_base_image_path),
107
+ diff: resolve_image(compare.reporter.annotated_image_path),
108
+ heatmap: resolve_image(compare.reporter.heatmap_diff_path),
109
+ diff_level: difference.ratio && (difference.ratio * 100).round(2),
110
+ area_size: difference.region_area_size,
111
+ max_color_distance: difference.meta[:max_color_distance]&.round(1)
112
+ }
113
+ end
114
+
115
+ def resolve_image(path)
116
+ return unless path
117
+
118
+ pathname = Pathname.new(path).expand_path
119
+ return unless pathname.exist?
120
+
121
+ @embed_images ? data_uri(pathname) : pathname.relative_path_from(output_path.dirname.expand_path).to_s
122
+ end
123
+
124
+ def data_uri(pathname)
125
+ ext = pathname.extname.delete_prefix(".")
126
+ mime = (ext == "webp") ? "image/webp" : "image/png"
127
+ "data:#{mime};base64,#{Base64.strict_encode64(pathname.binread)}"
128
+ end
129
+
130
+ def write_report
131
+ FileUtils.mkdir_p(output_path.dirname)
132
+ File.write(output_path, render)
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ # Auto-register reporter.
139
+ # Framework adapters (Minitest, RSpec, Cucumber) call finalize_reporters! via native hooks.
140
+ # For custom frameworks, call CapybaraScreenshotDiff.finalize_reporters! manually.
141
+ unless CapybaraScreenshotDiff.reporters.any?(SnapDiff::Reporters::HTML)
142
+ CapybaraScreenshotDiff.reporters << SnapDiff::Reporters::HTML.new(embed_images: !!ENV["CI"])
143
+ end