lookbook_visual_tester 0.1.4 → 0.1.5

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: cc6d36810c9ef2a567bcf6fd89685b3d2cc6cf70ef73151d0fc7cf2a284f0d65
4
- data.tar.gz: e16804e522edb44ac1751317b21e9fcf8ed92665bf88f02318f3f816b32aa46f
3
+ metadata.gz: a29c54b1403f4faeaea71ef267faf9aba07d033c3c0bfc79617c0bf0cf77a490
4
+ data.tar.gz: 2d8cb90e20f1ad2dd92ca6c677bb4e8185c62a16b0b070721551e789575de219
5
5
  SHA512:
6
- metadata.gz: 41d15e7bff1c2d1634a0adddb580b5aa97bb0b869ee8ce91e1b71d6b751addfd00d5349a458a41c7c023487ea6a4a5ffde4129c57a284a3f4af35b30e2cf9cb2
7
- data.tar.gz: c8f00b84c80303323689404c89cea2b2f6dabf94ac501eae16278ef51484206bf5007b9b18c55394a3d52b6fef4fb7c9047a8c562430f836887da9787eddd325
6
+ metadata.gz: 8328f592622c337d11dca1c10811834e85f0a00cecc92eeb6f6a62c04cf2e1df50e9eed9700ac1b11041e39373710c371454c19d71db0b17c95b2790a67ebaeb
7
+ data.tar.gz: 3dba60c0c0d2867b185e45c53d68569b29e87c4ac57bf649767aa6c17ada5405d1cf0f493c6b96d75a1755405c70d050b165a72fe58a9f39648609ea703adecb
data/.rubocop.yml CHANGED
@@ -1,8 +1,209 @@
1
1
  AllCops:
2
- TargetRubyVersion: 3.0
2
+ Exclude:
3
+ - "bin/**/*"
4
+ - "db/**/*"
5
+ - "config/**/*"
6
+ - "node_modules/**/*"
7
+ - "vendor/**/*"
8
+ - "app/assets/**/*"
9
+ - "db/schema.rb"
10
+ TargetRubyVersion: 3.2.1
11
+ DisplayCopNames: true
12
+ NewCops: enable
3
13
 
4
- Style/StringLiterals:
5
- EnforcedStyle: double_quotes
14
+ Metrics/BlockLength:
15
+ Exclude:
16
+ - "spec/**/*"
17
+ - "lib/tasks/**/*"
6
18
 
