percy-selenium 1.1.1 → 1.1.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 +4 -4
- data/Gemfile +1 -1
- data/README.md +133 -0
- data/lib/cache.rb +49 -0
- data/lib/driver_metadata.rb +57 -0
- data/lib/percy.rb +261 -39
- data/lib/version.rb +1 -1
- data/package.json +1 -1
- data/spec/lib/percy/cache_spec.rb +127 -0
- data/spec/lib/percy/driver_metadata_spec.rb +139 -0
- data/spec/lib/percy/percy_spec.rb +1025 -37
- data/spec/spec_helper.rb +1 -1
- metadata +9 -6
|
@@ -73,9 +73,43 @@ RSpec.describe Percy, type: :feature do
|
|
|
73
73
|
expect { Percy.snapshot(page) }.to raise_error(ArgumentError)
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
it '
|
|
76
|
+
it 'raises when session_type is automate' do
|
|
77
77
|
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck")
|
|
78
|
-
.to_return(
|
|
78
|
+
.to_return(
|
|
79
|
+
status: 200,
|
|
80
|
+
body: '{"success":true,"type":"automate"}',
|
|
81
|
+
headers: {'x-percy-core-version': '1.0.0'},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
expect { Percy.snapshot(page, 'Name') }
|
|
85
|
+
.to raise_error(StandardError, /Invalid function call - percy_snapshot\(\)/)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'does not raise when older CLI omits type from healthcheck' do
|
|
89
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck")
|
|
90
|
+
.to_return(
|
|
91
|
+
status: 200,
|
|
92
|
+
body: '{"success":true}',
|
|
93
|
+
headers: {'x-percy-core-version': '1.0.0'},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/dom.js")
|
|
97
|
+
.to_return(
|
|
98
|
+
status: 200,
|
|
99
|
+
body: fetch_script_string,
|
|
100
|
+
headers: {},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
stub_request(:post, 'http://localhost:5338/percy/snapshot')
|
|
104
|
+
.to_return(status: 200, body: '{"success":true}', headers: {})
|
|
105
|
+
|
|
106
|
+
visit 'index.html'
|
|
107
|
+
expect { Percy.snapshot(page, 'Name') }.to_not raise_error
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'logs an error when sending a snapshot fails' do
|
|
111
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck")
|
|
112
|
+
.to_return(status: 200, body: '{"success":true}',
|
|
79
113
|
headers: {'x-percy-core-version': '1.0.0'},)
|
|
80
114
|
|
|
81
115
|
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/dom.js")
|
|
@@ -94,7 +128,7 @@ RSpec.describe Percy, type: :feature do
|
|
|
94
128
|
|
|
95
129
|
it 'sends snapshots to the local server' do
|
|
96
130
|
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck")
|
|
97
|
-
.to_return(status: 200, body: '{"success":
|
|
131
|
+
.to_return(status: 200, body: '{"success":true}', headers: {
|
|
98
132
|
'x-percy-core-version': '1.0.0',
|
|
99
133
|
},)
|
|
100
134
|
|
|
@@ -106,7 +140,7 @@ RSpec.describe Percy, type: :feature do
|
|
|
106
140
|
)
|
|
107
141
|
|
|
108
142
|
stub_request(:post, 'http://localhost:5338/percy/snapshot')
|
|
109
|
-
.to_return(status: 200, body: '{"success":
|
|
143
|
+
.to_return(status: 200, body: '{"success":true}', headers: {})
|
|
110
144
|
|
|
111
145
|
visit 'index.html'
|
|
112
146
|
data = Percy.snapshot(page, 'Name', widths: [944])
|
|
@@ -130,7 +164,7 @@ RSpec.describe Percy, type: :feature do
|
|
|
130
164
|
it 'sends multiple dom snapshots to the local server' do
|
|
131
165
|
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck").to_return(
|
|
132
166
|
status: 200,
|
|
133
|
-
body: '{"success":
|
|
167
|
+
body: '{"success":true, "widths": { "mobile": [390], "config": [765, 1280]} }',
|
|
134
168
|
headers: {
|
|
135
169
|
'x-percy-core-version': '1.0.0',
|
|
136
170
|
'config': {}, 'widths': {'mobile': [375], 'config': [765, 1280]},
|
|
@@ -144,8 +178,15 @@ RSpec.describe Percy, type: :feature do
|
|
|
144
178
|
headers: {},
|
|
145
179
|
)
|
|
146
180
|
|
|
181
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/widths-config")
|
|
182
|
+
.to_return(
|
|
183
|
+
status: 200,
|
|
184
|
+
body: {widths: [{width: 390}, {width: 765}, {width: 1280}]}.to_json,
|
|
185
|
+
headers: {},
|
|
186
|
+
)
|
|
187
|
+
|
|
147
188
|
stub_request(:post, 'http://localhost:5338/percy/snapshot')
|
|
148
|
-
.to_return(status: 200, body: '{"success":
|
|
189
|
+
.to_return(status: 200, body: '{"success":true}', headers: {})
|
|
149
190
|
|
|
150
191
|
visit 'index.html'
|
|
151
192
|
data = Percy.snapshot(page, 'Name', {responsive_snapshot_capture: true})
|
|
@@ -172,7 +213,7 @@ RSpec.describe Percy, type: :feature do
|
|
|
172
213
|
it 'sends multiple dom snapshots to the local server using selenium' do
|
|
173
214
|
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck").to_return(
|
|
174
215
|
status: 200,
|
|
175
|
-
body: '{"success":
|
|
216
|
+
body: '{"success":true, "widths": { "mobile": [390], "config": [765, 1280]} }',
|
|
176
217
|
headers: {
|
|
177
218
|
'x-percy-core-version': '1.0.0',
|
|
178
219
|
'config': {}, 'widths': {'mobile': [375], 'config': [765, 1280]},
|
|
@@ -186,41 +227,46 @@ RSpec.describe Percy, type: :feature do
|
|
|
186
227
|
headers: {},
|
|
187
228
|
)
|
|
188
229
|
|
|
189
|
-
stub_request(:
|
|
190
|
-
.to_return(
|
|
191
|
-
|
|
192
|
-
|
|
230
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/widths-config")
|
|
231
|
+
.to_return(
|
|
232
|
+
status: 200,
|
|
233
|
+
body: {widths: [{width: 390}, {width: 765}, {width: 1280}]}.to_json,
|
|
234
|
+
headers: {},
|
|
235
|
+
)
|
|
193
236
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
237
|
+
received_body = nil
|
|
238
|
+
stub_request(:post, 'http://localhost:5338/percy/snapshot').to_return do |request|
|
|
239
|
+
received_body = JSON.parse(request.body)
|
|
240
|
+
{status: 200, body: '{"success":true}', headers: {}}
|
|
241
|
+
end
|
|
197
242
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
url: 'http://localhost:5338/test/snapshot',
|
|
207
|
-
dom_snapshot: [
|
|
208
|
-
{'cookies': [expected_cookie], 'html': expected_dom, 'width': 390},
|
|
209
|
-
{'cookies': [expected_cookie], 'html': expected_dom, 'width': 765},
|
|
210
|
-
{'cookies': [expected_cookie], 'html': expected_dom, 'width': 1280},
|
|
211
|
-
],
|
|
212
|
-
client_info: "percy-selenium-ruby/#{Percy::VERSION}",
|
|
213
|
-
environment_info: "selenium/#{Selenium::WebDriver::VERSION} ruby/#{RUBY_VERSION}",
|
|
214
|
-
responsive_snapshot_capture: true,
|
|
215
|
-
}.to_json,
|
|
216
|
-
).once
|
|
243
|
+
driver = Selenium::WebDriver.for :firefox
|
|
244
|
+
begin
|
|
245
|
+
# Use the Capybara fixture server (already running for this describe block)
|
|
246
|
+
# instead of the percy test-mode server endpoint which is not available under
|
|
247
|
+
# normal percy exec.
|
|
248
|
+
driver.navigate.to 'http://127.0.0.1:3003/index.html'
|
|
249
|
+
driver.manage.add_cookie({name: 'cookie-name', value: 'cookie-value'})
|
|
250
|
+
data = Percy.snapshot(driver, 'Name', {responsive_snapshot_capture: true})
|
|
217
251
|
|
|
218
|
-
|
|
252
|
+
expect(received_body['name']).to eq('Name')
|
|
253
|
+
expect(received_body['url']).to eq('http://127.0.0.1:3003/index.html')
|
|
254
|
+
expect(received_body['dom_snapshot'].length).to eq(3)
|
|
255
|
+
expect(received_body['dom_snapshot'].map { |s| s['width'] }).to eq([390, 765, 1280])
|
|
256
|
+
expect(received_body['dom_snapshot'].first['cookies'].first['name']).to eq('cookie-name')
|
|
257
|
+
expect(data).to eq(nil)
|
|
258
|
+
ensure
|
|
259
|
+
begin
|
|
260
|
+
driver.quit
|
|
261
|
+
rescue StandardError
|
|
262
|
+
nil
|
|
263
|
+
end
|
|
264
|
+
end
|
|
219
265
|
end
|
|
220
266
|
|
|
221
267
|
it 'sends snapshots for sync' do
|
|
222
268
|
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck")
|
|
223
|
-
.to_return(status: 200, body: '{"success":
|
|
269
|
+
.to_return(status: 200, body: '{"success":true}',
|
|
224
270
|
headers: {'x-percy-core-version': '1.0.0'},)
|
|
225
271
|
|
|
226
272
|
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/dom.js")
|
|
@@ -231,7 +277,7 @@ RSpec.describe Percy, type: :feature do
|
|
|
231
277
|
)
|
|
232
278
|
|
|
233
279
|
stub_request(:post, 'http://localhost:5338/percy/snapshot')
|
|
234
|
-
.to_return(status: 200, body: '{"success":
|
|
280
|
+
.to_return(status: 200, body: '{"success":true, "data": "sync_data"}', headers: {})
|
|
235
281
|
|
|
236
282
|
visit 'index.html'
|
|
237
283
|
data = Percy.snapshot(page, 'Name', {sync: true})
|
|
@@ -304,6 +350,18 @@ RSpec.describe Percy do
|
|
|
304
350
|
expect(region[:assertion][:diffIgnoreThreshold]).to eq(0.2)
|
|
305
351
|
end
|
|
306
352
|
|
|
353
|
+
it 'creates a region with configuration settings when algorithm is intelliignore' do
|
|
354
|
+
region = Percy.create_region(
|
|
355
|
+
algorithm: 'intelliignore',
|
|
356
|
+
diff_sensitivity: 0.8,
|
|
357
|
+
carousels_enabled: true,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
expect(region[:algorithm]).to eq('intelliignore')
|
|
361
|
+
expect(region[:configuration][:diffSensitivity]).to eq(0.8)
|
|
362
|
+
expect(region[:configuration][:carouselsEnabled]).to eq(true)
|
|
363
|
+
end
|
|
364
|
+
|
|
307
365
|
it 'does not add empty configuration or assertion keys' do
|
|
308
366
|
region = Percy.create_region(algorithm: 'ignore')
|
|
309
367
|
expect(region).to_not have_key(:configuration)
|
|
@@ -312,6 +370,642 @@ RSpec.describe Percy do
|
|
|
312
370
|
end
|
|
313
371
|
end
|
|
314
372
|
|
|
373
|
+
RSpec.describe Percy do
|
|
374
|
+
before(:each) do
|
|
375
|
+
WebMock.disable_net_connect!(allow: '127.0.0.1')
|
|
376
|
+
stub_request(:post, 'http://localhost:5338/percy/log').to_raise(StandardError)
|
|
377
|
+
Percy._clear_cache!
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
describe '.get_responsive_widths' do
|
|
381
|
+
it 'fetches widths from the /percy/widths-config endpoint' do
|
|
382
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/widths-config")
|
|
383
|
+
.to_return(status: 200, body: {widths: [{'width' => 375}, {'width' => 768}]}.to_json)
|
|
384
|
+
|
|
385
|
+
result = Percy.get_responsive_widths
|
|
386
|
+
expect(result).to eq([{'width' => 375}, {'width' => 768}])
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
it 'passes user-supplied widths as a query parameter' do
|
|
390
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/widths-config?widths=375,1280")
|
|
391
|
+
.to_return(status: 200, body: {widths: [{'width' => 375}, {'width' => 1280}]}.to_json)
|
|
392
|
+
|
|
393
|
+
result = Percy.get_responsive_widths([375, 1280])
|
|
394
|
+
expect(result).to eq([{'width' => 375}, {'width' => 1280}])
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
it 'omits query param when widths array is empty' do
|
|
398
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/widths-config")
|
|
399
|
+
.to_return(status: 200, body: {widths: [{'width' => 1280}]}.to_json)
|
|
400
|
+
|
|
401
|
+
result = Percy.get_responsive_widths([])
|
|
402
|
+
expect(result).to eq([{'width' => 1280}])
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
it 'raises when the response widths key is not an array' do
|
|
406
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/widths-config")
|
|
407
|
+
.to_return(status: 200, body: {widths: nil}.to_json)
|
|
408
|
+
|
|
409
|
+
cli_error = 'Update Percy CLI to the latest version to use responsiveSnapshotCapture'
|
|
410
|
+
expect { Percy.get_responsive_widths }.to raise_error(StandardError, cli_error)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
it 'raises when the HTTP request fails' do
|
|
414
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/widths-config")
|
|
415
|
+
.to_return(status: 500, body: '')
|
|
416
|
+
|
|
417
|
+
cli_error = 'Update Percy CLI to the latest version to use responsiveSnapshotCapture'
|
|
418
|
+
expect { Percy.get_responsive_widths }.to raise_error(StandardError, cli_error)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
it 'raises when the endpoint is unreachable' do
|
|
422
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/widths-config")
|
|
423
|
+
.to_raise(StandardError, 'connection refused')
|
|
424
|
+
|
|
425
|
+
cli_error = 'Update Percy CLI to the latest version to use responsiveSnapshotCapture'
|
|
426
|
+
expect { Percy.get_responsive_widths }.to raise_error(StandardError, cli_error)
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
describe '.unsupported_iframe_src?' do
|
|
431
|
+
it 'returns true for nil src' do
|
|
432
|
+
expect(Percy.unsupported_iframe_src?(nil)).to be true
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
it 'returns true for empty string src' do
|
|
436
|
+
expect(Percy.unsupported_iframe_src?('')).to be true
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
it 'returns true for about:blank' do
|
|
440
|
+
expect(Percy.unsupported_iframe_src?('about:blank')).to be true
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
it 'returns true for javascript: src' do
|
|
444
|
+
expect(Percy.unsupported_iframe_src?('javascript:void(0)')).to be true
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
it 'returns true for data: src' do
|
|
448
|
+
expect(Percy.unsupported_iframe_src?('data:text/html,<h1>Hi</h1>')).to be true
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
it 'returns true for vbscript: src' do
|
|
452
|
+
expect(Percy.unsupported_iframe_src?('vbscript:MsgBox()')).to be true
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
it 'returns false for a valid https url' do
|
|
456
|
+
expect(Percy.unsupported_iframe_src?('https://example.com/page')).to be false
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
it 'returns false for a relative url' do
|
|
460
|
+
expect(Percy.unsupported_iframe_src?('/embed.html')).to be false
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
describe '.get_origin' do
|
|
465
|
+
it 'returns scheme + host for an https url' do
|
|
466
|
+
expect(Percy.get_origin('https://example.com/path')).to eq('https://example.com')
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
it 'returns scheme + host for an http url' do
|
|
470
|
+
expect(Percy.get_origin('http://example.com/path')).to eq('http://example.com')
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
it 'omits the default http port 80' do
|
|
474
|
+
expect(Percy.get_origin('http://example.com:80/path')).to eq('http://example.com')
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
it 'omits the default https port 443' do
|
|
478
|
+
expect(Percy.get_origin('https://example.com:443/path')).to eq('https://example.com')
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
it 'includes non-default ports' do
|
|
482
|
+
expect(Percy.get_origin('http://example.com:3000/path')).to eq('http://example.com:3000')
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
it 'treats same host with different scheme as different origins' do
|
|
486
|
+
http = Percy.get_origin('http://example.com')
|
|
487
|
+
https = Percy.get_origin('https://example.com')
|
|
488
|
+
expect(http).to_not eq(https)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
it 'treats same host with different ports as different origins' do
|
|
492
|
+
port_a = Percy.get_origin('http://example.com:3000')
|
|
493
|
+
port_b = Percy.get_origin('http://example.com:4000')
|
|
494
|
+
expect(port_a).to_not eq(port_b)
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
describe '.process_frame' do
|
|
499
|
+
let(:driver) { double('driver') }
|
|
500
|
+
let(:frame_element) { double('frame_element') }
|
|
501
|
+
let(:switch_to) { double('switch_to') }
|
|
502
|
+
|
|
503
|
+
before(:each) do
|
|
504
|
+
allow(driver).to receive(:switch_to).and_return(switch_to)
|
|
505
|
+
allow(switch_to).to receive(:frame)
|
|
506
|
+
allow(switch_to).to receive(:parent_frame)
|
|
507
|
+
allow(switch_to).to receive(:default_content)
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
it 'returns a hash with iframeData, iframeSnapshot, and frameUrl on success' do
|
|
511
|
+
allow(frame_element).to receive(:attribute).with('src')
|
|
512
|
+
.and_return('https://other.example.com/page')
|
|
513
|
+
allow(frame_element).to receive(:attribute).with('data-percy-element-id')
|
|
514
|
+
.and_return('elem-123')
|
|
515
|
+
allow(driver).to receive(:execute_script).and_return(nil, {'html' => '<html/>'})
|
|
516
|
+
|
|
517
|
+
result = Percy.process_frame(driver, frame_element, {}, 'percy_dom_script')
|
|
518
|
+
|
|
519
|
+
expect(result).to_not be_nil
|
|
520
|
+
expect(result['iframeData']['percyElementId']).to eq('elem-123')
|
|
521
|
+
expect(result['iframeSnapshot']).to eq({'html' => '<html/>'})
|
|
522
|
+
expect(result['frameUrl']).to eq('https://other.example.com/page')
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
it 'returns nil when data-percy-element-id attribute is missing' do
|
|
526
|
+
allow(frame_element).to receive(:attribute).with('src').and_return('https://other.example.com/page')
|
|
527
|
+
allow(frame_element).to receive(:attribute).with('data-percy-element-id').and_return(nil)
|
|
528
|
+
allow(driver).to receive(:execute_script).and_return(nil, {'html' => '<html/>'})
|
|
529
|
+
|
|
530
|
+
result = Percy.process_frame(driver, frame_element, {}, 'percy_dom_script')
|
|
531
|
+
expect(result).to be_nil
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
it 'returns nil when execute_script raises inside the iframe' do
|
|
535
|
+
allow(frame_element).to receive(:attribute).with('src').and_return('https://other.example.com/page')
|
|
536
|
+
allow(driver).to receive(:execute_script).and_raise(StandardError, 'injection error')
|
|
537
|
+
|
|
538
|
+
result = Percy.process_frame(driver, frame_element, {}, 'percy_dom_script')
|
|
539
|
+
expect(result).to be_nil
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
it 'returns nil when switching to the frame fails' do
|
|
543
|
+
allow(frame_element).to receive(:attribute).with('src').and_return('https://other.example.com/page')
|
|
544
|
+
allow(switch_to).to receive(:frame).and_raise(StandardError, 'no such frame')
|
|
545
|
+
|
|
546
|
+
result = Percy.process_frame(driver, frame_element, {}, 'percy_dom_script')
|
|
547
|
+
expect(result).to be_nil
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
it 'uses unknown-src fallback when frame has no src attribute' do
|
|
551
|
+
allow(frame_element).to receive(:attribute).with('src').and_return(nil)
|
|
552
|
+
allow(frame_element).to receive(:attribute).with('data-percy-element-id')
|
|
553
|
+
.and_return('elem-nosrc')
|
|
554
|
+
allow(driver).to receive(:execute_script).and_return(nil, {'html' => '<html/>'})
|
|
555
|
+
|
|
556
|
+
result = Percy.process_frame(driver, frame_element, {}, 'percy_dom_script')
|
|
557
|
+
expect(result['frameUrl']).to eq('unknown-src')
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
it 'merges enableJavaScript into the PercyDOM.serialize call' do
|
|
561
|
+
allow(frame_element).to receive(:attribute).with('src')
|
|
562
|
+
.and_return('https://other.example.com/page')
|
|
563
|
+
allow(frame_element).to receive(:attribute).with('data-percy-element-id')
|
|
564
|
+
.and_return('elem-abc')
|
|
565
|
+
|
|
566
|
+
captured_serialize_call = nil
|
|
567
|
+
call_count = 0
|
|
568
|
+
allow(driver).to receive(:execute_script) do |script|
|
|
569
|
+
call_count += 1
|
|
570
|
+
if call_count == 2
|
|
571
|
+
captured_serialize_call = script
|
|
572
|
+
{'html' => '<html/>'}
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
Percy.process_frame(driver, frame_element, {someOpt: 1}, 'percy_dom_script')
|
|
577
|
+
|
|
578
|
+
expect(captured_serialize_call).to include('enableJavaScript')
|
|
579
|
+
expect(captured_serialize_call).to include('true')
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
it 'always switches back to default content even when script injection fails' do
|
|
583
|
+
allow(frame_element).to receive(:attribute).with('src')
|
|
584
|
+
.and_return('https://other.example.com/page')
|
|
585
|
+
expect(switch_to).to receive(:default_content).once
|
|
586
|
+
allow(driver).to receive(:execute_script).and_raise(StandardError, 'error')
|
|
587
|
+
|
|
588
|
+
Percy.process_frame(driver, frame_element, {}, 'percy_dom_script')
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
describe '.get_serialized_dom' do
|
|
593
|
+
let(:driver) { double('driver') }
|
|
594
|
+
let(:manage) { double('manage') }
|
|
595
|
+
let(:switch_to) { double('switch_to') }
|
|
596
|
+
|
|
597
|
+
before(:each) do
|
|
598
|
+
allow(driver).to receive(:manage).and_return(manage)
|
|
599
|
+
allow(manage).to receive(:all_cookies).and_return([])
|
|
600
|
+
allow(driver).to receive(:switch_to).and_return(switch_to)
|
|
601
|
+
allow(switch_to).to receive(:frame)
|
|
602
|
+
allow(switch_to).to receive(:parent_frame)
|
|
603
|
+
allow(switch_to).to receive(:default_content)
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
it 'returns the serialized dom with cookies when no iframes present' do
|
|
607
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
608
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
609
|
+
allow(driver).to receive(:find_elements).and_return([])
|
|
610
|
+
|
|
611
|
+
dom = Percy.get_serialized_dom(driver, {})
|
|
612
|
+
expect(dom['html']).to eq('<html/>')
|
|
613
|
+
expect(dom['cookies']).to eq([])
|
|
614
|
+
expect(dom).to_not have_key('corsIframes')
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
it 'populates corsIframes for cross-origin frames' do
|
|
618
|
+
frame = double('frame')
|
|
619
|
+
allow(frame).to receive(:attribute).with('src').and_return('https://cross.example.com/page')
|
|
620
|
+
allow(frame).to receive(:attribute).with('data-percy-element-id').and_return('cid-1')
|
|
621
|
+
|
|
622
|
+
call_count = 0
|
|
623
|
+
allow(driver).to receive(:execute_script) do
|
|
624
|
+
call_count += 1
|
|
625
|
+
case call_count
|
|
626
|
+
when 1 then {'html' => '<main/>'}
|
|
627
|
+
when 2 then nil
|
|
628
|
+
when 3 then {'html' => '<frame/>'}
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
632
|
+
allow(driver).to receive(:find_elements).and_return([frame])
|
|
633
|
+
|
|
634
|
+
dom = Percy.get_serialized_dom(driver, {}, percy_dom_script: 'percy_dom_script')
|
|
635
|
+
|
|
636
|
+
expect(dom).to have_key('corsIframes')
|
|
637
|
+
expect(dom['corsIframes'].length).to eq(1)
|
|
638
|
+
expect(dom['corsIframes'][0]['iframeData']['percyElementId']).to eq('cid-1')
|
|
639
|
+
expect(dom['corsIframes'][0]['iframeSnapshot']['html']).to eq('<frame/>')
|
|
640
|
+
expect(dom['corsIframes'][0]['frameUrl']).to eq('https://cross.example.com/page')
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
it 'skips same-origin iframes' do
|
|
644
|
+
frame = double('frame')
|
|
645
|
+
allow(frame).to receive(:attribute).with('src').and_return('http://main.example.com/inner.html')
|
|
646
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
647
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
648
|
+
allow(driver).to receive(:find_elements).and_return([frame])
|
|
649
|
+
|
|
650
|
+
dom = Percy.get_serialized_dom(driver, {}, percy_dom_script: 'percy_dom_script')
|
|
651
|
+
expect(dom).to_not have_key('corsIframes')
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
it 'skips iframes with about:blank src' do
|
|
655
|
+
frame = double('frame')
|
|
656
|
+
allow(frame).to receive(:attribute).with('src').and_return('about:blank')
|
|
657
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
658
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
659
|
+
allow(driver).to receive(:find_elements).and_return([frame])
|
|
660
|
+
|
|
661
|
+
dom = Percy.get_serialized_dom(driver, {}, percy_dom_script: 'percy_dom_script')
|
|
662
|
+
expect(dom).to_not have_key('corsIframes')
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
it 'does not process cross-origin iframes when percy_dom_script is nil' do
|
|
666
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
667
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
668
|
+
expect(driver).to_not receive(:find_elements)
|
|
669
|
+
|
|
670
|
+
dom = Percy.get_serialized_dom(driver, {}, percy_dom_script: nil)
|
|
671
|
+
expect(dom).to_not have_key('corsIframes')
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
it 'treats same host with different scheme as cross-origin' do
|
|
675
|
+
frame = double('frame')
|
|
676
|
+
allow(frame).to receive(:attribute).with('src').and_return('https://main.example.com/widget')
|
|
677
|
+
allow(frame).to receive(:attribute).with('data-percy-element-id').and_return('percy-id-1')
|
|
678
|
+
|
|
679
|
+
call_count = 0
|
|
680
|
+
allow(driver).to receive(:execute_script) do
|
|
681
|
+
call_count += 1
|
|
682
|
+
if call_count == 1
|
|
683
|
+
{'html' => '<html/>'}
|
|
684
|
+
else
|
|
685
|
+
call_count == 2 ? nil : {'html' => '<frame/>'}
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
689
|
+
allow(driver).to receive(:find_elements).and_return([frame])
|
|
690
|
+
|
|
691
|
+
dom = Percy.get_serialized_dom(driver, {}, percy_dom_script: 'script')
|
|
692
|
+
expect(dom).to have_key('corsIframes')
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
it 'treats same host with different port as cross-origin' do
|
|
696
|
+
frame = double('frame')
|
|
697
|
+
allow(frame).to receive(:attribute).with('src').and_return('http://main.example.com:4000/widget')
|
|
698
|
+
allow(frame).to receive(:attribute).with('data-percy-element-id').and_return('percy-id-port')
|
|
699
|
+
|
|
700
|
+
call_count = 0
|
|
701
|
+
allow(driver).to receive(:execute_script) do
|
|
702
|
+
call_count += 1
|
|
703
|
+
if call_count == 1
|
|
704
|
+
{'html' => '<html/>'}
|
|
705
|
+
else
|
|
706
|
+
call_count == 2 ? nil : {'html' => '<frame/>'}
|
|
707
|
+
end
|
|
708
|
+
end
|
|
709
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com:3000/')
|
|
710
|
+
allow(driver).to receive(:find_elements).and_return([frame])
|
|
711
|
+
|
|
712
|
+
dom = Percy.get_serialized_dom(driver, {}, percy_dom_script: 'script')
|
|
713
|
+
expect(dom).to have_key('corsIframes')
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
it 'always attaches cookies to the snapshot' do
|
|
717
|
+
cookies_data = [{'name' => 'session', 'value' => 'abc'}]
|
|
718
|
+
allow(manage).to receive(:all_cookies).and_return(cookies_data)
|
|
719
|
+
allow(driver).to receive(:execute_script).and_return({'html' => '<html/>'})
|
|
720
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
721
|
+
allow(driver).to receive(:find_elements).and_return([])
|
|
722
|
+
|
|
723
|
+
dom = Percy.get_serialized_dom(driver, {})
|
|
724
|
+
expect(dom['cookies']).to eq(cookies_data)
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
it 'skips same-origin frame and processes only cross-origin frame' do
|
|
728
|
+
same_frame = double('same_frame')
|
|
729
|
+
allow(same_frame).to receive(:attribute).with('src').and_return('http://main.example.com/inner')
|
|
730
|
+
|
|
731
|
+
cross_frame = double('cross_frame')
|
|
732
|
+
allow(cross_frame).to receive(:attribute).with('src').and_return('https://other.example.com/page')
|
|
733
|
+
allow(cross_frame).to receive(:attribute).with('data-percy-element-id').and_return('cid-x')
|
|
734
|
+
|
|
735
|
+
call_count = 0
|
|
736
|
+
allow(driver).to receive(:execute_script) do
|
|
737
|
+
call_count += 1
|
|
738
|
+
if call_count == 1
|
|
739
|
+
{'html' => '<main/>'}
|
|
740
|
+
else
|
|
741
|
+
call_count == 2 ? nil : {'html' => '<cross/>'}
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
allow(driver).to receive(:current_url).and_return('http://main.example.com/')
|
|
745
|
+
allow(driver).to receive(:find_elements).and_return([same_frame, cross_frame])
|
|
746
|
+
|
|
747
|
+
dom = Percy.get_serialized_dom(driver, {}, percy_dom_script: 'script')
|
|
748
|
+
expect(dom['corsIframes'].length).to eq(1)
|
|
749
|
+
expect(dom['corsIframes'][0]['frameUrl']).to eq('https://other.example.com/page')
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
describe '.change_window_dimension_and_wait' do
|
|
754
|
+
let(:driver) { double('driver') }
|
|
755
|
+
let(:manage) { double('manage') }
|
|
756
|
+
let(:window) { double('window') }
|
|
757
|
+
let(:wait) { instance_double(Selenium::WebDriver::Wait) }
|
|
758
|
+
let(:caps_firefox) { double('capabilities', browser_name: 'firefox') }
|
|
759
|
+
|
|
760
|
+
before(:each) do
|
|
761
|
+
allow(driver).to receive(:manage).and_return(manage)
|
|
762
|
+
allow(manage).to receive(:window).and_return(window)
|
|
763
|
+
allow(window).to receive(:resize_to)
|
|
764
|
+
allow(driver).to receive(:capabilities).and_return(caps_firefox)
|
|
765
|
+
allow(driver).to receive(:respond_to?).with(:driver).and_return(false)
|
|
766
|
+
allow(driver).to receive(:respond_to?).with(:execute_cdp).and_return(false)
|
|
767
|
+
allow(driver).to receive(:execute_script) do |script|
|
|
768
|
+
{'w' => 1024, 'h' => 768} if script.include?('innerWidth')
|
|
769
|
+
end
|
|
770
|
+
allow(Selenium::WebDriver::Wait).to receive(:new).and_return(wait)
|
|
771
|
+
allow(wait).to receive(:until)
|
|
772
|
+
allow(Percy).to receive(:log)
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
it 'resizes the window using resize_to for non-chrome browsers' do
|
|
776
|
+
expect(window).to receive(:resize_to).with(768, 1024)
|
|
777
|
+
Percy.change_window_dimension_and_wait(driver, 768, 1024, 1)
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
it 'dispatches a resize event after resizing for non-chrome browsers' do
|
|
781
|
+
expect(driver).to receive(:execute_script)
|
|
782
|
+
.with("window.dispatchEvent(new Event('resize'));")
|
|
783
|
+
Percy.change_window_dimension_and_wait(driver, 768, 1024, 1)
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
it 'uses execute_cdp for chrome when execute_cdp is available' do
|
|
787
|
+
chrome_caps = double('capabilities', browser_name: 'chrome')
|
|
788
|
+
allow(driver).to receive(:capabilities).and_return(chrome_caps)
|
|
789
|
+
allow(driver).to receive(:respond_to?).with(:execute_cdp).and_return(true)
|
|
790
|
+
expect(driver).to receive(:execute_cdp).with(
|
|
791
|
+
'Emulation.setDeviceMetricsOverride',
|
|
792
|
+
{height: 812, width: 375, deviceScaleFactor: 1, mobile: false},
|
|
793
|
+
)
|
|
794
|
+
Percy.change_window_dimension_and_wait(driver, 375, 812, 1)
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
it 'does not call resize_to when cdp succeeds for chrome' do
|
|
798
|
+
chrome_caps = double('capabilities', browser_name: 'chrome')
|
|
799
|
+
allow(driver).to receive(:capabilities).and_return(chrome_caps)
|
|
800
|
+
allow(driver).to receive(:respond_to?).with(:execute_cdp).and_return(true)
|
|
801
|
+
allow(driver).to receive(:execute_cdp)
|
|
802
|
+
expect(window).to_not receive(:resize_to)
|
|
803
|
+
Percy.change_window_dimension_and_wait(driver, 375, 812, 1)
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
it 'falls back to resize_to when execute_cdp raises' do
|
|
807
|
+
chrome_caps = double('capabilities', browser_name: 'chrome')
|
|
808
|
+
allow(driver).to receive(:capabilities).and_return(chrome_caps)
|
|
809
|
+
allow(driver).to receive(:respond_to?).with(:execute_cdp).and_return(true)
|
|
810
|
+
allow(driver).to receive(:execute_cdp).and_raise(StandardError, 'cdp error')
|
|
811
|
+
expect(window).to receive(:resize_to).with(375, 812)
|
|
812
|
+
Percy.change_window_dimension_and_wait(driver, 375, 812, 1)
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
it 'dispatches a resize event in the cdp fallback path' do
|
|
816
|
+
chrome_caps = double('capabilities', browser_name: 'chrome')
|
|
817
|
+
allow(driver).to receive(:capabilities).and_return(chrome_caps)
|
|
818
|
+
allow(driver).to receive(:respond_to?).with(:execute_cdp).and_return(true)
|
|
819
|
+
allow(driver).to receive(:execute_cdp).and_raise(StandardError, 'cdp error')
|
|
820
|
+
expect(driver).to receive(:execute_script)
|
|
821
|
+
.with("window.dispatchEvent(new Event('resize'));")
|
|
822
|
+
Percy.change_window_dimension_and_wait(driver, 375, 812, 1)
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
describe '.capture_responsive_dom' do
|
|
827
|
+
let(:driver) { double('driver') }
|
|
828
|
+
let(:manage) { double('manage') }
|
|
829
|
+
let(:window) { double('window') }
|
|
830
|
+
let(:size) { double('size', width: 1280, height: 900) }
|
|
831
|
+
let(:navigate) { double('navigate') }
|
|
832
|
+
|
|
833
|
+
before(:each) do
|
|
834
|
+
allow(driver).to receive(:manage).and_return(manage)
|
|
835
|
+
allow(manage).to receive(:window).and_return(window)
|
|
836
|
+
allow(window).to receive(:size).and_return(size)
|
|
837
|
+
allow(driver).to receive(:respond_to?).with(:driver).and_return(false)
|
|
838
|
+
allow(driver).to receive(:execute_script).and_return(nil)
|
|
839
|
+
allow(Percy).to receive(:get_serialized_dom).and_return({'html' => '<html/>'})
|
|
840
|
+
allow(Percy).to receive(:change_window_dimension_and_wait)
|
|
841
|
+
allow(Percy).to receive(:log)
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
# -----------------------------------------------------------------------
|
|
845
|
+
# Resize behavior
|
|
846
|
+
# -----------------------------------------------------------------------
|
|
847
|
+
|
|
848
|
+
it 'calls change_window_dimension_and_wait for each distinct width' do
|
|
849
|
+
allow(Percy).to receive(:get_responsive_widths).and_return(
|
|
850
|
+
[{'width' => 375}, {'width' => 768}, {'width' => 1280}],
|
|
851
|
+
)
|
|
852
|
+
# Each of the 3 widths differs from the previous + final restore = 4 calls
|
|
853
|
+
expect(Percy).to receive(:change_window_dimension_and_wait).exactly(4).times
|
|
854
|
+
Percy.capture_responsive_dom(driver, {})
|
|
855
|
+
end
|
|
856
|
+
|
|
857
|
+
it 'skips resize when consecutive entries have the same width and height' do
|
|
858
|
+
allow(Percy).to receive(:get_responsive_widths).and_return(
|
|
859
|
+
[{'width' => 375}, {'width' => 375}],
|
|
860
|
+
)
|
|
861
|
+
# 375 (first, differs from 1280) + final restore = 2; second 375 skipped
|
|
862
|
+
expect(Percy).to receive(:change_window_dimension_and_wait).twice
|
|
863
|
+
Percy.capture_responsive_dom(driver, {})
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
it 'passes the requested width and window height to change_window_dimension_and_wait' do
|
|
867
|
+
allow(Percy).to receive(:get_responsive_widths).and_return([{'width' => 390}])
|
|
868
|
+
expect(Percy).to receive(:change_window_dimension_and_wait)
|
|
869
|
+
.with(driver, 390, 900, anything)
|
|
870
|
+
Percy.capture_responsive_dom(driver, {})
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
it 'uses per-entry height from widths list over the default target_height' do
|
|
874
|
+
allow(Percy).to receive(:get_responsive_widths).and_return(
|
|
875
|
+
[{'width' => 390, 'height' => 844}],
|
|
876
|
+
)
|
|
877
|
+
expect(Percy).to receive(:change_window_dimension_and_wait)
|
|
878
|
+
.with(driver, 390, 844, anything)
|
|
879
|
+
Percy.capture_responsive_dom(driver, {})
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
it 'restores original window dimensions after processing all widths' do
|
|
883
|
+
allow(Percy).to receive(:get_responsive_widths).and_return([{'width' => 375}])
|
|
884
|
+
expect(Percy).to receive(:change_window_dimension_and_wait)
|
|
885
|
+
.with(driver, 1280, 900, anything)
|
|
886
|
+
Percy.capture_responsive_dom(driver, {})
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
# -----------------------------------------------------------------------
|
|
890
|
+
# Reload-page flag
|
|
891
|
+
# -----------------------------------------------------------------------
|
|
892
|
+
|
|
893
|
+
context 'when PERCY_RESPONSIVE_CAPTURE_RELOAD_PAGE is true' do
|
|
894
|
+
before(:each) do
|
|
895
|
+
allow(Percy).to receive(:responsive_capture_reload_page?).and_return(true)
|
|
896
|
+
allow(Percy).to receive(:fetch_percy_dom).and_return('percy_dom_script')
|
|
897
|
+
allow(Percy).to receive(:get_responsive_widths).and_return(
|
|
898
|
+
[{'width' => 375}, {'width' => 768}],
|
|
899
|
+
)
|
|
900
|
+
allow(driver).to receive(:navigate).and_return(navigate)
|
|
901
|
+
allow(navigate).to receive(:refresh)
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
it 'calls driver.navigate.refresh once per width' do
|
|
905
|
+
expect(navigate).to receive(:refresh).twice
|
|
906
|
+
Percy.capture_responsive_dom(driver, {})
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
it 're-fetches percy_dom after each reload' do
|
|
910
|
+
expect(Percy).to receive(:fetch_percy_dom).twice
|
|
911
|
+
Percy.capture_responsive_dom(driver, {})
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
it 'falls back to driver.driver.browser.navigate.refresh when direct refresh raises' do
|
|
915
|
+
allow(Percy).to receive(:get_responsive_widths).and_return([{'width' => 375}])
|
|
916
|
+
allow(navigate).to receive(:refresh).and_raise(StandardError, 'direct refresh failed')
|
|
917
|
+
|
|
918
|
+
inner_nav = double('inner_navigate')
|
|
919
|
+
inner_browser = double('inner_browser')
|
|
920
|
+
inner_drv = double('inner_driver', browser: inner_browser)
|
|
921
|
+
allow(driver).to receive(:driver).and_return(inner_drv)
|
|
922
|
+
allow(inner_browser).to receive(:navigate).and_return(inner_nav)
|
|
923
|
+
expect(inner_nav).to receive(:refresh).once
|
|
924
|
+
Percy.capture_responsive_dom(driver, {})
|
|
925
|
+
end
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
# -----------------------------------------------------------------------
|
|
929
|
+
# minHeight / PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT flag
|
|
930
|
+
# -----------------------------------------------------------------------
|
|
931
|
+
|
|
932
|
+
context 'when PERCY_RESPONSIVE_CAPTURE_MIN_HEIGHT is true' do
|
|
933
|
+
before(:each) do
|
|
934
|
+
allow(Percy).to receive(:responsive_capture_min_height?).and_return(true)
|
|
935
|
+
allow(Percy).to receive(:get_responsive_widths).and_return([{'width' => 390}])
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
it 'uses the minHeight option as the target height' do
|
|
939
|
+
expect(Percy).to receive(:change_window_dimension_and_wait)
|
|
940
|
+
.with(driver, 390, 800, anything)
|
|
941
|
+
Percy.capture_responsive_dom(driver, {minHeight: 800})
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
it 'uses minHeight from cli_config when not provided in options' do
|
|
945
|
+
begin
|
|
946
|
+
Percy.instance_variable_set(:@cli_config, {'snapshot' => {'minHeight' => 700}})
|
|
947
|
+
expect(Percy).to receive(:change_window_dimension_and_wait)
|
|
948
|
+
.with(driver, 390, 700, anything)
|
|
949
|
+
Percy.capture_responsive_dom(driver, {})
|
|
950
|
+
ensure
|
|
951
|
+
Percy.instance_variable_set(:@cli_config, nil)
|
|
952
|
+
end
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
it 'uses the current window height unchanged when minHeight is not set' do
|
|
956
|
+
expect(Percy).to receive(:change_window_dimension_and_wait)
|
|
957
|
+
.with(driver, 390, 900, anything)
|
|
958
|
+
Percy.capture_responsive_dom(driver, {})
|
|
959
|
+
end
|
|
960
|
+
end
|
|
961
|
+
end
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
RSpec.describe Percy do
|
|
965
|
+
before(:each) do
|
|
966
|
+
Percy._clear_cache!
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
describe '.responsive_snapshot_capture?' do
|
|
970
|
+
it 'returns false when deferUploads is enabled in cli_config' do
|
|
971
|
+
begin
|
|
972
|
+
Percy.instance_variable_set(:@cli_config, {'percy' => {'deferUploads' => true}})
|
|
973
|
+
result = Percy.responsive_snapshot_capture?({responsive_snapshot_capture: true})
|
|
974
|
+
expect(result).to be false
|
|
975
|
+
ensure
|
|
976
|
+
Percy.instance_variable_set(:@cli_config, nil)
|
|
977
|
+
end
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
it 'returns true when responsive_snapshot_capture option is set' do
|
|
981
|
+
result = Percy.responsive_snapshot_capture?({responsive_snapshot_capture: true})
|
|
982
|
+
expect(result).to be true
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
it 'returns true when responsiveSnapshotCapture option is set' do
|
|
986
|
+
result = Percy.responsive_snapshot_capture?({responsiveSnapshotCapture: true})
|
|
987
|
+
expect(result).to be true
|
|
988
|
+
end
|
|
989
|
+
|
|
990
|
+
it 'returns true when responsiveSnapshotCapture is set in cli_config' do
|
|
991
|
+
begin
|
|
992
|
+
Percy.instance_variable_set(
|
|
993
|
+
:@cli_config, {'snapshot' => {'responsiveSnapshotCapture' => true}},
|
|
994
|
+
)
|
|
995
|
+
result = Percy.responsive_snapshot_capture?({})
|
|
996
|
+
expect(result).to be true
|
|
997
|
+
ensure
|
|
998
|
+
Percy.instance_variable_set(:@cli_config, nil)
|
|
999
|
+
end
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
it 'returns nil when no responsive capture option is set' do
|
|
1003
|
+
result = Percy.responsive_snapshot_capture?({})
|
|
1004
|
+
expect(result).to be_falsey
|
|
1005
|
+
end
|
|
1006
|
+
end
|
|
1007
|
+
end
|
|
1008
|
+
|
|
315
1009
|
RSpec.describe Percy, type: :feature do
|
|
316
1010
|
before(:each) do
|
|
317
1011
|
WebMock.reset!
|
|
@@ -329,7 +1023,8 @@ RSpec.describe Percy, type: :feature do
|
|
|
329
1023
|
healthcheck = requests[0]
|
|
330
1024
|
expect(healthcheck['url']).to eq('/percy/healthcheck')
|
|
331
1025
|
|
|
332
|
-
|
|
1026
|
+
snap_request = requests.find { |r| r['url'] == '/percy/snapshot' }
|
|
1027
|
+
snap = snap_request['body']
|
|
333
1028
|
expect(snap['name']).to eq('Name')
|
|
334
1029
|
expect(snap['url']).to eq('http://127.0.0.1:3003/index.html')
|
|
335
1030
|
expect(snap['client_info']).to include('percy-selenium-ruby')
|
|
@@ -338,4 +1033,297 @@ RSpec.describe Percy, type: :feature do
|
|
|
338
1033
|
end
|
|
339
1034
|
end
|
|
340
1035
|
end
|
|
1036
|
+
|
|
1037
|
+
RSpec.describe Percy do
|
|
1038
|
+
describe '.percy_screenshot' do
|
|
1039
|
+
let(:driver) { double('driver') }
|
|
1040
|
+
let(:metadata) do
|
|
1041
|
+
instance_double(
|
|
1042
|
+
DriverMetaData,
|
|
1043
|
+
session_id: 'sess-abc-123',
|
|
1044
|
+
command_executor_url: 'http://hub.browserstack.com/wd/hub',
|
|
1045
|
+
capabilities: {'browserName' => 'chrome', 'version' => '114'},
|
|
1046
|
+
)
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
let(:automate_url) { "#{Percy::PERCY_SERVER_ADDRESS}/percy/automateScreenshot" }
|
|
1050
|
+
|
|
1051
|
+
before(:each) do
|
|
1052
|
+
WebMock.disable_net_connect!
|
|
1053
|
+
stub_request(:post, 'http://localhost:5338/percy/log').to_raise(StandardError)
|
|
1054
|
+
Percy._clear_cache!
|
|
1055
|
+
allow(Percy).to receive(:get_driver_metadata).with(driver).and_return(metadata)
|
|
1056
|
+
end
|
|
1057
|
+
|
|
1058
|
+
def stub_automate_healthcheck
|
|
1059
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck")
|
|
1060
|
+
.to_return(
|
|
1061
|
+
status: 200,
|
|
1062
|
+
body: '{"success":true,"type":"automate"}',
|
|
1063
|
+
headers: {'x-percy-core-version': '1.0.0'},
|
|
1064
|
+
)
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
def stub_web_healthcheck
|
|
1068
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck")
|
|
1069
|
+
.to_return(
|
|
1070
|
+
status: 200,
|
|
1071
|
+
body: '{"success":true,"type":"web"}',
|
|
1072
|
+
headers: {'x-percy-core-version': '1.0.0'},
|
|
1073
|
+
)
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
# -------------------------------------------------------------------------
|
|
1077
|
+
# percy_enabled? gating
|
|
1078
|
+
# -------------------------------------------------------------------------
|
|
1079
|
+
|
|
1080
|
+
it 'returns nil without posting when percy is not running' do
|
|
1081
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck")
|
|
1082
|
+
.to_return(status: 500, body: '')
|
|
1083
|
+
|
|
1084
|
+
result = nil
|
|
1085
|
+
expect { result = Percy.percy_screenshot(driver, 'DisabledShot') }
|
|
1086
|
+
.to output(/Percy is not running/).to_stdout
|
|
1087
|
+
expect(result).to be_nil
|
|
1088
|
+
expect(WebMock).to_not have_requested(:post, /automateScreenshot/)
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
it 'returns nil without posting when healthcheck raises a connection error' do
|
|
1092
|
+
stub_request(:get, "#{Percy::PERCY_SERVER_ADDRESS}/percy/healthcheck")
|
|
1093
|
+
.to_raise(StandardError, 'connection refused')
|
|
1094
|
+
|
|
1095
|
+
result = nil
|
|
1096
|
+
expect { result = Percy.percy_screenshot(driver, 'ConnErrShot') }
|
|
1097
|
+
.to output(/Percy is not running/).to_stdout
|
|
1098
|
+
expect(result).to be_nil
|
|
1099
|
+
end
|
|
1100
|
+
|
|
1101
|
+
# -------------------------------------------------------------------------
|
|
1102
|
+
# Session-type enforcement
|
|
1103
|
+
# -------------------------------------------------------------------------
|
|
1104
|
+
|
|
1105
|
+
it 'raises with a descriptive message when session type is not automate' do
|
|
1106
|
+
stub_web_healthcheck
|
|
1107
|
+
|
|
1108
|
+
expect { Percy.percy_screenshot(driver, 'WebShot') }
|
|
1109
|
+
.to raise_error(
|
|
1110
|
+
StandardError,
|
|
1111
|
+
/Invalid function call - percy_screenshot\(\)/,
|
|
1112
|
+
)
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
it 'error message for wrong session type includes guidance to use percy_snapshot' do
|
|
1116
|
+
stub_web_healthcheck
|
|
1117
|
+
|
|
1118
|
+
expect { Percy.percy_screenshot(driver, 'WebShot') }
|
|
1119
|
+
.to raise_error(StandardError, /percy_snapshot\(\)/)
|
|
1120
|
+
end
|
|
1121
|
+
|
|
1122
|
+
it 'does not raise when session type is automate' do
|
|
1123
|
+
stub_automate_healthcheck
|
|
1124
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/automateScreenshot")
|
|
1125
|
+
.to_return(status: 200, body: '{"success":true}')
|
|
1126
|
+
|
|
1127
|
+
expect { Percy.percy_screenshot(driver, 'AutoShot') }.to_not raise_error
|
|
1128
|
+
end
|
|
1129
|
+
|
|
1130
|
+
# -------------------------------------------------------------------------
|
|
1131
|
+
# Request payload construction
|
|
1132
|
+
# -------------------------------------------------------------------------
|
|
1133
|
+
|
|
1134
|
+
it 'posts to percy/automateScreenshot with correct top-level fields' do
|
|
1135
|
+
stub_automate_healthcheck
|
|
1136
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/automateScreenshot")
|
|
1137
|
+
.to_return(status: 200, body: '{"success":true,"data":"snap-result"}')
|
|
1138
|
+
|
|
1139
|
+
Percy.percy_screenshot(driver, 'PayloadShot')
|
|
1140
|
+
|
|
1141
|
+
expect(WebMock).to have_requested(:post, automate_url)
|
|
1142
|
+
.with { |req|
|
|
1143
|
+
body = JSON.parse(req.body)
|
|
1144
|
+
body['snapshotName'] == 'PayloadShot' &&
|
|
1145
|
+
body['sessionId'] == 'sess-abc-123' &&
|
|
1146
|
+
body['commandExecutorUrl'] == 'http://hub.browserstack.com/wd/hub' &&
|
|
1147
|
+
body['capabilities'] == {'browserName' => 'chrome', 'version' => '114'} &&
|
|
1148
|
+
body['client_info'] == "percy-selenium-ruby/#{Percy::VERSION}" &&
|
|
1149
|
+
body['environment_info'] ==
|
|
1150
|
+
"selenium/#{Selenium::WebDriver::VERSION} ruby/#{RUBY_VERSION}"
|
|
1151
|
+
}.once
|
|
1152
|
+
end
|
|
1153
|
+
|
|
1154
|
+
# -------------------------------------------------------------------------
|
|
1155
|
+
# Response handling - success
|
|
1156
|
+
# -------------------------------------------------------------------------
|
|
1157
|
+
|
|
1158
|
+
it 'returns body["data"] when the response indicates success' do
|
|
1159
|
+
stub_automate_healthcheck
|
|
1160
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/automateScreenshot")
|
|
1161
|
+
.to_return(status: 200, body: '{"success":true,"data":"my-screenshot-data"}')
|
|
1162
|
+
|
|
1163
|
+
result = Percy.percy_screenshot(driver, 'SuccessShot')
|
|
1164
|
+
expect(result).to eq('my-screenshot-data')
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
it 'returns nil when data key is absent in a successful response' do
|
|
1168
|
+
stub_automate_healthcheck
|
|
1169
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/automateScreenshot")
|
|
1170
|
+
.to_return(status: 200, body: '{"success":true}')
|
|
1171
|
+
|
|
1172
|
+
result = Percy.percy_screenshot(driver, 'NoDataShot')
|
|
1173
|
+
expect(result).to be_nil
|
|
1174
|
+
end
|
|
1175
|
+
|
|
1176
|
+
# -------------------------------------------------------------------------
|
|
1177
|
+
# Response handling - errors
|
|
1178
|
+
# -------------------------------------------------------------------------
|
|
1179
|
+
|
|
1180
|
+
it 'logs and returns nil when response success is false' do
|
|
1181
|
+
stub_automate_healthcheck
|
|
1182
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/automateScreenshot")
|
|
1183
|
+
.to_return(status: 200, body: '{"success":false,"error":"upstream failure"}')
|
|
1184
|
+
|
|
1185
|
+
result = nil
|
|
1186
|
+
expect { result = Percy.percy_screenshot(driver, 'FailShot') }
|
|
1187
|
+
.to output("#{Percy::LABEL} Could not take Screenshot 'FailShot'\n").to_stdout
|
|
1188
|
+
expect(result).to be_nil
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
it 'logs and returns nil when the HTTP request returns a non-success status' do
|
|
1192
|
+
stub_automate_healthcheck
|
|
1193
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/automateScreenshot")
|
|
1194
|
+
.to_return(status: 500, body: 'Internal Server Error')
|
|
1195
|
+
|
|
1196
|
+
result = nil
|
|
1197
|
+
expect { result = Percy.percy_screenshot(driver, 'HttpErrShot') }
|
|
1198
|
+
.to output("#{Percy::LABEL} Could not take Screenshot 'HttpErrShot'\n").to_stdout
|
|
1199
|
+
expect(result).to be_nil
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
it 'logs and returns nil when the automateScreenshot endpoint raises' do
|
|
1203
|
+
stub_automate_healthcheck
|
|
1204
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/automateScreenshot")
|
|
1205
|
+
.to_raise(StandardError, 'network timeout')
|
|
1206
|
+
|
|
1207
|
+
result = nil
|
|
1208
|
+
expect { result = Percy.percy_screenshot(driver, 'RaiseShot') }
|
|
1209
|
+
.to output("#{Percy::LABEL} Could not take Screenshot 'RaiseShot'\n").to_stdout
|
|
1210
|
+
expect(result).to be_nil
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
# -------------------------------------------------------------------------
|
|
1214
|
+
# Option key translation
|
|
1215
|
+
# -------------------------------------------------------------------------
|
|
1216
|
+
|
|
1217
|
+
it 'translates camelCase ignoreRegionSeleniumElements to snake_case and extracts ids' do
|
|
1218
|
+
stub_automate_healthcheck
|
|
1219
|
+
elem1 = double('element1', id: 'elem-id-1')
|
|
1220
|
+
elem2 = double('element2', id: 'elem-id-2')
|
|
1221
|
+
|
|
1222
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/automateScreenshot")
|
|
1223
|
+
.to_return(status: 200, body: '{"success":true}')
|
|
1224
|
+
|
|
1225
|
+
Percy.percy_screenshot(driver, 'IgnoreShot', ignoreRegionSeleniumElements: [elem1, elem2])
|
|
1226
|
+
|
|
1227
|
+
expect(WebMock).to have_requested(:post, automate_url)
|
|
1228
|
+
.with { |req|
|
|
1229
|
+
opts = JSON.parse(req.body)['options']
|
|
1230
|
+
opts['ignore_region_elements'] == %w[elem-id-1 elem-id-2] &&
|
|
1231
|
+
!opts.key?('ignoreRegionSeleniumElements') &&
|
|
1232
|
+
!opts.key?('ignore_region_selenium_elements')
|
|
1233
|
+
}.once
|
|
1234
|
+
end
|
|
1235
|
+
|
|
1236
|
+
it 'translates camelCase considerRegionSeleniumElements to snake_case and extracts ids' do
|
|
1237
|
+
stub_automate_healthcheck
|
|
1238
|
+
elem = double('element', id: 'consider-id-1')
|
|
1239
|
+
|
|
1240
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/automateScreenshot")
|
|
1241
|
+
.to_return(status: 200, body: '{"success":true}')
|
|
1242
|
+
|
|
1243
|
+
Percy.percy_screenshot(driver, 'ConsiderShot', considerRegionSeleniumElements: [elem])
|
|
1244
|
+
|
|
1245
|
+
expect(WebMock).to have_requested(:post, automate_url)
|
|
1246
|
+
.with { |req|
|
|
1247
|
+
opts = JSON.parse(req.body)['options']
|
|
1248
|
+
opts['consider_region_elements'] == ['consider-id-1'] &&
|
|
1249
|
+
!opts.key?('considerRegionSeleniumElements') &&
|
|
1250
|
+
!opts.key?('consider_region_selenium_elements')
|
|
1251
|
+
}.once
|
|
1252
|
+
end
|
|
1253
|
+
|
|
1254
|
+
it 'also accepts already-snake_case ignore_region_selenium_elements' do
|
|
1255
|
+
stub_automate_healthcheck
|
|
1256
|
+
elem = double('element', id: 'snake-id-1')
|
|
1257
|
+
|
|
1258
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/automateScreenshot")
|
|
1259
|
+
.to_return(status: 200, body: '{"success":true}')
|
|
1260
|
+
|
|
1261
|
+
Percy.percy_screenshot(driver, 'SnakeShot', ignore_region_selenium_elements: [elem])
|
|
1262
|
+
|
|
1263
|
+
expect(WebMock).to have_requested(:post, automate_url)
|
|
1264
|
+
.with { |req|
|
|
1265
|
+
opts = JSON.parse(req.body)['options']
|
|
1266
|
+
opts['ignore_region_elements'] == ['snake-id-1']
|
|
1267
|
+
}.once
|
|
1268
|
+
end
|
|
1269
|
+
|
|
1270
|
+
# -------------------------------------------------------------------------
|
|
1271
|
+
# Element ID extraction
|
|
1272
|
+
# -------------------------------------------------------------------------
|
|
1273
|
+
|
|
1274
|
+
it 'uses empty arrays for element ids when no element options are supplied' do
|
|
1275
|
+
stub_automate_healthcheck
|
|
1276
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/automateScreenshot")
|
|
1277
|
+
.to_return(status: 200, body: '{"success":true}')
|
|
1278
|
+
|
|
1279
|
+
Percy.percy_screenshot(driver, 'NoElemsShot')
|
|
1280
|
+
|
|
1281
|
+
expect(WebMock).to have_requested(:post, automate_url)
|
|
1282
|
+
.with { |req|
|
|
1283
|
+
opts = JSON.parse(req.body)['options']
|
|
1284
|
+
opts['ignore_region_elements'] == [] &&
|
|
1285
|
+
opts['consider_region_elements'] == []
|
|
1286
|
+
}.once
|
|
1287
|
+
end
|
|
1288
|
+
|
|
1289
|
+
it 'extracts the id from each selenium element object' do
|
|
1290
|
+
stub_automate_healthcheck
|
|
1291
|
+
elements = [
|
|
1292
|
+
double('el_a', id: 'id-a'),
|
|
1293
|
+
double('el_b', id: 'id-b'),
|
|
1294
|
+
double('el_c', id: 'id-c'),
|
|
1295
|
+
]
|
|
1296
|
+
|
|
1297
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/automateScreenshot")
|
|
1298
|
+
.to_return(status: 200, body: '{"success":true}')
|
|
1299
|
+
|
|
1300
|
+
Percy.percy_screenshot(driver, 'MultiElemShot',
|
|
1301
|
+
ignore_region_selenium_elements: elements,)
|
|
1302
|
+
|
|
1303
|
+
expect(WebMock).to have_requested(:post, automate_url)
|
|
1304
|
+
.with { |req|
|
|
1305
|
+
opts = JSON.parse(req.body)['options']
|
|
1306
|
+
opts['ignore_region_elements'] == %w[id-a id-b id-c]
|
|
1307
|
+
}.once
|
|
1308
|
+
end
|
|
1309
|
+
|
|
1310
|
+
# -------------------------------------------------------------------------
|
|
1311
|
+
# Passthrough of additional options
|
|
1312
|
+
# -------------------------------------------------------------------------
|
|
1313
|
+
|
|
1314
|
+
it 'passes unknown options through to the request payload unchanged' do
|
|
1315
|
+
stub_automate_healthcheck
|
|
1316
|
+
stub_request(:post, "#{Percy::PERCY_SERVER_ADDRESS}/percy/automateScreenshot")
|
|
1317
|
+
.to_return(status: 200, body: '{"success":true}')
|
|
1318
|
+
|
|
1319
|
+
Percy.percy_screenshot(driver, 'ExtraOptsShot', sync: true, fullPage: true)
|
|
1320
|
+
|
|
1321
|
+
expect(WebMock).to have_requested(:post, automate_url)
|
|
1322
|
+
.with { |req|
|
|
1323
|
+
opts = JSON.parse(req.body)['options']
|
|
1324
|
+
opts['sync'] == true && opts['fullPage'] == true
|
|
1325
|
+
}.once
|
|
1326
|
+
end
|
|
1327
|
+
end
|
|
1328
|
+
end
|
|
341
1329
|
# rubocop:enable RSpec/MultipleDescribes
|