lookbook_visual_tester 0.6.0 → 0.7.0

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.
@@ -38,8 +38,7 @@ namespace :lookbook do
38
38
  task :list, [:format] => :environment do |_, args|
39
39
  require 'lookbook'
40
40
  previews = Lookbook.previews.flat_map do |preview|
41
- group = preview.respond_to?(:scenarios) ? preview.scenarios : preview.examples
42
- group.map { |scenario| scenario.lookup_path }
41
+ preview.scenarios.map { |scenario| scenario.lookup_path }
43
42
  end.sort
44
43
 
45
44
  if args[:format] == 'json'
@@ -54,8 +53,10 @@ namespace :lookbook do
54
53
  preview_name = args[:preview_name]
55
54
  format = args[:format]
56
55
 
56
+ json_mode = format == 'json'
57
+
57
58
  unless preview_name
58
- if format == 'json'
59
+ if json_mode
59
60
  LookbookVisualTester::JsonOutputHandler.print({ error: 'Please provide a preview name' })
60
61
  else
61
62
  puts 'Please provide a preview name: rake lookbook:screenshot[Button]'
@@ -63,21 +64,12 @@ namespace :lookbook do
63
64
  exit 1
64
65
  end
65
66
 
66
- # Run the test (which generates screenshots)
67
- runner = LookbookVisualTester::Runner.new(pattern: preview_name)
68
-
69
- # Silence stdout if json format, to avoid pollution
70
- original_stdout = $stdout
71
- $stdout = File.new('/dev/null', 'w') if format == 'json'
72
-
73
- begin
74
- results = runner.run
75
- ensure
76
- $stdout = original_stdout if format == 'json'
77
- end
67
+ output = json_mode ? File.open(File::NULL, 'w') : $stdout
68
+ runner = LookbookVisualTester::Runner.new(pattern: preview_name, output: output)
69
+ results = runner.run
70
+ output.close if json_mode
78
71
 
79
- if format == 'json'
80
- # Transform results to simple hash
72
+ if json_mode
81
73
  json_results = results.map do |r|
82
74
  {
83
75
  scenario_name: r.scenario_name,
@@ -90,9 +82,8 @@ namespace :lookbook do
90
82
  }
91
83
  end
92
84
 
93
- # If single result, return just object, else array
94
- output = json_results.size == 1 ? json_results.first : json_results
95
- LookbookVisualTester::JsonOutputHandler.print(output)
85
+ output_payload = json_results.size == 1 ? json_results.first : json_results
86
+ LookbookVisualTester::JsonOutputHandler.print(output_payload)
96
87
  else
97
88
  print_cli_summary(results)
98
89
  end
@@ -100,21 +91,16 @@ namespace :lookbook do
100
91
 
101
92
  desc 'Run visual regression tests for all previews'
102
93
  task :test, [:format] => :environment do |_, args|
103
- runner = LookbookVisualTester::Runner.new(force_update: args[:format] == 'force' || ENV['UPDATE'] == 'true')
104
-
105
- # Check for ENV var or arg
106
94
  json_mode = args[:format] == 'json' || ENV['JSON_OUTPUT'] == 'true'
95
+ output = json_mode ? File.open(File::NULL, 'w') : $stdout
107
96
 
108
- original_stdout = $stdout
109
- $stdout = File.new('/dev/null', 'w') if json_mode
110
-
111
- begin
112
- results = runner.run
113
- ensure
114
- $stdout = original_stdout if json_mode
115
- end
97
+ runner = LookbookVisualTester::Runner.new(
98
+ force_update: args[:format] == 'force' || ENV['UPDATE'] == 'true',
99
+ output: output
100
+ )
101
+ results = runner.run
102
+ output.close if json_mode
116
103
 
117
- # Save results for retry logic
118
104
  if defined?(LookbookVisualTester.config.diff_dir)
119
105
  result_file = LookbookVisualTester.config.diff_dir.join('last_run.json')
120
106
  FileUtils.mkdir_p(File.dirname(result_file))
@@ -160,35 +146,45 @@ namespace :lookbook do
160
146
  end
161
147
  end
162
148
 
