lookbook_visual_tester 0.1.6 → 0.5.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.
@@ -0,0 +1,260 @@
1
+ require 'concurrent'
2
+ require 'benchmark'
3
+
4
+ module LookbookVisualTester
5
+ class PreviewChecker
6
+ CheckResult = Struct.new(:preview_name, :example_name, :status, :error, :backtrace, :duration,
7
+ keyword_init: true)
8
+ MissingResult = Struct.new(:component_path, keyword_init: true)
9
+
10
+ def initialize(config = LookbookVisualTester.config)
11
+ @config = config
12
+ end
13
+
14
+ def check
15
+ run_checks(:basic_check)
16
+ end
17
+
18
+ def deep_check
19
+ # Ensure custom setup is run before deep checks
20
+ run_setup
21
+ run_checks(:deep_render_check)
22
+ end
23
+
24
+ def missing
25
+ components_dir = Rails.root.join(@config.components_folder)
26
+ # Assuming standard structure: test/components/previews for previews
27
+ # But Lookbook can be configured differently. We should use Lookbook's config if possible to know where previews are.
28
+ # For now, let's stick to the user's script logic which assumes standard paths or iterate through loaded components.
29
+
30
+ # Better approach: Iterate through all known components and check if they have a preview.
31
+ # However, "all known components" might be hard to get if they aren't loaded.
32
+ # Let's use the file system approach as in the user script.
33
+
34
+ components = Dir.glob(File.join(components_dir, '**', '*_component.rb'))
35
+ previews_dir = Rails.root.join('test/components/previews') # Default, maybe make configurable?
36
+
37
+ # Trying to find where previews are located from Rails config if possible
38
+ if defined?(Rails) && Rails.application.config.view_component.preview_paths.any?
39
+ # Use simple heuristic: first path
40
+ previews_dir = Pathname.new(Rails.application.config.view_component.preview_paths.first)
41
+ end
42
+
43
+ missing = []
44
+ components.each do |component_path|
45
+ next if component_path.end_with?('application_component.rb')
46
+ next if component_path.include?('/concerns/')
47
+
48
+ relative_path = Pathname.new(component_path).relative_path_from(components_dir).to_s
49
+ preview_relative_path = relative_path.sub('_component.rb', '_component_preview.rb')
50
+ preview_path = File.join(previews_dir, preview_relative_path)
51
+
52
+ missing << MissingResult.new(component_path: relative_path) unless File.exist?(preview_path)
53
+ end
54
+ missing
55
+ end
56
+
57
+ private
58
+
59
+ def run_checks(check_method)
60
+ previews = Lookbook.previews
61
+ results = []
62
+
63
+ # We want to flatten the work items: (PreviewClass, example_name)
64
+ work_items = []
65
+ previews.each do |preview|
66
+ # preview is a Lookbook::Preview object which wraps the class
67
+ # But for checking we might want the class directly or iterate examples
68
+ examples = preview.examples
69
+ examples.each do |example|
70
+ work_items << { preview: preview, example: example }
71
+ end
72
+ end
73
+
74
+ if @config.threads > 1
75
+ pool = Concurrent::FixedThreadPool.new(@config.threads)
76
+ promises = work_items.map do |item|
77
+ Concurrent::Promises.future_on(pool) do
78
+ measure_and_send(item[:preview], item[:example], check_method)
79
+ end
80
+ end
81
+ results = Concurrent::Promises.zip(*promises).value
82
+ pool.shutdown
83
+ pool.wait_for_termination
84
+ else
85
+ results = work_items.map do |item|
86
+ measure_and_send(item[:preview], item[:example], check_method)
87
+ end
88
+ end
89
+
90
+ results
91
+ end
92
+
93
+ def measure_and_send(preview, example, method_name)
94
+ result = nil
95
+ time = Benchmark.realtime do
96
+ result = send(method_name, preview, example)
97
+ end
98
+ result.duration = time
99
+ result
100
+ end
101
+
102
+ def basic_check(preview, example)
103
+ # user script logic:
104
+ # preview_instance = preview_class.new
105
+ # component = preview_instance.public_send(preview_example)
106
+ # if component.respond_to?(:render_in) ...
107
+
108
+ # Lookbook::Preview wrapper might help, but let's go to the class
109
+ preview_class = preview.preview_class
110
+ example_name = example.name
111
+
112
+ begin
113
+ preview_instance = preview_class.new
114
+ preview_instance.public_send(example_name)
115
+
116
+ # We don't render, just verify we can call it.
117
+ CheckResult.new(preview_name: preview.name, example_name: example_name, status: :passed)
118
+ rescue StandardError => e
119
+ CheckResult.new(preview_name: preview.name, example_name: example_name, status: :failed,
120
+ error: e.message, backtrace: e.backtrace)
121
+ end
122
+ end
123
+
124
+ def deep_render_check(preview, example)
125
+ preview_class = preview.preview_class
126
+ example_name = example.name
127
+
128
+ begin
129
+ preview_instance = preview_class.new
130
+ result = preview_instance.public_send(example_name)
131
+
132
+ if result.respond_to?(:render_in)
133
+ # Mock current_user/pundit if needed on the component itself if possible
134
+ # But mainly we render it with a view context
135
+ view_context = setup_view_context
136
+
137
+ # Inject mocks into result if it supports it or reliance on global view_context
138
+ # The user script defines singleton methods on the result.
139
+ if @mocks
140
+ @mocks.each do |key, value|
141
+ if result.respond_to?(key) || !result.respond_to?(key) # Force define
142
+ result.define_singleton_method(key) { value }
143
+ end
144
+ end
145
+ end
146
+
147
+ result.render_in(view_context)
148
+ elsif result.is_a?(String)
149
+ # Rendered string, good.
150
+ end
151
+
152
+ CheckResult.new(preview_name: preview.name, example_name: example_name, status: :passed)
153
+ rescue StandardError => e
154
+ CheckResult.new(preview_name: preview.name, example_name: example_name, status: :failed,
155
+ error: e.message, backtrace: e.backtrace)
156
+ end
157
+ end
158
+
159
+ def run_setup
160
+ if @config.preview_checker_setup
161
+ @config.preview_checker_setup.call
162
+ else
163
+ default_setup
164
+ end
165
+ end
166
+
167
+ def default_setup
168
+ # Logic from the user's script
169
+ # We need to set up a controller and view context globally or for use in checks
170
+
171
+ # This part is tricky because we need to make these available to the `deep_render_check` method.
172
+ # We can store the view_context in an instance variable or re-create it.
173
+
174
+ # Let's perform the class-level mocks here (User, etc.)
175
+
176
+ # Mock User
177
+ unless defined?(User)
178
+ # Defining a dummy user if not exists is risky if the app doesn't have User.
179
+ # But the script assumes User exists or creates a mock.
180
+ # Let's create a OpenStruct-like mock for typical Devise/Pundit usage.
181
+ end
182
+
183
+ # The script logic:
184
+ # controller = ApplicationController.new ...
185
+ # We will do this in `setup_view_context` called per check or once?
186
+ # `ApplicationController` might not be thread safe if we modify it?
187
+ # Actually we create a new controller instance.
188
+
189
+ # Define @mocks to be injected
190
+ @mocks = {}
191
+ @mocks[:current_user] = build_mock_user
192
+ @mocks[:pundit_user] = @mocks[:current_user]
193
+ end
194
+
195
+ def build_mock_user
196
+ # Try to load a real user or build a struct
197
+ if defined?(User) && User.respond_to?(:first) && User.first
198
+ User.first
199
+ else
200
+ # Fallback mock
201
+ u = Object.new
202
+ u.define_singleton_method(:email) { 'test@example.com' }
203
+ u.define_singleton_method(:id) { 1 }
204
+ # Add other common methods as needed or let them fail/mock dynamic
205
+ u
206
+ end
207
+ end
208
+
209
+ def setup_view_context
210
+ # We need a controller to get a view context
211
+ controller = if defined?(ApplicationController)
212
+ ApplicationController.new
213
+ else
214
+ ActionController::Base.new
215
+ end
216
+
217
+ controller.request = ActionDispatch::TestRequest.create
218
+ if controller.request.env
219
+ controller.request.env['rack.session'] = {}
220
+ controller.request.env['rack.session.options'] = { id: SecureRandom.uuid }
221
+ end
222
+
223
+ # Devise mapping
224
+ if defined?(Devise)
225
+ controller.request.env['devise.mapping'] = Devise.mappings[:user] if Devise.mappings[:user]
226
+
227
+ # Mock warden
228
+ warden = Object.new
229
+ warden.define_singleton_method(:authenticate!) { |*| true }
230
+ warden.define_singleton_method(:authenticate) { |*| true }
231
+ warden.define_singleton_method(:user) { |*| @mocks[:current_user] }
232
+ controller.request.env['warden'] = warden
233
+ end
234
+
235
+ view_context = controller.view_context
236
+
237
+ # Add helper methods to view_context
238
+ # We can use `class_eval` on the singleton class of the view context
239
+ vc_singleton = view_context.singleton_class
240
+
241
+ if @mocks
242
+ @mocks.each do |key, value|
243
+ vc_singleton.send(:define_method, key) { value }
244
+ end
245
+ end
246
+
247
+ # Common auth helpers
248
+ vc_singleton.send(:define_method, :signed_in?) { true }
249
+
250
+ # Pundit policy mock
251
+ vc_singleton.send(:define_method, :policy) do |_record|
252
+ Struct.new(:show?, :index?, :create?, :update?, :destroy?, :edit?, :new?, :manage?).new(
253
+ true, true, true, true, true, true, true, true
254
+ )
255
+ end
256
+
257
+ view_context
258
+ end
259
+ end
260
+ end
@@ -1,17 +1,20 @@
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
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
+ path = File.expand_path('../tasks/lookbook_visual_tester.rake', __dir__)
10
+ load path
10
11
  end
