lookbook_visual_tester 0.5.4 → 0.5.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52de57eac4b35b3aba43c82000005b796b7bfb405fed5914ffda3ff3d6039b2f
4
- data.tar.gz: a6c8e5faf0af703732a0438766299bb1300879997c64d6cc96188e1a2a3ab47b
3
+ metadata.gz: c4dcc1d8c929065528b1f5ad9ee02dac8a2b0520e72d110ec105f954fcdfcf65
4
+ data.tar.gz: ce57354faceb6b0035fd89ef6bfbe6c394c0cc616bd8093929d68a26cd323b88
5
5
  SHA512:
6
- metadata.gz: 78ee1ea32f42cbf2fe58f4178f03e7db493ab1598d1d80a65918c497bff2cb51778f6413b321ae8cda01248bd1714fd72ec0b44d6a968d33273234de21836d5c
7
- data.tar.gz: 945d4767c88ec3121c11c6ea83e87ca286a5f5d8a1feb07b1fb10275b0c9139066047a348f9b6ce2f3d06d25f1ae09645a75aeba5dfdd99497cf2def48dddddf
6
+ metadata.gz: cc7e7d15b48f2d4aef69244851d1bba41dd8b40dc0709562bcdf764485dee2846ec69848891671701fc89ea1f001555627aed0fe631fd823cc527a1e3ead108a
7
+ data.tar.gz: 593a40641363733cffbc4c578b3e9ef94c25491ccb6f48a2230ddb12e91290baaea9738387225adfe7b45a36fb95c8002ea63d098e37be8d12a2815f82990939
@@ -0,0 +1,59 @@
1
+ # Lookbook Visual Tester Style Guide & Best Practices
2
+
3
+ ## Project Overview
4
+ `lookbook_visual_tester` is a Ruby gem designed for visual regression testing of Lookbook previews. It uses `Ferrum` for headless browser interactions and `ChunkyPNG` for image comparison.
5
+
6
+ ## Architecture & patterns
7
+
8
+ ### Service Objects
9
+ - Isolate complex logic in service objects inheriting from `LookbookVisualTester::Service` (if applicable) or plain Ruby classes in `lib/lookbook_visual_tester/services/`.
10
+ - Examples: `ImageComparator`, `ScenarioFinder`.
11
+
12
+ ### Configuration
13
+ - Use `LookbookVisualTester.configure` to set global options.
14
+ - Options are defined in `lib/lookbook_visual_tester/configuration.rb`.
15
+ - Default base path for screenshots is `coverage/screenshots` (as of v0.5.5).
16
+
17
+ ### Path Handling
18
+ - Use `Pathname` for all file system paths.
19
+ - **Screenshot Structure**:
20
+ - `[base_path]/[baseline|current|diffs]/[folder_name]/[filename].png`
21
+ - `folder_name` is either the variant slug (e.g., `dark-mode`) or `default` if no variant is present.
22
+ - Never allow files to be dumped at the root of the screenshot directories; always enforce a subdirectory.
23
+
24
+ ## Visual Testing Logic
25
+
26
+ ### Image Comparison
27
+ - **Dimension Mismatch**: Do not raise errors for images of different sizes.
28
+ - Create a canvas size equal to the maximum width and height of the two images.
29
+ - Compare overlapping pixels.
30
+ - Mark non-overlapping pixels (from the larger image) as differences.
31
+ - Used in: `LookbookVisualTester::Services::ImageComparator`.
32
+
33
+ ### Screenshot Capture
34
+ - Use `Ferrum` to capture screenshots.
35
+ - Ensure consistent viewport sizes and wait for animations/fonts if necessary (managed in `Runner` logic).
36
+
37
+ ## Testing (`RSpec`)
38
+
39
+ ### Structure
40
+ - **Unit Tests**: `spec/lib/lookbook_visual_tester/...`
41
+ - **Integration Tests**: `spec/integration/...`
42
+ - Use `spec_helper.rb` for common setup.
43
+
44
+ ### Best Practices
45
+ - **Reproduction Scripts**: somewhat complex bugs (like visual mismatches) should be isolated in top-level `repro_*.rb` scripts before fixing.
46
+ - **Mocking**: Be careful with circular dependencies when mocking `Lookbook` objects (e.g., `Scenario` <-> `Preview`). Use `allow(...).to receive(...)` logic in `before` blocks rather than definitions to break cycles.
47
+
48
+ ## Versioning & Changelog
49
+ - **Versioning**: Semantic versioning in `lib/lookbook_visual_tester/version.rb`.
50
+ - **Changelog**: Maintain `CHANGELOG.md` with `[Unreleased]` and version headers. Group changes by `Added`, `Changed`, `Fixed`.
51
+
52
+ ## Coding Style
53
+ - Standard Ruby style (indentation: 2 spaces).
54
+ - Prefer descriptive variable names over short cryptic ones.
55
+ - Use `frozen_string_literal: true` where appropriate (though not strictly enforced everywhere yet).
56
+
57
+ ## Versioning & Changelog
58
+
59
+ When updating the version, make sure to update the `CHANGELOG.md` file with the new version number and a list of changes since the last release. The changes should be grouped by `Added`, `Changed`, and `Fixed`. Also update `README.md` accordingly.
data/CHANGELOG.md CHANGED
@@ -1,4 +1,25 @@
1
1
  # Changelog