149
+ desc 'Start Rails server, run visual tests, then stop the server'
150
+ task server_and_test: :environment do
151
+ LookbookVisualTester::ServerTestRunner.call(
152
+ timeout: ENV.fetch('LOOKBOOK_SERVER_TIMEOUT', '60').to_i,
153
+ test_task: 'lookbook:test',
154
+ log_path: ENV['LOOKBOOK_SERVER_LOG']
155
+ )
156
+ end
157
+
163
158
  desc 'Approve a specific preview change (update baseline)'
164
159
  task :approve, [:preview_name] => :environment do |_, args|
165
160
  preview_name = args[:preview_name]
166
161
  unless preview_name
167
- puts 'Please provide a preview name: rake lookbook:approve[Button/default]'
162
+ puts 'Please provide a preview name: rake "lookbook:approve[ui/button/default]"'
168
163
  exit 1
169
164
  end
170
165
 
166
+ normalized = preview_name.tr('/', '_').tr(' ', '_')
171
167
  baseline_dir = LookbookVisualTester.config.baseline_dir
172
168
  current_dir = LookbookVisualTester.config.current_dir
173
169
 
174
- # Find matching files in current_dir
175
- candidates = Dir.glob(current_dir.join('*')).select do |f|
176
- File.basename(f).include?(preview_name.gsub('/', '_'))
170
+ candidates = Dir.glob(current_dir.join('**', '*.png')).reject do |f|
171
+ File.basename(f).end_with?('_diff.png')
172
+ end.select do |f|
173
+ base = File.basename(f, '.png')
174
+ base == normalized || base.end_with?("_#{normalized}")
177
175
  end
178
176
 
179
177
  if candidates.empty?
180
- puts "No current runs found matching '#{preview_name}'."
178
+ puts "No current runs found matching '#{preview_name}' in #{current_dir}."
181
179
  exit 1
182
180
  end
183
181
 
184
182
  candidates.each do |current_file|
185
- filename = File.basename(current_file)
186
- next if filename.include?('_diff.png') # Don't copy diffs
187
-
188
- baseline_file = baseline_dir.join(filename)
189
- FileUtils.mkdir_p(baseline_dir)
183
+ relative = Pathname.new(current_file).relative_path_from(current_dir)
184
+ baseline_file = baseline_dir.join(relative)
185
+ FileUtils.mkdir_p(File.dirname(baseline_file))
190
186
  FileUtils.cp(current_file, baseline_file)
191
- puts "Approved: #{filename}"
187
+ puts "Approved: #{relative}"
192
188
  end
193
189
  end
194
190
 
@@ -210,10 +206,15 @@ namespace :lookbook do
210
206
 
211
207
  puts "Retrying #{failures.size} failure(s)..."
212
208
 
213
- failures.each do |failure|
214
- puts "Retrying #{failure[:scenario_name]}..."
215
- runner = LookbookVisualTester::Runner.new(pattern: failure[:scenario_name])
216
- runner.run
209
+ json_mode = args[:format] == 'json' || ENV['JSON_OUTPUT'] == 'true'
210
+ output = json_mode ? File.open(File::NULL, 'w') : $stdout
211
+ begin
212
+ failures.each do |failure|
213
+ puts "Retrying #{failure[:scenario_name]}..."
214
+ LookbookVisualTester::Runner.new(pattern: failure[:scenario_name], output: output).run
215
+ end
216
+ ensure
217
+ output.close if json_mode
217
218
  end
218
219
  end
219
220
 
@@ -435,11 +436,9 @@ namespace :lookbook_visual_tester do
435
436
  if args[:skip_capture].to_s == 'true'
436
437
  # Just print existing
437
438
  else
438
- # Run it first but silenty
439
- runner = LookbookVisualTester::Runner.new(pattern: args[:name])
440
- $stdout = File.new('/dev/null', 'w')
439
+ # Run it first, silently
440
+ runner = LookbookVisualTester::Runner.new(pattern: args[:name], output: File.open(File::NULL, 'w'))
441
441
  runner.run
442
- $stdout = STDOUT
443
442
  end
444
443
 
