lookbook_visual_tester 0.1.6 → 0.3.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +0 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +18 -3
- data/README.md +96 -32
- data/RELEASING.md +31 -0
- data/Rakefile +12 -0
- data/lib/lookbook_visual_tester/configuration.rb +25 -4
- data/lib/lookbook_visual_tester/driver.rb +51 -0
- data/lib/lookbook_visual_tester/drivers/ferrum_driver.rb +111 -0
- data/lib/lookbook_visual_tester/json_output_handler.rb +9 -0
- data/lib/lookbook_visual_tester/railtie.rb +3 -1
- data/lib/lookbook_visual_tester/report_generator.rb +25 -48
- data/lib/lookbook_visual_tester/runner.rb +192 -0
- data/lib/lookbook_visual_tester/scenario_finder.rb +7 -2
- data/lib/lookbook_visual_tester/scenario_run.rb +2 -2
- data/lib/lookbook_visual_tester/services/image_comparator.rb +66 -0
- data/lib/lookbook_visual_tester/templates/report.html.erb +206 -0
- data/lib/lookbook_visual_tester/update_previews.rb +3 -2
- data/lib/lookbook_visual_tester/version.rb +1 -1
- data/lib/lookbook_visual_tester.rb +11 -3
- data/lib/tasks/lookbook_visual_tester.rake +264 -58
- metadata +27 -35
- data/tasks/lookbook_visual_tester.rake +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
require 'lookbook'
|
|
2
|
+
require_relative 'configuration'
|
|
3
|
+
require_relative 'scenario_run'
|
|
4
|
+
require_relative 'services/image_comparator'
|
|
5
|
+
require_relative 'drivers/ferrum_driver'
|
|
6
|
+
|
|
7
|
+
module LookbookVisualTester
|
|
8
|
+
class Runner
|
|
9
|
+
Result = Struct.new(:scenario_name, :status, :mismatch, :diff_path, :error, :baseline_path,
|
|
10
|
+
:current_path, keyword_init: true)
|
|
11
|
+
|
|
12
|
+
def initialize(config = LookbookVisualTester.config, pattern: nil)
|
|
13
|
+
@config = config
|
|
14
|
+
@pattern = pattern
|
|
15
|
+
@driver_pool = Queue.new
|
|
16
|
+
init_driver_pool
|
|
17
|
+
@results = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
previews = Lookbook.previews
|
|
22
|
+
|
|
23
|
+
if @pattern.present?
|
|
24
|
+
previews = previews.select do |preview|
|
|
25
|
+
preview.label.downcase.include?(@pattern.downcase) ||
|
|
26
|
+
preview.name.downcase.include?(@pattern.downcase)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
puts "Found #{previews.count} previews matching '#{@pattern}'."
|
|
31
|
+
|
|
32
|
+
if @config.threads > 1
|
|
33
|
+
run_concurrently(previews)
|
|
34
|
+
else
|
|
35
|
+
run_sequentially(previews)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@results
|
|
39
|
+
ensure
|
|
40
|
+
cleanup_drivers
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def run_sequentially(previews)
|
|
46
|
+
previews.each do |preview|
|
|
47
|
+
group = preview.respond_to?(:scenarios) ? preview.scenarios : preview.examples
|
|
48
|
+
group.each do |scenario|
|
|
49
|
+
driver = checkout_driver
|
|
50
|
+
begin
|
|
51
|
+
@results << run_scenario(scenario, driver)
|
|
52
|
+
ensure
|
|
53
|
+
return_driver(driver)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def run_concurrently(previews)
|
|
60
|
+
require 'concurrent-ruby'
|
|
61
|
+
pool = Concurrent::FixedThreadPool.new(@config.threads)
|
|
62
|
+
promises = []
|
|
63
|
+
|
|
64
|
+
previews.each do |preview|
|
|
65
|
+
group = preview.respond_to?(:scenarios) ? preview.scenarios : preview.examples
|
|
66
|
+
group.each do |scenario|
|
|
67
|
+
promises << Concurrent::Promises.future_on(pool) do
|
|
68
|
+
driver = checkout_driver
|
|
69
|
+
begin
|
|
70
|
+
run_scenario(scenario, driver)
|
|
71
|
+
ensure
|
|
72
|
+
return_driver(driver)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@results = Concurrent::Promises.zip(*promises).value
|
|
79
|
+
pool.shutdown
|
|
80
|
+
pool.wait_for_termination
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
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
|
+
count = @config.threads > 1 ? @config.threads : 1
|
|
92
|
+
count.times do
|
|
93
|
+
@driver_pool << LookbookVisualTester::Drivers::FerrumDriver.new(@config)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def checkout_driver
|
|
98
|
+
@driver_pool.pop
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def return_driver(driver)
|
|
102
|
+
@driver_pool << driver
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def cleanup_drivers
|
|
106
|
+
until @driver_pool.empty?
|
|
107
|
+
driver = @driver_pool.pop
|
|
108
|
+
driver.cleanup
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def run_scenario(scenario, driver)
|
|
113
|
+
run_data = ScenarioRun.new(scenario)
|
|
114
|
+
puts "Running visual test for: #{run_data.name}"
|
|
115
|
+
|
|
116
|
+
begin
|
|
117
|
+
driver.resize_window(1280, 800) # Default or config
|
|
118
|
+
driver.visit(run_data.preview_url)
|
|
119
|
+
|
|
120
|
+
# Determine paths
|
|
121
|
+
current_path = run_data.current_path
|
|
122
|
+
baseline_path = run_data.baseline_path
|
|
123
|
+
diff_path = @config.diff_dir.join(run_data.diff_filename)
|
|
124
|
+
|
|
125
|
+
FileUtils.mkdir_p(File.dirname(current_path))
|
|
126
|
+
|
|
127
|
+
driver.save_screenshot(current_path.to_s)
|
|
128
|
+
|
|
129
|
+
# Trimming (Feature parity with legacy ScreenshotTaker)
|
|
130
|
+
if File.exist?(current_path)
|
|
131
|
+
system("convert #{current_path} -trim -bordercolor white -border 10x10 #{current_path}")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
comparator = LookbookVisualTester::ImageComparator.new(
|
|
135
|
+
baseline_path.to_s,
|
|
136
|
+
current_path.to_s,
|
|
137
|
+
diff_path.to_s
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
result = comparator.call
|
|
141
|
+
|
|
142
|
+
status = :passed
|
|
143
|
+
mismatch = 0.0
|
|
144
|
+
error = nil
|
|
145
|
+
|
|
146
|
+
if result[:error]
|
|
147
|
+
if result[:error] == 'Baseline not found'
|
|
148
|
+
# First run, maybe auto-approve or just report
|
|
149
|
+
puts ' [NEW] Baseline not found. Saved current as potential baseline.'
|
|
150
|
+
status = :new
|
|
151
|
+
else
|
|
152
|
+
puts " [ERROR] #{result[:error]}"
|
|
153
|
+
status = :error
|
|
154
|
+
error = result[:error]
|
|
155
|
+
end
|
|
156
|
+
elsif result[:mismatch] > 0
|
|
157
|
+
mismatch = result[:mismatch]
|
|
158
|
+
puts " [FAIL] Mismatch: #{mismatch.round(2)}%. Diff saved to #{diff_path}"
|
|
159
|
+
status = :failed
|
|
160
|
+
|
|
161
|
+
# Clipboard (Feature parity with legacy ScreenshotTaker)
|
|
162
|
+
if @config.copy_to_clipboard
|
|
163
|
+
system("xclip -selection clipboard -t image/png -i #{current_path}")
|
|
164
|
+
end
|
|
165
|
+
else
|
|
166
|
+
puts ' [PASS] Identical.'
|
|
167
|
+
status = :passed
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
Result.new(
|
|
171
|
+
scenario_name: run_data.name,
|
|
172
|
+
status: status,
|
|
173
|
+
mismatch: mismatch,
|
|
174
|
+
diff_path: diff_path.to_s,
|
|
175
|
+
error: error,
|
|
176
|
+
baseline_path: baseline_path.to_s,
|
|
177
|
+
current_path: current_path.to_s
|
|
178
|
+
)
|
|
179
|
+
rescue StandardError => e
|
|
180
|
+
puts " [ERROR] Exception: #{e.message}"
|
|
181
|
+
puts e.backtrace.take(5)
|
|
182
|
+
Result.new(
|
|
183
|
+
scenario_name: run_data.name,
|
|
184
|
+
status: :error,
|
|
185
|
+
error: e.message,
|
|
186
|
+
baseline_path: run_data.baseline_path.to_s,
|
|
187
|
+
current_path: run_data.current_path.to_s
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -12,19 +12,24 @@ module LookbookVisualTester
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def regex
|
|
15
|
-
@regex = Regexp.new(
|
|
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
|
-
|
|
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
|
|
|
@@ -8,11 +8,11 @@ module LookbookVisualTester
|
|
|
8
8
|
@scenario = scenario
|
|
9
9
|
@preview = scenario.preview
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
LookbookVisualTester.config.logger.info " Scenario: #{scenario_name}"
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def preview_name
|
|
15
|
-
preview.name.underscore
|
|
15
|
+
preview.name.underscore.gsub('/', '_')
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def scenario_name
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require 'chunky_png'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
module LookbookVisualTester
|
|
5
|
+
class ImageComparator
|
|
6
|
+
attr_reader :baseline_path, :current_path, :diff_path
|
|
7
|
+
|
|
8
|
+
# Neon Red for differences
|
|
9
|
+
DIFF_COLOR = ChunkyPNG::Color.from_hex('#FF073A')
|
|
10
|
+
|
|
11
|
+
def initialize(baseline_path, current_path, diff_path)
|
|
12
|
+
@baseline_path = baseline_path
|
|
13
|
+
@current_path = current_path
|
|
14
|
+
@diff_path = diff_path
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
unless File.exist?(baseline_path)
|
|
19
|
+
return { diff_path: nil, mismatch: 0.0, error: "Baseline not found" }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
baseline = ChunkyPNG::Image.from_file(baseline_path)
|
|
23
|
+
current = ChunkyPNG::Image.from_file(current_path)
|
|
24
|
+
|
|
25
|
+
if baseline.dimension != current.dimension
|
|
26
|
+
return {
|
|
27
|
+
diff_path: nil,
|
|
28
|
+
mismatch: 100.0,
|
|
29
|
+
error: "Dimensions mismatch: #{baseline.width}x#{baseline.height} vs #{current.width}x#{current.height}"
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
diff_pixels_count = 0
|
|
34
|
+
diff_image = ChunkyPNG::Image.new(baseline.width, baseline.height, ChunkyPNG::Color::WHITE)
|
|
35
|
+
|
|
36
|
+
baseline.height.times do |y|
|
|
37
|
+
baseline.width.times do |x|
|
|
38
|
+
pixel1 = baseline[x, y]
|
|
39
|
+
pixel2 = current[x, y]
|
|
40
|
+
|
|
41
|
+
if pixel1 != pixel2
|
|
42
|
+
diff_image[x, y] = DIFF_COLOR
|
|
43
|
+
diff_pixels_count += 1
|
|
44
|
+
else
|
|
45
|
+
# Blue context for unchanged pixels to make it easier for humans
|
|
46
|
+
gray_val = ChunkyPNG::Color.r(ChunkyPNG::Color.grayscale_teint(pixel1))
|
|
47
|
+
# Keep intensity in R/G but push blue to make it the dominant tint
|
|
48
|
+
diff_image[x, y] = ChunkyPNG::Color.rgba(gray_val, gray_val, 255, 50)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
mismatch_percentage = (diff_pixels_count.to_f / baseline.pixels.size) * 100.0
|
|
54
|
+
|
|
55
|
+
if diff_pixels_count > 0
|
|
56
|
+
FileUtils.mkdir_p(File.dirname(diff_path))
|
|
57
|
+
diff_image.save(diff_path)
|
|
58
|
+
{ diff_path: diff_path, mismatch: mismatch_percentage, error: nil }
|
|
59
|
+
else
|
|
60
|
+
{ diff_path: nil, mismatch: 0.0, error: nil }
|
|
61
|
+
end
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
{ diff_path: nil, mismatch: 0.0, error: e.message }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Visual Regression Report</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg-dark: #0f172a;
|
|
10
|
+
--bg-card: #1e293b;
|
|
11
|
+
--text-main: #f8fafc;
|
|
12
|
+
--text-muted: #94a3b8;
|
|
13
|
+
--danger: #ef4444;
|
|
14
|
+
--success: #22c55e;
|
|
15
|
+
--new: #3b82f6;
|
|
16
|
+
--border: #334155;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
body {
|
|
20
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
21
|
+
background-color: var(--bg-dark);
|
|
22
|
+
color: var(--text-main);
|
|
23
|
+
margin: 0;
|
|
24
|
+
padding: 20px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Header & Stats */
|
|
28
|
+
.header {
|
|
29
|
+
display: flex;
|
|
30
|
+
justify-content: space-between;
|
|
31
|
+
align-items: center;
|
|
32
|
+
margin-bottom: 30px;
|
|
33
|
+
border-bottom: 1px solid var(--border);
|
|
34
|
+
padding-bottom: 20px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.stats {
|
|
38
|
+
display: flex;
|
|
39
|
+
gap: 15px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.badge {
|
|
43
|
+
padding: 5px 12px;
|
|
44
|
+
border-radius: 9999px;
|
|
45
|
+
font-weight: bold;
|
|
46
|
+
font-size: 0.9rem;
|
|
47
|
+
}
|
|
48
|
+
.badge-failed { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
|
|
49
|
+
.badge-passed { background: rgba(34, 197, 94, 0.2); color: var(--success); }
|
|
50
|
+
.badge-new { background: rgba(59, 130, 246, 0.2); color: var(--new); }
|
|
51
|
+
|
|
52
|
+
/* Test Case Card */
|
|
53
|
+
.test-case {
|
|
54
|
+
background: var(--bg-card);
|
|
55
|
+
border: 1px solid var(--border);
|
|
56
|
+
border-radius: 8px;
|
|
57
|
+
margin-bottom: 20px;
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.test-header {
|
|
62
|
+
padding: 15px;
|
|
63
|
+
display: flex;
|
|
64
|
+
justify-content: space-between;
|
|
65
|
+
align-items: center;
|
|
66
|
+
background: rgba(0,0,0,0.2);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.test-title { font-size: 1.1rem; font-weight: 600; }
|
|
70
|
+
|
|
71
|
+
/* Comparison View */
|
|
72
|
+
.comparison-grid {
|
|
73
|
+
display: grid;
|
|
74
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
75
|
+
gap: 2px;
|
|
76
|
+
background: var(--border);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.image-col {
|
|
80
|
+
background: var(--bg-card);
|
|
81
|
+
padding: 10px;
|
|
82
|
+
text-align: center;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.image-col h4 {
|
|
86
|
+
color: var(--text-muted);
|
|
87
|
+
margin: 0 0 10px 0;
|
|
88
|
+
font-size: 0.8rem;
|
|
89
|
+
text-transform: uppercase;
|
|
90
|
+
letter-spacing: 1px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.image-col img {
|
|
94
|
+
max-width: 100%;
|
|
95
|
+
height: auto;
|
|
96
|
+
border: 1px solid var(--border);
|
|
97
|
+
display: block; /* Removes bottom spacing */
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Actions */
|
|
101
|
+
.actions {
|
|
102
|
+
padding: 15px;
|
|
103
|
+
text-align: right;
|
|
104
|
+
border-top: 1px solid var(--border);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.btn {
|
|
108
|
+
background: var(--bg-dark);
|
|
109
|
+
border: 1px solid var(--border);
|
|
110
|
+
color: var(--text-main);
|
|
111
|
+
padding: 8px 16px;
|
|
112
|
+
border-radius: 4px;
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
font-size: 0.9rem;
|
|
115
|
+
transition: all 0.2s;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.btn:hover { background: #334155; }
|
|
119
|
+
|
|
120
|
+
.btn-copy {
|
|
121
|
+
color: var(--new);
|
|
122
|
+
border-color: var(--new);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Helper helper */
|
|
126
|
+
.hidden { display: none; }
|
|
127
|
+
</style>
|
|
128
|
+
</head>
|
|
129
|
+
<body>
|
|
130
|
+
|
|
131
|
+
<div class="header">
|
|
132
|
+
<h1>Visual Test Report</h1>
|
|
133
|
+
<div class="stats">
|
|
134
|
+
<span class="badge badge-failed"><%= @stats[:failed] %> Failed</span>
|
|
135
|
+
<span class="badge badge-new"><%= @stats[:new] %> New</span>
|
|
136
|
+
<span class="badge badge-passed"><%= @stats[:passed] %> Passed</span>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<% @results.each do |result| %>
|
|
141
|
+
<% next if result.status == :passed # Skip passed tests to reduce noise %>
|
|
142
|
+
|
|
143
|
+
<div class="test-case">
|
|
144
|
+
<div class="test-header">
|
|
145
|
+
<span class="test-title">
|
|
146
|
+
<%= result.scenario_name %>
|
|
147
|
+
<span class="badge badge-<%= result.status %>"><%= result.status.upcase %></span>
|
|
148
|
+
</span>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="comparison-grid">
|
|
152
|
+
<div class="image-col">
|
|
153
|
+
<h4>Baseline</h4>
|
|
154
|
+
<% if result.baseline_path && File.exist?(result.baseline_path) %>
|
|
155
|
+
<img src="<%= File.expand_path(result.baseline_path, Dir.pwd) %>" alt="Baseline">
|
|
156
|
+
<% else %>
|
|
157
|
+
<div style="padding: 40px; color: var(--text-muted);">No Baseline</div>
|
|
158
|
+
<% end %>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div class="image-col">
|
|
162
|
+
<h4>Diff</h4>
|
|
163
|
+
<% if result.diff_path && File.exist?(result.diff_path) %>
|
|
164
|
+
<img src="<%= File.expand_path(result.diff_path, Dir.pwd) %>" alt="Diff">
|
|
165
|
+
<% else %>
|
|
166
|
+
<div style="padding: 40px; color: var(--text-muted);">No Diff Available</div>
|
|
167
|
+
<% end %>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div class="image-col">
|
|
171
|
+
<h4>New / Actual</h4>
|
|
172
|
+
<% if result.current_path && File.exist?(result.current_path) %>
|
|
173
|
+
<img src="<%= File.expand_path(result.current_path, Dir.pwd) %>" alt="Actual">
|
|
174
|
+
<% else %>
|
|
175
|
+
<div style="padding: 40px; color: var(--text-muted);">No Capture</div>
|
|
176
|
+
<% end %>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div class="actions">
|
|
181
|
+
<input type="hidden" value='<%= approve_command(result) %>' id="cmd-<%= result.object_id %>">
|
|
182
|
+
|
|
183
|
+
<button class="btn btn-copy" onclick="copyCommand('<%= result.object_id %>')">
|
|
184
|
+
📋 Copy Approval Command
|
|
185
|
+
</button>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
<% end %>
|
|
189
|
+
|
|
190
|
+
<% if @stats[:failed] == 0 && @stats[:new] == 0 %>
|
|
191
|
+
<div style="text-align: center; padding: 50px; color: var(--success);">
|
|
192
|
+
<h2>All clear! ✨</h2>
|
|
193
|
+
<p>No visual regressions detected.</p>
|
|
194
|
+
</div>
|
|
195
|
+
<% end %>
|
|
196
|
+
|
|
197
|
+
<script>
|
|
198
|
+
function copyCommand(id) {
|
|
199
|
+
const cmd = document.getElementById('cmd-' + id).value;
|
|
200
|
+
navigator.clipboard.writeText(cmd).then(() => {
|
|
201
|
+
alert('Command copied! Paste it in your terminal to approve this change.');
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
</script>
|
|
205
|
+
</body>
|
|
206
|
+
</html>
|
|
@@ -63,10 +63,11 @@ module LookbookVisualTester
|
|
|
63
63
|
selected_previews.each do |preview|
|
|
64
64
|
Rails.logger.info "LookbookVisualTester: entering #{preview.inspect}"
|
|
65
65
|
|
|
66
|
-
preview.scenarios.
|
|
66
|
+
group = preview.respond_to?(:scenarios) ? preview.scenarios : preview.examples
|
|
67
|
+
group.each do |scenario|
|
|
67
68
|
scenario_run = LookbookVisualTester::ScenarioRun.new(scenario)
|
|
68
69
|
Rails.logger.info "LookbookVisualTester: Processing scenario #{scenario_run.inspect}"
|
|
69
|
-
LookbookVisualTester::ScreenshotTaker.call(scenario_run:)
|
|
70
|
+
LookbookVisualTester::ScreenshotTaker.call(scenario_run: scenario_run)
|
|
70
71
|
end
|
|
71
72
|
end
|
|
72
73
|
end
|
|
@@ -3,11 +3,19 @@
|
|
|
3
3
|
# lib/lookbook_visual_tester.rb
|
|
4
4
|
|
|
5
5
|
require_relative 'lookbook_visual_tester/version'
|
|
6
|
+
require_relative 'lookbook_visual_tester/configuration'
|
|
6
7
|
require_relative 'lookbook_visual_tester/railtie' if defined?(Rails)
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
require_relative 'lookbook_visual_tester/scenario_finder'
|
|
9
|
+
require_relative 'lookbook_visual_tester/store'
|
|
10
|
+
require_relative 'lookbook_visual_tester/runner'
|
|
11
|
+
require_relative 'lookbook_visual_tester/driver'
|
|
12
|
+
require_relative 'lookbook_visual_tester/drivers/ferrum_driver'
|
|
13
|
+
require_relative 'lookbook_visual_tester/services/image_comparator'
|
|
9
14
|
|
|
10
15
|
module LookbookVisualTester
|
|
11
16
|
class Error < StandardError; end
|
|
12
|
-
|
|
17
|
+
|
|
18
|
+
def self.configure
|
|
19
|
+
yield(config)
|
|
20
|
+
end
|
|
13
21
|
end
|