apparition 0.1.0 → 0.6.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 +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))
|