445
444
  puts scenario_run.current_path
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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Murilo Vasconcelos
@@ -13,114 +13,100 @@ dependencies:
13
13
  name: benchmark
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - ">="
16
+ - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0'
18
+ version: '0.4'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - ">="
23
+ - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0'
25
+ version: '0.4'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: chunky_png
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - ">="
30
+ - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '0'
32
+ version: '1.4'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - ">="
37
+ - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0'
39
+ version: '1.4'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: concurrent-ruby
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: '0'
47
- type: :runtime
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
- - !ruby/object:Gem::Dependency
55
- name: cuprite
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
44
+ - - "~>"
59
45
  - !ruby/object:Gem::Version
60
- version: '0'
46
+ version: '1.3'
61
47
  type: :runtime
62
48
  prerelease: false
63
49
  version_requirements: !ruby/object:Gem::Requirement
64
50
  requirements:
65
- - - ">="
51
+ - - "~>"
66
52
  - !ruby/object:Gem::Version
67
- version: '0'
53
+ version: '1.3'
68
54
  - !ruby/object:Gem::Dependency
69
55
  name: ferrum
70
56
  requirement: !ruby/object:Gem::Requirement
71
57
  requirements:
72
- - - ">="
58
+ - - "~>"
73
59
  - !ruby/object:Gem::Version
74
- version: '0'
60
+ version: '0.16'
75
61
  type: :runtime
76
62
  prerelease: false
77
63
  version_requirements: !ruby/object:Gem::Requirement
78
64
  requirements:
79
- - - ">="
65
+ - - "~>"
80
66
  - !ruby/object:Gem::Version
81
- version: '0'
67
+ version: '0.16'
82
68
  - !ruby/object:Gem::Dependency
83
69
  name: lookbook
84
70
  requirement: !ruby/object:Gem::Requirement
85
71
  requirements:
86
- - - ">="
72
+ - - "~>"
87
73
  - !ruby/object:Gem::Version
88
- version: '0'
74
+ version: '2.0'
89
75
  type: :runtime
90
76
  prerelease: false
91
77
  version_requirements: !ruby/object:Gem::Requirement
92
78
  requirements:
93
- - - ">="
79
+ - - "~>"
94
80
  - !ruby/object:Gem::Version
95
- version: '0'
81
+ version: '2.0'
96
82
  - !ruby/object:Gem::Dependency
97
83
  name: rails
98
84
  requirement: !ruby/object:Gem::Requirement
99
85
  requirements:
100
- - - ">="
86
+ - - "~>"
101
87
  - !ruby/object:Gem::Version
102
- version: '0'
88
+ version: '7.0'
103
89
  type: :runtime
104
90
  prerelease: false
105
91
  version_requirements: !ruby/object:Gem::Requirement
106
92
  requirements:
107
- - - ">="
93
+ - - "~>"
108
94
  - !ruby/object:Gem::Version
109
- version: '0'
95
+ version: '7.0'
110
96
  - !ruby/object:Gem::Dependency
111
97
  name: rainbow
112
98
  requirement: !ruby/object:Gem::Requirement
113
99
  requirements:
114
- - - ">="
100
+ - - "~>"
115
101
  - !ruby/object:Gem::Version
116
- version: '0'
102
+ version: '3.0'
117
103
  type: :runtime
118
104
  prerelease: false
119
105
  version_requirements: !ruby/object:Gem::Requirement
120
106
  requirements:
121
- - - ">="
107
+ - - "~>"
122
108
  - !ruby/object:Gem::Version
123
- version: '0'
109
+ version: '3.0'
124
110
  - !ruby/object:Gem::Dependency
125
111
  name: bundler
126
112
  requirement: !ruby/object:Gem::Requirement
@@ -167,16 +153,16 @@ dependencies:
167
153
  name: ruby-prof
168
154
  requirement: !ruby/object:Gem::Requirement
169
155
  requirements:
170
- - - ">="
156
+ - - "~>"
171
157
  - !ruby/object:Gem::Version
172
- version: '0'
158
+ version: '1.7'
173
159
  type: :development
174
160
  prerelease: false
175
161
  version_requirements: !ruby/object:Gem::Requirement
176
162
  requirements:
177
- - - ">="
163
+ - - "~>"
178
164
  - !ruby/object:Gem::Version
179
- version: '0'
165
+ version: '1.7'
180
166
  description: A Rails gem that captures screenshots of Lookbook component previews,
181
167
  compares them against baseline images, and highlights visual differences to assist