7
- Style/StringLiteralsInInterpolation:
8
- EnforcedStyle: double_quotes
19
+ Style/Documentation:
20
+ Enabled: false
21
+
22
+ Style/HashEachMethods:
23
+ Enabled: true
24
+
25
+ Style/FormatString:
26
+ Exclude:
27
+ - "app/components/**/*"
28
+ Style/HashTransformKeys:
29
+ Enabled: true
30
+
31
+ Style/HashTransformValues:
32
+ Enabled: true
33
+
34
+ Style/NumericPredicate:
35
+ Enabled: false
36
+
37
+ Layout/EmptyLinesAroundAttributeAccessor:
38
+ Enabled: true
39
+
40
+ Layout/LineLength:
41
+ Max: 100
42
+ AllowedPatterns: ['(\A|\s)#']
43
+ Exclude:
44
+ - "spec/**/*"
45
+
46
+ Layout/SpaceAroundMethodCallOperator:
47
+ Enabled: true
48
+
49
+ Layout/SpaceInsideHashLiteralBraces:
50
+ Enabled: false
51
+
52
+ Style/FrozenStringLiteralComment:
53
+ Enabled: false
54
+
55
+ Metrics/AbcSize:
56
+ Max: 21
57
+
58
+ Style/AsciiComments:
59
+ Enabled: false
60
+
61
+ Metrics/MethodLength:
62
+ Max: 15
63
+
64
+ Bundler/OrderedGems:
65
+ Enabled: true
66
+
67
+ Style/ClassAndModuleChildren:
68
+ Enabled: false
69
+
70
+ Style/MethodCallWithoutArgsParentheses:
71
+ Enabled: false
72
+
73
+ Style/ExponentialNotation:
74
+ Enabled: true
75
+
76
+ Style/SlicingWithRange:
77
+ Enabled: true
78
+
79
+ Style/RedundantRegexpCharacterClass:
80
+ Enabled: true
81
+
82
+ Style/RedundantRegexpEscape:
83
+ Enabled: true
84
+
85
+ Style/AccessorGrouping:
86
+ Enabled: true
87
+
88
+ Style/ArrayCoercion:
89
+ Enabled: true
90
+
91
+ Style/BisectedAttrAccessor:
92
+ Enabled: false
93
+
94
+ Style/CaseLikeIf:
95
+ Enabled: true
96
+
97
+ Style/HashAsLastArrayItem:
98
+ Enabled: true
99
+
100
+ Style/HashLikeCase:
101
+ Enabled: false
102
+
103
+ Style/Lambda:
104
+ Enabled: false
105
+
106
+ Style/RedundantAssignment:
107
+ Enabled: true
108
+
109
+ Style/RedundantFetchBlock:
110
+ Enabled: true
111
+
112
+ Style/RedundantFileExtensionInRequire:
113
+ Enabled: true
114
+
115
+ Lint/StructNewOverride:
116
+ Enabled: true
117
+
118
+ Lint/AmbiguousBlockAssociation:
119
+ Enabled: false
120
+
121
+ Lint/RaiseException:
122
+ Enabled: true
123
+
124
+ Lint/DeprecatedOpenSSLConstant:
125
+ Enabled: true
126
+
127
+ Lint/MixedRegexpCaptureTypes:
128
+ Enabled: true
129
+
130
+ Lint/DuplicateElsifCondition:
131
+ Enabled: true
132
+
133
+ Layout/BeginEndAlignment:
134
+ Enabled: true
135
+
136
+ Lint/BinaryOperatorWithIdenticalOperands:
137
+ Enabled: true
138
+
139
+ Lint/ConstantDefinitionInBlock: # (new in 0.91)
140
+ Enabled: true
141
+
142
+ Lint/DuplicateRequire: # (new in 0.90)
143
+ Enabled: true
144
+
145
+ Lint/DuplicateRescueException: # (new in 0.89)
146
+ Enabled: true
147
+
148
+ Lint/EmptyConditionalBody: # (new in 0.89)
149
+ Enabled: true
150
+
151
+ Lint/EmptyFile: # (new in 0.90)
152
+ Enabled: true
153
+
154
+ Lint/FloatComparison: # (new in 0.89)
155
+ Enabled: true
156
+
157
+ Lint/IdentityComparison:
158
+ Enabled: true
159
+
160
+ Lint/MissingSuper: # (new in 0.89)
161
+ Enabled: true
162
+
163
+ Lint/OutOfRangeRegexpRef: # (new in 0.89)
164
+ Enabled: true
165
+
166
+ Lint/SelfAssignment: # (new in 0.89)
167
+ Enabled: true
168
+
169
+ Lint/TopLevelReturnWithArgument: # (new in 0.89)
170
+ Enabled: true
171
+
172
+ Lint/TrailingCommaInAttributeDeclaration: # (new in 0.90)
173
+ Enabled: true
174
+
175
+ Lint/UnreachableLoop: # (new in 0.89)
176
+ Enabled: true
177
+
178
+ Lint/UselessMethodDefinition: # (new in 0.90)
179
+ Enabled: true
180
+
181
+ Lint/UselessTimes: # (new in 0.91)
182
+ Enabled: true
183
+
184
+ Style/CombinableLoops:
185
+ Enabled: true
186
+
187
+ Style/ExplicitBlockArgument: # (new in 0.89)
188
+ Enabled: true
189
+
190
+ Style/GlobalStdStream: # (new in 0.89)
191
+ Enabled: true
192
+
193
+ Style/KeywordParametersOrder: # (new in 0.90)
194
+ Enabled: true
195
+
196
+ Style/OptionalBooleanParameter: # (new in 0.89)
197
+ Enabled: true
198
+
199
+ Style/RedundantSelfAssignment: # (new in 0.90)
200
+ Enabled: true
201
+
202
+ Style/SingleArgumentDig: # (new in 0.89)
203
+ Enabled: true
204
+
205
+ Style/SoleNestedConditional: # (new in 0.89)
206
+ Enabled: true
207
+
208
+ Style/StringConcatenation:
209
+ Enabled: true
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ Here is the changelog for version **0.1.5** of *LookbookVisualTester*:
4
+
5
+ ---
6
+
7
+ # Changelog
8
+
9
+ ## [0.1.5] - 2025-02-26
10
+
11
+ ### ✨ New Features & Improvements
12
+
13
+ - **State Management & History Tracking**
14
+ - Introduced a **state store** with the new `Store` class for better history management.
15
+ - Implemented file history tracking, enabling more efficient change monitoring.
16
+
17
+ - **Component Refactoring**
18
+ - Improved **state and history handling** in `LookbookVisualTester`.
19
+ - Updated `ScreenshotTaker` signature for better integration with other system components.
20
+ - Refactored `CapybaraSetup` to enhance test environment flexibility.
21
+ - `UpdatePreviews` now inherits from `Service`, standardizing code structure.
22
+ - Improved **clipboard functionality**, allowing better manipulation and copying of screenshots.
23
+
24
+ - **Testing & Code Quality Enhancements**
25
+ - Added new unit tests for `ScreenshotTaker` and `UpdatePreviews`.
26
+ - Refactored logging and scenario handling to improve maintainability.
27
+
28
+
3
29
  ## [0.1.4] - 2025-02-20
4
30
  ### Added
5
31
  - Extracted `n_threads` configuration for improved concurrency.
data/README.md CHANGED
@@ -1,9 +1,30 @@
1
- # LookbookVisualTester
1
+ <img src="./assets/logo.jpg" alt="Logo" width="300" />
2
+
3
+ # Lookbook Visual Tester
2
4
 
3
5
  This gem was built to serve as a lookbook regression tester, getting prints from previews, comparing them and generating a report of differences.
