percy-capybara 5.0.1.pre.1 → 5.0.1.pre.3
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/lib/percy/capybara.rb +176 -2
- data/lib/percy/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 55183f20f35bdf96129984ea3eafb21f484bf7d8abd648a07a5da0f9cbed8b20
|
|
4
|
+
data.tar.gz: 9f2338e88239dcb83767bfa8c0edac9a8e0cbe311c341fa2a43f0540dee295d7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5638fbbfc7505c07fa32a638a415f8ff0dc46ffcdb4d74695800158e8ee85ec011eea3cee429495baf24f34aba256b370e2cc73f0c7c6369a72a143ee638780f
|
|
7
|
+
data.tar.gz: 51aa201e617f53d1e75592cb64f4920f7c1717966e5f990b12c977b819fca7a37a7a812e7e100c727f1d96fd61da63800ea3666f99d21e1e17bb1920a8b60581
|
data/lib/percy/capybara.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'json'
|
|
1
2
|
require 'net/http'
|
|
2
3
|
require 'uri'
|
|
3
4
|
require 'capybara/dsl'
|
|
@@ -14,6 +15,12 @@ module PercyCapybara
|
|
|
14
15
|
private_constant :CLIENT_INFO
|
|
15
16
|
private_constant :ENV_INFO
|
|
16
17
|
|
|
18
|
+
UNSUPPORTED_IFRAME_SRCS = %w[
|
|
19
|
+
about:blank about:srcdoc javascript: data: blob: vbscript: chrome: chrome-extension:
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
private_constant :UNSUPPORTED_IFRAME_SRCS
|
|
23
|
+
|
|
17
24
|
# Take a DOM snapshot and post it to the snapshot endpoint
|
|
18
25
|
def percy_snapshot(name, options = {})
|
|
19
26
|
return unless percy_enabled?
|
|
@@ -21,6 +28,9 @@ module PercyCapybara
|
|
|
21
28
|
page = Capybara.current_session
|
|
22
29
|
|
|
23
30
|
begin
|
|
31
|
+
percy_dom_script = fetch_percy_dom
|
|
32
|
+
page.evaluate_script(percy_dom_script)
|
|
33
|
+
dom_snapshot = get_serialized_dom(page, options, percy_dom_script)
|
|
24
34
|
page.evaluate_script(fetch_percy_dom)
|
|
25
35
|
|
|
26
36
|
# Readiness gate -- runs before serialize when CLI supports it.
|
|
@@ -29,8 +39,14 @@ module PercyCapybara
|
|
|
29
39
|
# older CLIs that lack waitForReady.
|
|
30
40
|
readiness_diagnostics = wait_for_ready(page, options)
|
|
31
41
|
|
|
42
|
+
# Merge .percy.yml config options with snapshot options (snapshot options take priority).
|
|
43
|
+
# Deep merge with consistent string keys at all levels: nested Hashes merge
|
|
44
|
+
# recursively, per-call wins at leaves, arrays/scalars replace.
|
|
45
|
+
config_options = @cli_config&.dig('snapshot') || {}
|
|
46
|
+
merged_options = deep_merge_options(config_options, deep_stringify(options))
|
|
47
|
+
|
|
32
48
|
dom_snapshot = page
|
|
33
|
-
.evaluate_script("(function() { return PercyDOM.serialize(#{
|
|
49
|
+
.evaluate_script("(function() { return PercyDOM.serialize(#{merged_options.to_json}) })()")
|
|
34
50
|
|
|
35
51
|
if readiness_diagnostics && dom_snapshot.is_a?(Hash)
|
|
36
52
|
dom_snapshot['readiness_diagnostics'] = readiness_diagnostics
|
|
@@ -44,7 +60,16 @@ module PercyCapybara
|
|
|
44
60
|
environment_info: ENV_INFO,
|
|
45
61
|
**options,)
|
|
46
62
|
|
|
47
|
-
|
|
63
|
+
data =
|
|
64
|
+
begin
|
|
65
|
+
JSON.parse(response.body)
|
|
66
|
+
rescue JSON::ParserError => e
|
|
67
|
+
log("Could not parse snapshot response for '#{name}': invalid JSON")
|
|
68
|
+
if PERCY_DEBUG then log(e) end
|
|
69
|
+
return
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
unless data['success']
|
|
48
73
|
raise StandardError, data['error']
|
|
49
74
|
end
|
|
50
75
|
rescue StandardError => e
|
|
@@ -54,6 +79,135 @@ module PercyCapybara
|
|
|
54
79
|
end
|
|
55
80
|
end
|
|
56
81
|
|
|
82
|
+
private def get_serialized_dom(page, options, percy_dom_script)
|
|
83
|
+
dom_snapshot = page
|
|
84
|
+
.evaluate_script("(function() { return PercyDOM.serialize(#{options.to_json}) })()")
|
|
85
|
+
|
|
86
|
+
driver = page.driver.browser
|
|
87
|
+
begin
|
|
88
|
+
page_origin = get_origin(page.current_url)
|
|
89
|
+
iframes = driver.find_elements(:tag_name, 'iframe')
|
|
90
|
+
log("Found #{iframes.length} total iframe(s) on page") if PERCY_DEBUG
|
|
91
|
+
|
|
92
|
+
processed_frames = []
|
|
93
|
+
iframes.each do |frame|
|
|
94
|
+
frame_src = frame.attribute('src')
|
|
95
|
+
|
|
96
|
+
if unsupported_iframe_src?(frame_src)
|
|
97
|
+
log("Skipping unsupported iframe src: #{frame_src}") if PERCY_DEBUG && frame_src
|
|
98
|
+
next
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
begin
|
|
102
|
+
frame_origin = get_origin(URI.join(page.current_url, frame_src).to_s)
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
log("Skipping iframe with invalid URL \"#{frame_src}\": #{e}") if PERCY_DEBUG
|
|
105
|
+
next
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
if frame_origin == page_origin
|
|
109
|
+
log("Skipping same-origin iframe: #{frame_src}") if PERCY_DEBUG
|
|
110
|
+
next
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Pre-flight check: avoid the switch/serialize round-trip if there's no
|
|
114
|
+
# data-percy-element-id, since process_frame would discard the snapshot
|
|
115
|
+
# anyway and we'd silently waste a frame switch.
|
|
116
|
+
unless frame.attribute('data-percy-element-id')
|
|
117
|
+
log("Skipping iframe #{frame_src}: no data-percy-element-id found") if PERCY_DEBUG
|
|
118
|
+
next
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
result = process_frame(driver, frame, options, percy_dom_script)
|
|
122
|
+
processed_frames << result if result
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if processed_frames.any?
|
|
126
|
+
dom_snapshot['corsIframes'] = processed_frames
|
|
127
|
+
log("Captured #{processed_frames.length} cross-origin iframe(s)")
|
|
128
|
+
end
|
|
129
|
+
rescue StandardError => e
|
|
130
|
+
log("Failed to process cross-origin iframes: #{e}") if PERCY_DEBUG
|
|
131
|
+
begin
|
|
132
|
+
driver.switch_to.default_content
|
|
133
|
+
rescue StandardError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
dom_snapshot
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private def unsupported_iframe_src?(src)
|
|
142
|
+
return true if src.nil? || src.empty?
|
|
143
|
+
|
|
144
|
+
UNSUPPORTED_IFRAME_SRCS.any? { |prefix| src == prefix || src.start_with?(prefix) }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private def get_origin(url)
|
|
148
|
+
uri = URI.parse(url)
|
|
149
|
+
raise URI::InvalidURIError, "no host in #{url}" if uri.host.nil?
|
|
150
|
+
|
|
151
|
+
default_ports = {'http' => 80, 'https' => 443}
|
|
152
|
+
netloc = uri.host.to_s
|
|
153
|
+
netloc += ":#{uri.port}" if uri.port && uri.port != default_ports[uri.scheme]
|
|
154
|
+
"#{uri.scheme}://#{netloc}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private def process_frame(driver, frame_element, options, percy_dom_script)
|
|
158
|
+
frame_url = frame_element.attribute('src') || 'unknown-src'
|
|
159
|
+
log("Processing cross-origin iframe: #{frame_url}") if PERCY_DEBUG
|
|
160
|
+
|
|
161
|
+
begin
|
|
162
|
+
driver.switch_to.frame(frame_element)
|
|
163
|
+
begin
|
|
164
|
+
driver.execute_script(percy_dom_script)
|
|
165
|
+
iframe_options = options.merge('enableJavaScript' => true)
|
|
166
|
+
iframe_snapshot =
|
|
167
|
+
driver.execute_script("return PercyDOM.serialize(#{iframe_options.to_json})")
|
|
168
|
+
log("Serialized cross-origin iframe: #{frame_url}") if PERCY_DEBUG
|
|
169
|
+
rescue StandardError => e
|
|
170
|
+
log("Failed to serialize cross-origin iframe #{frame_url}: #{e}") if PERCY_DEBUG
|
|
171
|
+
return nil
|
|
172
|
+
ensure
|
|
173
|
+
begin
|
|
174
|
+
driver.switch_to.default_content
|
|
175
|
+
rescue StandardError
|
|
176
|
+
begin
|
|
177
|
+
driver.switch_to.parent_frame
|
|
178
|
+
rescue StandardError
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
rescue StandardError => e
|
|
184
|
+
log("Failed to switch to frame #{frame_url}: #{e}") if PERCY_DEBUG
|
|
185
|
+
begin
|
|
186
|
+
driver.switch_to.default_content
|
|
187
|
+
rescue StandardError
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
return nil
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
percy_element_id = frame_element.attribute('data-percy-element-id')
|
|
194
|
+
unless percy_element_id
|
|
195
|
+
log("Skipping frame #{frame_url}: no data-percy-element-id found") if PERCY_DEBUG
|
|
196
|
+
return nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
if PERCY_DEBUG
|
|
200
|
+
log("Successfully captured cross-origin iframe: #{frame_url} " \
|
|
201
|
+
"(percyElementId: #{percy_element_id})")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
{
|
|
205
|
+
'iframeData' => {'percyElementId' => percy_element_id},
|
|
206
|
+
'iframeSnapshot' => iframe_snapshot,
|
|
207
|
+
'frameUrl' => frame_url,
|
|
208
|
+
}
|
|
209
|
+
end
|
|
210
|
+
|
|
57
211
|
# Determine if the Percy server is running, caching the result so it is only checked once
|
|
58
212
|
private def percy_enabled?
|
|
59
213
|
return @percy_enabled unless @percy_enabled.nil?
|
|
@@ -77,6 +231,8 @@ module PercyCapybara
|
|
|
77
231
|
return false
|
|
78
232
|
end
|
|
79
233
|
|
|
234
|
+
body = response.body.to_s.empty? ? {} : JSON.parse(response.body)
|
|
235
|
+
@cli_config = body['config'] || {}
|
|
80
236
|
@percy_enabled = true
|
|
81
237
|
true
|
|
82
238
|
rescue StandardError => e
|
|
@@ -129,6 +285,24 @@ module PercyCapybara
|
|
|
129
285
|
end
|
|
130
286
|
end
|
|
131
287
|
|
|
288
|
+
# Recursively stringify all Hash keys so per-call options match the
|
|
289
|
+
# string-keyed config parsed from .percy.yml JSON at every nesting level.
|
|
290
|
+
private def deep_stringify(obj)
|
|
291
|
+
case obj
|
|
292
|
+
when Hash then obj.each_with_object({}) { |(k, v), h| h[k.to_s] = deep_stringify(v) }
|
|
293
|
+
when Array then obj.map { |e| deep_stringify(e) }
|
|
294
|
+
else obj
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Deep-merge two string-keyed Hashes: nested Hashes merge recursively,
|
|
299
|
+
# per-call (override) wins at the leaves, arrays/scalars replace.
|
|
300
|
+
private def deep_merge_options(base, override)
|
|
301
|
+
base.merge(override) do |_key, old_val, new_val|
|
|
302
|
+
old_val.is_a?(Hash) && new_val.is_a?(Hash) ? deep_merge_options(old_val, new_val) : new_val
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
132
306
|
private def log(msg)
|
|
133
307
|
puts "#{PERCY_LABEL} #{msg}"
|
|
134
308
|
end
|
data/lib/percy/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: percy-capybara
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.0.1.pre.
|
|
4
|
+
version: 5.0.1.pre.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Perceptual Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: capybara
|