2
+ ## [0.5.6] - 2026-01-06
3
+
4
+ ### Fixed
5
+ - **Image Comparison**: Fixed `Dimensions mismatch` error by creating a diff canvas sized to the maximum dimensions of widely differing images.
6
+ - **Missing Action Error**: Resolved `ActionNotFound` for `render_scenario_to_string` by ensuring the preview controller properly includes `Lookbook::PreviewControllerActions`.
7
+ - **Unexpected Headers**: Fixed an issue where the `example_icons` preview in the dummy app included an unwanted `<h1>Icons</h1>` header.
8
+ - **Spec Fix**: Resolved a `SystemStackError` (circular dependency) in `ScenarioFinder` specs.
9
+
10
+ ### Changed
11
+ - **Folder Structure**: Refactored screenshot organization. Non-variant screenshots are now stored in a `default` subfolder within `coverage/screenshots`.
12
+ - **Deep Check**: Enhanced `rake lookbook:deep_check` to validate that the configured preview controller supports required Lookbook actions.
13
+
14
+ ## [0.5.5] - 2026-01-06
15
+
16
+ ### Fixed
17
+ - **Image Comparison**: Fixed `Dimensions mismatch` error. Now, when images have different dimensions, a diff image is generated on a canvas sized to the maximum dimensions, clearly showing the differences and the mismatched areas.
18
+
19
+ ### Changed
20
+ - **Folder Structure**: Screenshots are now saved in `coverage/screenshots` by default (previously `spec/visual_screenshots`).
21
+ - **Default Subfolder**: Non-variant screenshots are now stored in a `default` subfolder to maintain consistency with variant-based runs.
22
+
2
23
  ## [0.5.4] - 2026-01-06
3
24
 
4
25
  ### Fixed
