percy-capybara 5.0.1.pre.1 → 5.0.1.pre.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b09892a41c8741ae0fc2df13506fea8cf230eed9378c88959e162d472e45920a
4
- data.tar.gz: ad15f33efc210f01b3c41bb22105ccd1d09b789ab1ab17aa598c9184ddcd8b1f
3
+ metadata.gz: 6958cc3537971e6eab60cb6025b1e3a9efd55fb5ca28440444590470bb88eea8
4
+ data.tar.gz: c010a8c95a4633f85546b2bfd748a4523bfd38a2e20ffe3e6782ff582ce8f10e
5
5
  SHA512:
6
- metadata.gz: cd030fd64faa05dd1bb59d75003615280ad8aa53eec6d295843b2b750b9ae298a4ee2171ffd291c3571af57a9feff127c45a2ea6deb02b77fc78bdcb6ab0ecd3
7
- data.tar.gz: cda536b6071654fe1be5e359bf72c737c303fdb23822f863796d6163959ca5b5e4e4b609a4f5d2348111147f050c9323e5e49be517fa67a38d189f04aa3067e4
6
+ metadata.gz: 305fb4b9322e2bbf30d1abd0814edf3be79b0fae8e55acd9d3822c2cd79e52f6f255ccc2cb4c3eed3e6cd15da591bd1c1b20c481cc32f575b2f9f1d5eb384495
7
+ data.tar.gz: 1dc9d2230d122d6c92b8bb1417fdd2c8c9a62d85167938088cbf6f52575328343925b748f11e6c8abf111f480bfcd74ef417b08826a2f462d40ad3fee177363f
@@ -14,6 +14,12 @@ module PercyCapybara
14
14
  private_constant :CLIENT_INFO
15
15
  private_constant :ENV_INFO
16
16
 
17
+ UNSUPPORTED_IFRAME_SRCS = %w[
18
+ about:blank about:srcdoc javascript: data: blob: vbscript: chrome: chrome-extension:
19
+ ].freeze
20
+
21
+ private_constant :UNSUPPORTED_IFRAME_SRCS
22
+
17
23
  # Take a DOM snapshot and post it to the snapshot endpoint
18
24
  def percy_snapshot(name, options = {})
19
25
  return unless percy_enabled?
@@ -21,6 +27,9 @@ module PercyCapybara
21
27
  page = Capybara.current_session
22
28
 
23
29
  begin
30
+ percy_dom_script = fetch_percy_dom
31
+ page.evaluate_script(percy_dom_script)
32
+ dom_snapshot = get_serialized_dom(page, options, percy_dom_script)
24
33
  page.evaluate_script(fetch_percy_dom)
25
34
 
26
35
  # Readiness gate -- runs before serialize when CLI supports it.
@@ -44,7 +53,16 @@ module PercyCapybara
44
53
  environment_info: ENV_INFO,
45
54
  **options,)
46
55
 
47
- unless response.body.to_json['success']
56
+ data =
57
+ begin
58
+ JSON.parse(response.body)
59
+ rescue JSON::ParserError => e
60
+ log("Could not parse snapshot response for '#{name}': invalid JSON")
61
+ if PERCY_DEBUG then log(e) end
62
+ return
63
+ end
64
+
65
+ unless data['success']
48
66
  raise StandardError, data['error']
49
67
  end
50
68
  rescue StandardError => e
@@ -54,6 +72,135 @@ module PercyCapybara
54
72
  end
55
73
  end
56
74
 
