lookbook_visual_tester 0.1.3 → 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: 3f05265af62c730e38c094caa8628914cbfa5cc374dd80a10e0e04eeaa74c9b7
4
- data.tar.gz: bdd9e634a4cc0be9ae9b75c68097f5f5d6d92030b4eccbda2ecdab0dff3a7f63
3
+ metadata.gz: a29c54b1403f4faeaea71ef267faf9aba07d033c3c0bfc79617c0bf0cf77a490
4
+ data.tar.gz: 2d8cb90e20f1ad2dd92ca6c677bb4e8185c62a16b0b070721551e789575de219
5
5
  SHA512:
6
- metadata.gz: cf97827f66239e93300fabd2aecb06616ba873fe6b4d870238d3e50349f48d5d69438462303adf28a796fb56006c2f319f6b4f2d766d9ecc46f8d18e755de217
7
- data.tar.gz: dbc55c0b4171a656d3b7b1fd41e2a8e8d2cf227d9fb7fa5205d968bfeab958e7f5fca7da557f4a132fa69d653d4d7405d4d1b275fa17ed556da838e8a9ae5e03
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,55 @@
1
- ## [Unreleased]
1
+ # Changelog
2
2
 
3
- ## [0.1.0] - 2024-12-18
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.
4
27
 
5
- - Initial release
28
+
29
+ ## [0.1.4] - 2025-02-20
30
+ ### Added
31
+ - Extracted `n_threads` configuration for improved concurrency.
32
+ - Introduced `save_to_clipboard` functionality.
33
+ - Extracted configuration and scenario run logic into separate classes.
34
+
35
+ ### Changed
36
+ - Migrated to **Concurrent Ruby** for better parallel execution.
37
+ - Refactored `report_generator`, `screenshot_taker`, and `session_manager`.
38
+
39
+ ### Fixed
40
+ - Minor fixes in `report_generator` and `tasks`.
41
+
42
+ ---
43
+
44
+ ## [0.1.1] - 2024-12-18
45
+ ### Added
46
+ - Instructions added to the README.
47
+ - Initial version bump to `0.1.1`.
48
+
49
+ ---
50
+
51
+ ## [0.1.0] - 2024-12-18
52
+ ### Added
53
+ - Initial working version.
54
+ - Set up **Capybara**, **Railtie**, and core functionalities.
55
+ - Added basic documentation and CI/CD setup.
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.
@@ -30,6 +56,16 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
30
56
 
31
57
  Bug reports and pull requests are welcome on GitHub at https://github.com/muriloime/lookbook_visual_tester. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/lookbook_visual_tester/blob/main/CODE_OF_CONDUCT.md).
32
58
 
59
+
60
+ ## Deployment
61
+
62
+ Generate artifacts for changelog :
63
+
64
+ `git log --pretty=format:"%h %ad | %s [%an]" --date=short --no-merges --name-only | xclip -selection clipboard`
65
+
66
+ Update changelog
67
+ `rake realease`
68
+
33
69
  ## Code of Conduct
34
70
 
35
71
  Everyone interacting in the LookbookVisualTester project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/lookbook_visual_tester/blob/main/CODE_OF_CONDUCT.md).
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,11 +1,42 @@
1
1
  # lib/lookbook_visual_tester/railtie.rb
2
2
 
3
- require "lookbook_visual_tester/capybara_setup"
3
+ require 'lookbook_visual_tester/capybara_setup'
4
+ require 'lookbook_visual_tester/update_previews'
4
5
 
5
6
  module LookbookVisualTester
6
7
  class Railtie < ::Rails::Railtie
7
8
  rake_tasks do
8
- load "tasks/lookbook_visual_tester.rake"
9
+ load 'tasks/lookbook_visual_tester.rake'
10
+ end
11
+
12
+ initializer 'LookbookVisualTester.lookbook_after_change' do |_app|
13
+ Rails.logger.info "LookbookVisualTester initialized with host: #{LookbookVisualTester.config.lookbook_host}"
14
+ Lookbook.after_change do |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
39
+ end
9
40
  end
10
41
  end
11
42
  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
7
+ class ScreenshotTaker < Service
8
+ attr_reader :scenario_run, :path, :crop, :logger
8
9
 
9
10
  CLIPBOARD = 'clipboard'
10
11
 
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
12
+ delegate :preview_url, to: :scenario_run
21
13
 
22
- Capybara.default_driver = :cuprite
23
- Capybara.default_max_wait_time = 15
24
- @session = Capybara::Session.new(:cuprite)
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)
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)
@@ -33,31 +33,86 @@ module LookbookVisualTester
33
33
  # Wait for network requests to complete
