apparition 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|