apparition 0.3.0 → 0.4.0
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/README.md +2 -2
- data/lib/capybara/apparition/browser.rb +3 -3
- data/lib/capybara/apparition/configuration.rb +1 -1
- data/lib/capybara/apparition/dev_tools_protocol/remote_object.rb +6 -2
- data/lib/capybara/apparition/driver.rb +1 -1
- data/lib/capybara/apparition/driver/chrome_client.rb +10 -1
- data/lib/capybara/apparition/driver/launcher.rb +19 -35
- data/lib/capybara/apparition/network_traffic/request.rb +5 -5
- data/lib/capybara/apparition/node.rb +33 -11
- data/lib/capybara/apparition/node/drag.rb +88 -65
- data/lib/capybara/apparition/page.rb +123 -65
- data/lib/capybara/apparition/page/keyboard.rb +7 -5
- data/lib/capybara/apparition/page/mouse.rb +14 -1
- data/lib/capybara/apparition/version.rb +1 -1
- metadata +2 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 350a10ffb794579fb942f37a1739bf104b0230bd831945cbc22a20e2273927fd
|
4
|
+
data.tar.gz: e0bbdc5f79d03fcdedabd5db29ff990ee57801087d9c33ef98e85f9de833be8d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 313247fbbaccd8197d36ccd68adede1a33c944f46a5b30dce143606d4cc00bc61a3aab99ba35820dc6e2e3cbb7c09432084f221a36fee2f355fbd7d59639bb6f
|
7
|
+
data.tar.gz: 73f91c766ab2bc02f2a58b07c95cce2a01bc71dff6ca8f6eb3374b36b5a8efbc73c95d26f48766e00d3fbd7f49003765b293af8725028afa64d798f2c5ebddb2
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
[](http://travis-ci.org/twalpole/apparition)
|
4
4
|
|
5
|
-
Apparition is a driver for [Capybara](https://github.com/
|
5
|
+
Apparition is a driver for [Capybara](https://github.com/teamcapybara/capybara). It allows you to
|
6
6
|
run your Capybara tests in the Chrome browser via CDP (no selenium or chromedriver needed) in a headless or
|
7
7
|
headed configuration. It started as a fork of Poltergeist and attempts to maintain as much compatibility
|
8
8
|
with the Poltergeist API as possible. Implementing the `capybara-webkit` specific driver methods has also begun.
|
@@ -205,7 +205,7 @@ test to allow sufficient time for the page to settle.
|
|
205
205
|
|
206
206
|
If you have these types of problems, read through the [Capybara
|
207
207
|
documentation on asynchronous
|
208
|
-
JavaScript](https://github.com/
|
208
|
+
JavaScript](https://github.com/teamcapybara/capybara#asynchronous-javascript-ajax-and-friends)
|
209
209
|
which explains the tools that Capybara provides for dealing with this.
|
210
210
|
|
211
211
|
### Filing a bug ###
|
@@ -203,9 +203,9 @@ module Capybara::Apparition
|
|
203
203
|
# @current_page_handle ||= target_info['targetId'] if target_info['type'] == 'page'
|
204
204
|
# end
|
205
205
|
|
206
|
-
@client.on 'Target.targetDestroyed' do |info|
|
207
|
-
puts "**** Target Destroyed Info: #{info}" if ENV['DEBUG']
|
208
|
-
@pages.delete(
|
206
|
+
@client.on 'Target.targetDestroyed' do |target_id:, **info|
|
207
|
+
puts "**** Target Destroyed Info: #{target_id} - #{info}" if ENV['DEBUG']
|
208
|
+
@pages.delete(target_id)
|
209
209
|
end
|
210
210
|
|
211
211
|
# @client.on 'Target.targetInfoChanged' do |info|
|
@@ -25,10 +25,12 @@ module Capybara::Apparition
|
|
25
25
|
elsif date?
|
26
26
|
res = get_date_string(id)
|
27
27
|
DateTime.parse(res)
|
28
|
-
elsif object_class? || css_style?
|
29
|
-
extract_properties_object(get_remote_object(id), object_cache)
|
30
28
|
elsif window_class?
|
31
29
|
{ object_id: id }
|
30
|
+
elsif validity_state?
|
31
|
+
extract_properties_object(get_remote_object(id, false), object_cache)
|
32
|
+
elsif object_class? || css_style? || classname?
|
33
|
+
extract_properties_object(get_remote_object(id), object_cache)
|
32
34
|
else
|
33
35
|
params['value']
|
34
36
|
end
|
@@ -46,6 +48,8 @@ module Capybara::Apparition
|
|
46
48
|
def object_class?; classname == 'Object' end
|
47
49
|
def css_style?; classname == 'CSSStyleDeclaration' end
|
48
50
|
def window_class?; classname == 'Window' end
|
51
|
+
def validity_state?; classname == 'ValidityState' end
|
52
|
+
def classname?; !classname.nil? end
|
49
53
|
|
50
54
|
def type; params['type'] end
|
51
55
|
def subtype; params['subtype'] end
|
@@ -206,7 +206,9 @@ module Capybara::Apparition
|
|
206
206
|
event_name = event['method']
|
207
207
|
handlers[event_name].each do |handler|
|
208
208
|
puts "Calling handler for #{event_name}" if ENV['DEBUG'] == 'V'
|
209
|
-
|
209
|
+
# TODO: Update this to use transform_keys when we dump Ruby 2.4
|
210
|
+
# handler.call(event['params'].transform_keys(&method(:snake_sym)))
|
211
|
+
handler.call(event['params'].each_with_object({}) { |(k, v), hash| hash[snake_sym(k)] = v })
|
210
212
|
end
|
211
213
|
end
|
212
214
|
|
@@ -229,5 +231,12 @@ module Capybara::Apparition
|
|
229
231
|
end
|
230
232
|
# @listener.abort_on_exception = true
|
231
233
|
end
|
234
|
+
|
235
|
+
def snake_sym(str)
|
236
|
+
str.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
237
|
+
.tr('-', '_')
|
238
|
+
.downcase
|
239
|
+
.to_sym
|
240
|
+
end
|
232
241
|
end
|
233
242
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'open3'
|
4
|
+
|
3
5
|
module Capybara::Apparition
|
4
6
|
class Browser
|
5
7
|
class Launcher
|
@@ -44,23 +46,23 @@ module Capybara::Apparition
|
|
44
46
|
|
45
47
|
def start
|
46
48
|
@output = Queue.new
|
47
|
-
|
49
|
+
|
50
|
+
process_options = {}
|
51
|
+
process_options[:pgroup] = true unless Capybara::Apparition.windows?
|
52
|
+
cmd = [path] + @options.map { |k, v| v.nil? ? "--#{k}" : "--#{k}=#{v}" }
|
53
|
+
|
54
|
+
stdin, @stdout_stderr, wait_thr = Open3.popen2e(*cmd, process_options)
|
55
|
+
stdin.close
|
56
|
+
|
57
|
+
@pid = wait_thr.pid
|
48
58
|
|
49
59
|
@out_thread = Thread.new do
|
50
|
-
while !@
|
60
|
+
while !@stdout_stderr.eof? && (data = @stdout_stderr.readpartial(512))
|
51
61
|
@output << data
|
52
62
|
end
|
53
63
|
end
|
54
64
|
|
55
|
-
|
56
|
-
process_options[:pgroup] = true unless Capybara::Apparition.windows?
|
57
|
-
process_options[:out] = process_options[:err] = @write_io if Capybara::Apparition.mri?
|
58
|
-
|
59
|
-
redirect_stdout do
|
60
|
-
cmd = [path] + @options.map { |k, v| v.nil? ? "--#{k}" : "--#{k}=#{v}" }
|
61
|
-
@pid = ::Process.spawn(*cmd, process_options)
|
62
|
-
ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))
|
63
|
-
end
|
65
|
+
ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))
|
64
66
|
end
|
65
67
|
|
66
68
|
def stop
|
@@ -87,10 +89,13 @@ module Capybara::Apparition
|
|
87
89
|
@ws_url ||= begin
|
88
90
|
regexp = %r{DevTools listening on (ws://.*)}
|
89
91
|
url = nil
|
92
|
+
|
93
|
+
sleep 3
|
90
94
|
loop do
|
91
95
|
break if (url = @output.pop.scan(regexp)[0])
|
92
96
|
end
|
93
97
|
@out_thread.kill
|
98
|
+
@out_thread.join # wait for thread to end before closing io
|
94
99
|
close_io
|
95
100
|
Addressable::URI.parse(url[0])
|
96
101
|
end
|
@@ -98,36 +103,15 @@ module Capybara::Apparition
|
|
98
103
|
|
99
104
|
private
|
100
105
|
|
101
|
-
def redirect_stdout
|
102
|
-
if Capybara::Apparition.mri?
|
103
|
-
yield
|
104
|
-
else
|
105
|
-
begin
|
106
|
-
prev = STDOUT.dup
|
107
|
-
$stdout = @write_io
|
108
|
-
STDOUT.reopen(@write_io)
|
109
|
-
yield
|
110
|
-
ensure
|
111
|
-
STDOUT.reopen(prev)
|
112
|
-
$stdout = STDOUT
|
113
|
-
prev.close
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
106
|
def kill
|
119
107
|
self.class.process_killer(@pid).call
|
120
108
|
@pid = nil
|
121
109
|
end
|
122
110
|
|
123
111
|
def close_io
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
rescue IOError
|
128
|
-
raise unless RUBY_ENGINE == 'jruby'
|
129
|
-
end
|
130
|
-
end
|
112
|
+
@stdout_stderr.close unless @stdout_stderr.closed?
|
113
|
+
rescue IOError
|
114
|
+
raise unless RUBY_ENGINE == 'jruby'
|
131
115
|
end
|
132
116
|
|
133
117
|
def path
|
@@ -17,23 +17,23 @@ module Capybara::Apparition::NetworkTraffic
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def request_id
|
20
|
-
@data[
|
20
|
+
@data[:request_id]
|
21
21
|
end
|
22
22
|
|
23
23
|
def url
|
24
|
-
@data
|
24
|
+
@data[:request]&.dig('url')
|
25
25
|
end
|
26
26
|
|
27
27
|
def method
|
28
|
-
@data
|
28
|
+
@data[:request]&.dig('method')
|
29
29
|
end
|
30
30
|
|
31
31
|
def headers
|
32
|
-
@data
|
32
|
+
@data[:request]&.dig('headers')
|
33
33
|
end
|
34
34
|
|
35
35
|
def time
|
36
|
-
@data[
|
36
|
+
@data[:timestamp] && Time.parse(@data[:timestamp])
|
37
37
|
end
|
38
38
|
|
39
39
|
def blocked?
|
@@ -130,6 +130,8 @@ module Capybara::Apparition
|
|
130
130
|
set_time(value)
|
131
131
|
when 'datetime-local'
|
132
132
|
set_datetime_local(value)
|
133
|
+
when 'color'
|
134
|
+
set_color(value)
|
133
135
|
else
|
134
136
|
set_text(value.to_s, { delay: 0 }.merge(options))
|
135
137
|
end
|
@@ -199,7 +201,10 @@ module Capybara::Apparition
|
|
199
201
|
|
200
202
|
test = mouse_event_test(pos)
|
201
203
|
raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['click']) if test.nil?
|
202
|
-
|
204
|
+
|
205
|
+
unless options[:x] && options[:y]
|
206
|
+
raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['click', test.selector, pos]) unless test.success
|
207
|
+
end
|
203
208
|
|
204
209
|
@page.mouse.click_at pos.merge(button: button, count: count, modifiers: keys)
|
205
210
|
if ENV['DEBUG']
|
@@ -278,9 +283,13 @@ module Capybara::Apparition
|
|
278
283
|
evaluate_on GET_PATH_JS
|
279
284
|
end
|
280
285
|
|
281
|
-
def element_click_pos(x: nil, y: nil, **)
|
286
|
+
def element_click_pos(x: nil, y: nil, offset: nil, **)
|
282
287
|
if x && y
|
283
|
-
|
288
|
+
if offset == :center
|
289
|
+
visible_center
|
290
|
+
else
|
291
|
+
visible_top_left
|
292
|
+
end.tap do |p|
|
284
293
|
p[:x] += x
|
285
294
|
p[:y] += y
|
286
295
|
end
|
@@ -290,7 +299,7 @@ module Capybara::Apparition
|
|
290
299
|
end
|
291
300
|
|
292
301
|
def visible_top_left
|
293
|
-
rect =
|
302
|
+
rect = in_view_client_rect
|
294
303
|
return nil if rect.nil?
|
295
304
|
|
296
305
|
frame_offset = @page.current_frame_offset
|
@@ -324,7 +333,7 @@ module Capybara::Apparition
|
|
324
333
|
end
|
325
334
|
|
326
335
|
def visible_center(allow_scroll: true)
|
327
|
-
rect =
|
336
|
+
rect = in_view_client_rect(allow_scroll: allow_scroll)
|
328
337
|
return nil if rect.nil?
|
329
338
|
|
330
339
|
frame_offset = @page.current_frame_offset
|
@@ -367,12 +376,16 @@ module Capybara::Apparition
|
|
367
376
|
end
|
368
377
|
|
369
378
|
def top_left
|
370
|
-
result = evaluate_on
|
379
|
+
result = evaluate_on GET_CLIENT_RECT_JS
|
371
380
|
return nil if result.nil?
|
372
381
|
|
373
382
|
{ x: result['x'], y: result['y'] }
|
374
383
|
end
|
375
384
|
|
385
|
+
def rect
|
386
|
+
evaluate_on GET_CLIENT_RECT_JS
|
387
|
+
end
|
388
|
+
|
376
389
|
def scroll_by(x, y)
|
377
390
|
evaluate_on <<~JS, { value: x }, value: y
|
378
391
|
(x, y) => this.scrollBy(x,y)
|
@@ -447,9 +460,9 @@ module Capybara::Apparition
|
|
447
460
|
@page.keyboard.type(keys, delay: delay)
|
448
461
|
end
|
449
462
|
|
450
|
-
def
|
463
|
+
def in_view_client_rect(allow_scroll: true)
|
451
464
|
evaluate_on('() => this.scrollIntoViewIfNeeded()') if allow_scroll
|
452
|
-
result = evaluate_on
|
465
|
+
result = evaluate_on GET_CLIENT_RECT_JS
|
453
466
|
result = result['model'] if result && result['model']
|
454
467
|
result
|
455
468
|
end
|
@@ -517,6 +530,10 @@ module Capybara::Apparition
|
|
517
530
|
update_value_js(value.to_datetime_str)
|
518
531
|
end
|
519
532
|
|
533
|
+
def set_color(value)
|
534
|
+
update_value_js(value.to_s)
|
535
|
+
end
|
536
|
+
|
520
537
|
def update_value_js(value)
|
521
538
|
evaluate_on(<<~JS, value: value)
|
522
539
|
value => {
|
@@ -760,7 +777,11 @@ module Capybara::Apparition
|
|
760
777
|
(parseFloat(style.opacity) == 0)) {
|
761
778
|
return false;
|
762
779
|
}
|
763
|
-
|
780
|
+
var parent = el.parentElement;
|
781
|
+
if (parent && (parent.tagName == 'DETAILS') && !parent.open && (el.tagName != 'SUMMARY')) {
|
782
|
+
return false;
|
783
|
+
}
|
784
|
+
el = parent;
|
764
785
|
}
|
765
786
|
return true;
|
766
787
|
}
|
@@ -777,9 +798,10 @@ module Capybara::Apparition
|
|
777
798
|
}
|
778
799
|
JS
|
779
800
|
|
780
|
-
|
801
|
+
GET_CLIENT_RECT_JS = <<~JS
|
781
802
|
function(){
|
782
|
-
|
803
|
+
var rects = [...this.getClientRects()]
|
804
|
+
var rect = rects.find(r => (r.height && r.width)) || this.getBoundingClientRect();
|
783
805
|
return rect.toJSON();
|
784
806
|
}
|
785
807
|
JS
|
@@ -2,26 +2,27 @@
|
|
2
2
|
|
3
3
|
module Capybara::Apparition
|
4
4
|
module Drag
|
5
|
-
def drag_to(other, delay: 0.1)
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
5
|
+
def drag_to(other, delay: 0.1, html5: nil)
|
6
|
+
driver.execute_script MOUSEDOWN_TRACKER
|
7
|
+
scroll_if_needed
|
8
|
+
m = @page.mouse
|
9
|
+
m.move_to(visible_center)
|
10
|
+
sleep delay
|
11
|
+
m.down
|
12
|
+
html5 = !driver.evaluate_script(LEGACY_DRAG_CHECK, self) if html5.nil?
|
13
|
+
if html5
|
14
|
+
driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, other, delay
|
15
|
+
m.up(other.visible_center)
|
16
|
+
else
|
17
|
+
begin
|
18
|
+
other.scroll_if_needed
|
19
|
+
sleep delay
|
20
|
+
m.move_to(other.visible_center)
|
21
|
+
sleep delay
|
22
|
+
ensure
|
23
|
+
m.up
|
24
|
+
sleep delay
|
25
|
+
end
|
25
26
|
end
|
26
27
|
end
|
27
28
|
|
@@ -34,7 +35,7 @@ module Capybara::Apparition
|
|
34
35
|
|
35
36
|
@page.mouse.move_to(pos).down
|
36
37
|
sleep delay
|
37
|
-
@page.mouse.move_to(other_pos
|
38
|
+
@page.mouse.move_to(other_pos)
|
38
39
|
sleep delay
|
39
40
|
@page.mouse.up
|
40
41
|
end
|
@@ -104,34 +105,29 @@ module Capybara::Apparition
|
|
104
105
|
}
|
105
106
|
JS
|
106
107
|
|
107
|
-
private
|
108
|
-
|
109
|
-
def html5_drag_to(element)
|
110
|
-
driver.execute_script MOUSEDOWN_TRACKER
|
111
|
-
scroll_if_needed
|
112
|
-
@page.mouse.move_to(visible_center).down
|
113
|
-
if driver.evaluate_script('window.capybara_mousedown_prevented')
|
114
|
-
element.scroll_if_needed
|
115
|
-
@page.mouse.move_to(element.visible_center).up
|
116
|
-
else
|
117
|
-
driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, element
|
118
|
-
@page.mouse.up(element.visible_center)
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
def html5_draggable?
|
123
|
-
native.property('draggable')
|
124
|
-
end
|
125
|
-
|
126
108
|
MOUSEDOWN_TRACKER = <<~JS
|
109
|
+
window.capybara_mousedown_prevented = null;
|
127
110
|
document.addEventListener('mousedown', ev => {
|
128
111
|
window.capybara_mousedown_prevented = ev.defaultPrevented;
|
129
112
|
}, { once: true, passive: true })
|
130
113
|
JS
|
131
114
|
|
115
|
+
LEGACY_DRAG_CHECK = <<~JS
|
116
|
+
(function(el){
|
117
|
+
if ([true, null].includes(window.capybara_mousedown_prevented)){
|
118
|
+
return true;
|
119
|
+
}
|
120
|
+
do {
|
121
|
+
if (el.draggable) return false;
|
122
|
+
} while (el = el.parentElement );
|
123
|
+
return true;
|
124
|
+
})(arguments[0])
|
125
|
+
JS
|
126
|
+
|
132
127
|
HTML5_DRAG_DROP_SCRIPT = <<~JS
|
133
128
|
var source = arguments[0];
|
134
129
|
var target = arguments[1];
|
130
|
+
var step_delay = arguments[2] * 1000;
|
135
131
|
|
136
132
|
function rectCenter(rect){
|
137
133
|
return new DOMPoint(
|
@@ -171,9 +167,60 @@ module Capybara::Apparition
|
|
171
167
|
return new DOMPoint(pt.x,pt.y);
|
172
168
|
}
|
173
169
|
|
170
|
+
function dragStart() {
|
171
|
+
return new Promise( resolve => {
|
172
|
+
var dragEvent = new DragEvent('dragstart', opts);
|
173
|
+
source.dispatchEvent(dragEvent);
|
174
|
+
setTimeout(resolve, step_delay)
|
175
|
+
})
|
176
|
+
}
|
177
|
+
|
178
|
+
function dragEnter() {
|
179
|
+
return new Promise( resolve => {
|
180
|
+
target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
|
181
|
+
let targetRect = target.getBoundingClientRect(),
|
182
|
+
sourceCenter = rectCenter(source.getBoundingClientRect());
|
183
|
+
|
184
|
+
// fire 2 dragover events to simulate dragging with a direction
|
185
|
+
let entryPoint = pointOnRect(sourceCenter, targetRect);
|
186
|
+
let dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
|
187
|
+
let dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
188
|
+
target.dispatchEvent(dragOverEvent);
|
189
|
+
setTimeout(resolve, step_delay)
|
190
|
+
})
|
191
|
+
}
|
192
|
+
|
193
|
+
function dragOnto() {
|
194
|
+
return new Promise( resolve => {
|
195
|
+
var targetCenter = rectCenter(target.getBoundingClientRect());
|
196
|
+
dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
|
197
|
+
dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
198
|
+
target.dispatchEvent(dragOverEvent);
|
199
|
+
setTimeout(resolve, step_delay, dragOverEvent.defaultPrevented);
|
200
|
+
})
|
201
|
+
}
|
202
|
+
|
203
|
+
function dragLeave(drop) {
|
204
|
+
return new Promise( resolve => {
|
205
|
+
var dragLeaveEvent = new DragEvent('dragleave', opts);
|
206
|
+
target.dispatchEvent(dragLeaveEvent);
|
207
|
+
if (drop) {
|
208
|
+
var dropEvent = new DragEvent('drop', opts);
|
209
|
+
target.dispatchEvent(dropEvent);
|
210
|
+
}
|
211
|
+
var dragEndEvent = new DragEvent('dragend', opts);
|
212
|
+
source.dispatchEvent(dragEndEvent);
|
213
|
+
setTimeout(resolve, step_delay);
|
214
|
+
})
|
215
|
+
}
|
216
|
+
|
174
217
|
var dt = new DataTransfer();
|
175
218
|
var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
|
176
219
|
|
220
|
+
while (source && !source.draggable) {
|
221
|
+
source = source.parentElement;
|
222
|
+
}
|
223
|
+
|
177
224
|
if (source.tagName == 'A'){
|
178
225
|
dt.setData('text/uri-list', source.href);
|
179
226
|
dt.setData('text', source.href);
|
@@ -183,31 +230,7 @@ module Capybara::Apparition
|
|
183
230
|
dt.setData('text', source.src);
|
184
231
|
}
|
185
232
|
|
186
|
-
|
187
|
-
source.dispatchEvent(dragEvent);
|
188
|
-
target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
|
189
|
-
var targetRect = target.getBoundingClientRect();
|
190
|
-
var sourceCenter = rectCenter(source.getBoundingClientRect());
|
191
|
-
|
192
|
-
// fire 2 dragover events to simulate dragging with a direction
|
193
|
-
var entryPoint = pointOnRect(sourceCenter, targetRect)
|
194
|
-
var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
|
195
|
-
var dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
196
|
-
target.dispatchEvent(dragOverEvent);
|
197
|
-
|
198
|
-
var targetCenter = rectCenter(targetRect);
|
199
|
-
dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
|
200
|
-
dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
201
|
-
target.dispatchEvent(dragOverEvent);
|
202
|
-
|
203
|
-
var dragLeaveEvent = new DragEvent('dragleave', opts);
|
204
|
-
target.dispatchEvent(dragLeaveEvent);
|
205
|
-
if (dragOverEvent.defaultPrevented) {
|
206
|
-
var dropEvent = new DragEvent('drop', opts);
|
207
|
-
target.dispatchEvent(dropEvent);
|
208
|
-
}
|
209
|
-
var dragEndEvent = new DragEvent('dragend', opts);
|
210
|
-
source.dispatchEvent(dragEndEvent);
|
233
|
+
dragStart().then(dragEnter).then(dragOnto).then(dragLeave)
|
211
234
|
JS
|
212
235
|
end
|
213
236
|
end
|
@@ -285,7 +285,6 @@ module Capybara::Apparition
|
|
285
285
|
navigate_opts = { url: url, transitionType: 'reload' }
|
286
286
|
navigate_opts[:referrer] = extra_headers['Referer'] if extra_headers['Referer']
|
287
287
|
response = command('Page.navigate', navigate_opts)
|
288
|
-
|
289
288
|
raise StatusFailError, 'args' => [url, response['errorText']] if response['errorText']
|
290
289
|
|
291
290
|
main_frame.loading(response['loaderId'])
|
@@ -426,9 +425,9 @@ module Capybara::Apparition
|
|
426
425
|
end
|
427
426
|
|
428
427
|
def register_event_handlers
|
429
|
-
@session.on 'Page.javascriptDialogOpening' do |params|
|
430
|
-
type =
|
431
|
-
accept = accept_modal?(type, message:
|
428
|
+
@session.on 'Page.javascriptDialogOpening' do |type:, message:, has_browser_handler:, **params|
|
429
|
+
type = type.to_sym
|
430
|
+
accept = accept_modal?(type, message: message, manual: has_browser_handler)
|
432
431
|
next if accept.nil?
|
433
432
|
|
434
433
|
if type == :prompt
|
@@ -436,7 +435,7 @@ module Capybara::Apparition
|
|
436
435
|
when false
|
437
436
|
async_command('Page.handleJavaScriptDialog', accept: false)
|
438
437
|
when true
|
439
|
-
async_command('Page.handleJavaScriptDialog', accept: true, promptText: params[
|
438
|
+
async_command('Page.handleJavaScriptDialog', accept: true, promptText: params[:default_prompt])
|
440
439
|
else
|
441
440
|
async_command('Page.handleJavaScriptDialog', accept: true, promptText: accept)
|
442
441
|
end
|
@@ -451,39 +450,38 @@ module Capybara::Apparition
|
|
451
450
|
end
|
452
451
|
end
|
453
452
|
|
454
|
-
@session.on 'Page.windowOpen' do
|
453
|
+
@session.on 'Page.windowOpen' do |**params|
|
455
454
|
puts "**** windowOpen was called with: #{params}" if ENV['DEBUG']
|
456
455
|
@browser.refresh_pages(opener: self)
|
457
456
|
end
|
458
457
|
|
459
|
-
@session.on 'Page.frameAttached' do
|
458
|
+
@session.on 'Page.frameAttached' do |**params|
|
460
459
|
puts "**** frameAttached called with #{params}" if ENV['DEBUG']
|
461
460
|
# @frames.get(params["frameId"]) = Frame.new(params)
|
462
461
|
end
|
463
462
|
|
464
|
-
@session.on 'Page.frameDetached' do |params|
|
465
|
-
@frames.delete(
|
466
|
-
puts "**** frameDetached called with #{params}" if ENV['DEBUG']
|
463
|
+
@session.on 'Page.frameDetached' do |frame_id:, **params|
|
464
|
+
@frames.delete(frame_id)
|
465
|
+
puts "**** frameDetached called with #{frame_id} : #{params}" if ENV['DEBUG']
|
467
466
|
end
|
468
467
|
|
469
|
-
@session.on 'Page.frameNavigated' do |
|
470
|
-
puts "**** frameNavigated called with #{
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
@frames.add(frame_params['id'], frame_params)
|
468
|
+
@session.on 'Page.frameNavigated' do |frame:|
|
469
|
+
puts "**** frameNavigated called with #{frame}" if ENV['DEBUG']
|
470
|
+
unless @frames.exists?(frame['id'])
|
471
|
+
puts "**** creating frame for #{frame['id']}" if ENV['DEBUG']
|
472
|
+
@frames.add(frame['id'], frame)
|
475
473
|
end
|
476
|
-
@frames.get(
|
474
|
+
@frames.get(frame['id'])&.loading(frame['loaderId'] || -1)
|
477
475
|
end
|
478
476
|
|
479
|
-
@session.on 'Page.frameStartedLoading' do |
|
480
|
-
puts "Setting loading for #{
|
481
|
-
@frames.get(
|
477
|
+
@session.on 'Page.frameStartedLoading' do |frame_id:|
|
478
|
+
puts "Setting loading for #{frame_id}" if ENV['DEBUG']
|
479
|
+
@frames.get(frame_id)&.loading(-1)
|
482
480
|
end
|
483
481
|
|
484
|
-
@session.on 'Page.frameStoppedLoading' do |
|
485
|
-
puts "Setting loaded for #{
|
486
|
-
@frames.get(
|
482
|
+
@session.on 'Page.frameStoppedLoading' do |frame_id:|
|
483
|
+
puts "Setting loaded for #{frame_id}" if ENV['DEBUG']
|
484
|
+
@frames.get(frame_id)&.loaded!
|
487
485
|
end
|
488
486
|
|
489
487
|
# @session.on 'Page.lifecycleEvent' do |params|
|
@@ -505,16 +503,14 @@ module Capybara::Apparition
|
|
505
503
|
main_frame.loaded! if @status_code != 200
|
506
504
|
end
|
507
505
|
|
508
|
-
@session.on 'Page.navigatedWithinDocument' do |params|
|
509
|
-
puts "**** navigatedWithinDocument called with #{params}" if ENV['DEBUG']
|
510
|
-
frame_id = params['frameId']
|
511
|
-
# @frames.get(frame_id).state = :loaded if frame_id == main_frame.id
|
506
|
+
@session.on 'Page.navigatedWithinDocument' do |frame_id:, **params|
|
507
|
+
puts "**** navigatedWithinDocument called with #{frame_id}: #{params}" if ENV['DEBUG']
|
512
508
|
@frames.get(frame_id).loaded! if frame_id == main_frame.id
|
513
509
|
end
|
514
510
|
|
515
|
-
@session.on 'Runtime.executionContextCreated' do |
|
511
|
+
@session.on 'Runtime.executionContextCreated' do |context:|
|
516
512
|
puts "**** executionContextCreated: #{params}" if ENV['DEBUG']
|
517
|
-
context = params['context']
|
513
|
+
# context = params['context']
|
518
514
|
frame_id = context.dig('auxData', 'frameId')
|
519
515
|
if context.dig('auxData', 'isDefault') && frame_id
|
520
516
|
if (frame = @frames.get(frame_id))
|
@@ -525,63 +521,92 @@ module Capybara::Apparition
|
|
525
521
|
end
|
526
522
|
end
|
527
523
|
|
528
|
-
@session.on 'Runtime.executionContextDestroyed' do |params|
|
529
|
-
puts "executionContextDestroyed: #{params}" if ENV['DEBUG']
|
530
|
-
@frames.destroy_context(
|
524
|
+
@session.on 'Runtime.executionContextDestroyed' do |execution_context_id:, **params|
|
525
|
+
puts "executionContextDestroyed: #{execution_context_id} : #{params}" if ENV['DEBUG']
|
526
|
+
@frames.destroy_context(execution_context_id)
|
531
527
|
end
|
532
528
|
|
533
|
-
@session.on 'Network.requestWillBeSent' do |
|
534
|
-
@open_resource_requests[
|
529
|
+
@session.on 'Network.requestWillBeSent' do |request_id:, request: nil, **|
|
530
|
+
@open_resource_requests[request_id] = request&.dig('url')
|
535
531
|
end
|
536
532
|
|
537
|
-
@session.on 'Network.responseReceived' do |
|
538
|
-
@open_resource_requests.delete(
|
533
|
+
@session.on 'Network.responseReceived' do |request_id:, **|
|
534
|
+
@open_resource_requests.delete(request_id)
|
539
535
|
temp_headers.clear
|
540
536
|
update_headers(async: true)
|
541
537
|
end
|
542
538
|
|
543
|
-
@session.on 'Network.requestWillBeSent' do
|
539
|
+
@session.on 'Network.requestWillBeSent' do |**params|
|
544
540
|
@network_traffic.push(NetworkTraffic::Request.new(params))
|
545
541
|
end
|
546
542
|
|
547
|
-
@session.on 'Network.responseReceived' do |
|
548
|
-
req = @network_traffic.find { |request| request.request_id ==
|
549
|
-
req.response = NetworkTraffic::Response.new(
|
543
|
+
@session.on 'Network.responseReceived' do |request_id:, response:, **|
|
544
|
+
req = @network_traffic.find { |request| request.request_id == request_id }
|
545
|
+
req.response = NetworkTraffic::Response.new(response) if req
|
550
546
|
end
|
551
547
|
|
552
|
-
@session.on 'Network.responseReceived' do |
|
553
|
-
if
|
554
|
-
@response_headers[
|
555
|
-
@status_code =
|
548
|
+
@session.on 'Network.responseReceived' do |type:, frame_id: nil, response: nil, **|
|
549
|
+
if type == 'Document'
|
550
|
+
@response_headers[frame_id] = response['headers']
|
551
|
+
@status_code = response['status']
|
556
552
|
end
|
557
553
|
end
|
558
554
|
|
559
|
-
@session.on 'Network.loadingFailed' do |params|
|
560
|
-
req = @network_traffic.find { |request| request.request_id ==
|
561
|
-
req&.blocked_params = params if
|
562
|
-
if
|
563
|
-
puts "Loading Failed - request: #{
|
555
|
+
@session.on 'Network.loadingFailed' do |type:, request_id:, blocked_reason: nil, error_text: nil, **params|
|
556
|
+
req = @network_traffic.find { |request| request.request_id == request_id }
|
557
|
+
req&.blocked_params = params if blocked_reason
|
558
|
+
if type == 'Document'
|
559
|
+
puts "Loading Failed - request: #{request_id} : #{error_text}" if ENV['DEBUG']
|
564
560
|
end
|
565
561
|
end
|
566
562
|
|
567
|
-
@session.on
|
568
|
-
|
569
|
-
|
570
|
-
|
563
|
+
@session.on(
|
564
|
+
'Network.requestIntercepted'
|
565
|
+
) do |request:, interception_id:, auth_challenge: nil, is_navigation_request: nil, **|
|
566
|
+
if auth_challenge
|
567
|
+
if auth_challenge['source'] == 'Proxy'
|
571
568
|
handle_proxy_auth(interception_id)
|
572
569
|
else
|
573
570
|
handle_user_auth(interception_id)
|
574
571
|
end
|
575
572
|
else
|
576
|
-
process_intercepted_request(interception_id, request,
|
573
|
+
process_intercepted_request(interception_id, request, is_navigation_request)
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
@session.on 'Fetch.requestPaused' do |request:, request_id:, resource_type:, **|
|
578
|
+
process_intercepted_fetch(request_id, request, resource_type)
|
579
|
+
end
|
580
|
+
|
581
|
+
@session.on 'Fetch.authRequired' do |request_id:, auth_challenge: nil, **|
|
582
|
+
next unless auth_challenge
|
583
|
+
|
584
|
+
credentials_response = if auth_challenge['source'] == 'Proxy'
|
585
|
+
if @proxy_auth_attempts.include?(request_id)
|
586
|
+
puts 'Cancelling proxy auth' if ENV['DEBUG']
|
587
|
+
{ response: 'CancelAuth' }
|
588
|
+
else
|
589
|
+
puts 'Replying with proxy auth credentials' if ENV['DEBUG']
|
590
|
+
@proxy_auth_attempts.push(request_id)
|
591
|
+
{ response: 'ProvideCredentials' }.merge(@browser.proxy_auth || {})
|
592
|
+
end
|
593
|
+
elsif @auth_attempts.include?(request_id)
|
594
|
+
puts 'Cancelling auth' if ENV['DEBUG']
|
595
|
+
{ response: 'CancelAuth' }
|
596
|
+
else
|
597
|
+
@auth_attempts.push(request_id)
|
598
|
+
puts 'Replying with auth credentials' if ENV['DEBUG']
|
599
|
+
{ response: 'ProvideCredentials' }.merge(@credentials || {})
|
577
600
|
end
|
601
|
+
|
602
|
+
async_command('Fetch.continueWithAuth', requestId: request_id, authChallengeResponse: credentials_response)
|
578
603
|
end
|
579
604
|
|
580
|
-
@session.on 'Runtime.consoleAPICalled' do
|
605
|
+
@session.on 'Runtime.consoleAPICalled' do |**params|
|
581
606
|
# {"type"=>"log", "args"=>[{"type"=>"string", "value"=>"hello"}], "executionContextId"=>2, "timestamp"=>1548722854903.285, "stackTrace"=>{"callFrames"=>[{"functionName"=>"", "scriptId"=>"15", "url"=>"http://127.0.0.1:53977/", "lineNumber"=>6, "columnNumber"=>22}]}}
|
582
|
-
details = params.dig(
|
583
|
-
@browser.console.log(params[
|
584
|
-
params[
|
607
|
+
details = params.dig(:stack_trace, 'callFrames')&.first
|
608
|
+
@browser.console.log(params[:type],
|
609
|
+
params[:args].map { |arg| arg['description'] || arg['value'] }.join(' ').to_s,
|
585
610
|
source: details['url'].empty? ? nil : details['url'],
|
586
611
|
line_number: details['lineNumber'].zero? ? nil : details['lineNumber'],
|
587
612
|
columnNumber: details['columnNumber'].zero? ? nil : details['columnNumber'])
|
@@ -600,15 +625,16 @@ module Capybara::Apparition
|
|
600
625
|
end
|
601
626
|
|
602
627
|
def register_js_error_handler
|
603
|
-
@session.on 'Runtime.exceptionThrown' do |
|
604
|
-
@js_error ||=
|
628
|
+
@session.on 'Runtime.exceptionThrown' do |exception_details: nil, **|
|
629
|
+
@js_error ||= exception_details&.dig('exception', 'description') if @raise_js_errors
|
605
630
|
|
606
|
-
details =
|
631
|
+
details = exception_details&.dig('stackTrace', 'callFrames')&.first ||
|
632
|
+
exception_details || {}
|
607
633
|
@browser.console.log('error',
|
608
|
-
|
609
|
-
source: details['url'].empty? ? nil : details['url'],
|
610
|
-
line_number: details['lineNumber'].zero? ? nil : details['lineNumber'],
|
611
|
-
columnNumber: details['columnNumber'].zero? ? nil : details['columnNumber'])
|
634
|
+
exception_details&.dig('exception', 'description'),
|
635
|
+
source: details['url'].to_s.empty? ? nil : details['url'],
|
636
|
+
line_number: details['lineNumber'].to_i.zero? ? nil : details['lineNumber'],
|
637
|
+
columnNumber: details['columnNumber'].to_i.zero? ? nil : details['columnNumber'])
|
612
638
|
end
|
613
639
|
end
|
614
640
|
|
@@ -619,6 +645,7 @@ module Capybara::Apparition
|
|
619
645
|
|
620
646
|
def setup_network_interception
|
621
647
|
async_command 'Network.setCacheDisabled', cacheDisabled: true
|
648
|
+
# async_command 'Fetch.enable', handleAuthRequests: true
|
622
649
|
async_command 'Network.setRequestInterception', patterns: [{ urlPattern: '*' }]
|
623
650
|
end
|
624
651
|
|
@@ -648,6 +675,37 @@ module Capybara::Apparition
|
|
648
675
|
end
|
649
676
|
end
|
650
677
|
|
678
|
+
def process_intercepted_fetch(interception_id, request, resource_type)
|
679
|
+
navigation = (resource_type == 'Document')
|
680
|
+
headers, url = request.values_at('headers', 'url')
|
681
|
+
|
682
|
+
unless @temp_headers.empty? || navigation # rubocop:disable Style/IfUnlessModifier
|
683
|
+
headers.delete_if { |name, value| @temp_headers[name] == value }
|
684
|
+
end
|
685
|
+
unless @temp_no_redirect_headers.empty? || !navigation
|
686
|
+
headers.delete_if { |name, value| @temp_no_redirect_headers[name] == value }
|
687
|
+
end
|
688
|
+
if (accept = perm_headers.keys.find { |k| /accept/i.match? k })
|
689
|
+
headers[accept] = perm_headers[accept]
|
690
|
+
end
|
691
|
+
|
692
|
+
if @url_blacklist.any? { |r| url.match Regexp.escape(r).gsub('\*', '.*?') }
|
693
|
+
async_command('Fetch.failRequest', errorReason: 'Failed', requestId: interception_id)
|
694
|
+
elsif @url_whitelist.any?
|
695
|
+
if @url_whitelist.any? { |r| url.match Regexp.escape(r).gsub('\*', '.*?') }
|
696
|
+
async_command('Fetch.continueRequest',
|
697
|
+
requestId: interception_id,
|
698
|
+
headers: headers.map { |k, v| { name: k, value: v } })
|
699
|
+
else
|
700
|
+
async_command('Fetch.failRequest', errorReason: 'Failed', requestId: interception_id)
|
701
|
+
end
|
702
|
+
else
|
703
|
+
async_command('Fetch.continueRequest',
|
704
|
+
requestId: interception_id,
|
705
|
+
headers: headers.map { |k, v| { name: k, value: v } })
|
706
|
+
end
|
707
|
+
end
|
708
|
+
|
651
709
|
def continue_request(id, **params)
|
652
710
|
async_command 'Network.continueInterceptedRequest', interceptionId: id, **params
|
653
711
|
end
|
@@ -71,11 +71,13 @@ module Capybara::Apparition
|
|
71
71
|
|
72
72
|
Array(keys).each do |sequence|
|
73
73
|
case sequence
|
74
|
-
when Array
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
74
|
+
when Array
|
75
|
+
type_with_modifiers(sequence, delay: delay)
|
76
|
+
when String
|
77
|
+
sequence.each_char do |char|
|
78
|
+
press char
|
79
|
+
sleep delay
|
80
|
+
end
|
79
81
|
else
|
80
82
|
press sequence
|
81
83
|
sleep delay
|
@@ -6,6 +6,7 @@ module Capybara::Apparition
|
|
6
6
|
@page = page
|
7
7
|
@keyboard = keyboard
|
8
8
|
@current_pos = { x: 0, y: 0 }
|
9
|
+
@current_buttons = BUTTONS[:none]
|
9
10
|
end
|
10
11
|
|
11
12
|
def click_at(x:, y:, button: 'left', count: 1, modifiers: [])
|
@@ -29,11 +30,13 @@ module Capybara::Apparition
|
|
29
30
|
def down(button: 'left', **options)
|
30
31
|
options = @current_pos.merge(button: button).merge(options)
|
31
32
|
mouse_event('mousePressed', options)
|
33
|
+
@current_buttons |= BUTTONS[button.to_sym]
|
32
34
|
self
|
33
35
|
end
|
34
36
|
|
35
37
|
def up(button: 'left', **options)
|
36
38
|
options = @current_pos.merge(button: button).merge(options)
|
39
|
+
@current_buttons &= ~BUTTONS[button.to_sym]
|
37
40
|
mouse_event('mouseReleased', options)
|
38
41
|
self
|
39
42
|
end
|
@@ -43,11 +46,21 @@ module Capybara::Apparition
|
|
43
46
|
def mouse_event(type, x:, y:, button: 'none', count: 1)
|
44
47
|
@page.command('Input.dispatchMouseEvent',
|
45
48
|
type: type,
|
46
|
-
button: button,
|
49
|
+
button: button.to_s,
|
50
|
+
buttons: @current_buttons,
|
47
51
|
x: x,
|
48
52
|
y: y,
|
49
53
|
modifiers: @keyboard.modifiers,
|
50
54
|
clickCount: count)
|
51
55
|
end
|
56
|
+
|
57
|
+
BUTTONS = {
|
58
|
+
left: 1,
|
59
|
+
right: 2,
|
60
|
+
middle: 4,
|
61
|
+
back: 8,
|
62
|
+
forward: 16,
|
63
|
+
none: 0
|
64
|
+
}.freeze
|
52
65
|
end
|
53
66
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: apparition
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Thomas Walpole
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-08-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: capybara
|
@@ -44,20 +44,6 @@ dependencies:
|
|
44
44
|
- - ">="
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: 0.6.5
|
47
|
-
- !ruby/object:Gem::Dependency
|
48
|
-
name: byebug
|
49
|
-
requirement: !ruby/object:Gem::Requirement
|
50
|
-
requirements:
|
51
|
-
- - ">="
|
52
|
-
- !ruby/object:Gem::Version
|
53
|
-
version: '0'
|
54
|
-
type: :development
|
55
|
-
prerelease: false
|
56
|
-
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
requirements:
|
58
|
-
- - ">="
|
59
|
-
- !ruby/object:Gem::Version
|
60
|
-
version: '0'
|
61
47
|
- !ruby/object:Gem::Dependency
|
62
48
|
name: chunky_png
|
63
49
|
requirement: !ruby/object:Gem::Requirement
|