11
12
 
12
13
  initializer 'LookbookVisualTester.lookbook_after_change' do |_app|
13
14
  Rails.logger.info "LookbookVisualTester initialized with host: #{LookbookVisualTester.config.lookbook_host}"
14
15
  Lookbook.after_change do |app, changes|
16
+ next unless LookbookVisualTester.config.automatic_run
17
+
15
18
  # get hash of content of modified files to see if has changed
16
19
  modified = changes[:modified]
17
20
  my_hash = modified.sort.map { |f| File.read(f) }.hash
@@ -1,57 +1,34 @@
1
+ require 'erb'
2
+ require 'fileutils'
3
+
1
4
  module LookbookVisualTester
2
5
  class ReportGenerator
3
- attr_reader :report_path, :baseline_dir, :current_dir, :diff_dir, :base_path
4
-
5
- def initialize
6
- @baseline_dir, @current_dir, @diff_dir, @base_path = LookbookVisualTester.config.then do |config|
7
- [config.baseline_dir, config.current_dir, config.diff_dir, config.base_path]
8
- end
9
-
10
- @report_path = base_path.join('report.html')
6
+ TEMPLATE_PATH = File.expand_path("../templates/report.html.erb", __FILE__)
7
+ OUTPUT_PATH = "coverage/visual_report.html"
8
+
9
+ def initialize(results)
10
+ @results = results
11
+ @stats = {
12
+ total: results.size,
13
+ failed: results.count { |r| r.status == :failed },
14
+ new: results.count { |r| r.status == :new },
15
+ passed: results.count { |r| r.status == :passed }
16
+ }
11
17
  end
