percy-selenium 1.1.2 → 1.1.3.pre.beta.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/.github/workflows/Semgrep.yml +1 -2
- data/.github/workflows/test.yml +1 -0
- data/.gitignore +2 -0
- data/.npmrc +6 -0
- data/Gemfile +7 -2
- data/lib/percy.rb +76 -2
- data/lib/version.rb +1 -1
- data/package.json +1 -1
- data/spec/lib/percy/percy_spec.rb +350 -21
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 599af2bc8ab99bf3bb74f396232a4be8a6fd7cac209f9a159a5a2e9850581139
|
|
4
|
+
data.tar.gz: 38a4b597b1ed391675bc6ce07e6bba7b6820691ecf969a84bfd98a8609b4de50
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8d0dbb6269542d41b36883985f7ffd390b5d74595752a7604eceaf26dda0503817b15fc3b129f24f3ec2f64e46715b8fc9a2768390e07ec53f22a57a43c1fe89
|
|
7
|
+
data.tar.gz: f0960498a183952e6f7fdc7c9a8d5a84125bffa11ac34e3f5b4ba781ec9bd3596c45e07b11a39120723e8e35884eb1288d44e7bf1bd9506f667e926600ebc9da
|
|
@@ -27,8 +27,7 @@ jobs:
|
|
|
27
27
|
|
|
28
28
|
container:
|
|
29
29
|
# A Docker image with Semgrep installed. Do not change this.
|
|
30
|
-
image: returntocorp/semgrep
|
|
31
|
-
|
|
30
|
+
image: returntocorp/semgrep:1.166.0
|
|
32
31
|
# Skip any PR created by dependabot to avoid permission issues:
|
|
33
32
|
if: (github.actor != 'dependabot[bot]')
|
|
34
33
|
|
data/.github/workflows/test.yml
CHANGED
data/.gitignore
CHANGED
data/.npmrc
ADDED
data/Gemfile
CHANGED
|
@@ -7,8 +7,13 @@ gem "guard-rspec", require: false
|
|
|
7
7
|
|
|
8
8
|
group :test, :development do
|
|
9
9
|
gem "webmock"
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
# Capybara 3.36 (the newest Capybara that supports Ruby 2.6, which CI still
|
|
11
|
+
# targets) uses the Puma 5 events API; Puma 6 removed Puma::Events.strings,
|
|
12
|
+
# which broke the Capybara server boot. Pin to Puma 5 for compatibility.
|
|
13
|
+
gem "puma", '~> 5'
|
|
14
|
+
# Puma 5's rack handler requires `rack/handler`, which Rack 3 removed (it
|
|
15
|
+
# moved to the separate `rackup` gem). Pin Rack 2 so Capybara can boot Puma.
|
|
16
|
+
gem "rack", '~> 2.2'
|
|
12
17
|
gem "pry"
|
|
13
18
|
gem "simplecov", require: false
|
|
14
19
|
end
|
data/lib/percy.rb
CHANGED
|
@@ -85,13 +85,16 @@ module Percy
|
|
|
85
85
|
get_serialized_dom(driver, options, percy_dom_script: percy_dom_script)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# Strip `readiness` before POSTing -- SDK-local config that the CLI
|
|
89
|
+
# already has via healthcheck.
|
|
90
|
+
post_options = options.reject { |k, _| k.to_s == 'readiness' }
|
|
88
91
|
response = fetch('percy/snapshot',
|
|
89
92
|
name: name,
|
|
90
93
|
url: driver.current_url,
|
|
91
94
|
dom_snapshot: dom_snapshot,
|
|
92
95
|
client_info: CLIENT_INFO,
|
|
93
96
|
environment_info: ENV_INFO,
|
|
94
|
-
**
|
|
97
|
+
**post_options,)
|
|
95
98
|
|
|
96
99
|
body = JSON.parse(response.body)
|
|
97
100
|
unless body['success']
|
|
@@ -113,8 +116,79 @@ module Percy
|
|
|
113
116
|
driver.manage
|
|
114
117
|
end
|
|
115
118
|
|
|
119
|
+
# Shallow-merge of global (@cli_config.snapshot.readiness) and per-snapshot
|
|
120
|
+
# (options[:readiness] / options['readiness']) readiness config. Per-snapshot
|
|
121
|
+
# keys win, unspecified global keys (notably preset: disabled) are inherited.
|
|
122
|
+
def self.resolve_readiness_config(options)
|
|
123
|
+
global = @cli_config&.dig('snapshot', 'readiness')
|
|
124
|
+
global = {} unless global.is_a?(Hash)
|
|
125
|
+
per_snapshot = options[:readiness] || options['readiness']
|
|
126
|
+
per_snapshot = {} unless per_snapshot.is_a?(Hash)
|
|
127
|
+
# Normalise symbol keys to strings so the merge collapses :preset and 'preset'.
|
|
128
|
+
[global, per_snapshot].each_with_object({}) do |hash, merged|
|
|
129
|
+
hash.each { |k, v| merged[k.to_s] = v }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Readiness gate: runs PercyDOM.waitForReady via
|
|
134
|
+
# execute_async_script BEFORE serialize. Graceful on old CLIs that lack the
|
|
135
|
+
# method. Returns readiness diagnostics (or nil) for attachment to domSnapshot.
|
|
136
|
+
def self.wait_for_ready(driver, options)
|
|
137
|
+
readiness_config = resolve_readiness_config(options)
|
|
138
|
+
return nil if readiness_config['preset'] == 'disabled'
|
|
139
|
+
|
|
140
|
+
# Match the driver's async-script timeout to readiness.timeoutMs so a
|
|
141
|
+
# higher user-configured timeout isn't silently capped by Selenium's
|
|
142
|
+
# default (~30s) firing ScriptTimeoutException before the in-page
|
|
143
|
+
# Promise resolves.
|
|
144
|
+
timeout_ms = readiness_config['timeoutMs']
|
|
145
|
+
previous_timeout = nil
|
|
146
|
+
if timeout_ms.is_a?(Numeric) && timeout_ms > 0
|
|
147
|
+
begin
|
|
148
|
+
previous_timeout = driver.manage.timeouts.script_timeout
|
|
149
|
+
driver.manage.timeouts.script_timeout = (timeout_ms / 1000.0) + 2
|
|
150
|
+
rescue StandardError
|
|
151
|
+
previous_timeout = nil # best-effort; older Selenium / unsupported
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
begin
|
|
156
|
+
script = <<~JS
|
|
157
|
+
var cfg = #{readiness_config.to_json};
|
|
158
|
+
var done = arguments[arguments.length - 1];
|
|
159
|
+
try {
|
|
160
|
+
if (typeof PercyDOM !== 'undefined' && typeof PercyDOM.waitForReady === 'function') {
|
|
161
|
+
PercyDOM.waitForReady(cfg).then(function(r){ done(r); }).catch(function(){ done(); });
|
|
162
|
+
} else { done(); }
|
|
163
|
+
} catch (e) { done(); }
|
|
164
|
+
JS
|
|
165
|
+
driver.execute_async_script(script)
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
log("waitForReady failed, proceeding to serialize: #{e}", 'debug')
|
|
168
|
+
nil
|
|
169
|
+
ensure
|
|
170
|
+
if previous_timeout
|
|
171
|
+
begin
|
|
172
|
+
driver.manage.timeouts.script_timeout = previous_timeout
|
|
173
|
+
rescue StandardError
|
|
174
|
+
# best-effort
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
116
180
|
def self.get_serialized_dom(driver, options, percy_dom_script: nil)
|
|
117
|
-
|
|
181
|
+
# Readiness gate before serialize. Graceful on old CLI.
|
|
182
|
+
readiness_diagnostics = wait_for_ready(driver, options)
|
|
183
|
+
# Strip `readiness` from forwarded serialize args -- it's consumed by
|
|
184
|
+
# wait_for_ready upstream, not a PercyDOM.serialize argument.
|
|
185
|
+
serialize_options = options.reject { |k, _| k.to_s == 'readiness' }
|
|
186
|
+
dom_snapshot = driver.execute_script("return PercyDOM.serialize(#{serialize_options.to_json})")
|
|
187
|
+
# `!nil?` preserves legitimate falsy returns like {} ("gate ran, no
|
|
188
|
+
# notable diagnostics").
|
|
189
|
+
if !readiness_diagnostics.nil? && dom_snapshot.is_a?(Hash)
|
|
190
|
+
dom_snapshot['readiness_diagnostics'] = readiness_diagnostics
|
|
191
|
+
end
|
|
118
192
|
begin
|
|
119
193
|
page_origin = get_origin(driver.current_url)
|
|
120
194
|
iframes = percy_dom_script ? driver.find_elements(:tag_name, 'iframe') : []
|
data/lib/version.rb
CHANGED
data/package.json
CHANGED
|
@@ -210,6 +210,19 @@ RSpec.describe Percy, type: :feature do
|
|
|
210
210
|
expect(data).to eq(nil)
|
|
211
211
|
end
|
|
212
212
|
|
|
213
|
+
# Drives the full responsive `Percy.snapshot` path (capture_responsive_dom ->
|
|
214
|
+
# get_serialized_dom -> POST /percy/snapshot) and asserts on the real
|
|
215
|
+
# webmock-captured POST body.
|
|
216
|
+
#
|
|
217
|
+
# A faithful Selenium driver double is used instead of a live Firefox: a
|
|
218
|
+
# real headless Firefox is not deterministic for this flow on CI. The
|
|
219
|
+
# responsive capture resizes the window per width and then restores it in an
|
|
220
|
+
# `ensure`; headless Firefox / geckodriver intermittently crashes marionette
|
|
221
|
+
# on resize ("Failed to decode response from marionette" -> a dead session),
|
|
222
|
+
# whereupon the next WebDriver command raises InvalidSessionIdError. That
|
|
223
|
+
# error propagated out of capture_responsive_dom and was swallowed by
|
|
224
|
+
# Percy.snapshot's rescue, so no snapshot POST was ever sent and the captured
|
|
225
|
+
# body stayed nil. The double exercises the same code paths every time.
|
|
213
226
|
it 'sends multiple dom snapshots to the local server using selenium' do
|
|
214
227
|
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck").to_return(
|
|
215
228
|
status: 200,
|
|
@@ -240,28 +253,60 @@ RSpec.describe Percy, type: :feature do
|
|
|
240
253
|
{status: 200, body: '{"success":true}', headers: {}}
|
|
241
254
|
end
|
|
242
255
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
256
|
+
# Faithful Selenium::WebDriver driver double covering every call the
|
|
257
|
+
# responsive snapshot path makes.
|
|
258
|
+
cookies = [{'name' => 'cookie-name', 'value' => 'cookie-value', 'path' => '/'}]
|
|
259
|
+
driver = double('driver')
|
|
260
|
+
manage = double('manage')
|
|
261
|
+
window = double('window')
|
|
262
|
+
window_size = double('window_size', width: 1280, height: 900)
|
|
263
|
+
capabilities = double('capabilities', browser_name: 'firefox')
|
|
264
|
+
|
|
265
|
+
allow(driver).to receive(:respond_to?).and_return(false)
|
|
266
|
+
allow(driver).to receive(:respond_to?).with(:driver).and_return(false)
|
|
267
|
+
allow(driver).to receive(:respond_to?).with(:execute_cdp).and_return(false)
|
|
268
|
+
allow(driver).to receive(:capabilities).and_return(capabilities)
|
|
269
|
+
allow(driver).to receive(:current_url).and_return('http://127.0.0.1:3003/index.html')
|
|
270
|
+
allow(driver).to receive(:find_elements).and_return([])
|
|
271
|
+
allow(driver).to receive(:manage).and_return(manage)
|
|
272
|
+
allow(manage).to receive(:window).and_return(window)
|
|
273
|
+
allow(manage).to receive(:all_cookies).and_return(cookies)
|
|
274
|
+
allow(window).to receive(:size).and_return(window_size)
|
|
275
|
+
allow(window).to receive(:resize_to)
|
|
276
|
+
# Resize wait: return immediately (no 1s timeout per width) and skip the
|
|
277
|
+
# innerWidth/innerHeight diagnostics read.
|
|
278
|
+
wait = instance_double(Selenium::WebDriver::Wait)
|
|
279
|
+
allow(Selenium::WebDriver::Wait).to receive(:new).and_return(wait)
|
|
280
|
+
allow(wait).to receive(:until)
|
|
281
|
+
# waitForReady gate: fake PercyDOM has no waitForReady, so the async script
|
|
282
|
+
# resolves with nil, exactly like a real browser would here.
|
|
283
|
+
allow(driver).to receive(:execute_async_script).and_return(nil)
|
|
284
|
+
# PercyDOM injection / waitForResize / dispatchEvent / resizeCount poll
|
|
285
|
+
# return nil; the innerWidth/innerHeight diagnostic read returns a size
|
|
286
|
+
# hash; the serialize call returns the serialized DOM (its `cookies` field
|
|
287
|
+
# is overwritten by the SDK from all_cookies afterward).
|
|
288
|
+
allow(driver).to receive(:execute_script) do |script|
|
|
289
|
+
if script.include?('PercyDOM.serialize')
|
|
290
|
+
{'html' => dom_string, 'cookies' => ''}
|
|
291
|
+
elsif script.include?('innerWidth')
|
|
292
|
+
{'w' => 1280, 'h' => 900}
|
|
263
293
|
end
|
|
264
294
|
end
|
|
295
|
+
|
|
296
|
+
data = Percy.snapshot(driver, 'Name', {responsive_snapshot_capture: true})
|
|
297
|
+
|
|
298
|
+
# Fail loudly with a meaningful message if the snapshot POST never fired
|
|
299
|
+
# (Percy.snapshot swallows StandardErrors), instead of a cryptic
|
|
300
|
+
# NoMethodError on nil when the body assertions run below.
|
|
301
|
+
expect(received_body).to_not(
|
|
302
|
+
be_nil, 'expected Percy.snapshot to POST /percy/snapshot, but no request was captured',
|
|
303
|
+
)
|
|
304
|
+
expect(received_body['name']).to eq('Name')
|
|
305
|
+
expect(received_body['url']).to eq('http://127.0.0.1:3003/index.html')
|
|
306
|
+
expect(received_body['dom_snapshot'].length).to eq(3)
|
|
307
|
+
expect(received_body['dom_snapshot'].map { |s| s['width'] }).to eq([390, 765, 1280])
|
|
308
|
+
expect(received_body['dom_snapshot'].first['cookies'].first['name']).to eq('cookie-name')
|
|
309
|
+
expect(data).to eq(nil)
|
|
265
310
|
end
|
|
266
311
|
|
|
267
312
|
it 'sends snapshots for sync' do
|
|
@@ -587,6 +632,42 @@ RSpec.describe Percy do
|
|
|
587
632
|
|
|
588
633
|
Percy.process_frame(driver, frame_element, {}, 'percy_dom_script')
|
|
589
634
|
end
|
|
635
|
+
|
|
636
|
+
it 'falls back to parent_frame when default_content fails in the inner ensure' do
|
|
637
|
+
allow(frame_element).to receive(:attribute).with('src')
|
|
638
|
+
.and_return('https://other.example.com/page')
|
|
639
|
+
allow(frame_element).to receive(:attribute).with('data-percy-element-id')
|
|
640
|
+
.and_return('elem-pf')
|
|
641
|
+
allow(driver).to receive(:execute_script).and_return(nil, {'html' => '<html/>'})
|
|
642
|
+
allow(switch_to).to receive(:default_content).and_raise(StandardError, 'dc boom')
|
|
643
|
+
expect(switch_to).to receive(:parent_frame).once
|
|
644
|
+
|
|
645
|
+
result = Percy.process_frame(driver, frame_element, {}, 'percy_dom_script')
|
|
646
|
+
expect(result['frameUrl']).to eq('https://other.example.com/page')
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
it 'swallows a parent_frame failure during inner-ensure recovery' do
|
|
650
|
+
allow(frame_element).to receive(:attribute).with('src')
|
|
651
|
+
.and_return('https://other.example.com/page')
|
|
652
|
+
allow(frame_element).to receive(:attribute).with('data-percy-element-id')
|
|
653
|
+
.and_return('elem-pf2')
|
|
654
|
+
allow(driver).to receive(:execute_script).and_return(nil, {'html' => '<html/>'})
|
|
655
|
+
allow(switch_to).to receive(:default_content).and_raise(StandardError, 'dc boom')
|
|
656
|
+
allow(switch_to).to receive(:parent_frame).and_raise(StandardError, 'pf boom')
|
|
657
|
+
|
|
658
|
+
expect { Percy.process_frame(driver, frame_element, {}, 'percy_dom_script') }
|
|
659
|
+
.to_not raise_error
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
it 'swallows a default_content failure in the outer rescue when frame switch fails' do
|
|
663
|
+
allow(frame_element).to receive(:attribute).with('src')
|
|
664
|
+
.and_return('https://other.example.com/page')
|
|
665
|
+
allow(switch_to).to receive(:frame).and_raise(StandardError, 'no such frame')
|
|
666
|
+
allow(switch_to).to receive(:default_content).and_raise(StandardError, 'dc boom')
|
|
667
|
+
|
|
668
|
+
result = Percy.process_frame(driver, frame_element, {}, 'percy_dom_script')
|
|
669
|
+
expect(result).to be_nil
|
|
670
|
+
end
|
|
590
671
|
end
|
|
591
672
|
|
|
592
673
|
describe '.get_serialized_dom' do
|
|
@@ -601,6 +682,9 @@ RSpec.describe Percy do
|
|
|
601
682
|
allow(switch_to).to receive(:frame)
|
|
602
683
|
allow(switch_to).to receive(:parent_frame)
|
|
603
684
|
allow(switch_to).to receive(:default_content)
|
|
685
|
+
# Readiness gate runs execute_async_script before serialize; stub it so
|
|
686
|
+
# these unit tests exercise serialization without a real browser.
|
|
687
|
+
allow(driver).to receive(:execute_async_script).and_return(nil)
|
|
604
688
|
end
|
|
605
689
|
|
|
606
690
|
it 'returns the serialized dom with cookies when no iframes present' do
|
|
@@ -748,6 +832,120 @@ RSpec.describe Percy do
|
|
|
748
832
|
expect(dom['corsIframes'].length).to eq(1)
|
|
749
833
|
expect(dom['corsIframes'][0]['frameUrl']).to eq('https://other.example.com/page')
|
|
750
834
|
end
|
|
835
|
+
|
|
836
|
+
# --- Readiness gate --------------------------------------
|
|
837
|
+
|
|
838
|
+
it 'runs waitForReady before serialize and attaches diagnostics' do
|
|
839
|
+
allow(driver).to receive(:execute_async_script).and_return(
|
|
840
|
+
'ok' => true, 'timed_out' => false,
|
|
841
|
+
)
|
|
842
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
843
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
844
|
+
allow(driver).to receive(:find_elements).and_return([])
|
|
845
|
+
|
|
846
|
+
dom = Percy.get_serialized_dom(driver, {})
|
|
847
|
+
expect(driver).to have_received(:execute_async_script) do |script| # rubocop:disable RSpec/MessageSpies
|
|
848
|
+
expect(script).to include('waitForReady')
|
|
849
|
+
expect(script).to include("typeof PercyDOM.waitForReady === 'function'")
|
|
850
|
+
end
|
|
851
|
+
expect(dom['readiness_diagnostics']).to eq('ok' => true, 'timed_out' => false)
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
it 'embeds per-snapshot readiness config in the script' do
|
|
855
|
+
allow(driver).to receive(:execute_async_script).and_return(nil)
|
|
856
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
857
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
858
|
+
allow(driver).to receive(:find_elements).and_return([])
|
|
859
|
+
|
|
860
|
+
Percy.get_serialized_dom(driver, readiness: {preset: 'strict', stabilityWindowMs: 500})
|
|
861
|
+
expect(driver).to have_received(:execute_async_script) do |script| # rubocop:disable RSpec/MessageSpies
|
|
862
|
+
expect(script).to include('"preset":"strict"')
|
|
863
|
+
expect(script).to include('"stabilityWindowMs":500')
|
|
864
|
+
end
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
it 'skips execute_async_script when preset is disabled' do
|
|
868
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
869
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
870
|
+
allow(driver).to receive(:find_elements).and_return([])
|
|
871
|
+
expect(driver).to_not receive(:execute_async_script)
|
|
872
|
+
|
|
873
|
+
dom = Percy.get_serialized_dom(driver, readiness: {preset: 'disabled'})
|
|
874
|
+
expect(dom).to_not have_key('readiness_diagnostics')
|
|
875
|
+
expect(dom['html']).to eq('<html/>')
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
it 'still serializes when execute_async_script raises' do
|
|
879
|
+
allow(driver).to receive(:execute_async_script).and_raise(StandardError, 'readiness boom')
|
|
880
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
881
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
882
|
+
allow(driver).to receive(:find_elements).and_return([])
|
|
883
|
+
|
|
884
|
+
dom = Percy.get_serialized_dom(driver, {})
|
|
885
|
+
expect(dom).to_not have_key('readiness_diagnostics')
|
|
886
|
+
expect(dom['html']).to eq('<html/>')
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
it 'raises the async-script timeout to match readiness timeoutMs and restores it after' do
|
|
890
|
+
timeouts = double('timeouts')
|
|
891
|
+
allow(manage).to receive(:timeouts).and_return(timeouts)
|
|
892
|
+
allow(timeouts).to receive(:script_timeout).and_return(30)
|
|
893
|
+
allow(driver).to receive(:execute_async_script).and_return(nil)
|
|
894
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
895
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
896
|
+
allow(driver).to receive(:find_elements).and_return([])
|
|
897
|
+
|
|
898
|
+
# 8000ms -> 8s + 2s buffer is applied, then the previous 30s is restored.
|
|
899
|
+
expect(timeouts).to receive(:script_timeout=).with(10.0).ordered
|
|
900
|
+
expect(timeouts).to receive(:script_timeout=).with(30).ordered
|
|
901
|
+
|
|
902
|
+
Percy.get_serialized_dom(driver, readiness: {timeoutMs: 8000})
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
it 'proceeds when reading/setting the script timeout is unsupported' do
|
|
906
|
+
timeouts = double('timeouts')
|
|
907
|
+
allow(manage).to receive(:timeouts).and_return(timeouts)
|
|
908
|
+
allow(timeouts).to receive(:script_timeout).and_raise(StandardError, 'unsupported')
|
|
909
|
+
allow(driver).to receive(:execute_async_script).and_return(nil)
|
|
910
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
911
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
912
|
+
allow(driver).to receive(:find_elements).and_return([])
|
|
913
|
+
|
|
914
|
+
expect { Percy.get_serialized_dom(driver, readiness: {timeoutMs: 5000}) }.to_not raise_error
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
it 'skips an iframe whose src cannot be resolved against the page url' do
|
|
918
|
+
frame = double('frame')
|
|
919
|
+
allow(frame).to receive(:attribute).with('src').and_return('ht!tp://%%%bad')
|
|
920
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
921
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
922
|
+
allow(driver).to receive(:find_elements).and_return([frame])
|
|
923
|
+
allow(URI).to receive(:join).and_raise(URI::InvalidURIError, 'bad uri')
|
|
924
|
+
|
|
925
|
+
dom = Percy.get_serialized_dom(driver, {}, percy_dom_script: 'script')
|
|
926
|
+
expect(dom).to_not have_key('corsIframes')
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
it 'logs and recovers when iframe processing raises unexpectedly' do
|
|
930
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
931
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
932
|
+
allow(driver).to receive(:find_elements).and_raise(StandardError, 'find boom')
|
|
933
|
+
|
|
934
|
+
dom = Percy.get_serialized_dom(driver, {}, percy_dom_script: 'script')
|
|
935
|
+
# find_elements raised inside the iframe block; cookies are still attached.
|
|
936
|
+
expect(dom['cookies']).to eq([])
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
it 'swallows a secondary error when recovering from an iframe-processing failure' do
|
|
940
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
941
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
942
|
+
allow(driver).to receive(:find_elements).and_raise(StandardError, 'find boom')
|
|
943
|
+
# default_content also fails during recovery -> inner rescue swallows it.
|
|
944
|
+
allow(switch_to).to receive(:default_content).and_raise(StandardError, 'switch boom')
|
|
945
|
+
|
|
946
|
+
dom = Percy.get_serialized_dom(driver, {}, percy_dom_script: 'script')
|
|
947
|
+
expect(dom['cookies']).to eq([])
|
|
948
|
+
end
|
|
751
949
|
end
|
|
752
950
|
|
|
753
951
|
describe '.change_window_dimension_and_wait' do
|
|
@@ -821,6 +1019,12 @@ RSpec.describe Percy do
|
|
|
821
1019
|
.with("window.dispatchEvent(new Event('resize'));")
|
|
822
1020
|
Percy.change_window_dimension_and_wait(driver, 375, 812, 1)
|
|
823
1021
|
end
|
|
1022
|
+
|
|
1023
|
+
it 'logs and swallows a TimeoutError when the resize event never fires' do
|
|
1024
|
+
allow(wait).to receive(:until).and_raise(Selenium::WebDriver::Error::TimeoutError)
|
|
1025
|
+
expect(Percy).to receive(:log).with(/Timed out waiting for window resize event/, 'debug')
|
|
1026
|
+
expect { Percy.change_window_dimension_and_wait(driver, 768, 1024, 1) }.to_not raise_error
|
|
1027
|
+
end
|
|
824
1028
|
end
|
|
825
1029
|
|
|
826
1030
|
describe '.capture_responsive_dom' do
|
|
@@ -923,6 +1127,21 @@ RSpec.describe Percy do
|
|
|
923
1127
|
expect(inner_nav).to receive(:refresh).once
|
|
924
1128
|
Percy.capture_responsive_dom(driver, {})
|
|
925
1129
|
end
|
|
1130
|
+
|
|
1131
|
+
it 'logs and continues when both the direct and fallback refresh fail' do
|
|
1132
|
+
allow(Percy).to receive(:get_responsive_widths).and_return([{'width' => 375}])
|
|
1133
|
+
allow(navigate).to receive(:refresh).and_raise(StandardError, 'direct refresh failed')
|
|
1134
|
+
|
|
1135
|
+
inner_browser = double('inner_browser')
|
|
1136
|
+
inner_drv = double('inner_driver', browser: inner_browser)
|
|
1137
|
+
inner_nav = double('inner_navigate')
|
|
1138
|
+
allow(driver).to receive(:driver).and_return(inner_drv)
|
|
1139
|
+
allow(inner_browser).to receive(:navigate).and_return(inner_nav)
|
|
1140
|
+
allow(inner_nav).to receive(:refresh).and_raise(StandardError, 'fallback refresh failed')
|
|
1141
|
+
|
|
1142
|
+
expect(Percy).to receive(:log).with(/Failed to refresh page/, 'debug')
|
|
1143
|
+
expect { Percy.capture_responsive_dom(driver, {}) }.to_not raise_error
|
|
1144
|
+
end
|
|
926
1145
|
end
|
|
927
1146
|
|
|
928
1147
|
# -----------------------------------------------------------------------
|
|
@@ -1006,6 +1225,14 @@ RSpec.describe Percy do
|
|
|
1006
1225
|
end
|
|
1007
1226
|
end
|
|
1008
1227
|
|
|
1228
|
+
# :nocov:
|
|
1229
|
+
# This whole describe is a live end-to-end test (xit, permanently skipped on
|
|
1230
|
+
# CI): it depends on the real @percy/cli test-mode `/test/requests` endpoint
|
|
1231
|
+
# being populated, which is not deterministic under `percy exec --testing`. It
|
|
1232
|
+
# exercises no lib lines not already covered by the stubbed snapshot specs.
|
|
1233
|
+
# Because it never executes, its body would otherwise count as uncovered lines
|
|
1234
|
+
# against the SimpleCov 100% gate, so it is wrapped in `# :nocov:` to exclude it
|
|
1235
|
+
# from coverage measurement while keeping the documented scenario in the suite.
|
|
1009
1236
|
RSpec.describe Percy, type: :feature do
|
|
1010
1237
|
before(:each) do
|
|
1011
1238
|
WebMock.reset!
|
|
@@ -1014,7 +1241,7 @@ RSpec.describe Percy, type: :feature do
|
|
|
1014
1241
|
end
|
|
1015
1242
|
|
|
1016
1243
|
describe 'integration', type: :feature do
|
|
1017
|
-
|
|
1244
|
+
xit 'sends snapshots to percy server' do
|
|
1018
1245
|
visit 'index.html'
|
|
1019
1246
|
Percy.snapshot(page, 'Name', widths: [375])
|
|
1020
1247
|
sleep 5 # wait for percy server to process
|
|
@@ -1033,6 +1260,7 @@ RSpec.describe Percy, type: :feature do
|
|
|
1033
1260
|
end
|
|
1034
1261
|
end
|
|
1035
1262
|
end
|
|
1263
|
+
# :nocov:
|
|
1036
1264
|
|
|
1037
1265
|
RSpec.describe Percy do
|
|
1038
1266
|
describe '.percy_screenshot' do
|
|
@@ -1326,4 +1554,105 @@ RSpec.describe Percy do
|
|
|
1326
1554
|
end
|
|
1327
1555
|
end
|
|
1328
1556
|
end
|
|
1557
|
+
|
|
1558
|
+
RSpec.describe Percy do
|
|
1559
|
+
before(:each) do
|
|
1560
|
+
# Allow loopback so Capybara's live selenium session (127.0.0.1:4444) can be
|
|
1561
|
+
# torn down at process exit even when this block's `before` is the last one
|
|
1562
|
+
# to run under random ordering; percy endpoints are stubbed explicitly.
|
|
1563
|
+
WebMock.disable_net_connect!(allow: '127.0.0.1')
|
|
1564
|
+
Percy._clear_cache!
|
|
1565
|
+
end
|
|
1566
|
+
|
|
1567
|
+
describe '.snapshot (mocked driver)' do
|
|
1568
|
+
let(:driver) { double('driver') }
|
|
1569
|
+
let(:manage) { double('manage') }
|
|
1570
|
+
let(:switch_to) { double('switch_to') }
|
|
1571
|
+
|
|
1572
|
+
def stub_web_snapshot_healthcheck
|
|
1573
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck")
|
|
1574
|
+
.to_return(status: 200, body: '{"success":true,"type":"web"}',
|
|
1575
|
+
headers: {'x-percy-core-version': '1.0.0'},)
|
|
1576
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/dom.js")
|
|
1577
|
+
.to_return(status: 200, body: 'window.PercyDOM = {};', headers: {})
|
|
1578
|
+
end
|
|
1579
|
+
|
|
1580
|
+
before(:each) do
|
|
1581
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/log")
|
|
1582
|
+
.to_return(status: 200, body: '', headers: {})
|
|
1583
|
+
allow(driver).to receive(:manage).and_return(manage)
|
|
1584
|
+
allow(manage).to receive(:all_cookies).and_return([])
|
|
1585
|
+
allow(driver).to receive(:respond_to?).with(:driver).and_return(false)
|
|
1586
|
+
allow(driver).to receive(:switch_to).and_return(switch_to)
|
|
1587
|
+
allow(switch_to).to receive(:default_content)
|
|
1588
|
+
allow(driver).to receive(:execute_async_script).and_return(nil)
|
|
1589
|
+
allow(driver).to receive(:current_url).and_return('http://127.0.0.1:3003/index.html')
|
|
1590
|
+
allow(driver).to receive(:find_elements).and_return([])
|
|
1591
|
+
allow(driver).to receive(:execute_script) do |script|
|
|
1592
|
+
{'html' => '<html/>'} if script.to_s.include?('PercyDOM.serialize')
|
|
1593
|
+
end
|
|
1594
|
+
end
|
|
1595
|
+
|
|
1596
|
+
it 'serializes the dom and posts to /percy/snapshot on the non-responsive path' do
|
|
1597
|
+
stub_web_snapshot_healthcheck
|
|
1598
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/snapshot")
|
|
1599
|
+
.to_return(status: 200, body: '{"success":true}')
|
|
1600
|
+
|
|
1601
|
+
Percy.snapshot(driver, 'MockedShot')
|
|
1602
|
+
|
|
1603
|
+
expect(WebMock).to have_requested(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/snapshot")
|
|
1604
|
+
.with { |req| JSON.parse(req.body)['name'] == 'MockedShot' }.once
|
|
1605
|
+
end
|
|
1606
|
+
|
|
1607
|
+
it 'logs the failure when the snapshot response success is false' do
|
|
1608
|
+
stub_web_snapshot_healthcheck
|
|
1609
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/snapshot")
|
|
1610
|
+
.to_return(status: 200, body: '{"success":false,"error":"server rejected"}')
|
|
1611
|
+
|
|
1612
|
+
# body['success'] is false -> raise body['error'] -> swallowed + logged.
|
|
1613
|
+
expect { Percy.snapshot(driver, 'RejectedShot') }
|
|
1614
|
+
.to output(/Could not take DOM snapshot 'RejectedShot'/).to_stdout
|
|
1615
|
+
end
|
|
1616
|
+
end
|
|
1617
|
+
|
|
1618
|
+
describe '.get_browser_instance' do
|
|
1619
|
+
it 'unwraps a Capybara-style session (driver.driver.browser.manage)' do
|
|
1620
|
+
inner_manage = double('inner_manage')
|
|
1621
|
+
inner_browser = double('inner_browser', manage: inner_manage)
|
|
1622
|
+
inner_driver = double('inner_driver')
|
|
1623
|
+
session = double('session')
|
|
1624
|
+
allow(session).to receive(:respond_to?).with(:driver).and_return(true)
|
|
1625
|
+
allow(session).to receive(:driver).and_return(inner_driver)
|
|
1626
|
+
allow(inner_driver).to receive(:respond_to?).with(:browser).and_return(true)
|
|
1627
|
+
allow(inner_driver).to receive(:browser).and_return(inner_browser)
|
|
1628
|
+
|
|
1629
|
+
expect(Percy.get_browser_instance(session)).to eq(inner_manage)
|
|
1630
|
+
end
|
|
1631
|
+
|
|
1632
|
+
it 'uses driver.manage for a plain WebDriver session' do
|
|
1633
|
+
manage = double('manage')
|
|
1634
|
+
driver = double('driver', manage: manage)
|
|
1635
|
+
allow(driver).to receive(:respond_to?).with(:driver).and_return(false)
|
|
1636
|
+
|
|
1637
|
+
expect(Percy.get_browser_instance(driver)).to eq(manage)
|
|
1638
|
+
end
|
|
1639
|
+
end
|
|
1640
|
+
|
|
1641
|
+
describe '.get_driver_metadata' do
|
|
1642
|
+
it 'wraps the driver in a DriverMetaData instance' do
|
|
1643
|
+
driver = double('driver')
|
|
1644
|
+
expect(Percy.get_driver_metadata(driver)).to be_a(DriverMetaData)
|
|
1645
|
+
end
|
|
1646
|
+
end
|
|
1647
|
+
|
|
1648
|
+
describe '.log' do
|
|
1649
|
+
it 'prints the CLI-send failure when PERCY_DEBUG is enabled' do
|
|
1650
|
+
stub_const('Percy::PERCY_DEBUG', true)
|
|
1651
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/log").to_raise(StandardError)
|
|
1652
|
+
|
|
1653
|
+
expect { Percy.log('hello', 'debug') }
|
|
1654
|
+
.to output(/Sending log to CLI Failed/).to_stdout
|
|
1655
|
+
end
|
|
1656
|
+
end
|
|
1657
|
+
end
|
|
1329
1658
|
# rubocop:enable RSpec/MultipleDescribes
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: percy-selenium
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.3.pre.beta.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Perceptual Inc.
|
|
@@ -110,6 +110,7 @@ files:
|
|
|
110
110
|
- ".github/workflows/stale.yml"
|
|
111
111
|
- ".github/workflows/test.yml"
|
|
112
112
|
- ".gitignore"
|
|
113
|
+
- ".npmrc"
|
|
113
114
|
- ".rspec"
|
|
114
115
|
- ".rubocop.yml"
|
|
115
116
|
- ".yardopts"
|