apparition 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +251 -0
- data/lib/capybara/apparition.rb +20 -0
- data/lib/capybara/apparition/browser.rb +532 -0
- data/lib/capybara/apparition/chrome_client.rb +235 -0
- data/lib/capybara/apparition/command.rb +21 -0
- data/lib/capybara/apparition/cookie.rb +51 -0
- data/lib/capybara/apparition/dev_tools_protocol/session.rb +29 -0
- data/lib/capybara/apparition/dev_tools_protocol/target.rb +52 -0
- data/lib/capybara/apparition/dev_tools_protocol/target_manager.rb +37 -0
- data/lib/capybara/apparition/driver.rb +505 -0
- data/lib/capybara/apparition/errors.rb +230 -0
- data/lib/capybara/apparition/frame.rb +90 -0
- data/lib/capybara/apparition/frame_manager.rb +81 -0
- data/lib/capybara/apparition/inspector.rb +49 -0
- data/lib/capybara/apparition/keyboard.rb +383 -0
- data/lib/capybara/apparition/launcher.rb +218 -0
- data/lib/capybara/apparition/mouse.rb +47 -0
- data/lib/capybara/apparition/network_traffic.rb +9 -0
- data/lib/capybara/apparition/network_traffic/error.rb +12 -0
- data/lib/capybara/apparition/network_traffic/request.rb +47 -0
- data/lib/capybara/apparition/network_traffic/response.rb +49 -0
- data/lib/capybara/apparition/node.rb +844 -0
- data/lib/capybara/apparition/page.rb +711 -0
- data/lib/capybara/apparition/utility.rb +15 -0
- data/lib/capybara/apparition/version.rb +7 -0
- data/lib/capybara/apparition/web_socket_client.rb +80 -0
- metadata +245 -0
@@ -0,0 +1,711 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'capybara/apparition/frame_manager'
|
4
|
+
require 'capybara/apparition/mouse'
|
5
|
+
require 'capybara/apparition/keyboard'
|
6
|
+
|
7
|
+
module Capybara::Apparition
|
8
|
+
class Page
|
9
|
+
attr_reader :modal_messages
|
10
|
+
attr_reader :mouse, :keyboard
|
11
|
+
attr_reader :viewport_size
|
12
|
+
attr_accessor :perm_headers, :temp_headers, :temp_no_redirect_headers
|
13
|
+
attr_reader :network_traffic
|
14
|
+
|
15
|
+
def self.create(browser, session, id, ignore_https_errors: nil, screenshot_task_queue: nil, js_errors: false)
|
16
|
+
session.command 'Page.enable'
|
17
|
+
session.command 'Page.setLifecycleEventsEnabled', enabled: true
|
18
|
+
session.command 'Page.setDownloadBehavior', behavior: 'allow', downloadPath: Capybara.save_path
|
19
|
+
|
20
|
+
page = Page.new(browser, session, id, ignore_https_errors, screenshot_task_queue, js_errors)
|
21
|
+
|
22
|
+
session.command 'Network.enable'
|
23
|
+
session.command 'Runtime.enable'
|
24
|
+
session.command 'Security.enable'
|
25
|
+
session.command 'Security.setOverrideCertificateErrors', override: true if ignore_https_errors
|
26
|
+
session.command 'DOM.enable'
|
27
|
+
# session.command 'Log.enable'
|
28
|
+
|
29
|
+
page
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(browser, session, id, _ignore_https_errors, _screenshot_task_queue, js_errors)
|
33
|
+
@target_id = id
|
34
|
+
@browser = browser
|
35
|
+
@session = session
|
36
|
+
@keyboard = Keyboard.new(self)
|
37
|
+
@mouse = Mouse.new(self, @keyboard)
|
38
|
+
@modals = []
|
39
|
+
@modal_messages = []
|
40
|
+
@frames = Capybara::Apparition::FrameManager.new(id)
|
41
|
+
@response_headers = {}
|
42
|
+
@status_code = nil
|
43
|
+
@url_blacklist = []
|
44
|
+
@url_whitelist = []
|
45
|
+
@auth_attempts = []
|
46
|
+
@perm_headers = {}
|
47
|
+
@temp_headers = {}
|
48
|
+
@temp_no_redirect_headers = {}
|
49
|
+
@viewport_size = nil
|
50
|
+
@network_traffic = []
|
51
|
+
@open_resource_requests = {}
|
52
|
+
@js_error = nil
|
53
|
+
|
54
|
+
register_event_handlers
|
55
|
+
|
56
|
+
register_js_error_handler if js_errors
|
57
|
+
end
|
58
|
+
|
59
|
+
def usable?
|
60
|
+
!!current_frame&.context_id
|
61
|
+
end
|
62
|
+
|
63
|
+
def reset
|
64
|
+
@modals.clear
|
65
|
+
@modal_messages.clear
|
66
|
+
@response_headers = {}
|
67
|
+
@status_code = nil
|
68
|
+
@auth_attempts = []
|
69
|
+
@perm_headers = {}
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_modal(modal_response)
|
73
|
+
@last_modal_message = nil
|
74
|
+
@modals.push(modal_response)
|
75
|
+
end
|
76
|
+
|
77
|
+
def credentials=(creds)
|
78
|
+
@credentials = creds
|
79
|
+
setup_network_interception
|
80
|
+
end
|
81
|
+
|
82
|
+
def url_blacklist=(blacklist)
|
83
|
+
@url_blacklist = blacklist
|
84
|
+
setup_network_blocking
|
85
|
+
end
|
86
|
+
|
87
|
+
def url_whitelist=(whitelist)
|
88
|
+
@url_whitelist = whitelist
|
89
|
+
setup_network_blocking
|
90
|
+
end
|
91
|
+
|
92
|
+
def clear_network_traffic
|
93
|
+
@network_traffic = []
|
94
|
+
end
|
95
|
+
|
96
|
+
def scroll_to(left, top)
|
97
|
+
wait_for_loaded
|
98
|
+
execute('window.scrollTo(arguments[0], arguments[1])', left, top)
|
99
|
+
end
|
100
|
+
|
101
|
+
def click_at(x, y)
|
102
|
+
wait_for_loaded
|
103
|
+
@mouse.click_at(x: x, y: y)
|
104
|
+
end
|
105
|
+
|
106
|
+
def current_state
|
107
|
+
main_frame.state
|
108
|
+
end
|
109
|
+
|
110
|
+
def current_frame_offset
|
111
|
+
return { x: 0, y: 0 } if current_frame.id == main_frame.id
|
112
|
+
|
113
|
+
result = command('DOM.getBoxModel', objectId: current_frame.element_id)
|
114
|
+
x, y = result['model']['content']
|
115
|
+
{ x: x, y: y }
|
116
|
+
end
|
117
|
+
|
118
|
+
def render(options)
|
119
|
+
wait_for_loaded
|
120
|
+
if options[:format].to_s == 'pdf'
|
121
|
+
params = {}
|
122
|
+
params[:paperWidth] = @browser.paper_size[:width].to_f if @browser.paper_size
|
123
|
+
params[:paperHeight] = @browser.paper_size[:height].to_f if @browser.paper_size
|
124
|
+
command('Page.printToPDF', params)
|
125
|
+
else
|
126
|
+
if options[:selector]
|
127
|
+
pos = evaluate("document.querySelector('#{options.delete(:selector)}').getBoundingClientRect().toJSON();")
|
128
|
+
options[:clip] = %w[x y width height].each_with_object('scale' => 1) { |key, hash| hash[key] = pos[key] }
|
129
|
+
elsif options[:full]
|
130
|
+
options[:clip] = evaluate <<~JS
|
131
|
+
{ width: document.documentElement.clientWidth, height: document.documentElement.clientHeight}
|
132
|
+
JS
|
133
|
+
options[:clip].merge!(x: 0, y: 0, scale: 1)
|
134
|
+
end
|
135
|
+
command('Page.captureScreenshot', options)
|
136
|
+
end['data']
|
137
|
+
end
|
138
|
+
|
139
|
+
def push_frame(frame_el)
|
140
|
+
node = command('DOM.describeNode', objectId: frame_el.base.id)
|
141
|
+
frame_id = node['node']['frameId']
|
142
|
+
start = Time.now
|
143
|
+
while (frame = @frames.get(frame_id)).nil? || frame.loading?
|
144
|
+
# Wait for the frame creation messages to be processed
|
145
|
+
if Time.now - start > 10
|
146
|
+
puts 'Timed out waiting from frame to be ready'
|
147
|
+
# byebug
|
148
|
+
raise TimeoutError.new('push_frame')
|
149
|
+
end
|
150
|
+
sleep 0.1
|
151
|
+
end
|
152
|
+
return unless frame
|
153
|
+
|
154
|
+
frame.element_id = frame_el.base.id
|
155
|
+
@frames.push_frame(frame.id)
|
156
|
+
frame
|
157
|
+
end
|
158
|
+
|
159
|
+
def pop_frame(top: false)
|
160
|
+
@frames.pop_frame(top: top)
|
161
|
+
end
|
162
|
+
|
163
|
+
def find(method, selector)
|
164
|
+
wait_for_loaded
|
165
|
+
js_escaped_selector = selector.gsub('\\', '\\\\\\').gsub('"', '\"')
|
166
|
+
query = method == :css ? CSS_FIND_JS : XPATH_FIND_JS
|
167
|
+
result = _raw_evaluate(query % js_escaped_selector)
|
168
|
+
(result || []).map { |r_o| [self, r_o['objectId']] }
|
169
|
+
rescue ::Capybara::Apparition::BrowserError => e
|
170
|
+
raise unless e.name =~ /is not a valid (XPath expression|selector)/
|
171
|
+
|
172
|
+
raise Capybara::Apparition::InvalidSelector, [method, selector]
|
173
|
+
end
|
174
|
+
|
175
|
+
def execute(script, *args)
|
176
|
+
wait_for_loaded
|
177
|
+
_execute_script("function(){ #{script} }", *args)
|
178
|
+
nil
|
179
|
+
end
|
180
|
+
|
181
|
+
def evaluate(script, *args)
|
182
|
+
wait_for_loaded
|
183
|
+
_execute_script("function(){ return #{script} }", *args)
|
184
|
+
end
|
185
|
+
|
186
|
+
def evaluate_async(script, _wait_time, *args)
|
187
|
+
wait_for_loaded
|
188
|
+
_execute_script("function(){
|
189
|
+
var args = Array.prototype.slice.call(arguments);
|
190
|
+
return new Promise((resolve, reject)=>{
|
191
|
+
args.push(resolve);
|
192
|
+
var fn = function(){ #{script} };
|
193
|
+
fn.apply(this, args);
|
194
|
+
});
|
195
|
+
}", *args)
|
196
|
+
end
|
197
|
+
|
198
|
+
def refresh
|
199
|
+
wait_for_loaded
|
200
|
+
main_frame.reloading!
|
201
|
+
command('Page.reload', ignoreCache: true)
|
202
|
+
wait_for_loaded
|
203
|
+
end
|
204
|
+
|
205
|
+
def go_back
|
206
|
+
wait_for_loaded
|
207
|
+
go_history(-1)
|
208
|
+
end
|
209
|
+
|
210
|
+
def go_forward
|
211
|
+
wait_for_loaded
|
212
|
+
go_history(+1)
|
213
|
+
end
|
214
|
+
|
215
|
+
attr_reader :response_headers
|
216
|
+
|
217
|
+
attr_reader :status_code
|
218
|
+
|
219
|
+
def wait_for_loaded(allow_obsolete: false)
|
220
|
+
start = Time.now
|
221
|
+
cf = current_frame
|
222
|
+
until cf.usable? || (allow_obsolete && cf.obsolete?) || @js_error
|
223
|
+
if Time.now - start > 10
|
224
|
+
puts 'Timedout waiting for page to be loaded'
|
225
|
+
# byebug
|
226
|
+
raise TimeoutError.new('wait_for_loaded')
|
227
|
+
end
|
228
|
+
sleep 0.05
|
229
|
+
end
|
230
|
+
raise JavascriptError.new(js_error) if @js_error
|
231
|
+
end
|
232
|
+
|
233
|
+
def content
|
234
|
+
wait_for_loaded
|
235
|
+
_raw_evaluate("(function(){
|
236
|
+
let val = '';
|
237
|
+
if (document.doctype)
|
238
|
+
val = new XMLSerializer().serializeToString(document.doctype);
|
239
|
+
if (document.documentElement)
|
240
|
+
val += document.documentElement.outerHTML;
|
241
|
+
return val;
|
242
|
+
})()")
|
243
|
+
end
|
244
|
+
|
245
|
+
def visit(url)
|
246
|
+
wait_for_loaded
|
247
|
+
@status_code = nil
|
248
|
+
navigate_opts = { url: url, transitionType: 'reload' }
|
249
|
+
navigate_opts[:referrer] = extra_headers['Referer'] if extra_headers['Referer']
|
250
|
+
response = command('Page.navigate', navigate_opts)
|
251
|
+
main_frame.loading(response['loaderId'])
|
252
|
+
wait_for_loaded
|
253
|
+
rescue TimeoutError
|
254
|
+
raise StatusFailError.new('args' => [url])
|
255
|
+
end
|
256
|
+
|
257
|
+
def current_url
|
258
|
+
wait_for_loaded
|
259
|
+
_raw_evaluate('window.location.href', context_id: main_frame.context_id)
|
260
|
+
end
|
261
|
+
|
262
|
+
def frame_url
|
263
|
+
wait_for_loaded
|
264
|
+
_raw_evaluate('window.location.href')
|
265
|
+
end
|
266
|
+
|
267
|
+
def set_viewport(width:, height:, screen: nil)
|
268
|
+
wait_for_loaded
|
269
|
+
@viewport_size = { width: width, height: height }
|
270
|
+
result = @browser.command('Browser.getWindowForTarget', targetId: @target_id)
|
271
|
+
@browser.command('Browser.setWindowBounds',
|
272
|
+
windowId: result['windowId'],
|
273
|
+
bounds: { width: width, height: height })
|
274
|
+
metrics = {
|
275
|
+
mobile: false,
|
276
|
+
width: width,
|
277
|
+
height: height,
|
278
|
+
deviceScaleFactor: 1
|
279
|
+
}
|
280
|
+
metrics[:screenWidth], metrics[:screenHeight] = *screen if screen
|
281
|
+
|
282
|
+
command('Emulation.setDeviceMetricsOverride', metrics)
|
283
|
+
end
|
284
|
+
|
285
|
+
def fullscreen
|
286
|
+
result = @browser.command('Browser.getWindowForTarget', targetId: @target_id)
|
287
|
+
@browser.command('Browser.setWindowBounds', windowId: result['windowId'], bounds: { windowState: 'fullscreen' })
|
288
|
+
end
|
289
|
+
|
290
|
+
def maximize
|
291
|
+
screen_width, screen_height = *evaluate('[window.screen.width, window.screen.height]')
|
292
|
+
set_viewport(width: screen_width, height: screen_height)
|
293
|
+
|
294
|
+
result = @browser.command('Browser.getWindowForTarget', targetId: @target_id)
|
295
|
+
@browser.command('Browser.setWindowBounds', windowId: result['windowId'], bounds: { windowState: 'maximized' })
|
296
|
+
end
|
297
|
+
|
298
|
+
def title
|
299
|
+
wait_for_loaded
|
300
|
+
_raw_evaluate('document.title', context_id: main_frame.context_id)
|
301
|
+
end
|
302
|
+
|
303
|
+
def frame_title
|
304
|
+
wait_for_loaded
|
305
|
+
_raw_evaluate('document.title')
|
306
|
+
end
|
307
|
+
|
308
|
+
def command(name, **params)
|
309
|
+
@browser.command_for_session(@session.session_id, name, params)
|
310
|
+
end
|
311
|
+
|
312
|
+
def async_command(name, **params)
|
313
|
+
@browser.command_for_session(@session.session_id, name, params, async: true)
|
314
|
+
end
|
315
|
+
|
316
|
+
def extra_headers
|
317
|
+
temp_headers.merge(perm_headers).merge(temp_no_redirect_headers)
|
318
|
+
end
|
319
|
+
|
320
|
+
def update_headers(async: false)
|
321
|
+
method = async ? :async_command : :command
|
322
|
+
if extra_headers['User-Agent']
|
323
|
+
send(method, 'Network.setUserAgentOverride', userAgent: extra_headers['User-Agent'])
|
324
|
+
end
|
325
|
+
send(method, 'Network.setExtraHTTPHeaders', headers: extra_headers)
|
326
|
+
setup_network_interception
|
327
|
+
end
|
328
|
+
|
329
|
+
def inherit(page)
|
330
|
+
if page
|
331
|
+
self.url_whitelist = page.url_whitelist.dup
|
332
|
+
self.url_blacklist = page.url_blacklist.dup
|
333
|
+
set_viewport(page.viewport_size) if page.viewport_size
|
334
|
+
end
|
335
|
+
self
|
336
|
+
end
|
337
|
+
|
338
|
+
def js_error
|
339
|
+
res = @js_error
|
340
|
+
@js_error = nil
|
341
|
+
res
|
342
|
+
end
|
343
|
+
|
344
|
+
protected
|
345
|
+
|
346
|
+
attr_reader :url_blacklist, :url_whitelist
|
347
|
+
|
348
|
+
private
|
349
|
+
|
350
|
+
def register_event_handlers
|
351
|
+
@session.on 'Page.javascriptDialogOpening' do |params|
|
352
|
+
type = params['type'].to_sym
|
353
|
+
accept = if type == :beforeunload
|
354
|
+
true
|
355
|
+
else
|
356
|
+
response = @modals.pop
|
357
|
+
if !response&.key?(type)
|
358
|
+
handle_unexpected_modal(type)
|
359
|
+
else
|
360
|
+
@modal_messages.push(params['message'])
|
361
|
+
response[type]
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
if type == :prompt
|
366
|
+
case accept
|
367
|
+
when false
|
368
|
+
async_command('Page.handleJavaScriptDialog', accept: false)
|
369
|
+
when nil
|
370
|
+
async_command('Page.handleJavaScriptDialog', accept: true, promptText: params['defaultPrompt'])
|
371
|
+
else
|
372
|
+
async_command('Page.handleJavaScriptDialog', accept: true, promptText: accept)
|
373
|
+
end
|
374
|
+
else
|
375
|
+
async_command('Page.handleJavaScriptDialog', accept: accept)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
@session.on 'Page.windowOpen' do |params|
|
380
|
+
puts "**** windowOpen was called with: #{params}" if ENV['DEBUG']
|
381
|
+
end
|
382
|
+
|
383
|
+
@session.on 'Page.frameAttached' do |params|
|
384
|
+
puts "**** frameAttached called with #{params}" if ENV['DEBUG']
|
385
|
+
# @frames.get(params["frameId"]) = Frame.new(params)
|
386
|
+
end
|
387
|
+
|
388
|
+
@session.on 'Page.frameDetached' do |params|
|
389
|
+
@frames.delete(params['frameId'])
|
390
|
+
puts "**** frameDetached called with #{params}" if ENV['DEBUG']
|
391
|
+
end
|
392
|
+
|
393
|
+
@session.on 'Page.frameNavigated' do |params|
|
394
|
+
puts "**** frameNavigated called with #{params}" if ENV['DEBUG']
|
395
|
+
frame_params = params['frame']
|
396
|
+
unless @frames.exists?(frame_params['id'])
|
397
|
+
puts "**** creating frome for #{frame_params['id']}" if ENV['DEBUG']
|
398
|
+
@frames.add(frame_params['id'], frame_params)
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
@session.on 'Page.lifecycleEvent' do |params|
|
403
|
+
puts "Lifecycle: #{params['name']} - frame: #{params['frameId']} - loader: #{params['loaderId']}" if ENV['DEBUG']
|
404
|
+
case params['name']
|
405
|
+
when 'init'
|
406
|
+
@frames.get(params['frameId'])&.loading(params['loaderId'])
|
407
|
+
when 'firstMeaningfulPaintCandidate',
|
408
|
+
'networkIdle'
|
409
|
+
frame = @frames.get(params['frameId'])
|
410
|
+
frame.loaded! if frame.loader_id == params['loaderId']
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
@session.on 'Page.navigatedWithinDocument' do |params|
|
415
|
+
puts "**** navigatedWithinDocument called with #{params}" if ENV['DEBUG']
|
416
|
+
frame_id = params['frameId']
|
417
|
+
# @frames.get(frame_id).state = :loaded if frame_id == main_frame.id
|
418
|
+
@frames.get(frame_id).loaded! if frame_id == main_frame.id
|
419
|
+
end
|
420
|
+
|
421
|
+
@session.on 'Runtime.executionContextCreated' do |params|
|
422
|
+
puts "**** executionContextCreated: #{params}" if ENV['DEBUG']
|
423
|
+
context = params['context']
|
424
|
+
frame_id = context.dig('auxData', 'frameId')
|
425
|
+
if context.dig('auxData', 'isDefault') && frame_id
|
426
|
+
if (frame = @frames.get(frame_id))
|
427
|
+
frame.context_id = context['id']
|
428
|
+
elsif ENV['DEBUG']
|
429
|
+
puts "unknown frame for context #{frame_id}"
|
430
|
+
end
|
431
|
+
end
|
432
|
+
# command 'Network.setRequestInterception', patterns: [{urlPattern: '*'}]
|
433
|
+
end
|
434
|
+
|
435
|
+
@session.on 'Runtime.executionContextDestroyed' do |params|
|
436
|
+
puts "executionContextDestroyed: #{params}" if ENV['DEBUG']
|
437
|
+
@frames.destroy_context(params['executionContextId'])
|
438
|
+
end
|
439
|
+
|
440
|
+
@session.on 'Network.requestWillBeSent' do |params|
|
441
|
+
@open_resource_requests[params['requestId']] = params.dig('request', 'url')
|
442
|
+
end
|
443
|
+
|
444
|
+
@session.on 'Network.responseReceived' do |params|
|
445
|
+
@open_resource_requests.delete(params['requestId'])
|
446
|
+
temp_headers.clear
|
447
|
+
update_headers(async: true)
|
448
|
+
end
|
449
|
+
|
450
|
+
@session.on 'Network.requestWillBeSent' do |params|
|
451
|
+
@network_traffic.push(NetworkTraffic::Request.new(params))
|
452
|
+
end
|
453
|
+
|
454
|
+
@session.on 'Network.responseReceived' do |params|
|
455
|
+
req = @network_traffic.find { |request| request.request_id == params['requestId'] }
|
456
|
+
req.response = NetworkTraffic::Response.new(params['response']) if req
|
457
|
+
end
|
458
|
+
|
459
|
+
@session.on 'Network.responseReceived' do |params|
|
460
|
+
if params['type'] == 'Document'
|
461
|
+
@response_headers = params['response']['headers']
|
462
|
+
@status_code = params['response']['status']
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
@session.on 'Network.loadingFailed' do |params|
|
467
|
+
req = @network_traffic.find { |request| request.request_id == params['requestId'] }
|
468
|
+
req&.blocked_params = params if params['blockedReason']
|
469
|
+
puts "Loading Failed - request: #{params['requestId']}: #{params['errorText']}" if params['type'] == 'Document'
|
470
|
+
end
|
471
|
+
|
472
|
+
@session.on 'Network.requestIntercepted' do |params|
|
473
|
+
request, interception_id = *params.values_at('request', 'interceptionId')
|
474
|
+
if params['authChallenge']
|
475
|
+
handle_auth(interception_id)
|
476
|
+
else
|
477
|
+
process_intercepted_request(interception_id, request, params['isNavigationRequest'])
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
@session.on 'Runtime.consoleAPICalled' do |params|
|
482
|
+
@browser.logger&.puts(
|
483
|
+
"#{params['type']}: #{params['args'].map { |arg| arg['description'] || arg['value'] }.join(' ')}"
|
484
|
+
)
|
485
|
+
end
|
486
|
+
|
487
|
+
# @session.on 'Log.entryAdded' do |params|
|
488
|
+
# log_entry = params['entry']
|
489
|
+
# if params.values_at('source', 'level') == ['javascript', 'error']
|
490
|
+
# @js_error ||= params['string']
|
491
|
+
# end
|
492
|
+
# end
|
493
|
+
end
|
494
|
+
|
495
|
+
def register_js_error_handler
|
496
|
+
@session.on 'Runtime.exceptionThrown' do |params|
|
497
|
+
@js_error ||= params.dig('exceptionDetails', 'exception', 'description')
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
def setup_network_blocking
|
502
|
+
command 'Network.setBlockedURLs', urls: @url_blacklist
|
503
|
+
setup_network_interception
|
504
|
+
end
|
505
|
+
|
506
|
+
def setup_network_interception
|
507
|
+
async_command 'Network.setCacheDisabled', cacheDisabled: true
|
508
|
+
async_command 'Network.setRequestInterception', patterns: [{ urlPattern: '*' }]
|
509
|
+
end
|
510
|
+
|
511
|
+
def process_intercepted_request(interception_id, request, navigation)
|
512
|
+
headers, url = request.values_at('headers', 'url')
|
513
|
+
|
514
|
+
unless @temp_headers.empty? || navigation
|
515
|
+
headers.delete_if { |name, value| @temp_headers[name] == value }
|
516
|
+
end
|
517
|
+
unless @temp_no_redirect_headers.empty? || !navigation
|
518
|
+
headers.delete_if { |name, value| @temp_no_redirect_headers[name] == value }
|
519
|
+
end
|
520
|
+
|
521
|
+
if @url_blacklist.any? { |r| url.match Regexp.escape(r).gsub('\*', '.*?') }
|
522
|
+
block_request(interception_id, 'Failed')
|
523
|
+
elsif @url_whitelist.any?
|
524
|
+
if @url_whitelist.any? { |r| url.match Regexp.escape(r).gsub('\*', '.*?') }
|
525
|
+
continue_request(interception_id, headers: headers)
|
526
|
+
else
|
527
|
+
block_request(interception_id, 'Failed')
|
528
|
+
end
|
529
|
+
else
|
530
|
+
continue_request(interception_id, headers: headers)
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
def continue_request(id, **params)
|
535
|
+
async_command 'Network.continueInterceptedRequest', interceptionId: id, **params
|
536
|
+
end
|
537
|
+
|
538
|
+
def block_request(id, reason)
|
539
|
+
async_command 'Network.continueInterceptedRequest', errorReason: reason, interceptionId: id
|
540
|
+
end
|
541
|
+
|
542
|
+
def current_frame
|
543
|
+
@frames.current
|
544
|
+
end
|
545
|
+
|
546
|
+
def main_frame
|
547
|
+
@frames.main
|
548
|
+
end
|
549
|
+
|
550
|
+
def go_history(delta)
|
551
|
+
history = command('Page.getNavigationHistory')
|
552
|
+
entry = history['entries'][history['currentIndex'] + delta]
|
553
|
+
return nil unless entry
|
554
|
+
|
555
|
+
command('Page.navigateToHistoryEntry', entryId: entry['id'])
|
556
|
+
end
|
557
|
+
|
558
|
+
def _execute_script(script, *args)
|
559
|
+
args = args.map do |arg|
|
560
|
+
if arg.is_a? Capybara::Apparition::Node
|
561
|
+
{ objectId: arg.id }
|
562
|
+
else
|
563
|
+
{ value: arg }
|
564
|
+
end
|
565
|
+
end
|
566
|
+
context_id = current_frame&.context_id
|
567
|
+
response = command('Runtime.callFunctionOn',
|
568
|
+
functionDeclaration: script,
|
569
|
+
executionContextId: context_id,
|
570
|
+
arguments: args,
|
571
|
+
returnByValue: false,
|
572
|
+
awaitPromise: true)
|
573
|
+
process_response(response)
|
574
|
+
end
|
575
|
+
|
576
|
+
def _raw_evaluate(page_function, context_id: nil)
|
577
|
+
wait_for_loaded
|
578
|
+
return unless page_function.is_a? String
|
579
|
+
|
580
|
+
context_id ||= current_frame.context_id
|
581
|
+
|
582
|
+
response = command('Runtime.evaluate',
|
583
|
+
expression: page_function,
|
584
|
+
contextId: context_id,
|
585
|
+
returnByValue: false,
|
586
|
+
awaitPromise: true)
|
587
|
+
process_response(response)
|
588
|
+
end
|
589
|
+
|
590
|
+
def process_response(response)
|
591
|
+
return nil if response.nil?
|
592
|
+
|
593
|
+
exception_details = response['exceptionDetails']
|
594
|
+
if (exception = exception_details&.dig('exception'))
|
595
|
+
case exception['className']
|
596
|
+
when 'DOMException'
|
597
|
+
raise ::Capybara::Apparition::BrowserError.new('name' => exception['description'], 'args' => nil)
|
598
|
+
when 'ObsoleteException'
|
599
|
+
raise ::Capybara::Apparition::ObsoleteNode.new(self, '') if exception['value'] == 'ObsoleteNode'
|
600
|
+
else
|
601
|
+
raise Capybara::Apparition::JavascriptError, [exception['description']]
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
result = response['result']
|
606
|
+
decode_result(result)
|
607
|
+
end
|
608
|
+
|
609
|
+
def handle_unexpected_modal(type)
|
610
|
+
case type
|
611
|
+
when :prompt
|
612
|
+
warn 'Unexpected prompt modal - accepting with the default value.' \
|
613
|
+
'This is deprecated behavior, start using `accept_prompt`.'
|
614
|
+
nil
|
615
|
+
when :confirm
|
616
|
+
warn 'Unexpected confirm modal - accepting.' \
|
617
|
+
'This is deprecated behavior, start using `accept_confirm`.'
|
618
|
+
true
|
619
|
+
else
|
620
|
+
raise "Unexpected #{type} modal"
|
621
|
+
end
|
622
|
+
end
|
623
|
+
|
624
|
+
def handle_auth(interception_id)
|
625
|
+
credentials_response = if @auth_attempts.include?(interception_id)
|
626
|
+
{ response: 'CancelAuth' }
|
627
|
+
else
|
628
|
+
@auth_attempts.push(interception_id)
|
629
|
+
{ response: 'ProvideCredentials' }.merge(@credentials || {})
|
630
|
+
end
|
631
|
+
continue_request(interception_id, authChallengeResponse: credentials_response)
|
632
|
+
end
|
633
|
+
|
634
|
+
def decode_result(result, object_cache = {})
|
635
|
+
if result['type'] == 'object'
|
636
|
+
if result['subtype'] == 'array'
|
637
|
+
# remoteObject = @browser.command('Runtime.getProperties',
|
638
|
+
remote_object = command('Runtime.getProperties',
|
639
|
+
objectId: result['objectId'],
|
640
|
+
ownProperties: true)
|
641
|
+
|
642
|
+
properties = remote_object['result']
|
643
|
+
results = []
|
644
|
+
|
645
|
+
properties.each do |property|
|
646
|
+
next unless property['enumerable']
|
647
|
+
|
648
|
+
val = property['value']
|
649
|
+
results.push(decode_result(val, object_cache))
|
650
|
+
# TODO: Do we need to cleanup these resources?
|
651
|
+
# await Promise.all(releasePromises);
|
652
|
+
# id = (@page._elements.push(element)-1 for element from result)[0]
|
653
|
+
#
|
654
|
+
# new Apparition.Node @page, id
|
655
|
+
|
656
|
+
# releasePromises = [helper.releaseObject(@element._client, remoteObject)]
|
657
|
+
end
|
658
|
+
|
659
|
+
return results
|
660
|
+
elsif result['subtype'] == 'node'
|
661
|
+
return result
|
662
|
+
elsif result['className'] == 'Object'
|
663
|
+
remote_object = command('Runtime.getProperties',
|
664
|
+
objectId: result['objectId'],
|
665
|
+
ownProperties: true)
|
666
|
+
stable_id = remote_object['internalProperties']
|
667
|
+
.find { |prop| prop['name'] == '[[StableObjectId]]' }
|
668
|
+
.dig('value', 'value')
|
669
|
+
# We could actually return cyclic objects here but Capybara would need to be updated to support
|
670
|
+
return '(cyclic structure)' if object_cache.key?(stable_id)
|
671
|
+
|
672
|
+
# return object_cache[stable_id] if object_cache.key?(stable_id)
|
673
|
+
|
674
|
+
object_cache[stable_id] = {}
|
675
|
+
properties = remote_object['result']
|
676
|
+
|
677
|
+
return properties.each_with_object(object_cache[stable_id]) do |property, memo|
|
678
|
+
if property['enumerable']
|
679
|
+
memo[property['name']] = decode_result(property['value'], object_cache)
|
680
|
+
else
|
681
|
+
# TODO: Do we need to cleanup these resources?
|
682
|
+
# releasePromises.push(helper.releaseObject(@element._client, property.value))
|
683
|
+
end
|
684
|
+
# TODO: Do we need to cleanup these resources?
|
685
|
+
# releasePromises = [helper.releaseObject(@element._client, remote_object)]
|
686
|
+
end
|
687
|
+
elsif result['className'] == 'Window'
|
688
|
+
return { object_id: result['objectId'] }
|
689
|
+
end
|
690
|
+
nil
|
691
|
+
else
|
692
|
+
result['value']
|
693
|
+
end
|
694
|
+
end
|
695
|
+
|
696
|
+
CSS_FIND_JS = <<~JS
|
697
|
+
Array.from(document.querySelectorAll("%s"));
|
698
|
+
JS
|
699
|
+
|
700
|
+
XPATH_FIND_JS = <<~JS
|
701
|
+
(function(){
|
702
|
+
const xpath = document.evaluate("%s", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
703
|
+
let results = [];
|
704
|
+
for (let i=0; i < xpath.snapshotLength; i++){
|
705
|
+
results.push(xpath.snapshotItem(i))
|
706
|
+
};
|
707
|
+
return results;
|
708
|
+
})()
|
709
|
+
JS
|
710
|
+
end
|
711
|
+
end
|