percy-capybara 5.0.0 → 5.0.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/README.md +1 -1
- data/lib/percy/capybara.rb +221 -3
- data/lib/percy/version.rb +1 -1
- metadata +26 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7db2c14fc57a6c57c3b6622525553787384946fb9391a2f725cb2200798973a7
|
|
4
|
+
data.tar.gz: ac4086661c230766111c2a15ffebf403941f4e42349a2a550b90c2becfd7ecc9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6233ec353b93414b17a01300f2c3fdcbe5350a4e9a847c52e1a115da333cf9dbf96c8647540c64ddaeb7cb7cb9913594f16682c77e46e59f76f606b103200ff8
|
|
7
|
+
data.tar.gz: 44951b8de50525cd017ecc705a02ee57d6faf93f2fa36c174bdadb126ab66f1260191ca53abf046e7dda2ed8aa61961c202441f2aec9c4f3901b63fe70823a0e
|
data/README.md
CHANGED
|
@@ -66,7 +66,7 @@ $ percy exec -- [test command]
|
|
|
66
66
|
`page.snapshot(name[, options])`
|
|
67
67
|
|
|
68
68
|
- `name` (**required**) - The snapshot name; must be unique to each snapshot
|
|
69
|
-
- `options` - [See per-snapshot configuration options](https://
|
|
69
|
+
- `options` - [See per-snapshot configuration options](https://www.browserstack.com/docs/percy/take-percy-snapshots/overview#per-snapshot-configuration)
|
|
70
70
|
|
|
71
71
|
## Upgrading
|
|
72
72
|
|
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,9 +28,29 @@ 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)
|
|
35
|
+
|
|
36
|
+
# Readiness gate -- runs before serialize when CLI supports it.
|
|
37
|
+
# Uses evaluate_async_script with a callback signal so the SDK can block
|
|
38
|
+
# on PercyDOM.waitForReady. In-browser typeof guard makes this a no-op on
|
|
39
|
+
# older CLIs that lack waitForReady.
|
|
40
|
+
readiness_diagnostics = wait_for_ready(page, options)
|
|
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
|
+
|
|
25
48
|
dom_snapshot = page
|
|
26
|
-
.evaluate_script("(function() { return PercyDOM.serialize(#{
|
|
49
|
+
.evaluate_script("(function() { return PercyDOM.serialize(#{merged_options.to_json}) })()")
|
|
50
|
+
|
|
51
|
+
if readiness_diagnostics && dom_snapshot.is_a?(Hash)
|
|
52
|
+
dom_snapshot['readiness_diagnostics'] = readiness_diagnostics
|
|
53
|
+
end
|
|
27
54
|
|
|
28
55
|
response = fetch('percy/snapshot',
|
|
29
56
|
name: name,
|
|
@@ -33,7 +60,16 @@ module PercyCapybara
|
|
|
33
60
|
environment_info: ENV_INFO,
|
|
34
61
|
**options,)
|
|
35
62
|
|
|
36
|
-
|
|
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']
|
|
37
73
|
raise StandardError, data['error']
|
|
38
74
|
end
|
|
39
75
|
rescue StandardError => e
|
|
@@ -43,6 +79,135 @@ module PercyCapybara
|
|
|
43
79
|
end
|
|
44
80
|
end
|
|
45
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
|
+
|
|
46
211
|
# Determine if the Percy server is running, caching the result so it is only checked once
|
|
47
212
|
private def percy_enabled?
|
|
48
213
|
return @percy_enabled unless @percy_enabled.nil?
|
|
@@ -55,7 +220,7 @@ module PercyCapybara
|
|
|
55
220
|
log('You may be using @percy/agent ' \
|
|
56
221
|
'which is no longer supported by this SDK. ' \
|
|
57
222
|
'Please uninstall @percy/agent and install @percy/cli instead. ' \
|
|
58
|
-
'https://
|
|
223
|
+
'https://www.browserstack.com/docs/percy/migration/migrate-to-cli')
|
|
59
224
|
@percy_enabled = false
|
|
60
225
|
return false
|
|
61
226
|
end
|
|
@@ -66,6 +231,8 @@ module PercyCapybara
|
|
|
66
231
|
return false
|
|
67
232
|
end
|
|
68
233
|
|
|
234
|
+
body = response.body.to_s.empty? ? {} : JSON.parse(response.body)
|
|
235
|
+
@cli_config = body['config'] || {}
|
|
69
236
|
@percy_enabled = true
|
|
70
237
|
true
|
|
71
238
|
rescue StandardError => e
|
|
@@ -85,6 +252,57 @@ module PercyCapybara
|
|
|
85
252
|
@percy_dom = response.body
|
|
86
253
|
end
|
|
87
254
|
|
|
255
|
+
# Readiness gate: runs PercyDOM.waitForReady before serialize.
|
|
256
|
+
#
|
|
257
|
+
# Returns diagnostics to attach to the domSnapshot, or nil.
|
|
258
|
+
# Config precedence: options[:readiness] / options['readiness'] > {} (the
|
|
259
|
+
# CLI applies its balanced preset default when passed {}). preset='disabled'
|
|
260
|
+
# skips the script entirely. Opt-in: only runs when the caller explicitly
|
|
261
|
+
# passes a `readiness` option -- keeps non-opting tests insulated from
|
|
262
|
+
# Capybara drivers that don't implement evaluate_async_script.
|
|
263
|
+
# Any StandardError is caught at debug level.
|
|
264
|
+
private def wait_for_ready(page, options)
|
|
265
|
+
return nil unless options.key?(:readiness) || options.key?('readiness')
|
|
266
|
+
|
|
267
|
+
readiness_config = options[:readiness] || options['readiness'] || {}
|
|
268
|
+
return nil if readiness_config.is_a?(Hash) && (
|
|
269
|
+
readiness_config[:preset] == 'disabled' || readiness_config['preset'] == 'disabled'
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
begin
|
|
273
|
+
page.evaluate_async_script(<<~JS)
|
|
274
|
+
var cfg = #{readiness_config.to_json};
|
|
275
|
+
var done = arguments[arguments.length - 1];
|
|
276
|
+
try {
|
|
277
|
+
if (typeof PercyDOM !== 'undefined' && typeof PercyDOM.waitForReady === 'function') {
|
|
278
|
+
PercyDOM.waitForReady(cfg).then(function(r){ done(r); }).catch(function(){ done(); });
|
|
279
|
+
} else { done(); }
|
|
280
|
+
} catch (e) { done(); }
|
|
281
|
+
JS
|
|
282
|
+
rescue StandardError => e
|
|
283
|
+
if PERCY_DEBUG then log("waitForReady failed, proceeding to serialize: #{e}") end
|
|
284
|
+
nil
|
|
285
|
+
end
|
|
286
|
+
end
|
|
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
|
+
|
|
88
306
|
private def log(msg)
|
|
89
307
|
puts "#{PERCY_LABEL} #{msg}"
|
|
90
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.
|
|
4
|
+
version: 5.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Perceptual Inc.
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-07-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: capybara
|
|
@@ -30,14 +30,28 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 4.0.0
|
|
33
|
+
version: 4.0.0
|
|
34
34
|
type: :development
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 4.0.0
|
|
40
|
+
version: 4.0.0
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: geckodriver-bin
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: 0.28.0
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 0.28.0
|
|
41
55
|
- !ruby/object:Gem::Dependency
|
|
42
56
|
name: bundler
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -84,16 +98,16 @@ dependencies:
|
|
|
84
98
|
name: capybara
|
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
|
86
100
|
requirements:
|
|
87
|
-
- - "
|
|
101
|
+
- - ">="
|
|
88
102
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '3
|
|
103
|
+
version: '3'
|
|
90
104
|
type: :development
|
|
91
105
|
prerelease: false
|
|
92
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
107
|
requirements:
|
|
94
|
-
- - "
|
|
108
|
+
- - ">="
|
|
95
109
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '3
|
|
110
|
+
version: '3'
|
|
97
111
|
- !ruby/object:Gem::Dependency
|
|
98
112
|
name: percy-style
|
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -125,7 +139,7 @@ licenses:
|
|
|
125
139
|
metadata:
|
|
126
140
|
bug_tracker_uri: https://github.com/percy/percy-capybara/issues
|
|
127
141
|
source_code_uri: https://github.com/percy/percy-capybara
|
|
128
|
-
post_install_message:
|
|
142
|
+
post_install_message:
|
|
129
143
|
rdoc_options: []
|
|
130
144
|
require_paths:
|
|
131
145
|
- lib
|
|
@@ -140,8 +154,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
140
154
|
- !ruby/object:Gem::Version
|
|
141
155
|
version: '0'
|
|
142
156
|
requirements: []
|
|
143
|
-
rubygems_version: 3.0.3
|
|
144
|
-
signing_key:
|
|
157
|
+
rubygems_version: 3.0.3.1
|
|
158
|
+
signing_key:
|
|
145
159
|
specification_version: 4
|
|
146
160
|
summary: Percy visual testing for Capybara
|
|
147
161
|
test_files: []
|