34
34
  # session.driver.network_idle?
35
35
 
36
- # Additional wait for any JavaScript animations
37
- sleep 1
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
38
42
  if path == CLIPBOARD
39
- save_to_clipboard
43
+ print_and_save_to_clipboard
40
44
  else
41
- save_screenshot(path)
45
+ save_printscreen
42
46
  end
47
+ # Additional wait for any JavaScript animations
48
+ sleep 1
43
49
  rescue StandardError => e
44
- logger.puts "Error capturing screenshot for #{preview_url}: #{e.message}"
50
+ logger.info "Error capturing screenshot for #{preview_url}: #{e.message}"
45
51
  raise e
46
52
  end
47
53
 
54
+ def save_to_clipboard(path = @path)
55
+ system("xclip -selection clipboard -t image/png -i #{path}")
56
+ end
57
+
48
58
  private
49
- def save_to_clipboard
50
- Tempfile.create(['screenshot', '.png']) do |file|
51
- session.save_screenshot(file.path)
52
59
 
53
- # Example: Copy to clipboard (Linux xclip)
54
- 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)
55
64
  end
56
65
  end
57
66
 
58
- def save_screenshot(path)
67
+ def save_printscreen(path = @path)
59
68
  session.save_screenshot(path)
60
- logger.puts " Screenshot saved to #{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}"
61
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
62
117
  end
63
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
@@ -0,0 +1,67 @@
1
+ require_relative 'service'
2
+ require_relative 'screenshot_taker'
3
+
4
+ module LookbookVisualTester
5
+ class UpdatePreviews < Service
6
+ attr_reader :changes
7
+
8
+ def initialize(changes)
9
+ @changes = changes[:modified]
10
+ @changes_hash = changes
11
+ end
12
+
13
+ def update_app_data
14
+ LookbookVisualTester.data[:last_changed_files] = changes.presence || []
15
+ LookbookVisualTester.data[:last_changed_previews] = previews
16
+ end
17
+
18
+ def call
19
+ Rails.logger.info "LookbookVisualTester: Processing changes for #{should_process?} #{selected_changes.inspect}, #{changes.inspect}"
20
+ return unless should_process?
21
+
22
+ process_changes
23
+ rescue StandardError => e
24
+ Rails.logger.error "LookbookVisualTester: Error processing changes: #{e.message}"
25
+ Rails.logger.error e.backtrace.join("\n")
26
+ end
27
+
28
+ private
29
+
30
+ def selected_changes
31
+ @selected_changes ||= changes.select { |change| process_change?(change) }
32
+ end
33
+
34
+ def process_change?(change)
35
+ change.to_s.downcase.include?('preview.rb') || change.to_s.downcase.match?(/component\.(html|haml|rb|erb)/)
36
+ end
37
+
38
+ def should_process?
39
+ return false if changes.nil? || changes.empty?
40
+
41
+ selected_changes.any?
42
+ end
43
+
44
+ def clean_file_name(file)
45
+ '/' + file.split('/')[-1].split('.')[0]
46
+ end
47
+
48
+ def selected_previews
49
+ @selected_previews ||= Lookbook.previews.select do |preview|
50
+ selected_changes.any? { |file| preview.file_path.to_s.include?(clean_file_name(file)) }
51
+ end
52
+ end
53
+
54
+ def process_changes
55
+ Rails.logger.info "LookbookVisualTester: previews #{selected_previews.count}"
56
+ selected_previews.each do |preview|
57
+ Rails.logger.info "LookbookVisualTester: entering #{preview.inspect}"
58
+
59
+ preview.scenarios.each do |scenario|
60
+ scenario_run = LookbookVisualTester::ScenarioRun.new(scenario)
61
+ Rails.logger.info "LookbookVisualTester: Processing scenario #{scenario_run.inspect}"
62
+ LookbookVisualTester::ScreenshotTaker.call(scenario_run:)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LookbookVisualTester
4
- VERSION = "0.1.3"
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.3
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,21 @@ 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
181
+ - lib/lookbook_visual_tester/update_previews.rb
176
182
  - lib/lookbook_visual_tester/version.rb
177
183
  - lib/tasks/lookbook_visual_tester.rake
178
184
  - sig/lookbook_visual_tester.rbs
@@ -182,6 +188,7 @@ licenses:
182
188
  - MIT
183
189
  metadata:
184
190
  homepage_uri: https://github.com/muriloime/lookbook_visual_tester
191
+ rubygems_mfa_required: 'true'
185
192
  rdoc_options: []
186
193
  require_paths:
187
194
  - lib