75
+ private def get_serialized_dom(page, options, percy_dom_script)
76
+ dom_snapshot = page
77
+ .evaluate_script("(function() { return PercyDOM.serialize(#{options.to_json}) })()")
78
+
79
+ driver = page.driver.browser
80
+ begin
81
+ page_origin = get_origin(page.current_url)
82
+ iframes = driver.find_elements(:tag_name, 'iframe')
83
+ log("Found #{iframes.length} total iframe(s) on page") if PERCY_DEBUG
84
+
85
+ processed_frames = []
86
+ iframes.each do |frame|
87
+ frame_src = frame.attribute('src')
88
+
89
+ if unsupported_iframe_src?(frame_src)
90
+ log("Skipping unsupported iframe src: #{frame_src}") if PERCY_DEBUG && frame_src
91
+ next
92
+ end
93
+
94
+ begin
95
+ frame_origin = get_origin(URI.join(page.current_url, frame_src).to_s)
96
+ rescue StandardError => e
97
+ log("Skipping iframe with invalid URL \"#{frame_src}\": #{e}") if PERCY_DEBUG
98
+ next
99
+ end
100
+
101
+ if frame_origin == page_origin
102
+ log("Skipping same-origin iframe: #{frame_src}") if PERCY_DEBUG
103
+ next
104
+ end
105
+
106
+ # Pre-flight check: avoid the switch/serialize round-trip if there's no
107
+ # data-percy-element-id, since process_frame would discard the snapshot
108
+ # anyway and we'd silently waste a frame switch.
109
+ unless frame.attribute('data-percy-element-id')
110
+ log("Skipping iframe #{frame_src}: no data-percy-element-id found") if PERCY_DEBUG
111
+ next
112
+ end
113
+
114
+ result = process_frame(driver, frame, options, percy_dom_script)
115
+ processed_frames << result if result
116
+ end
117
+
118
+ if processed_frames.any?
119
+ dom_snapshot['corsIframes'] = processed_frames
120
+ log("Captured #{processed_frames.length} cross-origin iframe(s)")
121
+ end
122
+ rescue StandardError => e
123
+ log("Failed to process cross-origin iframes: #{e}") if PERCY_DEBUG
124
+ begin
125
+ driver.switch_to.default_content
126
+ rescue StandardError
127
+ nil
128
+ end
129
+ end
130
+
131
+ dom_snapshot
132
+ end
133
+
134
+ private def unsupported_iframe_src?(src)
135
+ return true if src.nil? || src.empty?
136
+
137
+ UNSUPPORTED_IFRAME_SRCS.any? { |prefix| src == prefix || src.start_with?(prefix) }
138
+ end
139
+
140
+ private def get_origin(url)
141
+ uri = URI.parse(url)
142
+ raise URI::InvalidURIError, "no host in #{url}" if uri.host.nil?
143
+
144
+ default_ports = {'http' => 80, 'https' => 443}
145
+ netloc = uri.host.to_s
146
+ netloc += ":#{uri.port}" if uri.port && uri.port != default_ports[uri.scheme]
147
+ "#{uri.scheme}://#{netloc}"
148
+ end
149
+
150
+ private def process_frame(driver, frame_element, options, percy_dom_script)
151
+ frame_url = frame_element.attribute('src') || 'unknown-src'
152
+ log("Processing cross-origin iframe: #{frame_url}") if PERCY_DEBUG
153
+
154
+ begin
155
+ driver.switch_to.frame(frame_element)
156
+ begin
157
+ driver.execute_script(percy_dom_script)
158
+ iframe_options = options.merge('enableJavaScript' => true)
159
+ iframe_snapshot =
160
+ driver.execute_script("return PercyDOM.serialize(#{iframe_options.to_json})")
161
+ log("Serialized cross-origin iframe: #{frame_url}") if PERCY_DEBUG
162
+ rescue StandardError => e
163
+ log("Failed to serialize cross-origin iframe #{frame_url}: #{e}") if PERCY_DEBUG
164
+ return nil
165
+ ensure
166
+ begin
167
+ driver.switch_to.default_content
168
+ rescue StandardError
169
+ begin
170
+ driver.switch_to.parent_frame
171
+ rescue StandardError
172
+ nil
173
+ end
174
+ end
175
+ end
176
+ rescue StandardError => e
177
+ log("Failed to switch to frame #{frame_url}: #{e}") if PERCY_DEBUG
178
+ begin
179
+ driver.switch_to.default_content
180
+ rescue StandardError
181
+ nil
182
+ end
183
+ return nil
184
+ end
185
+
186
+ percy_element_id = frame_element.attribute('data-percy-element-id')
187
+ unless percy_element_id
188
+ log("Skipping frame #{frame_url}: no data-percy-element-id found") if PERCY_DEBUG
189
+ return nil
190
+ end
191
+
192
+ if PERCY_DEBUG
193
+ log("Successfully captured cross-origin iframe: #{frame_url} " \
194
+ "(percyElementId: #{percy_element_id})")
195
+ end
196
+
197
+ {
198
+ 'iframeData' => {'percyElementId' => percy_element_id},
199
+ 'iframeSnapshot' => iframe_snapshot,
200
+ 'frameUrl' => frame_url,
201
+ }
202
+ end
203
+
57
204
  # Determine if the Percy server is running, caching the result so it is only checked once
58
205
  private def percy_enabled?
59
206
  return @percy_enabled unless @percy_enabled.nil?
data/lib/percy/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module PercyCapybara
2
- VERSION = '5.0.1.pre.1'.freeze
2
+ VERSION = '5.0.1.pre.2'.freeze
3
3
  end
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.1
4
+ version: 5.0.1.pre.2
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-06-04 00:00:00.000000000 Z
11
+ date: 2026-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: capybara