182
168
  in safe refactoring.
@@ -187,6 +173,7 @@ extensions: []
187
173
  extra_rdoc_files: []
188
174
  files:
189
175
  - ".agent/rules/style_guide.md"
176
+ - ".env.example"
190
177
  - ".rubocop.yml"
191
178
  - ".ruby-version"
192
179
  - CHANGELOG.md
@@ -195,9 +182,9 @@ files:
195
182
  - RELEASING.md
196
183
  - Rakefile
197
184
  - assets/logo.jpg
185
+ - docs/superpowers/plans/2026-08-09-lookbook-visual-tester-hardening.md
198
186
  - lib/lookbook_visual_tester.rb
199
- - lib/lookbook_visual_tester/baseline_manager.rb
200
- - lib/lookbook_visual_tester/capybara_setup.rb
187
+ - lib/lookbook_visual_tester/browser_discovery.rb
201
188
  - lib/lookbook_visual_tester/check_reporter.rb
202
189
  - lib/lookbook_visual_tester/configuration.rb
203
190
  - lib/lookbook_visual_tester/driver.rb
@@ -209,10 +196,10 @@ files:
209
196
  - lib/lookbook_visual_tester/runner.rb
210
197
  - lib/lookbook_visual_tester/scenario_finder.rb
211
198
  - lib/lookbook_visual_tester/scenario_run.rb
212
- - lib/lookbook_visual_tester/screenshot_taker.rb
199
+ - lib/lookbook_visual_tester/server_test_runner.rb
213
200
  - lib/lookbook_visual_tester/service.rb
214
201
  - lib/lookbook_visual_tester/services/image_comparator.rb
215
- - lib/lookbook_visual_tester/session_manager.rb
202
+ - lib/lookbook_visual_tester/services/image_trimmer.rb
216
203
  - lib/lookbook_visual_tester/store.rb
217
204
  - lib/lookbook_visual_tester/templates/preview_check_report.html.tt
218
205
  - lib/lookbook_visual_tester/templates/report.html.erb
