lookbook_visual_tester 0.3.0 → 0.5.1
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 +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +51 -3
- data/Rakefile +4 -5
- data/lib/lookbook_visual_tester/check_reporter.rb +81 -0
- data/lib/lookbook_visual_tester/configuration.rb +2 -0
- data/lib/lookbook_visual_tester/preview_checker.rb +260 -0
- data/lib/lookbook_visual_tester/railtie.rb +2 -1
- data/lib/lookbook_visual_tester/runner.rb +52 -20
- data/lib/lookbook_visual_tester/scenario_run.rb +25 -7
- data/lib/lookbook_visual_tester/templates/preview_check_report.html.tt +63 -0
- data/lib/lookbook_visual_tester/variant_resolver.rb +62 -0
- data/lib/lookbook_visual_tester/version.rb +1 -1
- data/lib/tasks/lookbook_visual_tester.rake +29 -0
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 865c3eeb875ed336338493ccbd620fcfeba15be5410c7d605cf73d94e79e3756
|
|
4
|
+
data.tar.gz: '081022c43fe54c60a95be903181d956223bb34800700a44a45baf3c69d7cc4a8'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4991161dee4f699f25517991abf504cc92ac8c229e4e61a985da90195d0238815423c9129f51cc5abf5ad0818f0bacfdf65a4aea087e93ad309fbe69ed3cbd9d
|
|
7
|
+
data.tar.gz: 9e3d4f144424de70fc3754b5939d65ee3f327cab3ad2dcecc6dd1923adc77d88db834bfaacbf059deb051fe66546248346f01d90f20593d25cea01e0f1449860
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
+
## [0.5.1] - 2026-01-03
|
|
3
|
+
|
|
4
|
+
### Fixed
|
|
5
|
+
- **Deprecation Warning**: Replaced usage of `examples` with `scenarios` to resolve Lookbook deprecation warnings.
|
|
6
|
+
|
|
7
|
+
## [0.5.0] - 2026-01-03
|
|
8
|
+
|
|
9
|
+
### ✨ New Features & Improvements
|
|
10
|
+
- **Preview Health Checks**: Added new Rake tasks to verify preview integrity:
|
|
11
|
+
- `lookbook:check`: Rapidly checks if previews load and instantiate without errors.
|
|
12
|
+
- `lookbook:deep_check`: Verified previews by effectively rendering them to catch runtime and template errors.
|
|
13
|
+
- `lookbook:missing`: Identifies ViewComponents that lack a corresponding preview.
|
|
14
|
+
- **Parallel Preview Checks**: Health checks run in parallel using `concurrent-ruby`.
|
|
15
|
+
- **Comprehensive Reporting**: Checks generate colored terminal output and a detailed HTML report (`coverage/preview_check_report.html`) including timing stats and slowest previews.
|
|
16
|
+
- **Custom Deep Check Setup**: Added `config.preview_checker_setup` to allow defining mocks (e.g., User, Warden) required for deep checking.
|
|
17
|
+
|
|
18
|
+
## [0.4.0] - 2026-01-03
|
|
19
|
+
|
|
20
|
+
### ✨ New Features & Improvements
|
|
21
|
+
- **Multiple Screenshot Variants**: Support for defining screenshot variants (e.g., specific viewports, themes) using Lookbook's `preview_display_options`.
|
|
22
|
+
- Configurable via `VARIANTS` environment variable (JSON array).
|
|
23
|
+
- Screenshots are saved in subdirectories corresponding to the variant options.
|
|
24
|
+
- Automatic browser resizing based on `width` options.
|
|
25
|
+
|
|
26
|
+
### 🧹 Housekeeping
|
|
27
|
+
- **Removed Minitest**: Switched completely to RSpec for internal testing. Deleted unused Minitest files and configuration.
|
|
2
28
|
|
|
3
29
|
## [0.3.0] - 2026-01-03
|
|
4
30
|
|
data/README.md
CHANGED
|
@@ -75,6 +75,35 @@ You can override the host or other settings inline:
|
|
|
75
75
|
LOOKBOOK_HOST=http://localhost:5000 LOOKBOOK_THREADS=8 bundle exec rake lookbook:test
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
#### Screenshot Variants
|
|
79
|
+
|
|
80
|
+
You can run your visual tests against multiple configurations (variants), such as different themes or viewports, by leveraging Lookbook's `preview_display_options`.
|
|
81
|
+
|
|
82
|
+
1. **Define Options in Lookbook**:
|
|
83
|
+
Ensure your Rails app has display options configured:
|
|
84
|
+
```ruby
|
|
85
|
+
# config/lookbook.rb
|
|
86
|
+
Lookbook.config.preview_display_options = {
|
|
87
|
+
theme: ["light", "dark"],
|
|
88
|
+
width: [["Mobile", "375px"], ["Desktop", "1280px"]]
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
2. **Run with Variants**:
|
|
93
|
+
Use the `VARIANTS` environment variable to define a JSON array of option sets to test.
|
|
94
|
+
|
|
95
|
+
*Example: Run standard tests + Dark Mode + Mobile View*
|
|
96
|
+
```bash
|
|
97
|
+
VARIANTS='[{}, {"theme":"dark"}, {"width":"Mobile"}]' bundle exec rake lookbook:test
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
* **`{}`**: Runs the default/standard preview.
|
|
101
|
+
* **`{"theme":"dark"}`**: Runs with `_display[theme]=dark`.
|
|
102
|
+
* **`{"width":"Mobile"}`**: Runs with `_display[width]=375px` AND automatically resizes the browser window to 375px width.
|
|
103
|
+
|
|
104
|
+
Screenshots for variants are saved in dedicated subfolders (e.g., `spec/visual_regression/baseline/theme-dark/`).
|
|
105
|
+
|
|
106
|
+
|
|
78
107
|
### Baseline Management
|
|
79
108
|
|
|
80
109
|
1. **First Run**: When you run the tests for the first time, all screenshots are saved as **Baselines**.
|
|
@@ -113,12 +142,31 @@ bundle exec rspec
|
|
|
113
142
|
bundle exec rspec spec/integration/full_flow_spec.rb
|
|
114
143
|
```
|
|
115
144
|
|
|
145
|
+
### Preview Health Checks
|
|
146
|
+
|
|
147
|
+
The gem provides tasks to ensure your previews are healthy and up-to-date.
|
|
148
|
+
|
|
149
|
+
#### Check Load/Syntax
|
|
150
|
+
Checks if all previews can be loaded and instantiated without errors.
|
|
151
|
+
```bash
|
|
152
|
+
bundle exec rake lookbook:check
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### Deep Check (Render)
|
|
156
|
+
effectively renders all previews to catch runtime and template errors.
|
|
157
|
+
```bash
|
|
158
|
+
bundle exec rake lookbook:deep_check
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### Find Missing Previews
|
|
162
|
+
Identifies components that don't have a corresponding preview file.
|
|
163
|
+
```bash
|
|
164
|
+
bundle exec rake lookbook:missing
|
|
165
|
+
```
|
|
166
|
+
|
|
116
167
|
## Next Steps
|
|
117
168
|
|
|
118
|
-
- **Multi-Viewport Support**: Add ability to capture screenshots at different screen widths (Mobile, Tablet, Desktop).
|
|
119
169
|
- **CI/CD Integration**: Provide recipes for GitHub Actions to run visual regression on PRs.
|
|
120
|
-
- **Reporting Dashboard**: Generate a static HTML report to easily browse all diffs in a single view.
|
|
121
|
-
- [x] **Concurrent Captures**: Optimize execution speed by parallelizing screenshot taking across multiple browser instances.
|
|
122
170
|
|
|
123
171
|
## Contributing
|
|
124
172
|
|
data/Rakefile
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'bundler/gem_tasks'
|
|
4
|
-
require '
|
|
5
|
-
|
|
6
|
-
Minitest::TestTask.create
|
|
7
|
-
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
8
5
|
require 'rubocop/rake_task'
|
|
9
6
|
|
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
8
|
+
|
|
10
9
|
RuboCop::RakeTask.new
|
|
11
10
|
|
|
12
|
-
task default: %i[
|
|
11
|
+
task default: %i[spec rubocop]
|
|
13
12
|
|
|
14
13
|
namespace :release do
|
|
15
14
|
desc 'Release with OTP (MFA) support'
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require 'rainbow'
|
|
2
|
+
require 'thor/group'
|
|
3
|
+
|
|
4
|
+
module LookbookVisualTester
|
|
5
|
+
class CheckReporter < Thor::Group
|
|
6
|
+
include Thor::Actions
|
|
7
|
+
|
|
8
|
+
argument :results
|
|
9
|
+
|
|
10
|
+
def self.source_root
|
|
11
|
+
File.dirname(__FILE__)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def report
|
|
15
|
+
@errors = results.select { |r| r.status == :failed }
|
|
16
|
+
@success_count = results.count { |r| r.status == :passed }
|
|
17
|
+
@total_duration = results.sum { |r| r.duration.to_f }
|
|
18
|
+
|
|
19
|
+
report_terminal
|
|
20
|
+
report_html
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.report_missing(missing)
|
|
24
|
+
if missing.any?
|
|
25
|
+
puts Rainbow("\nFound #{missing.size} components missing previews:").yellow
|
|
26
|
+
missing.sort_by { |m| m.component_path }.each do |m|
|
|
27
|
+
puts " - #{m.component_path}"
|
|
28
|
+
end
|
|
29
|
+
puts "\nTotal: #{missing.size} missing previews."
|
|
30
|
+
else
|
|
31
|
+
puts Rainbow('All components have previews!').green
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :errors, :success_count, :total_duration
|
|
38
|
+
|
|
39
|
+
def report_terminal
|
|
40
|
+
puts "\n--- Check Results ---"
|
|
41
|
+
|
|
42
|
+
results.each do |result|
|
|
43
|
+
if result.status == :passed
|
|
44
|
+
print Rainbow('.').green
|
|
45
|
+
else
|
|
46
|
+
print Rainbow('F').red
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
puts "\n"
|
|
50
|
+
|
|
51
|
+
if errors.any?
|
|
52
|
+
puts Rainbow("\n#{errors.size} errors found:").red
|
|
53
|
+
errors.each do |err|
|
|
54
|
+
puts "\n--------------------------------------------------"
|
|
55
|
+
puts "Preview: #{err.preview_name}##{err.example_name}"
|
|
56
|
+
puts "Error: #{err.error}"
|
|
57
|
+
puts "Time: #{err.duration.to_f.round(4)}s"
|
|
58
|
+
puts "Backtrace: #{err.backtrace&.first(3)&.join("\n ")}"
|
|
59
|
+
end
|
|
60
|
+
else
|
|
61
|
+
puts Rainbow("\nAll #{results.size} previews passed!").green
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
puts "\nTotal time: #{total_duration.round(2)}s"
|
|
65
|
+
|
|
66
|
+
report_slowest_previews
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def report_slowest_previews
|
|
70
|
+
puts "\n--- Top 5 Slowest Previews ---"
|
|
71
|
+
slowest = results.sort_by { |r| -r.duration.to_f }.first(5)
|
|
72
|
+
slowest.each do |res|
|
|
73
|
+
puts "#{Rainbow("#{res.duration.to_f.round(4)}s").yellow} - #{res.preview_name}##{res.example_name}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def report_html
|
|
78
|
+
template 'templates/preview_check_report.html.tt', 'coverage/preview_check_report.html'
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -7,6 +7,7 @@ module LookbookVisualTester
|
|
|
7
7
|
:components_folder,
|
|
8
8
|
:automatic_run,
|
|
9
9
|
:mask_selectors, :driver_adapter,
|
|
10
|
+
:preview_checker_setup,
|
|
10
11
|
:logger
|
|
11
12
|
|
|
12
13
|
DEFAULT_THREADS = 4
|
|
@@ -31,6 +32,7 @@ module LookbookVisualTester
|
|
|
31
32
|
@automatic_run = ENV.fetch('LOOKBOOK_AUTOMATIC_RUN', false)
|
|
32
33
|
@mask_selectors = []
|
|
33
34
|
@driver_adapter = :ferrum
|
|
35
|
+
@preview_checker_setup = nil
|
|
34
36
|
@logger = if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
35
37
|
Rails.logger
|
|
36
38
|
else
|
|
@@ -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 scenarios
|
|
68
|
+
examples = preview.scenarios
|
|
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
|
|
@@ -6,7 +6,8 @@ require 'lookbook_visual_tester/update_previews'
|
|
|
6
6
|
module LookbookVisualTester
|
|
7
7
|
class Railtie < ::Rails::Railtie
|
|
8
8
|
rake_tasks do
|
|
9
|
-
|
|
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|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
require 'lookbook'
|
|
2
|
+
require 'json'
|
|
2
3
|
require_relative 'configuration'
|
|
3
4
|
require_relative 'scenario_run'
|
|
4
5
|
require_relative 'services/image_comparator'
|
|
5
6
|
require_relative 'drivers/ferrum_driver'
|
|
7
|
+
require_relative 'variant_resolver'
|
|
6
8
|
|
|
7
9
|
module LookbookVisualTester
|
|
8
10
|
class Runner
|
|
@@ -15,6 +17,7 @@ module LookbookVisualTester
|
|
|
15
17
|
@driver_pool = Queue.new
|
|
16
18
|
init_driver_pool
|
|
17
19
|
@results = []
|
|
20
|
+
@variants = load_variants
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def run
|
|
@@ -28,11 +31,21 @@ module LookbookVisualTester
|
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
puts "Found #{previews.count} previews matching '#{@pattern}'."
|
|
34
|
+
puts "Running against #{@variants.size} variant(s)."
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
36
49
|
end
|
|
37
50
|
|
|
38
51
|
@results
|
|
@@ -42,13 +55,25 @@ module LookbookVisualTester
|
|
|
42
55
|
|
|
43
56
|
private
|
|
44
57
|
|
|
45
|
-
def
|
|
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)
|
|
46
71
|
previews.each do |preview|
|
|
47
72
|
group = preview.respond_to?(:scenarios) ? preview.scenarios : preview.examples
|
|
48
73
|
group.each do |scenario|
|
|
49
74
|
driver = checkout_driver
|
|
50
75
|
begin
|
|
51
|
-
@results << run_scenario(scenario, driver)
|
|
76
|
+
@results << run_scenario(scenario, driver, variant_slug, variant_options, width)
|
|
52
77
|
ensure
|
|
53
78
|
return_driver(driver)
|
|
54
79
|
end
|
|
@@ -56,7 +81,7 @@ module LookbookVisualTester
|
|
|
56
81
|
end
|
|
57
82
|
end
|
|
58
83
|
|
|
59
|
-
def run_concurrently(previews)
|
|
84
|
+
def run_concurrently(previews, variant_slug, variant_options, width)
|
|
60
85
|
require 'concurrent-ruby'
|
|
61
86
|
pool = Concurrent::FixedThreadPool.new(@config.threads)
|
|
62
87
|
promises = []
|
|
@@ -67,7 +92,7 @@ module LookbookVisualTester
|
|
|
67
92
|
promises << Concurrent::Promises.future_on(pool) do
|
|
68
93
|
driver = checkout_driver
|
|
69
94
|
begin
|
|
70
|
-
run_scenario(scenario, driver)
|
|
95
|
+
run_scenario(scenario, driver, variant_slug, variant_options, width)
|
|
71
96
|
ensure
|
|
72
97
|
return_driver(driver)
|
|
73
98
|
end
|
|
@@ -75,19 +100,14 @@ module LookbookVisualTester
|
|
|
75
100
|
end
|
|
76
101
|
end
|
|
77
102
|
|
|
78
|
-
|
|
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)
|
|
79
106
|
pool.shutdown
|
|
80
107
|
pool.wait_for_termination
|
|
81
108
|
end
|
|
82
109
|
|
|
83
110
|
def init_driver_pool
|
|
84
|
-
# Create N drivers where N = threads
|
|
85
|
-
# If sequential, we only need 1, but we can just simplify and create as many as updated threads config says
|
|
86
|
-
# Or just 1 if not concurrent?
|
|
87
|
-
# Actually, let's just stick to @config.threads.
|
|
88
|
-
# Even for sequential run, if threads was configured to 4, we might create 4 but only use 1.
|
|
89
|
-
# Optimization: if sequential, only create 1.
|
|
90
|
-
|
|
91
111
|
count = @config.threads > 1 ? @config.threads : 1
|
|
92
112
|
count.times do
|
|
93
113
|
@driver_pool << LookbookVisualTester::Drivers::FerrumDriver.new(@config)
|
|
@@ -109,20 +129,32 @@ module LookbookVisualTester
|
|
|
109
129
|
end
|
|
110
130
|
end
|
|
111
131
|
|
|
112
|
-
def run_scenario(scenario, driver)
|
|
113
|
-
run_data = ScenarioRun.new(scenario
|
|
114
|
-
|
|
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}]" : ''}"
|
|
115
136
|
|
|
116
137
|
begin
|
|
117
|
-
|
|
138
|
+
driver_width = width || 1280
|
|
139
|
+
driver.resize_window(driver_width, 800)
|
|
118
140
|
driver.visit(run_data.preview_url)
|
|
119
141
|
|
|
120
142
|
# Determine paths
|
|
121
143
|
current_path = run_data.current_path
|
|
122
144
|
baseline_path = run_data.baseline_path
|
|
123
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
|
|
124
155
|
|
|
125
156
|
FileUtils.mkdir_p(File.dirname(current_path))
|
|
157
|
+
FileUtils.mkdir_p(File.dirname(diff_path))
|
|
126
158
|
|
|
127
159
|
driver.save_screenshot(current_path.to_s)
|
|
128
160
|
|
|
@@ -2,13 +2,15 @@ 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
|
-
LookbookVisualTester.config.logger.info " Scenario: #{scenario_name}"
|
|
13
|
+
LookbookVisualTester.config.logger.info " Scenario: #{scenario_name} #{variant_suffix}"
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def preview_name
|
|
@@ -39,18 +41,34 @@ module LookbookVisualTester
|
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
def current_path
|
|
42
|
-
LookbookVisualTester.config.current_dir
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Preview Check Report</title>
|
|
5
|
+
<style>
|
|
6
|
+
body { font-family: system-ui, -apple-system, sans-serif; padding: 20px; color: #333; }
|
|
7
|
+
.summary { margin-bottom: 20px; padding: 15px; background: #f5f5f5; border-radius: 8px; }
|
|
8
|
+
.passed { color: green; }
|
|
9
|
+
.failed { color: red; }
|
|
10
|
+
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
|
11
|
+
th, td { text-align: left; padding: 10px; border-bottom: 1px solid #ddd; }
|
|
12
|
+
th { background: #f0f0f0; }
|
|
13
|
+
tr.error { background: #fff0f0; }
|
|
14
|
+
.backtrace { font-family: monospace; font-size: 0.9em; white-space: pre-wrap; color: #666; }
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<h1>Preview Check Report</h1>
|
|
19
|
+
|
|
20
|
+
<div class="summary">
|
|
21
|
+
<p><strong>Total Checks:</strong> <%= results.size %></p>
|
|
22
|
+
<p><strong>Passed:</strong> <span class="passed"><%= success_count %></span></p>
|
|
23
|
+
<p><strong>Failed:</strong> <span class="failed"><%= errors.size %></span></p>
|
|
24
|
+
<p><strong>Total Duration:</strong> <%= total_duration.round(2) %>s</p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<h2>Top 5 Slowest Previews</h2>
|
|
28
|
+
<ul>
|
|
29
|
+
<% results.sort_by { |r| -r.duration.to_f }.first(5).each do |res| %>
|
|
30
|
+
<li><strong><%= res.duration.to_f.round(4) %>s</strong> - <%= res.preview_name %>#<%= res.example_name %></li>
|
|
31
|
+
<% end %>
|
|
32
|
+
</ul>
|
|
33
|
+
|
|
34
|
+
<h2>Detailed Results</h2>
|
|
35
|
+
<table>
|
|
36
|
+
<thead>
|
|
37
|
+
<tr>
|
|
38
|
+
<th>Status</th>
|
|
39
|
+
<th>Preview</th>
|
|
40
|
+
<th>Example</th>
|
|
41
|
+
<th>Time (s)</th>
|
|
42
|
+
<th>Error</th>
|
|
43
|
+
</tr>
|
|
44
|
+
</thead>
|
|
45
|
+
<tbody>
|
|
46
|
+
<% results.sort_by { |r| r.status == :failed ? 0 : 1 }.each do |result| %>
|
|
47
|
+
<tr class="<%= 'error' if result.status == :failed %>">
|
|
48
|
+
<td class="<%= result.status %>"><%= result.status.to_s.upcase %></td>
|
|
49
|
+
<td><%= result.preview_name %></td>
|
|
50
|
+
<td><%= result.example_name %></td>
|
|
51
|
+
<td><%= result.duration.to_f.round(4) %></td>
|
|
52
|
+
<td>
|
|
53
|
+
<% if result.error %>
|
|
54
|
+
<div><strong><%= result.error %></strong></div>
|
|
55
|
+
<div class="backtrace"><%= result.backtrace&.first(5)&.join("\n") %></div>
|
|
56
|
+
<% end %>
|
|
57
|
+
</td>
|
|
58
|
+
</tr>
|
|
59
|
+
<% end %>
|
|
60
|
+
</tbody>
|
|
61
|
+
</table>
|
|
62
|
+
</body>
|
|
63
|
+
</html>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module LookbookVisualTester
|
|
2
|
+
class VariantResolver
|
|
3
|
+
attr_reader :input_variant
|
|
4
|
+
|
|
5
|
+
def initialize(input_variant)
|
|
6
|
+
@input_variant = input_variant || {}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def resolve
|
|
10
|
+
resolved = {}
|
|
11
|
+
@input_variant.each do |key, label|
|
|
12
|
+
resolved[key.to_sym] = resolve_value(key, label)
|
|
13
|
+
end
|
|
14
|
+
resolved
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def slug
|
|
18
|
+
return '' if @input_variant.empty?
|
|
19
|
+
|
|
20
|
+
@input_variant.sort_by { |k, _v| k.to_s }.map do |key, label|
|
|
21
|
+
"#{key}-#{sanitize(label)}"
|
|
22
|
+
end.join('_')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def width_in_pixels
|
|
26
|
+
resolved_width = resolve[:width]
|
|
27
|
+
return nil unless resolved_width
|
|
28
|
+
|
|
29
|
+
if resolved_width.to_s.end_with?('px')
|
|
30
|
+
resolved_width.to_i
|
|
31
|
+
else
|
|
32
|
+
nil # Ignore percentages or other units for resizing
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def resolve_value(key, label)
|
|
39
|
+
options = Lookbook.config.preview_display_options[key.to_sym]
|
|
40
|
+
return label unless options
|
|
41
|
+
|
|
42
|
+
# Options can be an array of strings or array of [label, value] arrays
|
|
43
|
+
found = options.find do |option|
|
|
44
|
+
if option.is_a?(Array)
|
|
45
|
+
option[0] == label
|
|
46
|
+
else
|
|
47
|
+
option == label
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if found
|
|
52
|
+
found.is_a?(Array) ? found[1] : found
|
|
53
|
+
else
|
|
54
|
+
label
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def sanitize(value)
|
|
59
|
+
value.to_s.gsub(/[^a-zA-Z0-9]/, '_').squeeze('_').gsub(/^_|_$/, '')
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -3,6 +3,35 @@ require 'lookbook_visual_tester/runner'
|
|
|
3
3
|
require 'lookbook_visual_tester/report_generator'
|
|
4
4
|
require 'lookbook_visual_tester/json_output_handler'
|
|
5
5
|
|
|
6
|
+
require 'lookbook_visual_tester/preview_checker'
|
|
7
|
+
require 'lookbook_visual_tester/check_reporter'
|
|
8
|
+
|
|
9
|
+
namespace :lookbook do
|
|
10
|
+
desc 'Check previews for load/syntax errors'
|
|
11
|
+
task check: :environment do
|
|
12
|
+
puts 'Checking previews...'
|
|
13
|
+
checker = LookbookVisualTester::PreviewChecker.new
|
|
14
|
+
results = checker.check
|
|
15
|
+
LookbookVisualTester::CheckReporter.start([results])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc 'Deep check previews by rendering them'
|
|
19
|
+
task deep_check: :environment do
|
|
20
|
+
puts 'Deep checking previews (render)...'
|
|
21
|
+
checker = LookbookVisualTester::PreviewChecker.new
|
|
22
|
+
results = checker.deep_check
|
|
23
|
+
LookbookVisualTester::CheckReporter.start([results])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
desc 'Find components missing previews'
|
|
27
|
+
task missing: :environment do
|
|
28
|
+
puts 'Finding missing previews...'
|
|
29
|
+
checker = LookbookVisualTester::PreviewChecker.new
|
|
30
|
+
missing = checker.missing
|
|
31
|
+
LookbookVisualTester::CheckReporter.report_missing(missing)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
6
35
|
namespace :lookbook do
|
|
7
36
|
desc 'List all available previews'
|
|
8
37
|
task :list, [:format] => :environment do |_, args|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lookbook_visual_tester
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Murilo Vasconcelos
|
|
@@ -79,6 +79,20 @@ dependencies:
|
|
|
79
79
|
- - ">="
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rainbow
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
82
96
|
- !ruby/object:Gem::Dependency
|
|
83
97
|
name: bundler
|
|
84
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -155,11 +169,13 @@ files:
|
|
|
155
169
|
- lib/lookbook_visual_tester.rb
|
|
156
170
|
- lib/lookbook_visual_tester/baseline_manager.rb
|
|
157
171
|
- lib/lookbook_visual_tester/capybara_setup.rb
|
|
172
|
+
- lib/lookbook_visual_tester/check_reporter.rb
|
|
158
173
|
- lib/lookbook_visual_tester/configuration.rb
|
|
159
174
|
- lib/lookbook_visual_tester/driver.rb
|
|
160
175
|
- lib/lookbook_visual_tester/drivers/ferrum_driver.rb
|
|
161
176
|
- lib/lookbook_visual_tester/image_comparator.rb
|
|
162
177
|
- lib/lookbook_visual_tester/json_output_handler.rb
|
|
178
|
+
- lib/lookbook_visual_tester/preview_checker.rb
|
|
163
179
|
- lib/lookbook_visual_tester/railtie.rb
|
|
164
180
|
- lib/lookbook_visual_tester/report_generator.rb
|
|
165
181
|
- lib/lookbook_visual_tester/runner.rb
|
|
@@ -170,8 +186,10 @@ files:
|
|
|
170
186
|
- lib/lookbook_visual_tester/services/image_comparator.rb
|
|
171
187
|
- lib/lookbook_visual_tester/session_manager.rb
|
|
172
188
|
- lib/lookbook_visual_tester/store.rb
|
|
189
|
+
- lib/lookbook_visual_tester/templates/preview_check_report.html.tt
|
|
173
190
|
- lib/lookbook_visual_tester/templates/report.html.erb
|
|
174
191
|
- lib/lookbook_visual_tester/update_previews.rb
|
|
192
|
+
- lib/lookbook_visual_tester/variant_resolver.rb
|
|
175
193
|
- lib/lookbook_visual_tester/version.rb
|
|
176
194
|
- lib/tasks/lookbook_visual_tester.rake
|
|
177
195
|
- sig/lookbook_visual_tester.rbs
|