4
6
 
7
+
8
+ ### Features
9
+
10
+ - Perform visual regression testing on changes.
11
+ - Integrate seamlessly with your Lookbook previews.
12
+ - Automatically generate image differences.
13
+ - Simplify debugging and quality checks.
14
+ - Very useful for AI coding (e.g. aider, etc.)
15
+
5
16
  ## Installation
6
17
 
18
+ Tested on linux. A couple of changes to work on mac, etc. You will need tools like xclip, imagemagick
19
+
20
+ For Ubuntu-based systems, install the necessary dependencies by running:
21
+
22
+ ```bash
23
+ sudo apt-get install xclip imagemagick
24
+ ```
25
+
26
+
27
+
7
28
  Install the gem and add to the application's Gemfile by executing:
8
29
 
9
30
  ```bash
@@ -18,8 +39,13 @@ gem install lookbook_visual_tester
18
39
 
19
40
  ## Usage
20
41
 
42
+ To run everything
21
43
  bundle exec rake lookbook_visual_tester:run LOOKBOOK_HOST=https://localhost:5000
22
44
 
45
+ ### Features
46
+
47
+ When installed this gem gets your changes and generates
48
+
23
49
  ## Development
24
50
 
25
51
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -33,6 +59,8 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/murilo
33
59
 
34
60
  ## Deployment
35
61
 
62
+ Generate artifacts for changelog :
63
+
36
64
  `git log --pretty=format:"%h %ad | %s [%an]" --date=short --no-merges --name-only | xclip -selection clipboard`
37
65
 
38
66
  Update changelog
data/Rakefile CHANGED
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "minitest/test_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'minitest/test_task'
5
5
 
6
6
  Minitest::TestTask.create
7
7
 
8
- require "rubocop/rake_task"
8
+ require 'rubocop/rake_task'
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
data/assets/logo.jpg ADDED
Binary file
@@ -0,0 +1,34 @@
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,15 +1,29 @@
1
- require "capybara"
2
- require "capybara/cuprite"
1
+ require 'capybara'
2
+ require 'capybara/cuprite'
3
3
 
4
4
  module LookbookVisualTester
5
5
  module CapybaraSetup
6
+ @setup_complete = false
7
+
6
8
  def self.setup
9
+ return if @setup_complete
10
+
7
11
  Capybara.register_driver :cuprite do |app|
8
- Capybara::Cuprite::Driver.new(app, headless: true, browser_options: { "ignore-certificate-errors" => true })
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
+ )
9
20
  end
10
21
 
11
22
  Capybara.default_driver = :cuprite
23
+ Capybara.default_max_wait_time = 15
12
24
  Capybara.server = :puma, { Silent: true }
25
+
26
+ @setup_complete = true
13
27
  end
14
28
  end
15
29
  end
@@ -1,20 +1,38 @@
1
1
  module LookbookVisualTester
2
2
  class Configuration
3
- attr_accessor :base_path, :baseline_dir, :current_dir, :diff_dir, :threads ,:host
3
+ attr_accessor :base_path, :lookbook_host,
4
+ :baseline_dir, :current_dir, :diff_dir, :history_dir,
5
+ :history_keep_last_n, :threads, :copy_to_clipboard
4
6
 
5
7
  DEFAULT_THREADS = 4
6
8
 
7
9
  def initialize
8
- @base_path = Rails.root.join("spec/visual_screenshots")
9
- @baseline_dir = @base_path.join("baseline")
10
- @current_dir = @base_path.join("current_run")
11
- @diff_dir = @base_path.join("diff")
10
+ @base_path = if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.test?
11
+ Pathname.new(Dir.pwd).join('spec/visual_screenshots')
12
+ elsif defined?(Rails) && Rails.respond_to?(:root) && Rails.root
13
+ Rails.root.join('spec/visual_screenshots')
14
+ else
15
+ # Fallback for non-Rails environments
16
+ Pathname.new(Dir.pwd).join('spec/visual_screenshots')
17
+ end
18
+ @baseline_dir = @base_path.join('baseline')
19
+ @current_dir = @base_path.join('current_run')
20
+ @diff_dir = @base_path.join('diff')
21
+ @history_dir = @base_path.join('history')
12
22
  @threads = DEFAULT_THREADS
23
+ @history_keep_last_n = 5
24
+ @copy_to_clipboard = true
13
25
 
14
- @host = ENV["LOOKBOOK_HOST"] || "https://localhost:5000"
26
+ @lookbook_host = ENV.fetch('LOOKBOOK_HOST', 'https://localhost:5000')
27
+ end
28
+
29
+ class << self
30
+ def config
31
+ @config ||= new
32
+ end
15
33
 
16
- [baseline_dir, current_dir, diff_dir].each do |dir|
17
- FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
34
+ def configure
35
+ yield(config)
18
36
  end
19
37
  end
20
38
  end
@@ -1,4 +1,4 @@
1
- require "lookbook_visual_tester/configuration"
1
+ require 'lookbook_visual_tester/configuration'
2
2
 
3
3
  module LookbookVisualTester
4
4
  class ImageComparator
@@ -22,7 +22,7 @@ module LookbookVisualTester
22
22
  current_image = MiniMagick::Image.open(current_path)
23
23
 
24
24
  unless baseline_image.dimensions == current_image.dimensions
25
- puts " Image dimensions do not match. Skipping comparison."
25
+ puts ' Image dimensions do not match. Skipping comparison.'
26
26
  return
27
27
  end
28
28
 
@@ -34,7 +34,7 @@ module LookbookVisualTester
34
34
  if distortion > 0
35
35
  puts " Differences found! Diff image saved to #{diff_path}"
36
36
  else
37
- puts " No differences detected."
37
+ puts ' No differences detected.'
38
38
  File.delete(diff_path) if diff_path.exist?
39
39
  end
40
40
  rescue StandardError => e
@@ -1,18 +1,41 @@
1
1
  # lib/lookbook_visual_tester/railtie.rb
2
2
 
3
- require "lookbook_visual_tester/capybara_setup"
4
- require "lookbook_visual_tester/update_previews"
3
+ require 'lookbook_visual_tester/capybara_setup'
4
+ require 'lookbook_visual_tester/update_previews'
5
5
 
6
6
  module LookbookVisualTester
7
7
  class Railtie < ::Rails::Railtie
8
8
  rake_tasks do
9
- load "tasks/lookbook_visual_tester.rake"
9
+ load 'tasks/lookbook_visual_tester.rake'
10
10
  end
11
11
 
12
- initializer "LookbookVisualTester.lookbook_after_change" do |app1|
13
- puts " >>>>> lookbook_after_change initialized: #{app1.inspect}"
12
+ initializer 'LookbookVisualTester.lookbook_after_change' do |_app|
13
+ Rails.logger.info "LookbookVisualTester initialized with host: #{LookbookVisualTester.config.lookbook_host}"
14
14
  Lookbook.after_change do |app, changes|
15
- LookbookVisualTester::UpdatePreviews.call(app, changes)
15
+ # get hash of content of modified files to see if has changed
16
+ modified = changes[:modified]
17
+ my_hash = modified.sort.map { |f| File.read(f) }.hash
18
+
19
+ lock_file = Rails.root.join('tmp', 'lookbook_visual_tester.lock')
20
+ Rails.logger.info ">>> LookbookVisualTester: No changes detected in #{LookbookVisualTester.data}"
21
+
22
+ # Rails.logger.info ">>> LookbookVisualTester: Stack trace: #{caller.join("\n")}"
23
+ Rails.logger.info ">>> LookbookVisualTester: Changes in #{Rails.cache.instance_variable_get(:@data).keys.inspect}"
24
+ File.open(lock_file, 'w') do |file|
25
+ if file.flock(File::LOCK_EX | File::LOCK_NB)
26
+ if LookbookVisualTester.data[:last_hash] == my_hash
27
+ Rails.logger.info 'LookbookVisualTester: No changes detected in Lookbook'
28
+ else
29
+ LookbookVisualTester.data[:last_hash] = my_hash
30
+ Rails.logger.info "LookbookVisualTester: Running UpdatePreviews, updattin to #{LookbookVisualTester.data.inspect}"
31
+ LookbookVisualTester::UpdatePreviews.call(changes)
32
+ end
33
+ file.flock(File::LOCK_UN)
34
+ Rails.logger.info 'LookbookVisualTester: UpdatePreviews File unlocked.'
35
+ else
36
+ Rails.logger.info 'LookbookVisualTester: UpdatePreviews already running, skipping this call.'
37
+ end
38
+ end
16
39
  end
17
40
  end
18
41
  end
@@ -7,29 +7,29 @@ module LookbookVisualTester
7
7
  [config.baseline_dir, config.current_dir, config.diff_dir, config.base_path]
8
8
  end
9
9
 
10
- @report_path = base_path.join("report.html")
10
+ @report_path = base_path.join('report.html')
11
11
  end
12
12
 
13
13
  def generate
14
- File.open(report_path, "w") do |file|
15
- file.puts "<!DOCTYPE html>"
14
+ File.open(report_path, 'w') do |file|
15
+ file.puts '<!DOCTYPE html>'
16
16
  file.puts "<html lang='en'>"
17
17
  file.puts "<head><meta charset='UTF-8'><title>Visual Regression Report</title></head>"
18
- file.puts "<body>"
19
- file.puts "<h1>Visual Regression Report</h1>"
20
- file.puts "<ul>"
18
+ file.puts '<body>'
19
+ file.puts '<h1>Visual Regression Report</h1>'
20
+ file.puts '<ul>'
21
21
 
22
- diff_files = Dir.glob(diff_dir.join("*_diff.png"))
22
+ diff_files = Dir.glob(diff_dir.join('*_diff.png'))
23
23
  diff_files.each do |diff_file|
24
24
  filename = File.basename(diff_file)
25
25
  # Extract preview and scenario names
26
- preview_scenario = filename.sub("_diff.png", "")
27
- preview, scenario = preview_scenario.split("_", 2)
26
+ preview_scenario = filename.sub('_diff.png', '')
27
+ preview, scenario = preview_scenario.split('_', 2)
28
28
 
29
29
  baseline_image = baseline_dir.join("#{preview}_#{scenario}.png")
30
30
  current_image = current_dir.join("#{preview}_#{scenario}.png")
31
31
 
32
- file.puts "<li>"
32
+ file.puts '<li>'
33
33
  file.puts "<h2>#{preview.titleize} - #{scenario.titleize}</h2>"
34
34
  file.puts "<div style='display: flex; gap: 10px;'>"
35
35
 
@@ -40,15 +40,15 @@ module LookbookVisualTester
40
40
  file.puts "<div><h3>Baseline</h3><img src='#{baseline_image_path.relative_path_from(base_path)}' alt='Baseline'></div>"
41
41
  file.puts "<div><h3>Current</h3><img src='#{current_image_path.relative_path_from(base_path)}' alt='Current'></div>"
42
42
  file.puts "<div><h3>Diff</h3><img src='#{diff_file_path.relative_path_from(base_path)}' alt='Diff'></div>"
43
- file.puts "</div>"
44
- file.puts "</li>"
43
+ file.puts '</div>'
44
+ file.puts '</li>'
45
45
  end
46
46
 
47
- file.puts "<li>No differences found!</li>" if diff_files.empty?
47
+ file.puts '<li>No differences found!</li>' if diff_files.empty?
48
48
 
49
- file.puts "</ul>"
50
- file.puts "</body>"
51
- file.puts "</html>"
49
+ file.puts '</ul>'
50
+ file.puts '</body>'
51
+ file.puts '</html>'
52
52
  end
53
53
 
54
54
  @report_path
@@ -0,0 +1,34 @@
1
+ require 'lookbook_visual_tester/service'
2
+ require 'lookbook_visual_tester/scenario_run'
3
+
4
+ module LookbookVisualTester
5
+ class ScenarioFinder < Service
6
+ attr_reader :fuzze, :search, :previews
7
+
8
+ def initialize(search, fuzzy: true, previews: Lookbook.previews)
9
+ @search = search
10
+ @fuzzy = fuzzy
11
+ @previews = previews
12
+ end
13
+
14
+ def regex
15
+ @regex = Regexp.new(search.chars.join('.*'), Regexp::IGNORECASE)
16
+ end
17
+
18
+ def matched_previews
19
+ @matched_previews ||= previews.select { |preview| regex.match?(preview.name.downcase) }
20
+ end
21
+
22
+ def call
23
+ return nil if search.nil? || search == '' || previews.empty?
24
+
25
+ previews.each do |preview|
26
+ preview.scenarios.each do |scenario|
27
+ return ScenarioRun.new(scenario) if scenario.name.downcase.include?(search.downcase)
28
+ end
29
+ end
30
+
31
+ nil
32
+ end
33
+ end
34
+ end
@@ -1,4 +1,4 @@
1
- require "lookbook_visual_tester/configuration"
1
+ require 'lookbook_visual_tester/configuration'
2
2
 
3
3
  module LookbookVisualTester
4
4
  class ScenarioRun
@@ -19,8 +19,19 @@ module LookbookVisualTester
19
19
  scenario.name.underscore
20
20
  end
21
21
 
22
+ def name
23
+ "#{preview_name}_#{scenario_name}"
24
+ end
25
+
22
26
  def filename
23
- "#{preview_name}_#{scenario_name}.png"
27
+ "#{name}.png"
28
+ end
29
+
30
+ def timestamp_filename
31
+ @timestamp_filename ||= begin
32
+ timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
33
+ "#{name}_#{timestamp}.png"
34
+ end
24
35
  end
25
36
 
26
37
  def diff_filename
@@ -37,8 +48,8 @@ module LookbookVisualTester
37
48
 
38
49
  def preview_url
39
50
  Lookbook::Engine.routes.url_helpers.lookbook_preview_url(
40
- path: preview.lookup_path + "/" + scenario.name,
41
- host: LookbookVisualTester.config.host
51
+ path: preview.lookup_path + '/' + scenario.name,
52
+ host: LookbookVisualTester.config.lookbook_host
42
53
  )
43
54
  end
44
55
  end
@@ -1,31 +1,31 @@
1
- require "lookbook_visual_tester/session_manager"
2
- require "capybara/cuprite"
3
- require "fileutils"
1
+ require 'lookbook_visual_tester/session_manager'
2
+ require 'lookbook_visual_tester/service'
3
+
4
+ require 'fileutils'
4
5
 
5
6
  module LookbookVisualTester
6
- class ScreenshotTaker
7
- attr_reader :session, :logger
8
-
9
- CLIPBOARD = "clipboard"
10
-
11
- def initialize(logger: Kernel)
12
- Capybara.register_driver :cuprite do |app|
13
- Capybara::Cuprite::Driver.new(
14
- app,
15
- window_size: [1400, 1400],
16
- browser_options: { "ignore-certificate-errors" => nil },
17
- timeout: 20,
18
- process_timeout: 20
19
- )
20
- end
7
+ class ScreenshotTaker < Service
8
+ attr_reader :scenario_run, :path, :crop, :logger
9
+
10
+ CLIPBOARD = 'clipboard'
21
11
 
22
- Capybara.default_driver = :cuprite
23
- Capybara.default_max_wait_time = 15
24
- @session = Capybara::Session.new(:cuprite)
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
25
21
  @logger = logger
26
22
  end
27
23
 
28
- def capture(preview_url, path = CLIPBOARD, crop: true)
24
+ def session
25
+ @session ||= SessionManager.instance.session
26
+ end
27
+
28
+ def call
29
29
  FileUtils.mkdir_p(File.dirname(path))
30
30
 
31
31
  session.visit(preview_url)
@@ -40,35 +40,79 @@ module LookbookVisualTester
40
40
  # nil
41
41
  # end
42
42
  if path == CLIPBOARD
43
- save_to_clipboard(crop: crop)
43
+ print_and_save_to_clipboard
44
44
  else
45
- save_printscreen(path: path, crop: crop)
45
+ save_printscreen
46
46
  end
47
47
  # Additional wait for any JavaScript animations
48
48
  sleep 1
49
49
  rescue StandardError => e
50
- logger.puts "Error capturing screenshot for #{preview_url}: #{e.message}"
50
+ logger.info "Error capturing screenshot for #{preview_url}: #{e.message}"
51
51
  raise e
52
52
  end
53
53
 
54
- private
54
+ def save_to_clipboard(path = @path)
55
+ system("xclip -selection clipboard -t image/png -i #{path}")
56
+ end
55
57
 
56
- def save_to_clipboard(crop: false)
57
- Tempfile.create(["screenshot", ".png"]) do |file|
58
- session.save_screenshot(file.path, crop: crop)
58
+ private
59
59
 
60
- # Example: Copy to clipboard (Linux xclip)
61
- system("xclip -selection clipboard -t image/png -i #{file.path}")
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)
62
64
  end
63
65
  end
64
66
 
65
- def save_printscreen(path: nil, crop: false)
67
+ def save_printscreen(path = @path)
66
68
  session.save_screenshot(path)
67
69
 
68
- # remove white space
69
70
  system("convert #{path} -trim -bordercolor white -border 10x10 #{path}") if crop
70
71
 
71
- logger.puts " Screenshot saved to #{path}"
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) }
72
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
73
117
  end
74
118
  end
@@ -0,0 +1,11 @@
1
+ module LookbookVisualTester
2
+ class Service
3
+ def self.call(...)
4
+ new(...).call
5
+ end
6
+
7
+ def call
8
+ raise NotImplementedError, 'You must implement the call method'
9
+ end
10
+ end
11
+ end
@@ -1,7 +1,7 @@
1
1
  # Setup Capybara
2
- require "singleton"
3
- require "capybara"
4
- require "capybara/cuprite"
2
+ require 'singleton'
3
+ require 'capybara'
4
+ require 'capybara/cuprite'
5
5
 
6
6
  module LookbookVisualTester
7
7
  class SessionManager
@@ -0,0 +1,45 @@
1
+ module LookbookVisualTester
2
+ class Store
3
+ attr_accessor :stored_hash
4
+
5
+ HASH_KEY = 'lookbook_visual_tester:stored_hash'
6
+
7
+ def initialize
8
+ @stored_hash = Rails.cache.read(HASH_KEY) || {}
9
+ end
10
+
11
+ def [](key)
12
+ @stored_hash[key.to_s]
13
+ end
14
+
15
+ def []=(key, value)
16
+ save(key.to_s, value)
17
+ end
18
+
19
+ def save(key, value)
20
+ @stored_hash[key.to_s] = value
21
+ Rails.cache.write(HASH_KEY, @stored_hash)
22
+ end
23
+
24
+ # pretty inspect of the object
25
+ def inspect
26
+ "#<#{self.class.name}
27
+ stored_hash: #{stored_hash}>"
28
+ end
29
+
30
+ class << self
31
+ def data
32
+ @data ||= new
33
+ # @data ||= new
34
+ end
35
+
36
+ def dataset
37
+ yield(data)
38
+ end
39
+ end
40
+ end
41
+
42
+ def self.data
43
+ @data ||= Store.new
44
+ end
45
+ end
@@ -1,20 +1,18 @@
1
- module LookbookVisualTester
2
- class UpdatePreviews
3
- def self.call(...)
4
- new(...).call
5
- end
1
+ require_relative 'service'
2
+ require_relative 'screenshot_taker'
6
3
 
7
- attr_reader :app, :changes
4
+ module LookbookVisualTester
5
+ class UpdatePreviews < Service
6
+ attr_reader :changes
8
7
 
9
- def initialize(app, changes)
10
- @app = app
8
+ def initialize(changes)
11
9
  @changes = changes[:modified]
12
10
  @changes_hash = changes
13
11
  end
14
12
 
15
13
  def update_app_data
16
- app.data.last_changed_files = changes.presence || []
17
- app.data.last_changed_previews = previews
14
+ LookbookVisualTester.data[:last_changed_files] = changes.presence || []
15
+ LookbookVisualTester.data[:last_changed_previews] = previews
18
16
  end
19
17
 
20
18
  def call
@@ -34,7 +32,7 @@ module LookbookVisualTester
34
32
  end
35
33
 
36
34
  def process_change?(change)
37
- change.to_s.downcase.include?("preview.rb") || change.to_s.downcase.match?(/component\.(html|haml|rb|erb)/)
35
+ change.to_s.downcase.include?('preview.rb') || change.to_s.downcase.match?(/component\.(html|haml|rb|erb)/)
38
36
  end
39
37
 
40
38
  def should_process?
@@ -44,24 +42,24 @@ module LookbookVisualTester
44
42
  end
45
43
 
46
44
  def clean_file_name(file)
47
- file.split("/")[-1].split(".")[0]
45
+ '/' + file.split('/')[-1].split('.')[0]
48
46
  end
49
47
 
50
- def previews
51
- @previews ||= Lookbook.previews.select do |preview|
48
+ def selected_previews
49
+ @selected_previews ||= Lookbook.previews.select do |preview|
52
50
  selected_changes.any? { |file| preview.file_path.to_s.include?(clean_file_name(file)) }
53
51
  end
54
52
  end
55
53
 
56
54
  def process_changes
57
- Rails.logger.info "LookbookVisualTester: previws #{previews.count}"
58
- previews.each do |preview|
55
+ Rails.logger.info "LookbookVisualTester: previews #{selected_previews.count}"
56
+ selected_previews.each do |preview|
59
57
  Rails.logger.info "LookbookVisualTester: entering #{preview.inspect}"
60
58
 
61
59
  preview.scenarios.each do |scenario|
62
60
  scenario_run = LookbookVisualTester::ScenarioRun.new(scenario)
63
61
  Rails.logger.info "LookbookVisualTester: Processing scenario #{scenario_run.inspect}"
64
- LookbookVisualTester::ScreenshotTaker.new.capture(scenario_run.preview_url, scenario_run.current_path)
62
+ LookbookVisualTester::ScreenshotTaker.call(scenario_run:)
65
63
  end
66
64
  end
67
65
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LookbookVisualTester
4
- VERSION = "0.1.4"
4
+ VERSION = '0.1.5'
5
5
  end
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  # lib/lookbook_visual_tester.rb
3
4
 
4
- require_relative "lookbook_visual_tester/version"
5
- require_relative "lookbook_visual_tester/railtie" if defined?(Rails)
5
+ require_relative 'lookbook_visual_tester/version'
6
+ require_relative 'lookbook_visual_tester/railtie' if defined?(Rails)
7
+ require 'lookbook_visual_tester/scenario_finder'
8
+ require 'lookbook_visual_tester/store'
6
9
 
7
10
  module LookbookVisualTester
8
11
  class Error < StandardError; end
@@ -1,57 +1,50 @@
1
1
  # lib/tasks/lookbook_visual_tester.rake
2
- require "fileutils"
3
- require "mini_magick"
4
- require "ruby-prof"
5
- require "concurrent-ruby"
2
+ require 'fileutils'
3
+ require 'mini_magick'
4
+ require 'ruby-prof'
5
+ require 'concurrent-ruby'
6
6
 
7
- require "lookbook_visual_tester/report_generator"
8
- require "lookbook_visual_tester/screenshot_taker"
9
- require "lookbook_visual_tester/image_comparator"
10
- require "lookbook_visual_tester/scenario_run"
7
+ require 'lookbook_visual_tester/report_generator'
8
+ require 'lookbook_visual_tester/screenshot_taker'
9
+ require 'lookbook_visual_tester/image_comparator'
10
+ require 'lookbook_visual_tester/scenario_run'
11
11
 
12
12
  namespace :lookbook_visual_tester do
13
- desc "Profile the lookbook_visual_tester:run task"
13
+ desc 'Profile the lookbook_visual_tester:run task'
14
14
  task profile: :environment do
15
15
  RubyProf.start
16
- Rake::Task["lookbook_visual_tester:run"].invoke
16
+ Rake::Task['lookbook_visual_tester:run'].invoke
17
17
  result = RubyProf.stop
18
18
 
19
19
  printer = RubyProf::FlatPrinter.new(result)
20
20
  printer.print(STDOUT)
21
21
  end
22
22
 
23
- desc "Run and copy to clipboard first scenario matching the given name"
23
+ desc 'Run and copy to clipboard first scenario matching the given name'
24
24
  task :copy, [:name] => :environment do |t, args|
25
25
  # example on how to run: `rake lookbook_visual_tester:copy["Button"]`
26
26
 
27
- screenshot_taker = LookbookVisualTester::ScreenshotTaker.new
28
27
  previews = Lookbook.previews
29
28
 
30
- regex = Regexp.new(args[:name].chars.join(".*"), Regexp::IGNORECASE)
31
- matched_previews = previews.select { |preview| regex.match?(preview.name.underscore) }
32
- if matched_previews.empty?
29
+ scenario_run = LookbookVisualTester::ScenarioFinder.call(args[:name], previews)
30
+ unless scenario_run
31
+ screenshot_taker = LookbookVisualTester::ScreenshotTaker.new(scenario_run:)
33
32
  puts "No Lookbook previews found matching #{args[:name]}"
34
33
  exit
35
34
  end
36
- matched_previews.each do |preview|
37
- preview.scenarios.each do |scenario|
38
- scenario_run = LookbookVisualTester::ScenarioRun.new(scenario)
39
- screenshot_taker.capture(scenario_run.preview_url)
40
- exit
41
- end
42
- end
35
+ screenshot_taker.capture(scenario_run.preview_url)
36
+ exit
43
37
  end
44
38
 
45
- desc "Run visual regression tests for Lookbook previews"
39
+ desc 'Run visual regression tests for Lookbook previews'
46
40
  task run: :environment do
47
- screenshot_taker = LookbookVisualTester::ScreenshotTaker.new
48
41
  image_comparator = LookbookVisualTester::ImageComparator.new
49
42
  report = LookbookVisualTester::ReportGenerator.new
50
43
 
51
44
  previews = Lookbook.previews
52
45
 
53
46
  if previews.empty?
54
- puts "No Lookbook previews found."
47
+ puts 'No Lookbook previews found.'
55
48
  exit
56
49
  end
57
50
 
@@ -63,7 +56,7 @@ namespace :lookbook_visual_tester do
63
56
  preview.scenarios.each do |scenario|
64
57
  Concurrent::Promises.future_on(pool) do
65
58
  scenario_run = LookbookVisualTester::ScenarioRun.new(scenario)
66
-
59
+ screenshot_taker = LookbookVisualTester::ScreenshotTaker.new(scenario_run:)
67
60
  screenshot_taker.capture(scenario_run.preview_url, scenario_run.current_path)
68
61
  puts " Visiting URL: #{preview_url}"
69
62
 
@@ -78,18 +71,18 @@ namespace :lookbook_visual_tester do
78
71
  puts "Visual regression report generated at #{report_path}"
79
72
  end
80
73
 
81
- desc "Update baseline screenshots with current_run screenshots"
74
+ desc 'Update baseline screenshots with current_run screenshots'
82
75
  task update_baseline: :environment do
83
- base_path = Rails.root.join("spec/visual_screenshots")
84
- baseline_dir = base_path.join("baseline")
85
- current_dir = base_path.join("current_run")
76
+ base_path = Rails.root.join('spec/visual_screenshots')
77
+ baseline_dir = base_path.join('baseline')
78
+ current_dir = base_path.join('current_run')
86
79
 
87
80
  unless current_dir.exist?
88
- puts "Current run directory does not exist. Run the visual regression tests first."
81
+ puts 'Current run directory does not exist. Run the visual regression tests first.'
89
82
  exit
90
83
  end
91
84
 
92
- Dir.glob(current_dir.join("*.png")).each do |current_file|
85
+ Dir.glob(current_dir.join('*.png')).each do |current_file|
93
86
  filename = File.basename(current_file)
94
87
  baseline_file = baseline_dir.join(filename)
95
88
  FileUtils.cp(current_file, baseline_file)
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lookbook_visual_tester
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Murilo Vasconcelos
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-02-21 00:00:00.000000000 Z
10
+ date: 2025-02-26 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: capybara
@@ -164,15 +164,20 @@ files:
164
164
  - CODE_OF_CONDUCT.md
165
165
  - README.md
166
166
  - Rakefile
167
+ - assets/logo.jpg
167
168
  - lib/lookbook_visual_tester.rb
169
+ - lib/lookbook_visual_tester/baseline_manager.rb
168
170
  - lib/lookbook_visual_tester/capybara_setup.rb
169
171
  - lib/lookbook_visual_tester/configuration.rb
170
172
  - lib/lookbook_visual_tester/image_comparator.rb
171
173
  - lib/lookbook_visual_tester/railtie.rb
172
174
  - lib/lookbook_visual_tester/report_generator.rb
175
+ - lib/lookbook_visual_tester/scenario_finder.rb
173
176
  - lib/lookbook_visual_tester/scenario_run.rb
174
177
  - lib/lookbook_visual_tester/screenshot_taker.rb
178
+ - lib/lookbook_visual_tester/service.rb
175
179
  - lib/lookbook_visual_tester/session_manager.rb
180
+ - lib/lookbook_visual_tester/store.rb
176
181
  - lib/lookbook_visual_tester/update_previews.rb
177
182
  - lib/lookbook_visual_tester/version.rb
178
183
  - lib/tasks/lookbook_visual_tester.rake
@@ -183,6 +188,7 @@ licenses:
183
188
  - MIT
184
189
  metadata:
185
190
  homepage_uri: https://github.com/muriloime/lookbook_visual_tester
191
+ rubygems_mfa_required: 'true'
186
192
  rdoc_options: []
187
193
  require_paths:
188
194
  - lib