@@ -1,34 +0,0 @@
1
- module LookbookVisualTester
2
- class BaselineManager < Service
3
- attr_reader :preview_name, :scenario_name
4
-
5
- def initialize(preview_name:, scenario_name:)
6
- @preview_name = preview_name
7
- @scenario_name = scenario_name
8
- end
9
-
10
- def self.update_baseline_if_approved(preview_name:, scenario_name:)
11
- new(preview_name:, scenario_name:).update_baseline_if_approved
12
- end
13
-
14
- def update_baseline_if_approved
15
- return false unless last_screenshot_path
16
-
17
- FileUtils.cp(
18
- last_screenshot_path,
19
- File.join(LookbookVisualTester.config.baseline_dir, scenario_path)
20
- )
21
- true
22
- end
23
-
24
- private
25
-
26
- def last_screenshot_path
27
- LookbookVisualTester.data[:last_changed_screenshot]&.[](preview_name)
28
- end
29
-
30
- def scenario_path
31
- "#{preview_name}/#{scenario_name}.png"
32
- end
33
- end
34
- end
@@ -1,29 +0,0 @@
1
- require 'capybara'
2
- require 'capybara/cuprite'
3
-
4
- module LookbookVisualTester
5
- module CapybaraSetup
6
- @setup_complete = false
7
-
8
- def self.setup
9
- return if @setup_complete
10
-
11
- Capybara.register_driver :cuprite do |app|
12
- Capybara::Cuprite::Driver.new(
13
- app,
14
- window_size: [1200, 800],
15
- timeout: 20,
16
- process_timeout: 20,
17
- headless: true,
18
- browser_options: { 'ignore-certificate-errors' => true }
19
- )
20
- end
21
-
22
- Capybara.default_driver = :cuprite
23
- Capybara.default_max_wait_time = 15
24
- Capybara.server = :puma, { Silent: true }
25
-
26
- @setup_complete = true
27
- end
28
- end
29
- end
@@ -1,118 +0,0 @@
1
- require 'lookbook_visual_tester/session_manager'
2
- require 'lookbook_visual_tester/service'
3
-
4
- require 'fileutils'
5
-
6
- module LookbookVisualTester
7
- class ScreenshotTaker < Service
8
- attr_reader :scenario_run, :path, :crop, :logger
9
-
10
- CLIPBOARD = 'clipboard'
11
-
12
- delegate :preview_url, to: :scenario_run
13
-
14
- def initialize(scenario_run:,
15
- path: nil,
16
- crop: true,
17
- logger: Rails.logger)
18
- @scenario_run = scenario_run
19
- @path = path || File.join(LookbookVisualTester.config.current_dir, scenario_run.filename)
20
- @crop = crop
21
- @logger = logger
22
- end
23
-
24
- def session
25
- @session ||= SessionManager.instance.session
26
- end
27
-
28
- def call
29
- FileUtils.mkdir_p(File.dirname(path))
30
-
31
- session.visit(preview_url)
32
-
33
- # Wait for network requests to complete
34
- # session.driver.network_idle?
35
-
36
- # # Wait for any loading indicators to disappear
37
- # begin
38
- # session.has_no_css?(".loading", wait: 10)
39
- # rescue StandardError
40
- # nil
41
- # end
42
- if path == CLIPBOARD
43
- print_and_save_to_clipboard
44
- else
45
- save_printscreen
46
- end
47
- # Additional wait for any JavaScript animations
48
- sleep 1
49
- rescue StandardError => e
50
- logger.info "Error capturing screenshot for #{preview_url}: #{e.message}"
51
- raise e
52
- end
53
-
54
- def save_to_clipboard(path = @path)
55
- system("xclip -selection clipboard -t image/png -i #{path}")
56
- end
57
-
58
- private
59
-
60
- def print_and_save_to_clipboard
61
- Tempfile.create(['screenshot', '.png']) do |file|
62
- save_printscreen(file.path)
63
- # save_to_clipboard(file.path)
64
- end
65
- end
66
-
67
- def save_printscreen(path = @path)
68
- session.save_screenshot(path)
69
-
70
- system("convert #{path} -trim -bordercolor white -border 10x10 #{path}") if crop
71
-
72
- if different_from_baseline?(path)
73
- save_to_clipboard(path) if LookbookVisualTester.config.copy_to_clipboard
74
-
75
- save_to_last_modified(path)
76
- save_to_history(path)
77
- LookbookVisualTester.data[:last_changed_screenshot] ||= {}
78
- LookbookVisualTester.data[:last_changed_screenshot][scenario_run.preview_name] =
79
- history_path
80
- end
81
-
82
- clean_old_history
83
- logger.info "Screenshot saved to #{path}"
84
- end
85
-
86
- def different_from_baseline?(current_path)
87
- baseline_path = File.join(LookbookVisualTester.config.baseline_dir, scenario_run.filename)
88
- return true unless File.exist?(baseline_path)
89
-
90
- system("compare -metric AE #{current_path} #{baseline_path} null: 2>&1") != '0'
91
- end
92
-
93
- def clean_old_history(keep: LookbookVisualTester.config.history_keep_last_n)
94
- history_files = Dir.glob(File.join(LookbookVisualTester.config.history_dir,
95
- "#{scenario_run.preview_name}_*.png"))
96
- history_files.sort_by! { |f| File.mtime(f) }
97
- history_files[0...-keep].each { |f| File.delete(f) }
98
- end
99
-
100
- def save_to_history(current_path)
101
- @history_path = File.join(
102
- LookbookVisualTester.config.history_dir,
103
- scenario_run.timestamp_filename
104
- )
105
- FileUtils.cp(current_path, history_path)
106
- end
107
-
108
- def save_to_last_modified(current_path)
109
- last_path = File.join(
110
- LookbookVisualTester.config.current_dir,
111
- 'last.png'
112
- )
113
- FileUtils.cp(current_path, last_path)
114
- end
115
-
116
- attr_reader :history_path
117
- end
118
- end
@@ -1,17 +0,0 @@
1
- # Setup Capybara
2
- require 'singleton'
3
- require 'capybara'
4
- require 'capybara/cuprite'
5
-
6
- module LookbookVisualTester
7
- class SessionManager
8
- include Singleton
9
-
10
- def initialize
11
- CapybaraSetup.setup
12
- @session = Capybara::Session.new(:cuprite)
13
- end
14
-
15
- attr_reader :session
16
- end
17
- end