12
18
 
13
- def generate
14
- File.open(report_path, 'w') do |file|
15
- file.puts '<!DOCTYPE html>'
16
- file.puts "<html lang='en'>"
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>'
21
-
22
- diff_files = Dir.glob(diff_dir.join('*_diff.png'))
23
- diff_files.each do |diff_file|
24
- filename = File.basename(diff_file)
25
- # Extract preview and scenario names
26
- preview_scenario = filename.sub('_diff.png', '')
27
- preview, scenario = preview_scenario.split('_', 2)
28
-
29
- baseline_image = baseline_dir.join("#{preview}_#{scenario}.png")
30
- current_image = current_dir.join("#{preview}_#{scenario}.png")
19
+ def call
20
+ template = File.read(TEMPLATE_PATH)
21
+ renderer = ERB.new(template)
22
+ html = renderer.result(binding)
31
23
 
32
- file.puts '<li>'
33
- file.puts "<h2>#{preview.titleize} - #{scenario.titleize}</h2>"
34
- file.puts "<div style='display: flex; gap: 10px;'>"
35
-
36
- baseline_image_path = Pathname.new(baseline_image)
37
- current_image_path = Pathname.new(current_image)
38
- diff_file_path = Pathname.new(diff_file)
39
-
40
- file.puts "<div><h3>Baseline</h3><img src='#{baseline_image_path.relative_path_from(base_path)}' alt='Baseline'></div>"
41
- file.puts "<div><h3>Current</h3><img src='#{current_image_path.relative_path_from(base_path)}' alt='Current'></div>"
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>'
45
- end
46
-
47
- file.puts '<li>No differences found!</li>' if diff_files.empty?
48
-
49
- file.puts '</ul>'
50
- file.puts '</body>'
51
- file.puts '</html>'
52
- end
24
+ FileUtils.mkdir_p("coverage")
25
+ File.write(OUTPUT_PATH, html)
26
+ puts "📊 Report generated at: #{OUTPUT_PATH}"
27
+ end
53
28
 