data/README.md CHANGED
@@ -45,7 +45,7 @@ You can configure the tester in a Rails initializer:
45
45
  ```ruby
46
46
  LookbookVisualTester.configure do |config|
47
47
  config.lookbook_host = "http://localhost:3000" # Where your rails app is running
48
- config.base_path = "spec/visual_regression" # Root for screenshots
48
+ config.base_path = "coverage/screenshots" # Root for screenshots
49
49
  config.copy_to_clipboard = true # Enable xclip support
50
50
  config.threads = 4 # Number of parallel threads (default: 4)
51
51
  end
@@ -101,7 +101,7 @@ You can run your visual tests against multiple configurations (variants), such a
101
101
  * **`{"theme":"dark"}`**: Runs with `_display[theme]=dark`.
102
102
  * **`{"width":"Mobile"}`**: Runs with `_display[width]=375px` AND automatically resizes the browser window to 375px width.
103
103
 
104
- Screenshots for variants are saved in dedicated subfolders (e.g., `spec/visual_regression/baseline/theme-dark/`).
104
+ Screenshots for variants are saved in dedicated subfolders (e.g., `coverage/screenshots/baseline/theme-dark/`).
105
105
 
106
106
 
107
107
  ### Baseline Management
data/Rakefile CHANGED
@@ -3,6 +3,8 @@
3
3
  require 'bundler/gem_tasks'
4
4
  require 'rspec/core/rake_task'
5
5
  require 'rubocop/rake_task'
6
+ # Load gem tasks
7
+ Dir.glob('lib/tasks/*.rake').each { |r| import r }
6
8
 
7
9
  RSpec::Core::RakeTask.new(:spec)
8
10
 
@@ -13,14 +13,12 @@ module LookbookVisualTester
13
13
  DEFAULT_THREADS = 4
14
14
 
15
15
  def initialize
16
- @base_path = if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.test?
17
- Pathname.new(Dir.pwd).join('spec/visual_screenshots')
18
- elsif defined?(Rails) && Rails.respond_to?(:root) && Rails.root
19
- Rails.root.join('spec/visual_screenshots')
20
- else
21
- # Fallback for non-Rails environments
22
- Pathname.new(Dir.pwd).join('spec/visual_screenshots')
23
- end
16
+ root_path = if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
17
+ Rails.root
18
+ else
19
+ Pathname.new(Dir.pwd)
20
+ end
21
+ @base_path = root_path.join('coverage/screenshots')
24
22
  @baseline_dir = @base_path.join('baseline')
25
23
  @current_dir = @base_path.join('current_run')
26
24
  @diff_dir = @base_path.join('diff')
@@ -18,6 +18,7 @@ module LookbookVisualTester
18
18
  def deep_check
19
19
  # Ensure custom setup is run before deep checks
20
20
  run_setup
21
+ check_preview_controller_config
21
22
  run_checks(:deep_render_check)
22
23
  end
23
24
 
@@ -204,6 +205,27 @@ module LookbookVisualTester
204
205
  end
205
206
  end
206
207
 
208
+ def check_preview_controller_config
209
+ return unless defined?(Rails)
210
+
211
+ controller_name = Rails.application.config.view_component.preview_controller
212
+ return unless controller_name
213
+
214
+ begin
215
+ controller_class = controller_name.constantize
216
+ unless controller_class.action_methods.include?('render_scenario_to_string')
217
+ puts "WARNING: Configured preview controller '#{controller_name}' does not have 'render_scenario_to_string' action."
218
+ puts ' This is required for Lookbook to render previews correctly.'
219
+ puts " Please ensure your preview controller inherits from 'Lookbook::PreviewController'."
220
+ # We could raise an error here to fail deep_check
221
+ raise "Preview Controller '#{controller_name}' missing required action 'render_scenario_to_string'"
222
+ end
223
+ rescue NameError
224
+ puts "WARNING: Configured preview controller '#{controller_name}' could not be loaded."
225
+ raise "Preview Controller '#{controller_name}' could not be loaded"
226
+ end
227
+ end
228
+
207
229
  def run_setup
208
230
  if @config.preview_checker_setup
209
231
  @config.preview_checker_setup.call
@@ -142,16 +142,8 @@ module LookbookVisualTester
142
142
  # Determine paths
143
143
  current_path = run_data.current_path
144
144
  baseline_path = run_data.baseline_path
145
- diff_path = @config.diff_dir.join(run_data.diff_filename)
146
- # Update diff path to respect variant structure if needed?
147
- # Actually ScenarioRun#diff_filename is just flat for now but let's fix that?
148
- # ScenarioRun doesn't expose diff_path with slug. Let's fix that manually here if needed or update ScenarioRun.
149
- # Wait, ScenarioRun stores baseline/current in folders but diff_filename is just name.
150
- # We should probably put diffs in folders too.
151
- # Let's adjust diff_path here:
152
- if variant_slug.present?
153
- diff_path = @config.diff_dir.join(variant_slug, run_data.diff_filename)
154
- end
145
+ folder_name = variant_slug.presence || 'default'
146
+ diff_path = @config.diff_dir.join(folder_name, run_data.diff_filename)
155
147
 
156
148
  FileUtils.mkdir_p(File.dirname(current_path))
157
149
  FileUtils.mkdir_p(File.dirname(diff_path))
@@ -40,16 +40,16 @@ module LookbookVisualTester
40
40
  "#{preview_name}_#{scenario_name}_diff.png"
41
41
  end
42
42
 
43
+ def folder_name
44
+ variant_slug.presence || 'default'
45
+ end
46
+
43
47
  def current_path
44
- base = LookbookVisualTester.config.current_dir
45
- base = base.join(variant_slug) if variant_slug.present?
46
- base.join(filename)
48
+ LookbookVisualTester.config.current_dir.join(folder_name).join(filename)
47
49
  end
48
50
 
49
51
  def baseline_path
50
- base = LookbookVisualTester.config.baseline_dir
51
- base = base.join(variant_slug) if variant_slug.present?
52
- base.join(filename)
52
+ LookbookVisualTester.config.baseline_dir.join(folder_name).join(filename)
53
53
  end
54
54
 
55
55
  def preview_url
@@ -16,41 +16,45 @@ module LookbookVisualTester
16
16
 
17
17
  def call
18
18
  unless File.exist?(baseline_path)
19
- return { diff_path: nil, mismatch: 0.0, error: "Baseline not found" }
19
+ return { diff_path: nil, mismatch: 0.0, error: 'Baseline not found' }
20
20
  end
21
21
 
22
22
  baseline = ChunkyPNG::Image.from_file(baseline_path)
23
23
  current = ChunkyPNG::Image.from_file(current_path)
24
24
 
25
- if baseline.dimension != current.dimension
26
- return {
27
- diff_path: nil,
28
- mismatch: 100.0,
29
- error: "Dimensions mismatch: #{baseline.width}x#{baseline.height} vs #{current.width}x#{current.height}"
30
- }
31
- end
25
+ max_width = [baseline.width, current.width].max
26
+ max_height = [baseline.height, current.height].max
32
27
 
33
28
  diff_pixels_count = 0
34
- diff_image = ChunkyPNG::Image.new(baseline.width, baseline.height, ChunkyPNG::Color::WHITE)
29
+ diff_image = ChunkyPNG::Image.new(max_width, max_height, ChunkyPNG::Color::WHITE)
30
+
31
+ max_height.times do |y|
32
+ max_width.times do |x|
33
+ in_baseline = x < baseline.width && y < baseline.height
34
+ in_current = x < current.width && y < current.height
35
35
 
36
- baseline.height.times do |y|
37
- baseline.width.times do |x|
38
- pixel1 = baseline[x, y]
39
- pixel2 = current[x, y]
36
+ if in_baseline && in_current
37
+ pixel1 = baseline[x, y]
38
+ pixel2 = current[x, y]
40
39
 
41
- if pixel1 != pixel2
40
+ if pixel1 == pixel2
41
+ # Blue context for unchanged pixels to make it easier for humans
42
+ gray_val = ChunkyPNG::Color.r(ChunkyPNG::Color.grayscale_teint(pixel1))
43
+ # Keep intensity in R/G but push blue to make it the dominant tint
44
+ diff_image[x, y] = ChunkyPNG::Color.rgba(gray_val, gray_val, 255, 50)
45
+ else
46
+ diff_image[x, y] = DIFF_COLOR
47
+ diff_pixels_count += 1
48
+ end
49
+ elsif in_baseline || in_current
50
+ # Pixel exists in one but not the other -> Mismatch
42
51
  diff_image[x, y] = DIFF_COLOR
43
52
  diff_pixels_count += 1
44
- else
45
- # Blue context for unchanged pixels to make it easier for humans
46
- gray_val = ChunkyPNG::Color.r(ChunkyPNG::Color.grayscale_teint(pixel1))
47
- # Keep intensity in R/G but push blue to make it the dominant tint
48
- diff_image[x, y] = ChunkyPNG::Color.rgba(gray_val, gray_val, 255, 50)
49
53
  end
50
54
  end
51
55
  end
52
56
 
53
- mismatch_percentage = (diff_pixels_count.to_f / baseline.pixels.size) * 100.0
57
+ mismatch_percentage = (diff_pixels_count.to_f / (max_width * max_height)) * 100.0
54
58
 
55
59
  if diff_pixels_count > 0
56
60
  FileUtils.mkdir_p(File.dirname(diff_path))
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LookbookVisualTester
4
- VERSION = '0.5.4'
4
+ VERSION = '0.5.6'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lookbook_visual_tester
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.5.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Murilo Vasconcelos
@@ -172,6 +172,7 @@ executables: []
172
172
  extensions: []
173
173
  extra_rdoc_files: []
174
174
  files:
175
+ - ".agent/rules/style_guide.md"
175
176
  - ".rubocop.yml"
176
177
  - ".ruby-version"
177
178
  - CHANGELOG.md