apparition 0.1.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +40 -4
- data/lib/capybara/apparition.rb +0 -2
- data/lib/capybara/apparition/browser.rb +75 -133
- data/lib/capybara/apparition/browser/cookie.rb +4 -16
- data/lib/capybara/apparition/browser/header.rb +2 -2
- data/lib/capybara/apparition/browser/launcher.rb +25 -0
- data/lib/capybara/apparition/browser/launcher/local.rb +213 -0
- data/lib/capybara/apparition/browser/launcher/remote.rb +55 -0
- data/lib/capybara/apparition/browser/page_manager.rb +90 -0
- data/lib/capybara/apparition/browser/window.rb +29 -29
- data/lib/capybara/apparition/configuration.rb +100 -0
- data/lib/capybara/apparition/console.rb +8 -1
- data/lib/capybara/apparition/dev_tools_protocol/remote_object.rb +23 -7
- data/lib/capybara/apparition/dev_tools_protocol/session.rb +3 -4
- data/lib/capybara/apparition/driver.rb +107 -35
- data/lib/capybara/apparition/driver/chrome_client.rb +13 -8
- data/lib/capybara/apparition/driver/response.rb +1 -1
- data/lib/capybara/apparition/driver/web_socket_client.rb +1 -0
- data/lib/capybara/apparition/errors.rb +3 -3
- data/lib/capybara/apparition/network_traffic/error.rb +1 -0
- data/lib/capybara/apparition/network_traffic/request.rb +5 -5
- data/lib/capybara/apparition/node.rb +142 -50
- data/lib/capybara/apparition/node/drag.rb +165 -65
- data/lib/capybara/apparition/page.rb +180 -142
- data/lib/capybara/apparition/page/frame.rb +3 -0
- data/lib/capybara/apparition/page/frame_manager.rb +2 -1
- data/lib/capybara/apparition/page/keyboard.rb +29 -7
- data/lib/capybara/apparition/page/mouse.rb +20 -6
- data/lib/capybara/apparition/utility.rb +1 -1
- data/lib/capybara/apparition/version.rb +1 -1
- metadata +53 -23
- data/lib/capybara/apparition/dev_tools_protocol/target.rb +0 -64
- data/lib/capybara/apparition/dev_tools_protocol/target_manager.rb +0 -48
- data/lib/capybara/apparition/driver/launcher.rb +0 -217
@@ -2,26 +2,29 @@
|
|
2
2
|
|
3
3
|
module Capybara::Apparition
|
4
4
|
module Drag
|
5
|
-
def drag_to(other, delay: 0.1)
|
6
|
-
|
5
|
+
def drag_to(other, delay: 0.1, html5: nil, drop_modifiers: [])
|
6
|
+
drop_modifiers = Array(drop_modifiers)
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
8
|
+
driver.execute_script MOUSEDOWN_TRACKER
|
9
|
+
scroll_if_needed
|
10
|
+
m = @page.mouse
|
11
|
+
m.move_to(**visible_center)
|
12
|
+
sleep delay
|
13
|
+
m.down
|
14
|
+
html5 = !driver.evaluate_script(LEGACY_DRAG_CHECK, self) if html5.nil?
|
15
|
+
if html5
|
16
|
+
driver.execute_script HTML5_DRAG_DROP_SCRIPT, self, other, delay, drop_modifiers
|
17
|
+
m.up(**other.visible_center)
|
18
|
+
else
|
19
|
+
@page.keyboard.with_keys(drop_modifiers) do
|
20
|
+
other.scroll_if_needed
|
21
|
+
sleep delay
|
22
|
+
m.move_to(**other.visible_center)
|
23
|
+
sleep delay
|
24
|
+
ensure
|
25
|
+
m.up
|
26
|
+
sleep delay
|
27
|
+
end
|
25
28
|
end
|
26
29
|
end
|
27
30
|
|
@@ -30,43 +33,109 @@ module Capybara::Apparition
|
|
30
33
|
raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['hover']) if pos.nil?
|
31
34
|
|
32
35
|
other_pos = { x: pos[:x] + x, y: pos[:y] + y }
|
33
|
-
raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['drag', test['selector'], pos]) unless mouse_event_test?(pos)
|
36
|
+
raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['drag', test['selector'], pos]) unless mouse_event_test?(**pos)
|
34
37
|
|
35
|
-
@page.mouse.move_to(pos).down
|
38
|
+
@page.mouse.move_to(**pos).down
|
36
39
|
sleep delay
|
37
|
-
@page.mouse.move_to(other_pos
|
40
|
+
@page.mouse.move_to(**other_pos)
|
38
41
|
sleep delay
|
39
42
|
@page.mouse.up
|
40
43
|
end
|
41
44
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
element.scroll_if_needed
|
50
|
-
@page.mouse.move_to(element.visible_center).up
|
45
|
+
def drop(*args)
|
46
|
+
if args[0].is_a? String
|
47
|
+
input = evaluate_on ATTACH_FILE
|
48
|
+
tag_name = input['description'].split(/[.#]/, 2)[0]
|
49
|
+
input = Capybara::Apparition::Node.new(driver, @page, input['objectId'], tag_name: tag_name)
|
50
|
+
input.set(args)
|
51
|
+
evaluate_on DROP_FILE, objectId: input.id
|
51
52
|
else
|
52
|
-
|
53
|
-
|
53
|
+
items = args.each_with_object([]) do |arg, arr|
|
54
|
+
arg.each_with_object(arr) do |(type, data), arr_|
|
55
|
+
arr_ << { type: type, data: data }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
evaluate_on DROP_STRING, value: items
|
54
59
|
end
|
55
60
|
end
|
56
61
|
|
57
|
-
|
58
|
-
|
59
|
-
|
62
|
+
DROP_STRING = <<~JS
|
63
|
+
function(strings){
|
64
|
+
var dt = new DataTransfer(),
|
65
|
+
opts = { cancelable: true, bubbles: true, dataTransfer: dt };
|
66
|
+
for (var i=0; i < strings.length; i++){
|
67
|
+
if (dt.items) {
|
68
|
+
dt.items.add(strings[i]['data'], strings[i]['type']);
|
69
|
+
} else {
|
70
|
+
dt.setData(strings[i]['type'], strings[i]['data']);
|
71
|
+
}
|
72
|
+
}
|
73
|
+
var dropEvent = new DragEvent('drop', opts);
|
74
|
+
this.dispatchEvent(dropEvent);
|
75
|
+
}
|
76
|
+
JS
|
77
|
+
|
78
|
+
DROP_FILE = <<~JS
|
79
|
+
function(input){
|
80
|
+
var files = input.files,
|
81
|
+
dt = new DataTransfer(),
|
82
|
+
opts = { cancelable: true, bubbles: true, dataTransfer: dt };
|
83
|
+
input.parentElement.removeChild(input);
|
84
|
+
if (dt.items){
|
85
|
+
for (var i=0; i<files.length; i++){
|
86
|
+
dt.items.add(files[i]);
|
87
|
+
}
|
88
|
+
} else {
|
89
|
+
Object.defineProperty(dt, "files", {
|
90
|
+
value: files,
|
91
|
+
writable: false
|
92
|
+
});
|
93
|
+
}
|
94
|
+
var dropEvent = new DragEvent('drop', opts);
|
95
|
+
this.dispatchEvent(dropEvent);
|
96
|
+
}
|
97
|
+
JS
|
98
|
+
|
99
|
+
ATTACH_FILE = <<~JS
|
100
|
+
function(){
|
101
|
+
var input = document.createElement('INPUT');
|
102
|
+
input.type = "file";
|
103
|
+
input.id = "_capybara_drop_file";
|
104
|
+
input.multiple = true;
|
105
|
+
document.body.appendChild(input);
|
106
|
+
return input;
|
107
|
+
}
|
108
|
+
JS
|
60
109
|
|
61
110
|
MOUSEDOWN_TRACKER = <<~JS
|
111
|
+
window.capybara_mousedown_prevented = null;
|
62
112
|
document.addEventListener('mousedown', ev => {
|
63
113
|
window.capybara_mousedown_prevented = ev.defaultPrevented;
|
64
114
|
}, { once: true, passive: true })
|
65
115
|
JS
|
66
116
|
|
117
|
+
LEGACY_DRAG_CHECK = <<~JS
|
118
|
+
(function(el){
|
119
|
+
if ([true, null].includes(window.capybara_mousedown_prevented)){
|
120
|
+
return true;
|
121
|
+
}
|
122
|
+
do {
|
123
|
+
if (el.draggable) return false;
|
124
|
+
} while (el = el.parentElement );
|
125
|
+
return true;
|
126
|
+
})(arguments[0])
|
127
|
+
JS
|
128
|
+
|
67
129
|
HTML5_DRAG_DROP_SCRIPT = <<~JS
|
68
|
-
|
69
|
-
|
130
|
+
let source = arguments[0];
|
131
|
+
const target = arguments[1];
|
132
|
+
const step_delay = arguments[2] * 1000;
|
133
|
+
const drop_modifiers = arguments[3];
|
134
|
+
const key_aliases = {
|
135
|
+
'cmd': 'meta',
|
136
|
+
'command': 'meta',
|
137
|
+
'control': 'ctrl',
|
138
|
+
};
|
70
139
|
|
71
140
|
function rectCenter(rect){
|
72
141
|
return new DOMPoint(
|
@@ -106,8 +175,63 @@ module Capybara::Apparition
|
|
106
175
|
return new DOMPoint(pt.x,pt.y);
|
107
176
|
}
|
108
177
|
|
109
|
-
|
110
|
-
|
178
|
+
function dragStart() {
|
179
|
+
return new Promise( resolve => {
|
180
|
+
var dragEvent = new DragEvent('dragstart', opts);
|
181
|
+
source.dispatchEvent(dragEvent);
|
182
|
+
setTimeout(resolve, step_delay)
|
183
|
+
})
|
184
|
+
}
|
185
|
+
|
186
|
+
function dragEnter() {
|
187
|
+
return new Promise( resolve => {
|
188
|
+
target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
|
189
|
+
let targetRect = target.getBoundingClientRect(),
|
190
|
+
sourceCenter = rectCenter(source.getBoundingClientRect());
|
191
|
+
|
192
|
+
drop_modifiers.map(key => key_aliases[key] || key)
|
193
|
+
.forEach(key => opts[key + 'Key'] = true);
|
194
|
+
|
195
|
+
// fire 2 dragover events to simulate dragging with a direction
|
196
|
+
let entryPoint = pointOnRect(sourceCenter, targetRect);
|
197
|
+
let dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
|
198
|
+
let dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
199
|
+
target.dispatchEvent(dragOverEvent);
|
200
|
+
setTimeout(resolve, step_delay)
|
201
|
+
})
|
202
|
+
}
|
203
|
+
|
204
|
+
function dragOnto() {
|
205
|
+
return new Promise( resolve => {
|
206
|
+
var targetCenter = rectCenter(target.getBoundingClientRect());
|
207
|
+
dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
|
208
|
+
dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
209
|
+
target.dispatchEvent(dragOverEvent);
|
210
|
+
setTimeout(resolve, step_delay, { drop: dragOverEvent.defaultPrevented, opts: dragOverOpts});
|
211
|
+
})
|
212
|
+
}
|
213
|
+
|
214
|
+
function dragLeave({ drop, opts: dragOverOpts }) {
|
215
|
+
return new Promise( resolve => {
|
216
|
+
var dragLeaveOptions = { ...opts, ...dragOverOpts };
|
217
|
+
var dragLeaveEvent = new DragEvent('dragleave', dragLeaveOptions);
|
218
|
+
target.dispatchEvent(dragLeaveEvent);
|
219
|
+
if (drop) {
|
220
|
+
var dropEvent = new DragEvent('drop', dragLeaveOptions);
|
221
|
+
target.dispatchEvent(dropEvent);
|
222
|
+
}
|
223
|
+
var dragEndEvent = new DragEvent('dragend', dragLeaveOptions);
|
224
|
+
source.dispatchEvent(dragEndEvent);
|
225
|
+
setTimeout(resolve, step_delay);
|
226
|
+
})
|
227
|
+
}
|
228
|
+
|
229
|
+
const dt = new DataTransfer();
|
230
|
+
const opts = { cancelable: true, bubbles: true, dataTransfer: dt };
|
231
|
+
|
232
|
+
while (source && !source.draggable) {
|
233
|
+
source = source.parentElement;
|
234
|
+
}
|
111
235
|
|
112
236
|
if (source.tagName == 'A'){
|
113
237
|
dt.setData('text/uri-list', source.href);
|
@@ -118,31 +242,7 @@ module Capybara::Apparition
|
|
118
242
|
dt.setData('text', source.src);
|
119
243
|
}
|
120
244
|
|
121
|
-
|
122
|
-
source.dispatchEvent(dragEvent);
|
123
|
-
target.scrollIntoView({behavior: 'instant', block: 'center', inline: 'center'});
|
124
|
-
var targetRect = target.getBoundingClientRect();
|
125
|
-
var sourceCenter = rectCenter(source.getBoundingClientRect());
|
126
|
-
|
127
|
-
// fire 2 dragover events to simulate dragging with a direction
|
128
|
-
var entryPoint = pointOnRect(sourceCenter, targetRect)
|
129
|
-
var dragOverOpts = Object.assign({clientX: entryPoint.x, clientY: entryPoint.y}, opts);
|
130
|
-
var dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
131
|
-
target.dispatchEvent(dragOverEvent);
|
132
|
-
|
133
|
-
var targetCenter = rectCenter(targetRect);
|
134
|
-
dragOverOpts = Object.assign({clientX: targetCenter.x, clientY: targetCenter.y}, opts);
|
135
|
-
dragOverEvent = new DragEvent('dragover', dragOverOpts);
|
136
|
-
target.dispatchEvent(dragOverEvent);
|
137
|
-
|
138
|
-
var dragLeaveEvent = new DragEvent('dragleave', opts);
|
139
|
-
target.dispatchEvent(dragLeaveEvent);
|
140
|
-
if (dragOverEvent.defaultPrevented) {
|
141
|
-
var dropEvent = new DragEvent('drop', opts);
|
142
|
-
target.dispatchEvent(dropEvent);
|
143
|
-
}
|
144
|
-
var dragEndEvent = new DragEvent('dragend', opts);
|
145
|
-
source.dispatchEvent(dragEndEvent);
|
245
|
+
dragStart().then(dragEnter).then(dragOnto).then(dragLeave)
|
146
246
|
JS
|
147
247
|
end
|
148
248
|
end
|
@@ -10,44 +10,43 @@ module Capybara::Apparition
|
|
10
10
|
attr_reader :modal_messages
|
11
11
|
attr_reader :mouse, :keyboard
|
12
12
|
attr_reader :viewport_size
|
13
|
+
attr_reader :browser_context_id
|
13
14
|
attr_accessor :perm_headers, :temp_headers, :temp_no_redirect_headers
|
14
15
|
attr_reader :network_traffic
|
16
|
+
attr_reader :target_id
|
15
17
|
|
16
|
-
def self.create(browser, session, id,
|
17
|
-
|
18
|
+
def self.create(browser, session, id, browser_context_id,
|
19
|
+
ignore_https_errors: false, **options)
|
20
|
+
session.async_command 'Page.enable'
|
18
21
|
|
19
22
|
# Provides a lot of info - but huge overhead
|
20
23
|
# session.command 'Page.setLifecycleEventsEnabled', enabled: true
|
21
24
|
|
22
|
-
page = Page.new(browser, session, id,
|
25
|
+
page = Page.new(browser, session, id, browser_context_id, **options)
|
23
26
|
|
24
27
|
session.async_commands 'Network.enable', 'Runtime.enable', 'Security.enable', 'DOM.enable'
|
25
|
-
|
26
|
-
# session.command 'Runtime.enable'
|
27
|
-
# session.command 'Security.enable'
|
28
|
-
# session.command 'Security.setOverrideCertificateErrors', override: true if ignore_https_errors
|
29
|
-
session.command 'Security.setIgnoreCertificateErrors', ignore: !!ignore_https_errors
|
30
|
-
# session.command 'DOM.enable'
|
31
|
-
# session.command 'Log.enable'
|
28
|
+
session.async_command 'Security.setIgnoreCertificateErrors', ignore: !!ignore_https_errors
|
32
29
|
if Capybara.save_path
|
33
|
-
session.
|
30
|
+
session.async_command 'Page.setDownloadBehavior', behavior: 'allow', downloadPath: Capybara.save_path
|
34
31
|
end
|
35
32
|
page
|
36
33
|
end
|
37
34
|
|
38
|
-
def initialize(browser, session,
|
39
|
-
|
35
|
+
def initialize(browser, session, target_id, browser_context_id,
|
36
|
+
js_errors: false, url_blacklist: [], url_whitelist: [], extensions: [])
|
37
|
+
@target_id = target_id
|
38
|
+
@browser_context_id = browser_context_id
|
40
39
|
@browser = browser
|
41
40
|
@session = session
|
42
41
|
@keyboard = Keyboard.new(self)
|
43
42
|
@mouse = Mouse.new(self, @keyboard)
|
44
43
|
@modals = []
|
45
44
|
@modal_messages = []
|
46
|
-
@frames = Capybara::Apparition::FrameManager.new(
|
45
|
+
@frames = Capybara::Apparition::FrameManager.new(@target_id)
|
47
46
|
@response_headers = {}
|
48
47
|
@status_code = 0
|
49
|
-
@url_blacklist = []
|
50
|
-
@url_whitelist = []
|
48
|
+
@url_blacklist = url_blacklist || []
|
49
|
+
@url_whitelist = url_whitelist || []
|
51
50
|
@credentials = nil
|
52
51
|
@auth_attempts = []
|
53
52
|
@proxy_credentials = nil
|
@@ -67,6 +66,10 @@ module Capybara::Apparition
|
|
67
66
|
|
68
67
|
register_js_error_handler # if js_errors
|
69
68
|
|
69
|
+
extensions.each do |name|
|
70
|
+
add_extension(name)
|
71
|
+
end
|
72
|
+
|
70
73
|
setup_network_interception if browser.proxy_auth
|
71
74
|
end
|
72
75
|
|
@@ -84,6 +87,12 @@ module Capybara::Apparition
|
|
84
87
|
@perm_headers = {}
|
85
88
|
end
|
86
89
|
|
90
|
+
def add_extension(filename)
|
91
|
+
command('Page.addScriptToEvaluateOnNewDocument', source: File.read(filename))
|
92
|
+
rescue Errno::ENOENT
|
93
|
+
raise ::Capybara::Apparition::BrowserError.new('name' => "Unable to load extension: #{filename}", 'args' => nil)
|
94
|
+
end
|
95
|
+
|
87
96
|
def add_modal(modal_response)
|
88
97
|
@last_modal_message = nil
|
89
98
|
@modals.push(modal_response)
|
@@ -136,11 +145,12 @@ module Capybara::Apparition
|
|
136
145
|
pixel_ratio = evaluate('window.devicePixelRatio')
|
137
146
|
scale = (@browser.zoom_factor || 1).to_f / pixel_ratio
|
138
147
|
if options[:format].to_s == 'pdf'
|
139
|
-
params = {}
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
148
|
+
params = { scale: scale }
|
149
|
+
if @browser.paper_size
|
150
|
+
params[:paperWidth] = @browser.paper_size[:width].to_f
|
151
|
+
params[:paperHeight] = @browser.paper_size[:height].to_f
|
152
|
+
end
|
153
|
+
command('Page.printToPDF', **params)
|
144
154
|
else
|
145
155
|
clip_options = if options[:selector]
|
146
156
|
pos = evaluate("document.querySelector('#{options.delete(:selector)}').getBoundingClientRect().toJSON();")
|
@@ -155,7 +165,7 @@ module Capybara::Apparition
|
|
155
165
|
JS
|
156
166
|
end
|
157
167
|
options[:clip] = { x: 0, y: 0, scale: scale }.merge(clip_options)
|
158
|
-
command('Page.captureScreenshot', options)
|
168
|
+
command('Page.captureScreenshot', **options)
|
159
169
|
end['data']
|
160
170
|
end
|
161
171
|
|
@@ -164,15 +174,14 @@ module Capybara::Apparition
|
|
164
174
|
frame_id = node['node']['frameId']
|
165
175
|
|
166
176
|
timer = Capybara::Helpers.timer(expire_in: 10)
|
167
|
-
while (frame = @frames
|
177
|
+
while (frame = @frames[frame_id]).nil? || frame.loading?
|
168
178
|
# Wait for the frame creation messages to be processed
|
169
179
|
if timer.expired?
|
170
|
-
puts 'Timed out waiting
|
180
|
+
puts 'Timed out waiting for frame to be ready'
|
171
181
|
raise TimeoutError.new('push_frame')
|
172
182
|
end
|
173
183
|
sleep 0.1
|
174
184
|
end
|
175
|
-
return unless frame
|
176
185
|
|
177
186
|
frame.element_id = frame_el.base.id
|
178
187
|
@frames.push_frame(frame.id)
|
@@ -187,12 +196,12 @@ module Capybara::Apparition
|
|
187
196
|
wait_for_loaded
|
188
197
|
js_escaped_selector = selector.gsub('\\', '\\\\\\').gsub('"', '\"')
|
189
198
|
query = method == :css ? CSS_FIND_JS : XPATH_FIND_JS
|
190
|
-
result = _raw_evaluate(query
|
191
|
-
(result || []).map { |r_o| [self, r_o['objectId']] }
|
199
|
+
result = _raw_evaluate(format(query, selector: js_escaped_selector))
|
200
|
+
(result || []).map { |r_o| [self, r_o['objectId'], tag_name: r_o['description'].split(/[.#]/, 2)[0]] }
|
192
201
|
rescue ::Capybara::Apparition::BrowserError => e
|
193
|
-
raise unless
|
202
|
+
raise unless /is not a valid (XPath expression|selector)/.match? e.name
|
194
203
|
|
195
|
-
raise Capybara::Apparition::InvalidSelector, [method, selector]
|
204
|
+
raise Capybara::Apparition::InvalidSelector, 'args' => [method, selector]
|
196
205
|
end
|
197
206
|
|
198
207
|
def execute(script, *args)
|
@@ -225,20 +234,36 @@ module Capybara::Apparition
|
|
225
234
|
go_history(+1)
|
226
235
|
end
|
227
236
|
|
228
|
-
|
237
|
+
def response_headers
|
238
|
+
@response_headers[current_frame.id] || {}
|
239
|
+
end
|
229
240
|
|
230
241
|
attr_reader :status_code
|
231
242
|
|
232
243
|
def wait_for_loaded(allow_obsolete: false)
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
244
|
+
# We can't reliably detect if the page is loaded, so just ensure the context
|
245
|
+
# is usable
|
246
|
+
timer = Capybara::Helpers.timer(expire_in: 30)
|
247
|
+
page_function = '(function(){ return 1 == 1; })()'
|
248
|
+
begin
|
249
|
+
response = command('Runtime.evaluate',
|
250
|
+
expression: page_function,
|
251
|
+
contextId: current_frame.context_id,
|
252
|
+
returnByValue: false,
|
253
|
+
awaitPromise: true)
|
254
|
+
process_response(response)
|
255
|
+
current_frame.loaded!
|
256
|
+
rescue # rubocop:disable Style/RescueStandardError
|
257
|
+
return if allow_obsolete && current_frame.obsolete?
|
258
|
+
|
259
|
+
unless timer.expired?
|
260
|
+
sleep 0.05
|
261
|
+
retry
|
239
262
|
end
|
240
|
-
|
263
|
+
puts 'Timedout waiting for page to be loaded' if ENV['DEBUG']
|
264
|
+
raise TimeoutError.new('wait_for_loaded')
|
241
265
|
end
|
266
|
+
|
242
267
|
raise JavascriptError.new(js_error) if @js_error
|
243
268
|
end
|
244
269
|
|
@@ -259,8 +284,7 @@ module Capybara::Apparition
|
|
259
284
|
@status_code = 0
|
260
285
|
navigate_opts = { url: url, transitionType: 'reload' }
|
261
286
|
navigate_opts[:referrer] = extra_headers['Referer'] if extra_headers['Referer']
|
262
|
-
response = command('Page.navigate', navigate_opts)
|
263
|
-
|
287
|
+
response = command('Page.navigate', **navigate_opts)
|
264
288
|
raise StatusFailError, 'args' => [url, response['errorText']] if response['errorText']
|
265
289
|
|
266
290
|
main_frame.loading(response['loaderId'])
|
@@ -276,7 +300,7 @@ module Capybara::Apparition
|
|
276
300
|
|
277
301
|
def element_from_point(x:, y:)
|
278
302
|
r_o = _raw_evaluate("document.elementFromPoint(#{x}, #{y})", context_id: main_frame.context_id)
|
279
|
-
while r_o
|
303
|
+
while r_o&.[]('description')&.start_with?('iframe')
|
280
304
|
frame_node = command('DOM.describeNode', objectId: r_o['objectId'])
|
281
305
|
frame = @frames.get(frame_node.dig('node', 'frameId'))
|
282
306
|
fo = frame_offset(frame)
|
@@ -291,7 +315,7 @@ module Capybara::Apparition
|
|
291
315
|
end
|
292
316
|
|
293
317
|
def set_viewport(width:, height:, screen: nil)
|
294
|
-
wait_for_loaded
|
318
|
+
# wait_for_loaded
|
295
319
|
@viewport_size = { width: width, height: height }
|
296
320
|
result = @browser.command('Browser.getWindowForTarget', targetId: @target_id)
|
297
321
|
begin
|
@@ -311,7 +335,7 @@ module Capybara::Apparition
|
|
311
335
|
}
|
312
336
|
metrics[:screenWidth], metrics[:screenHeight] = *screen if screen
|
313
337
|
|
314
|
-
command('Emulation.setDeviceMetricsOverride', metrics)
|
338
|
+
command('Emulation.setDeviceMetricsOverride', **metrics)
|
315
339
|
end
|
316
340
|
|
317
341
|
def fullscreen
|
@@ -350,11 +374,9 @@ module Capybara::Apparition
|
|
350
374
|
end
|
351
375
|
|
352
376
|
def update_headers(async: false)
|
353
|
-
|
354
|
-
|
355
|
-
send(method, 'Network.setUserAgentOverride', userAgent: ua[1])
|
377
|
+
if (ua = extra_headers.find { |k, _v| /^User-Agent$/i.match? k })
|
378
|
+
send(async ? :async_command : :command, 'Network.setUserAgentOverride', userAgent: ua[1])
|
356
379
|
end
|
357
|
-
send(method, 'Network.setExtraHTTPHeaders', headers: extra_headers)
|
358
380
|
setup_network_interception
|
359
381
|
end
|
360
382
|
|
@@ -362,7 +384,7 @@ module Capybara::Apparition
|
|
362
384
|
if page
|
363
385
|
self.url_whitelist = page.url_whitelist.dup
|
364
386
|
self.url_blacklist = page.url_blacklist.dup
|
365
|
-
set_viewport(page.viewport_size) if page.viewport_size
|
387
|
+
set_viewport(**page.viewport_size) if page.viewport_size
|
366
388
|
end
|
367
389
|
self
|
368
390
|
end
|
@@ -377,6 +399,14 @@ module Capybara::Apparition
|
|
377
399
|
|
378
400
|
attr_reader :url_blacklist, :url_whitelist
|
379
401
|
|
402
|
+
def current_frame
|
403
|
+
@frames.current
|
404
|
+
end
|
405
|
+
|
406
|
+
def main_frame
|
407
|
+
@frames.main
|
408
|
+
end
|
409
|
+
|
380
410
|
private
|
381
411
|
|
382
412
|
def eval_wrapped_script(wrapper, script, args)
|
@@ -393,9 +423,9 @@ module Capybara::Apparition
|
|
393
423
|
end
|
394
424
|
|
395
425
|
def register_event_handlers
|
396
|
-
@session.on 'Page.javascriptDialogOpening' do |params|
|
397
|
-
type =
|
398
|
-
accept = accept_modal?(type, message:
|
426
|
+
@session.on 'Page.javascriptDialogOpening' do |type:, message:, has_browser_handler:, **params|
|
427
|
+
type = type.to_sym
|
428
|
+
accept = accept_modal?(type, message: message, manual: has_browser_handler)
|
399
429
|
next if accept.nil?
|
400
430
|
|
401
431
|
if type == :prompt
|
@@ -403,7 +433,7 @@ module Capybara::Apparition
|
|
403
433
|
when false
|
404
434
|
async_command('Page.handleJavaScriptDialog', accept: false)
|
405
435
|
when true
|
406
|
-
async_command('Page.handleJavaScriptDialog', accept: true, promptText: params[
|
436
|
+
async_command('Page.handleJavaScriptDialog', accept: true, promptText: params[:default_prompt])
|
407
437
|
else
|
408
438
|
async_command('Page.handleJavaScriptDialog', accept: true, promptText: accept)
|
409
439
|
end
|
@@ -418,37 +448,38 @@ module Capybara::Apparition
|
|
418
448
|
end
|
419
449
|
end
|
420
450
|
|
421
|
-
@session.on 'Page.windowOpen' do
|
451
|
+
@session.on 'Page.windowOpen' do |**params|
|
422
452
|
puts "**** windowOpen was called with: #{params}" if ENV['DEBUG']
|
423
|
-
|
424
|
-
sleep 0.4 # wait a bit so the window has time to start loading
|
453
|
+
@browser.refresh_pages(opener: self)
|
425
454
|
end
|
426
455
|
|
427
|
-
@session.on 'Page.frameAttached' do
|
456
|
+
@session.on 'Page.frameAttached' do |**params|
|
428
457
|
puts "**** frameAttached called with #{params}" if ENV['DEBUG']
|
429
458
|
# @frames.get(params["frameId"]) = Frame.new(params)
|
430
459
|
end
|
431
460
|
|
432
|
-
@session.on 'Page.frameDetached' do |params|
|
433
|
-
@frames.delete(
|
434
|
-
puts "**** frameDetached called with #{params}" if ENV['DEBUG']
|
461
|
+
@session.on 'Page.frameDetached' do |frame_id:, **params|
|
462
|
+
@frames.delete(frame_id)
|
463
|
+
puts "**** frameDetached called with #{frame_id} : #{params}" if ENV['DEBUG']
|
435
464
|
end
|
436
465
|
|
437
|
-
@session.on 'Page.frameNavigated' do |
|
438
|
-
puts "**** frameNavigated called with #{
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
@frames.add(frame_params['id'], frame_params)
|
466
|
+
@session.on 'Page.frameNavigated' do |frame:|
|
467
|
+
puts "**** frameNavigated called with #{frame}" if ENV['DEBUG']
|
468
|
+
unless @frames.exists?(frame['id'])
|
469
|
+
puts "**** creating frame for #{frame['id']}" if ENV['DEBUG']
|
470
|
+
@frames.add(frame['id'], frame)
|
443
471
|
end
|
472
|
+
@frames.get(frame['id'])&.loading(frame['loaderId'] || -1)
|
444
473
|
end
|
445
474
|
|
446
|
-
@session.on 'Page.frameStartedLoading' do |
|
447
|
-
|
475
|
+
@session.on 'Page.frameStartedLoading' do |frame_id:|
|
476
|
+
puts "Setting loading for #{frame_id}" if ENV['DEBUG']
|
477
|
+
@frames.get(frame_id)&.loading(-1)
|
448
478
|
end
|
449
479
|
|
450
|
-
@session.on 'Page.frameStoppedLoading' do |
|
451
|
-
|
480
|
+
@session.on 'Page.frameStoppedLoading' do |frame_id:|
|
481
|
+
puts "Setting loaded for #{frame_id}" if ENV['DEBUG']
|
482
|
+
@frames.get(frame_id)&.loaded!
|
452
483
|
end
|
453
484
|
|
454
485
|
# @session.on 'Page.lifecycleEvent' do |params|
|
@@ -470,16 +501,12 @@ module Capybara::Apparition
|
|
470
501
|
main_frame.loaded! if @status_code != 200
|
471
502
|
end
|
472
503
|
|
473
|
-
@session.on 'Page.navigatedWithinDocument' do |params|
|
474
|
-
puts "**** navigatedWithinDocument called with #{params}" if ENV['DEBUG']
|
475
|
-
frame_id = params['frameId']
|
476
|
-
# @frames.get(frame_id).state = :loaded if frame_id == main_frame.id
|
504
|
+
@session.on 'Page.navigatedWithinDocument' do |frame_id:, **params|
|
505
|
+
puts "**** navigatedWithinDocument called with #{frame_id}: #{params}" if ENV['DEBUG']
|
477
506
|
@frames.get(frame_id).loaded! if frame_id == main_frame.id
|
478
507
|
end
|
479
508
|
|
480
|
-
@session.on 'Runtime.executionContextCreated' do |
|
481
|
-
puts "**** executionContextCreated: #{params}" if ENV['DEBUG']
|
482
|
-
context = params['context']
|
509
|
+
@session.on 'Runtime.executionContextCreated' do |context:|
|
483
510
|
frame_id = context.dig('auxData', 'frameId')
|
484
511
|
if context.dig('auxData', 'isDefault') && frame_id
|
485
512
|
if (frame = @frames.get(frame_id))
|
@@ -490,63 +517,78 @@ module Capybara::Apparition
|
|
490
517
|
end
|
491
518
|
end
|
492
519
|
|
493
|
-
@session.on 'Runtime.executionContextDestroyed' do |params|
|
494
|
-
puts "executionContextDestroyed: #{params}" if ENV['DEBUG']
|
495
|
-
@frames.destroy_context(
|
520
|
+
@session.on 'Runtime.executionContextDestroyed' do |execution_context_id:, **params|
|
521
|
+
puts "executionContextDestroyed: #{execution_context_id} : #{params}" if ENV['DEBUG']
|
522
|
+
@frames.destroy_context(execution_context_id)
|
496
523
|
end
|
497
524
|
|
498
|
-
@session.on 'Network.requestWillBeSent' do |
|
499
|
-
@open_resource_requests[
|
525
|
+
@session.on 'Network.requestWillBeSent' do |request_id:, request: nil, **|
|
526
|
+
@open_resource_requests[request_id] = request&.dig('url')
|
500
527
|
end
|
501
528
|
|
502
|
-
@session.on 'Network.responseReceived' do |
|
503
|
-
@open_resource_requests.delete(
|
529
|
+
@session.on 'Network.responseReceived' do |request_id:, **|
|
530
|
+
@open_resource_requests.delete(request_id)
|
504
531
|
temp_headers.clear
|
505
532
|
update_headers(async: true)
|
506
533
|
end
|
507
534
|
|
508
|
-
@session.on 'Network.requestWillBeSent' do
|
535
|
+
@session.on 'Network.requestWillBeSent' do |**params|
|
509
536
|
@network_traffic.push(NetworkTraffic::Request.new(params))
|
510
537
|
end
|
511
538
|
|
512
|
-
@session.on 'Network.responseReceived' do |
|
513
|
-
req = @network_traffic.find { |request| request.request_id ==
|
514
|
-
req.response = NetworkTraffic::Response.new(
|
539
|
+
@session.on 'Network.responseReceived' do |request_id:, response:, **|
|
540
|
+
req = @network_traffic.find { |request| request.request_id == request_id }
|
541
|
+
req.response = NetworkTraffic::Response.new(response) if req
|
515
542
|
end
|
516
543
|
|
517
|
-
@session.on 'Network.responseReceived' do |
|
518
|
-
if
|
519
|
-
@response_headers =
|
520
|
-
@status_code =
|
544
|
+
@session.on 'Network.responseReceived' do |type:, frame_id: nil, response: nil, **|
|
545
|
+
if type == 'Document'
|
546
|
+
@response_headers[frame_id] = response['headers']
|
547
|
+
@status_code = response['status']
|
521
548
|
end
|
522
549
|
end
|
523
550
|
|
524
|
-
@session.on 'Network.loadingFailed' do |params|
|
525
|
-
req = @network_traffic.find { |request| request.request_id ==
|
526
|
-
req&.blocked_params = params if
|
527
|
-
if
|
528
|
-
puts "Loading Failed - request: #{
|
551
|
+
@session.on 'Network.loadingFailed' do |type:, request_id:, blocked_reason: nil, error_text: nil, **params|
|
552
|
+
req = @network_traffic.find { |request| request.request_id == request_id }
|
553
|
+
req&.blocked_params = params if blocked_reason
|
554
|
+
if type == 'Document'
|
555
|
+
puts "Loading Failed - request: #{request_id} : #{error_text}" if ENV['DEBUG']
|
529
556
|
end
|
530
557
|
end
|
531
558
|
|
532
|
-
@session.on '
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
559
|
+
@session.on 'Fetch.requestPaused' do |request:, request_id:, resource_type:, **|
|
560
|
+
process_intercepted_fetch(request_id, request, resource_type)
|
561
|
+
end
|
562
|
+
|
563
|
+
@session.on 'Fetch.authRequired' do |request_id:, auth_challenge: nil, **|
|
564
|
+
next unless auth_challenge
|
565
|
+
|
566
|
+
credentials_response = if auth_challenge['source'] == 'Proxy'
|
567
|
+
if @proxy_auth_attempts.include?(request_id)
|
568
|
+
puts 'Cancelling proxy auth' if ENV['DEBUG']
|
569
|
+
{ response: 'CancelAuth' }
|
537
570
|
else
|
538
|
-
|
571
|
+
puts 'Replying with proxy auth credentials' if ENV['DEBUG']
|
572
|
+
@proxy_auth_attempts.push(request_id)
|
573
|
+
{ response: 'ProvideCredentials' }.merge(@browser.proxy_auth || {})
|
539
574
|
end
|
575
|
+
elsif @auth_attempts.include?(request_id)
|
576
|
+
puts 'Cancelling auth' if ENV['DEBUG']
|
577
|
+
{ response: 'CancelAuth' }
|
540
578
|
else
|
541
|
-
|
579
|
+
@auth_attempts.push(request_id)
|
580
|
+
puts 'Replying with auth credentials' if ENV['DEBUG']
|
581
|
+
{ response: 'ProvideCredentials' }.merge(@credentials || {})
|
542
582
|
end
|
583
|
+
|
584
|
+
async_command('Fetch.continueWithAuth', requestId: request_id, authChallengeResponse: credentials_response)
|
543
585
|
end
|
544
586
|
|
545
|
-
@session.on 'Runtime.consoleAPICalled' do
|
587
|
+
@session.on 'Runtime.consoleAPICalled' do |**params|
|
546
588
|
# {"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}]}}
|
547
|
-
details = params.dig(
|
548
|
-
@browser.console.log(params[
|
549
|
-
params[
|
589
|
+
details = params.dig(:stack_trace, 'callFrames')&.first
|
590
|
+
@browser.console.log(params[:type],
|
591
|
+
params[:args].map { |arg| arg['description'] || arg['value'] }.join(' ').to_s,
|
550
592
|
source: details['url'].empty? ? nil : details['url'],
|
551
593
|
line_number: details['lineNumber'].zero? ? nil : details['lineNumber'],
|
552
594
|
columnNumber: details['columnNumber'].zero? ? nil : details['columnNumber'])
|
@@ -565,15 +607,16 @@ module Capybara::Apparition
|
|
565
607
|
end
|
566
608
|
|
567
609
|
def register_js_error_handler
|
568
|
-
@session.on 'Runtime.exceptionThrown' do |
|
569
|
-
@js_error ||=
|
610
|
+
@session.on 'Runtime.exceptionThrown' do |exception_details: nil, **|
|
611
|
+
@js_error ||= exception_details&.dig('exception', 'description') if @raise_js_errors
|
570
612
|
|
571
|
-
details =
|
613
|
+
details = exception_details&.dig('stackTrace', 'callFrames')&.first ||
|
614
|
+
exception_details || {}
|
572
615
|
@browser.console.log('error',
|
573
|
-
|
574
|
-
source: details['url'].empty? ? nil : details['url'],
|
575
|
-
line_number: details['lineNumber'].zero? ? nil : details['lineNumber'],
|
576
|
-
columnNumber: details['columnNumber'].zero? ? nil : details['columnNumber'])
|
616
|
+
exception_details&.dig('exception', 'description'),
|
617
|
+
source: details['url'].to_s.empty? ? nil : details['url'],
|
618
|
+
line_number: details['lineNumber'].to_i.zero? ? nil : details['lineNumber'],
|
619
|
+
columnNumber: details['columnNumber'].to_i.zero? ? nil : details['columnNumber'])
|
577
620
|
end
|
578
621
|
end
|
579
622
|
|
@@ -584,11 +627,13 @@ module Capybara::Apparition
|
|
584
627
|
|
585
628
|
def setup_network_interception
|
586
629
|
async_command 'Network.setCacheDisabled', cacheDisabled: true
|
587
|
-
async_command '
|
630
|
+
async_command 'Fetch.enable', handleAuthRequests: true
|
588
631
|
end
|
589
632
|
|
590
|
-
def
|
633
|
+
def process_intercepted_fetch(interception_id, request, resource_type)
|
634
|
+
navigation = (resource_type == 'Document')
|
591
635
|
headers, url = request.values_at('headers', 'url')
|
636
|
+
headers = headers.merge(extra_headers)
|
592
637
|
|
593
638
|
unless @temp_headers.empty? || navigation # rubocop:disable Style/IfUnlessModifier
|
594
639
|
headers.delete_if { |name, value| @temp_headers[name] == value }
|
@@ -596,39 +641,27 @@ module Capybara::Apparition
|
|
596
641
|
unless @temp_no_redirect_headers.empty? || !navigation
|
597
642
|
headers.delete_if { |name, value| @temp_no_redirect_headers[name] == value }
|
598
643
|
end
|
599
|
-
if (accept = perm_headers.keys.find { |k|
|
644
|
+
if (accept = perm_headers.keys.find { |k| /accept/i.match? k })
|
600
645
|
headers[accept] = perm_headers[accept]
|
601
646
|
end
|
602
647
|
|
603
648
|
if @url_blacklist.any? { |r| url.match Regexp.escape(r).gsub('\*', '.*?') }
|
604
|
-
|
649
|
+
async_command('Fetch.failRequest', errorReason: 'Failed', requestId: interception_id)
|
605
650
|
elsif @url_whitelist.any?
|
606
651
|
if @url_whitelist.any? { |r| url.match Regexp.escape(r).gsub('\*', '.*?') }
|
607
|
-
|
652
|
+
async_command('Fetch.continueRequest',
|
653
|
+
requestId: interception_id,
|
654
|
+
headers: headers.map { |k, v| { name: k, value: v } })
|
608
655
|
else
|
609
|
-
|
656
|
+
async_command('Fetch.failRequest', errorReason: 'Failed', requestId: interception_id)
|
610
657
|
end
|
611
658
|
else
|
612
|
-
|
659
|
+
async_command('Fetch.continueRequest',
|
660
|
+
requestId: interception_id,
|
661
|
+
headers: headers.map { |k, v| { name: k, value: v } })
|
613
662
|
end
|
614
663
|
end
|
615
664
|
|
616
|
-
def continue_request(id, **params)
|
617
|
-
async_command 'Network.continueInterceptedRequest', interceptionId: id, **params
|
618
|
-
end
|
619
|
-
|
620
|
-
def block_request(id, reason)
|
621
|
-
async_command 'Network.continueInterceptedRequest', errorReason: reason, interceptionId: id
|
622
|
-
end
|
623
|
-
|
624
|
-
def current_frame
|
625
|
-
@frames.current
|
626
|
-
end
|
627
|
-
|
628
|
-
def main_frame
|
629
|
-
@frames.main
|
630
|
-
end
|
631
|
-
|
632
665
|
def go_history(delta)
|
633
666
|
history = command('Page.getNavigationHistory')
|
634
667
|
entry = history['entries'][history['currentIndex'] + delta]
|
@@ -667,7 +700,8 @@ module Capybara::Apparition
|
|
667
700
|
executionContextId: context_id,
|
668
701
|
arguments: args,
|
669
702
|
returnByValue: false,
|
670
|
-
awaitPromise: true
|
703
|
+
awaitPromise: true,
|
704
|
+
userGesture: true)
|
671
705
|
process_response(response)
|
672
706
|
end
|
673
707
|
|
@@ -688,8 +722,8 @@ module Capybara::Apparition
|
|
688
722
|
def process_response(response)
|
689
723
|
return nil if response.nil?
|
690
724
|
|
691
|
-
|
692
|
-
if
|
725
|
+
exception = response['exceptionDetails']&.dig('exception')
|
726
|
+
if exception
|
693
727
|
case exception['className']
|
694
728
|
when 'DOMException'
|
695
729
|
raise ::Capybara::Apparition::BrowserError.new('name' => exception['description'], 'args' => nil)
|
@@ -754,7 +788,11 @@ module Capybara::Apparition
|
|
754
788
|
function(){
|
755
789
|
let apparitionId=0;
|
756
790
|
return (function ider(obj){
|
757
|
-
if (obj &&
|
791
|
+
if (obj &&
|
792
|
+
(typeof obj == 'object') &&
|
793
|
+
!(obj instanceof HTMLElement) &&
|
794
|
+
!(obj instanceof CSSStyleDeclaration) &&
|
795
|
+
!obj.apparitionId){
|
758
796
|
obj.apparitionId = ++apparitionId;
|
759
797
|
Reflect.ownKeys(obj).forEach(key => ider(obj[key]))
|
760
798
|
}
|
@@ -780,12 +818,12 @@ module Capybara::Apparition
|
|
780
818
|
JS
|
781
819
|
|
782
820
|
CSS_FIND_JS = <<~JS
|
783
|
-
Array.from(document.querySelectorAll("
|
821
|
+
Array.from(document.querySelectorAll("%<selector>s"));
|
784
822
|
JS
|
785
823
|
|
786
824
|
XPATH_FIND_JS = <<~JS
|
787
825
|
(function(){
|
788
|
-
const xpath = document.evaluate("
|
826
|
+
const xpath = document.evaluate("%<selector>s", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
789
827
|
let results = [];
|
790
828
|
for (let i=0; i < xpath.snapshotLength; i++){
|
791
829
|
results.push(xpath.snapshotItem(i))
|