54
- @report_path
29
+ # Helper to generate the terminal command for the user
30
+ def approve_command(result)
31
+ "cp \"#{result.current_path}\" \"#{result.baseline_path}\""
55
32
  end
56
33
  end
57
34
  end
@@ -0,0 +1,224 @@
1
+ require 'lookbook'
2
+ require 'json'
3
+ require_relative 'configuration'
4
+ require_relative 'scenario_run'
5
+ require_relative 'services/image_comparator'
6
+ require_relative 'drivers/ferrum_driver'
7
+ require_relative 'variant_resolver'
8
+
9
+ module LookbookVisualTester
10
+ class Runner
11
+ Result = Struct.new(:scenario_name, :status, :mismatch, :diff_path, :error, :baseline_path,
12
+ :current_path, keyword_init: true)
13
+
14
+ def initialize(config = LookbookVisualTester.config, pattern: nil)
15
+ @config = config
16
+ @pattern = pattern
17
+ @driver_pool = Queue.new
18
+ init_driver_pool
19
+ @results = []
20
+ @variants = load_variants
21
+ end
22
+
23
+ def run
24
+ previews = Lookbook.previews
25
+
26
+ if @pattern.present?
27
+ previews = previews.select do |preview|
28
+ preview.label.downcase.include?(@pattern.downcase) ||
29
+ preview.name.downcase.include?(@pattern.downcase)
30
+ end
31
+ end
32
+
33
+ puts "Found #{previews.count} previews matching '#{@pattern}'."
34
+ puts "Running against #{@variants.size} variant(s)."
35
+
36
+ @variants.each do |variant_input|
37
+ resolver = VariantResolver.new(variant_input)
38
+ variant_options = resolver.resolve
39
+ variant_slug = resolver.slug
40
+ width = resolver.width_in_pixels
41
+
42
+ puts " Variant: #{variant_slug.presence || 'Default'}"
43
+
44
+ if @config.threads > 1
45
+ run_concurrently(previews, variant_slug, variant_options, width)
46
+ else
47
+ run_sequentially(previews, variant_slug, variant_options, width)
48
+ end
49
+ end
50
+
51
+ @results
52
+ ensure
53
+ cleanup_drivers
54
+ end
55
+
56
+ private
57
+
58
+ def load_variants
59
+ variants_json = ENV['VARIANTS'] || ENV.fetch('LOOKBOOK_VARIANTS', nil)
60
+ return [{}] if variants_json.blank?
61
+
62
+ begin
63
+ JSON.parse(variants_json)
64
+ rescue JSON::ParserError
65
+ puts 'Invalid JSON in VARIANTS env var. Defaulting to standard run.'
66
+ [{}]
67
+ end
68
+ end
69
+
70
+ def run_sequentially(previews, variant_slug, variant_options, width)
71
+ previews.each do |preview|
72
+ group = preview.respond_to?(:scenarios) ? preview.scenarios : preview.examples
73
+ group.each do |scenario|
74
+ driver = checkout_driver
75
+ begin
76
+ @results << run_scenario(scenario, driver, variant_slug, variant_options, width)
77
+ ensure
78
+ return_driver(driver)
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ def run_concurrently(previews, variant_slug, variant_options, width)
85
+ require 'concurrent-ruby'
86
+ pool = Concurrent::FixedThreadPool.new(@config.threads)
87
+ promises = []
88
+
89
+ previews.each do |preview|
90
+ group = preview.respond_to?(:scenarios) ? preview.scenarios : preview.examples
91
+ group.each do |scenario|
92
+ promises << Concurrent::Promises.future_on(pool) do
93
+ driver = checkout_driver
94
+ begin
95
+ run_scenario(scenario, driver, variant_slug, variant_options, width)
96
+ ensure
97
+ return_driver(driver)
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ # Zip results from this concurrent batch into results
104
+ # Note: This aggregates results per variant loop.
105
+ @results.concat(Concurrent::Promises.zip(*promises).value)
106
+ pool.shutdown
107
+ pool.wait_for_termination
108
+ end
109
+
110
+ def init_driver_pool
111
+ count = @config.threads > 1 ? @config.threads : 1
112
+ count.times do
113
+ @driver_pool << LookbookVisualTester::Drivers::FerrumDriver.new(@config)
114
+ end
115
+ end
116
+
117
+ def checkout_driver
118
+ @driver_pool.pop
119
+ end
120
+
121
+ def return_driver(driver)
122
+ @driver_pool << driver
123
+ end
124
+
125
+ def cleanup_drivers
126
+ until @driver_pool.empty?
127
+ driver = @driver_pool.pop
128
+ driver.cleanup
129
+ end
130
+ end
131
+
132
+ def run_scenario(scenario, driver, variant_slug, variant_options, width)
133
+ run_data = ScenarioRun.new(scenario, variant_slug: variant_slug,
134
+ display_params: variant_options)
135
+ puts "Running visual test for: #{run_data.name} #{variant_slug.present? ? "[#{variant_slug}]" : ''}"
136
+
137
+ begin
138
+ driver_width = width || 1280
139
+ driver.resize_window(driver_width, 800)
140
+ driver.visit(run_data.preview_url)
141
+
142
+ # Determine paths
143
+ current_path = run_data.current_path
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
155
+
156
+ FileUtils.mkdir_p(File.dirname(current_path))
157
+ FileUtils.mkdir_p(File.dirname(diff_path))
158
+
159
+ driver.save_screenshot(current_path.to_s)
160
+
161
+ # Trimming (Feature parity with legacy ScreenshotTaker)
162
+ if File.exist?(current_path)
163
+ system("convert #{current_path} -trim -bordercolor white -border 10x10 #{current_path}")
164
+ end
165
+
166
+ comparator = LookbookVisualTester::ImageComparator.new(
167
+ baseline_path.to_s,
168
+ current_path.to_s,
169
+ diff_path.to_s
170
+ )
171
+
172
+ result = comparator.call
173
+
174
+ status = :passed
175
+ mismatch = 0.0
176
+ error = nil
177
+
178
+ if result[:error]
179
+ if result[:error] == 'Baseline not found'
180
+ # First run, maybe auto-approve or just report
181
+ puts ' [NEW] Baseline not found. Saved current as potential baseline.'
182
+ status = :new
183
+ else
184
+ puts " [ERROR] #{result[:error]}"
185
+ status = :error
186
+ error = result[:error]
187
+ end
188
+ elsif result[:mismatch] > 0
189
+ mismatch = result[:mismatch]
190
+ puts " [FAIL] Mismatch: #{mismatch.round(2)}%. Diff saved to #{diff_path}"
191
+ status = :failed
192
+
193
+ # Clipboard (Feature parity with legacy ScreenshotTaker)
194
+ if @config.copy_to_clipboard
195
+ system("xclip -selection clipboard -t image/png -i #{current_path}")
196
+ end
197
+ else
198
+ puts ' [PASS] Identical.'
199
+ status = :passed
200
+ end
201
+
202
+ Result.new(
203
+ scenario_name: run_data.name,
204
+ status: status,
205
+ mismatch: mismatch,
206
+ diff_path: diff_path.to_s,
207
+ error: error,
208
+ baseline_path: baseline_path.to_s,
209
+ current_path: current_path.to_s
210
+ )
211
+ rescue StandardError => e
212
+ puts " [ERROR] Exception: #{e.message}"
213
+ puts e.backtrace.take(5)
214
+ Result.new(
215
+ scenario_name: run_data.name,
216
+ status: :error,
217
+ error: e.message,
218
+ baseline_path: run_data.baseline_path.to_s,
219
+ current_path: run_data.current_path.to_s
220
+ )
221
+ end
222
+ end
223
+ end
224
+ end
@@ -12,19 +12,24 @@ module LookbookVisualTester
12
12
  end
13
13
 
14
14
  def regex
15
- @regex = Regexp.new(search.chars.join('.*'), Regexp::IGNORECASE)
15
+ @regex = Regexp.new(clean_search.chars.join('.*'), Regexp::IGNORECASE)
16
16
  end
17
17
 
18
18
  def matched_previews
19
19
  @matched_previews ||= previews.select { |preview| regex.match?(preview.name.downcase) }
20
20
  end
21
21
 
22
+ def clean_search
23
+ @clean_search ||= search.downcase.gsub(/[^a-z0-9\s]/, '').strip
24
+ end
25
+
22
26
  def call
23
27
  return nil if search.nil? || search == '' || previews.empty?
24
28
 
25
29
  previews.each do |preview|
26
30
  preview.scenarios.each do |scenario|
27
- return ScenarioRun.new(scenario) if scenario.name.downcase.include?(search.downcase)
31
+ name = "#{preview.name} #{scenario.name}".downcase
32
+ return ScenarioRun.new(scenario) if regex.match?(name.downcase) #name.downcase.include?(clean_search)
28
33
  end
29
34
  end
30
35
 
@@ -2,17 +2,19 @@ require 'lookbook_visual_tester/configuration'
2
2
 
3
3
  module LookbookVisualTester
4
4
  class ScenarioRun
5
- attr_reader :scenario, :preview
5
+ attr_reader :scenario, :preview, :variant_slug, :display_params
6
6
 
7
- def initialize(scenario)
7
+ def initialize(scenario, variant_slug: nil, display_params: {})
8
8
  @scenario = scenario
9
9
  @preview = scenario.preview
10
+ @variant_slug = variant_slug
11
+ @display_params = display_params
10
12
 
11
- puts " Scenario: #{scenario_name}"
13
+ LookbookVisualTester.config.logger.info " Scenario: #{scenario_name} #{variant_suffix}"
12
14
  end
13
15
 
14
16
  def preview_name
15
- preview.name.underscore
17
+ preview.name.underscore.gsub('/', '_')
16
18
  end
17
19
 
18
20
  def scenario_name
@@ -39,18 +41,34 @@ module LookbookVisualTester
39
41
  end
40
42
 
41
43
  def current_path
42
- LookbookVisualTester.config.current_dir.join(filename)
44
+ base = LookbookVisualTester.config.current_dir
45
+ base = base.join(variant_slug) if variant_slug.present?
46
+ base.join(filename)
43
47
  end
44
48
 
45
49
  def baseline_path
46
- LookbookVisualTester.config.baseline_dir.join(filename)
50
+ base = LookbookVisualTester.config.baseline_dir
51
+ base = base.join(variant_slug) if variant_slug.present?
52
+ base.join(filename)
47
53
  end
48
54
 
49
55
  def preview_url
56
+ params = { path: preview.lookup_path + '/' + scenario.name }
57
+
58
+ if display_params.any?
59
+ # Transform display_params { theme: 'dark' } -> { _display: { theme: 'dark' } }
60
+ params[:_display] = display_params
61
+ end
62
+
50
63
  Lookbook::Engine.routes.url_helpers.lookbook_preview_url(
51
- path: preview.lookup_path + '/' + scenario.name,
52
- host: LookbookVisualTester.config.lookbook_host
64
+ params.merge(host: LookbookVisualTester.config.lookbook_host)
53
65
  )
54
66
  end
67
+
68
+ private
69
+
70
+ def variant_suffix
71
+ variant_slug.present? ? "[#{variant_slug}]" : ''
72
+ end
55
73
  end
56
74
  end