ferrum 0.1.2 → 0.2
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 +513 -10
- data/lib/ferrum.rb +85 -4
- data/lib/ferrum/browser.rb +16 -33
- data/lib/ferrum/browser/client.rb +18 -2
- data/lib/ferrum/browser/subscriber.rb +5 -1
- data/lib/ferrum/cookies.rb +97 -0
- data/lib/ferrum/headers.rb +50 -0
- data/lib/ferrum/{page/input.json → keyboard.json} +0 -0
- data/lib/ferrum/keyboard.rb +119 -0
- data/lib/ferrum/mouse.rb +53 -0
- data/lib/ferrum/network/intercepted_request.rb +53 -0
- data/lib/ferrum/node.rb +52 -115
- data/lib/ferrum/page.rb +30 -30
- data/lib/ferrum/page/dom.rb +22 -12
- data/lib/ferrum/page/frame.rb +13 -12
- data/lib/ferrum/page/input.rb +3 -148
- data/lib/ferrum/page/net.rb +66 -54
- data/lib/ferrum/page/runtime.rb +13 -19
- data/lib/ferrum/page/screenshot.rb +84 -0
- data/lib/ferrum/targets.rb +4 -8
- data/lib/ferrum/version.rb +1 -1
- metadata +9 -10
- data/lib/ferrum/browser/api.rb +0 -14
- data/lib/ferrum/browser/api/cookie.rb +0 -46
- data/lib/ferrum/browser/api/header.rb +0 -32
- data/lib/ferrum/browser/api/intercept.rb +0 -32
- data/lib/ferrum/browser/api/screenshot.rb +0 -78
- data/lib/ferrum/cookie.rb +0 -47
- data/lib/ferrum/errors.rb +0 -94
data/lib/ferrum/page/dom.rb
CHANGED
@@ -15,20 +15,32 @@ module Ferrum
|
|
15
15
|
evaluate("document.documentElement.outerHTML")
|
16
16
|
end
|
17
17
|
|
18
|
-
def property(node, name)
|
19
|
-
evaluate_on(node: node, expression: %Q(this["#{name}"]))
|
20
|
-
end
|
21
|
-
|
22
|
-
def select_file(node, value)
|
23
|
-
command("DOM.setFileInputFiles", nodeId: node.node_id, files: Array(value))
|
24
|
-
end
|
25
|
-
|
26
18
|
def at_xpath(selector, within: nil)
|
27
|
-
|
19
|
+
xpath(selector, within: within).first
|
28
20
|
end
|
29
21
|
|
22
|
+
# FIXME: Check within
|
30
23
|
def xpath(selector, within: nil)
|
31
|
-
|
24
|
+
evaluate_async(%(
|
25
|
+
try {
|
26
|
+
let selector = arguments[0];
|
27
|
+
let within = arguments[1] || document;
|
28
|
+
let results = [];
|
29
|
+
|
30
|
+
let xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
31
|
+
for (let i = 0; i < xpath.snapshotLength; i++) {
|
32
|
+
results.push(xpath.snapshotItem(i));
|
33
|
+
}
|
34
|
+
|
35
|
+
arguments[2](results);
|
36
|
+
} catch (error) {
|
37
|
+
// DOMException.INVALID_EXPRESSION_ERR is undefined, using pure code
|
38
|
+
if (error.code == DOMException.SYNTAX_ERR || error.code == 51) {
|
39
|
+
throw "Invalid Selector";
|
40
|
+
} else {
|
41
|
+
throw error;
|
42
|
+
}
|
43
|
+
}), timeout, selector, within)
|
32
44
|
end
|
33
45
|
|
34
46
|
def css(selector, within: nil)
|
@@ -54,8 +66,6 @@ module Ferrum
|
|
54
66
|
def build_node(node_id)
|
55
67
|
description = command("DOM.describeNode", nodeId: node_id)
|
56
68
|
Node.new(self, target_id, node_id, description["node"])
|
57
|
-
rescue BrowserError => e
|
58
|
-
node_id.zero? ? raise(NodeError.new(nil, e.response)) : raise
|
59
69
|
end
|
60
70
|
end
|
61
71
|
end
|
data/lib/ferrum/page/frame.rb
CHANGED
@@ -5,9 +5,9 @@ module Ferrum
|
|
5
5
|
module Frame
|
6
6
|
def execution_context_id
|
7
7
|
context_id = current_execution_context_id
|
8
|
-
raise
|
8
|
+
raise NoExecutionContextError unless context_id
|
9
9
|
context_id
|
10
|
-
rescue
|
10
|
+
rescue NoExecutionContextError
|
11
11
|
@event.reset
|
12
12
|
@event.wait(timeout) ? retry : raise
|
13
13
|
end
|
@@ -24,16 +24,17 @@ module Ferrum
|
|
24
24
|
evaluate("document.title")
|
25
25
|
end
|
26
26
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
@frame_stack.pop
|
31
|
-
when :top
|
32
|
-
@frame_stack = []
|
33
|
-
else
|
34
|
-
@frame_stack << handle
|
35
|
-
inject_extensions
|
27
|
+
def within_frame(frame)
|
28
|
+
unless frame.is_a?(Node)
|
29
|
+
raise ArgumentError, "Node is expected, but #{frame.class} is given"
|
36
30
|
end
|
31
|
+
|
32
|
+
frame_id = frame.description["frameId"]
|
33
|
+
@frame_stack << frame_id
|
34
|
+
inject_extensions
|
35
|
+
yield
|
36
|
+
ensure
|
37
|
+
@frame_stack.pop
|
37
38
|
end
|
38
39
|
|
39
40
|
private
|
@@ -53,7 +54,7 @@ module Ferrum
|
|
53
54
|
@client.on("Page.frameNavigated") do |params|
|
54
55
|
id = params["frame"]["id"]
|
55
56
|
if frame = @frames[id]
|
56
|
-
frame.merge!(params["frame"].select { |k,
|
57
|
+
frame.merge!(params["frame"].select { |k, _| k == "name" || k == "url" })
|
57
58
|
end
|
58
59
|
end
|
59
60
|
|
data/lib/ferrum/page/input.rb
CHANGED
@@ -5,154 +5,11 @@ require "json"
|
|
5
5
|
module Ferrum
|
6
6
|
class Page
|
7
7
|
module Input
|
8
|
-
KEYS = JSON.parse(File.read(File.expand_path("../input.json", __FILE__)))
|
9
|
-
MODIFIERS = { "alt" => 1, "ctrl" => 2, "control" => 2, "meta" => 4, "command" => 4, "shift" => 8 }
|
10
|
-
KEYS_MAPPING = {
|
11
|
-
cancel: "Cancel", help: "Help", backspace: "Backspace", tab: "Tab",
|
12
|
-
clear: "Clear", return: "Enter", enter: "Enter", shift: "Shift",
|
13
|
-
ctrl: "Control", control: "Control", alt: "Alt", pause: "Pause",
|
14
|
-
escape: "Escape", space: "Space", pageup: "PageUp", page_up: "PageUp",
|
15
|
-
pagedown: "PageDown", page_down: "PageDown", end: "End", home: "Home",
|
16
|
-
left: "ArrowLeft", up: "ArrowUp", right: "ArrowRight",
|
17
|
-
down: "ArrowDown", insert: "Insert", delete: "Delete",
|
18
|
-
semicolon: "Semicolon", equals: "Equal", numpad0: "Numpad0",
|
19
|
-
numpad1: "Numpad1", numpad2: "Numpad2", numpad3: "Numpad3",
|
20
|
-
numpad4: "Numpad4", numpad5: "Numpad5", numpad6: "Numpad6",
|
21
|
-
numpad7: "Numpad7", numpad8: "Numpad8", numpad9: "Numpad9",
|
22
|
-
multiply: "NumpadMultiply", add: "NumpadAdd",
|
23
|
-
separator: "NumpadDecimal", subtract: "NumpadSubtract",
|
24
|
-
decimal: "NumpadDecimal", divide: "NumpadDivide", f1: "F1", f2: "F2",
|
25
|
-
f3: "F3", f4: "F4", f5: "F5", f6: "F6", f7: "F7", f8: "F8", f9: "F9",
|
26
|
-
f10: "F10", f11: "F11", f12: "F12", meta: "Meta", command: "Meta",
|
27
|
-
}
|
28
|
-
|
29
|
-
def click(node, keys = [], offset = {})
|
30
|
-
x, y, modifiers = prepare_before_click(__method__, node, keys, offset)
|
31
|
-
command("Input.dispatchMouseEvent", type: "mousePressed", modifiers: modifiers, button: "left", x: x, y: y, clickCount: 1)
|
32
|
-
# Potential wait because if network event is triggered then we have to wait until it's over.
|
33
|
-
command("Input.dispatchMouseEvent", timeout: 0.05, type: "mouseReleased", modifiers: modifiers, button: "left", x: x, y: y, clickCount: 1)
|
34
|
-
end
|
35
|
-
|
36
|
-
def right_click(node, keys = [], offset = {})
|
37
|
-
x, y, modifiers = prepare_before_click(__method__, node, keys, offset)
|
38
|
-
command("Input.dispatchMouseEvent", type: "mousePressed", modifiers: modifiers, button: "right", x: x, y: y, clickCount: 1)
|
39
|
-
command("Input.dispatchMouseEvent", type: "mouseReleased", modifiers: modifiers, button: "right", x: x, y: y, clickCount: 1)
|
40
|
-
end
|
41
|
-
|
42
|
-
def double_click(node, keys = [], offset = {})
|
43
|
-
x, y, modifiers = prepare_before_click(__method__, node, keys, offset)
|
44
|
-
command("Input.dispatchMouseEvent", type: "mousePressed", modifiers: modifiers, button: "left", x: x, y: y, clickCount: 2)
|
45
|
-
command("Input.dispatchMouseEvent", type: "mouseReleased", modifiers: modifiers, button: "left", x: x, y: y, clickCount: 2)
|
46
|
-
end
|
47
|
-
|
48
|
-
def click_coordinates(x, y)
|
49
|
-
command("Input.dispatchMouseEvent", type: "mousePressed", button: "left", x: x, y: y, clickCount: 1)
|
50
|
-
# Potential wait because if network event is triggered then we have to wait until it's over.
|
51
|
-
command("Input.dispatchMouseEvent", timeout: 0.05, type: "mouseReleased", button: "left", x: x, y: y, clickCount: 1)
|
52
|
-
end
|
53
|
-
|
54
|
-
def focus(node)
|
55
|
-
command("DOM.focus", nodeId: node.node_id)
|
56
|
-
end
|
57
|
-
|
58
|
-
def hover(node)
|
59
|
-
raise NotImplemented
|
60
|
-
end
|
61
|
-
|
62
|
-
def set(node, value)
|
63
|
-
raise NotImplemented
|
64
|
-
end
|
65
|
-
|
66
|
-
def select(node, value)
|
67
|
-
raise NotImplemented
|
68
|
-
end
|
69
|
-
|
70
|
-
def trigger(node, event)
|
71
|
-
raise NotImplemented
|
72
|
-
end
|
73
|
-
|
74
8
|
def scroll_to(top, left)
|
75
9
|
execute("window.scrollTo(#{top}, #{left})")
|
76
10
|
end
|
77
11
|
|
78
|
-
def
|
79
|
-
# click(node)
|
80
|
-
# focus(node)
|
81
|
-
|
82
|
-
keys = normalize_keys(Array(keys))
|
83
|
-
|
84
|
-
keys.each do |key|
|
85
|
-
type = key[:text] ? "keyDown" : "rawKeyDown"
|
86
|
-
command("Input.dispatchKeyEvent", type: type, **key)
|
87
|
-
command("Input.dispatchKeyEvent", type: "keyUp", **key)
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
def normalize_keys(keys, pressed_keys = [], memo = [])
|
92
|
-
case keys
|
93
|
-
when Array
|
94
|
-
pressed_keys.push([])
|
95
|
-
memo += combine_strings(keys).map { |k| normalize_keys(k, pressed_keys, memo) }
|
96
|
-
pressed_keys.pop
|
97
|
-
memo.flatten.compact
|
98
|
-
when Symbol
|
99
|
-
key = keys.to_s.downcase
|
100
|
-
|
101
|
-
if MODIFIERS.keys.include?(key)
|
102
|
-
pressed_keys.last.push(key)
|
103
|
-
nil
|
104
|
-
else
|
105
|
-
_key = KEYS.fetch(KEYS_MAPPING[key.to_sym] || key.to_sym)
|
106
|
-
_key[:modifiers] = pressed_keys.flatten.map { |k| MODIFIERS[k] }.reduce(0, :|)
|
107
|
-
to_options(_key)
|
108
|
-
end
|
109
|
-
when String
|
110
|
-
pressed = pressed_keys.flatten
|
111
|
-
keys.each_char.map do |char|
|
112
|
-
if pressed.empty?
|
113
|
-
key = KEYS[char] || {}
|
114
|
-
key = key.merge(text: char, unmodifiedText: char)
|
115
|
-
[to_options(key)]
|
116
|
-
else
|
117
|
-
key = KEYS[char] || {}
|
118
|
-
text = pressed == ["shift"] ? char.upcase : char
|
119
|
-
key = key.merge(
|
120
|
-
text: text,
|
121
|
-
unmodifiedText: text,
|
122
|
-
isKeypad: key["location"] == 3,
|
123
|
-
modifiers: pressed.map { |k| MODIFIERS[k] }.reduce(0, :|),
|
124
|
-
)
|
125
|
-
|
126
|
-
modifiers = pressed.map { |k| to_options(KEYS.fetch(KEYS_MAPPING[k.to_sym])) }
|
127
|
-
modifiers + [to_options(key)]
|
128
|
-
end.flatten
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
def combine_strings(keys)
|
134
|
-
keys
|
135
|
-
.chunk { |k| k.is_a?(String) }
|
136
|
-
.map { |s, k| s ? [k.reduce(&:+)] : k }
|
137
|
-
.reduce(&:+)
|
138
|
-
end
|
139
|
-
|
140
|
-
private
|
141
|
-
|
142
|
-
def prepare_before_click(name, node, keys, offset)
|
143
|
-
# FIXME: scrollIntoViewport
|
144
|
-
# evaluate_on(node: node, expression: "this.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'})")
|
145
|
-
|
146
|
-
x, y = calculate_quads(node, offset[:x], offset[:y])
|
147
|
-
|
148
|
-
modifiers = keys.map { |k| MODIFIERS[k.to_s] }.compact.reduce(0, :|)
|
149
|
-
|
150
|
-
command("Input.dispatchMouseEvent", type: "mouseMoved", x: x, y: y)
|
151
|
-
|
152
|
-
[x, y, modifiers]
|
153
|
-
end
|
154
|
-
|
155
|
-
def calculate_quads(node, offset_x = nil, offset_y = nil)
|
12
|
+
def find_position(node, offset_x = nil, offset_y = nil)
|
156
13
|
quads = get_content_quads(node)
|
157
14
|
offset_x, offset_y = offset_x.to_i, offset_y.to_i
|
158
15
|
|
@@ -168,6 +25,8 @@ module Ferrum
|
|
168
25
|
end
|
169
26
|
end
|
170
27
|
|
28
|
+
private
|
29
|
+
|
171
30
|
def get_content_quads(node)
|
172
31
|
result = command("DOM.getContentQuads", nodeId: node.node_id)
|
173
32
|
raise "Node is either not visible or not an HTMLElement" if result["quads"].size == 0
|
@@ -180,10 +39,6 @@ module Ferrum
|
|
180
39
|
{x: quad[6], y: quad[7]}]
|
181
40
|
end.first
|
182
41
|
end
|
183
|
-
|
184
|
-
def to_options(hash)
|
185
|
-
hash.inject({}) { |memo, (k, v)| memo.merge(k.to_sym => v) }
|
186
|
-
end
|
187
42
|
end
|
188
43
|
end
|
189
44
|
end
|
data/lib/ferrum/page/net.rb
CHANGED
@@ -3,27 +3,75 @@
|
|
3
3
|
module Ferrum
|
4
4
|
class Page
|
5
5
|
module Net
|
6
|
+
RESOURCE_TYPES = %w[Document Stylesheet Image Media Font Script TextTrack
|
7
|
+
XHR Fetch EventSource WebSocket Manifest
|
8
|
+
SignedExchange Ping CSPViolationReport Other]
|
9
|
+
|
6
10
|
def proxy_authorize(user, password)
|
11
|
+
@proxy_authorized_ids ||= []
|
12
|
+
|
7
13
|
if user && password
|
8
|
-
|
9
|
-
|
14
|
+
intercept_request do |request, index, total|
|
15
|
+
if request.auth_challenge?(:proxy)
|
16
|
+
response = authorized_response(@proxy_authorized_ids,
|
17
|
+
request.interception_id,
|
18
|
+
user, password)
|
19
|
+
@proxy_authorized_ids << request.interception_id
|
20
|
+
request.continue(authChallengeResponse: response)
|
21
|
+
elsif index + 1 < total
|
22
|
+
next # There are other callbacks that can handle this, skip
|
23
|
+
else
|
24
|
+
request.continue
|
25
|
+
end
|
26
|
+
end
|
10
27
|
end
|
11
28
|
end
|
12
29
|
|
13
30
|
def authorize(user, password)
|
14
|
-
@
|
15
|
-
|
31
|
+
@authorized_ids ||= []
|
32
|
+
|
33
|
+
intercept_request do |request, index, total|
|
34
|
+
if request.auth_challenge?(:server)
|
35
|
+
response = authorized_response(@authorized_ids,
|
36
|
+
request.interception_id,
|
37
|
+
user, password)
|
38
|
+
|
39
|
+
@authorized_ids << request.interception_id
|
40
|
+
request.continue(authChallengeResponse: response)
|
41
|
+
elsif index + 1 < total
|
42
|
+
next # There are other callbacks that can handle this, skip
|
43
|
+
else
|
44
|
+
request.continue
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def intercept_request(pattern: "*", resource_type: nil, &block)
|
50
|
+
pattern = { urlPattern: pattern }
|
51
|
+
if resource_type && RESOURCE_TYPES.include?(resource_type.to_s)
|
52
|
+
pattern[:resourceType] = resource_type
|
53
|
+
end
|
54
|
+
|
55
|
+
command("Network.setRequestInterception", patterns: [pattern])
|
56
|
+
|
57
|
+
on_request_intercepted(&block) if block_given?
|
16
58
|
end
|
17
59
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
60
|
+
def on_request_intercepted(&block)
|
61
|
+
@client.on("Network.requestIntercepted") do |params, index, total|
|
62
|
+
request = Network::InterceptedRequest.new(self, params)
|
63
|
+
block.call(request, index, total)
|
64
|
+
end
|
21
65
|
end
|
22
66
|
|
23
67
|
def continue_request(interception_id, options = nil)
|
24
68
|
options ||= {}
|
25
69
|
options = options.merge(interceptionId: interception_id)
|
26
|
-
|
70
|
+
command("Network.continueInterceptedRequest", **options)
|
71
|
+
end
|
72
|
+
|
73
|
+
def abort_request(interception_id)
|
74
|
+
continue_request(interception_id, errorReason: "Aborted")
|
27
75
|
end
|
28
76
|
|
29
77
|
private
|
@@ -38,53 +86,17 @@ module Ferrum
|
|
38
86
|
@document_id = get_document_id
|
39
87
|
end
|
40
88
|
end
|
89
|
+
end
|
41
90
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
{ response: "CancelAuth" }
|
52
|
-
elsif @proxy_username && @proxy_password
|
53
|
-
{ response: "ProvideCredentials",
|
54
|
-
username: @proxy_username,
|
55
|
-
password: @proxy_password }
|
56
|
-
else
|
57
|
-
{ response: "CancelAuth" }
|
58
|
-
end
|
59
|
-
else
|
60
|
-
if @authorized_ids.include?(interception_id)
|
61
|
-
{ response: "CancelAuth" }
|
62
|
-
elsif @username && @password
|
63
|
-
{ response: "ProvideCredentials",
|
64
|
-
username: @username,
|
65
|
-
password: @password }
|
66
|
-
else
|
67
|
-
{ response: "CancelAuth" }
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
@authorized_ids << interception_id
|
72
|
-
continue_request(interception_id, authChallengeResponse: response)
|
73
|
-
elsif @browser.url_blacklist && !@browser.url_blacklist.empty?
|
74
|
-
if @browser.url_blacklist.any? { |r| r.match(url) }
|
75
|
-
continue_request(interception_id, errorReason: "Aborted")
|
76
|
-
else
|
77
|
-
continue_request(interception_id)
|
78
|
-
end
|
79
|
-
elsif @browser.url_whitelist && !@browser.url_whitelist.empty?
|
80
|
-
if @browser.url_whitelist.any? { |r| r.match(url) }
|
81
|
-
continue_request(interception_id)
|
82
|
-
else
|
83
|
-
continue_request(interception_id, errorReason: "Aborted")
|
84
|
-
end
|
85
|
-
else
|
86
|
-
continue_request(interception_id)
|
87
|
-
end
|
91
|
+
def authorized_response(ids, interception_id, username, password)
|
92
|
+
if ids.include?(interception_id)
|
93
|
+
{ response: "CancelAuth" }
|
94
|
+
elsif username && password
|
95
|
+
{ response: "ProvideCredentials",
|
96
|
+
username: username,
|
97
|
+
password: password }
|
98
|
+
else
|
99
|
+
{ response: "CancelAuth" }
|
88
100
|
end
|
89
101
|
end
|
90
102
|
end
|
data/lib/ferrum/page/runtime.rb
CHANGED
@@ -5,14 +5,14 @@ module Ferrum
|
|
5
5
|
module Runtime
|
6
6
|
EXECUTE_OPTIONS = {
|
7
7
|
returnByValue: true,
|
8
|
-
functionDeclaration: %
|
8
|
+
functionDeclaration: %(function() { %s })
|
9
9
|
}.freeze
|
10
10
|
DEFAULT_OPTIONS = {
|
11
|
-
functionDeclaration: %
|
11
|
+
functionDeclaration: %(function() { return %s })
|
12
12
|
}.freeze
|
13
13
|
EVALUATE_ASYNC_OPTIONS = {
|
14
14
|
awaitPromise: true,
|
15
|
-
functionDeclaration: %
|
15
|
+
functionDeclaration: %(
|
16
16
|
function() {
|
17
17
|
return new Promise((__resolve, __reject) => {
|
18
18
|
try {
|
@@ -42,7 +42,11 @@ module Ferrum
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def evaluate_on(node:, expression:, by_value: true, timeout: 0)
|
45
|
-
|
45
|
+
errors = [NodeNotFoundError, NoExecutionContextError]
|
46
|
+
max = ENV.fetch("FERRUM_INTERMITTENT_ATTEMPTS", 6).to_i
|
47
|
+
wait = ENV.fetch("FERRUM_INTERMITTENT_SLEEP", 0.1).to_f
|
48
|
+
|
49
|
+
Ferrum.with_attempts(errors: errors, max: max, wait: wait) do
|
46
50
|
response = command("DOM.resolveNode", nodeId: node.node_id)
|
47
51
|
object_id = response.dig("object", "objectId")
|
48
52
|
options = DEFAULT_OPTIONS.merge(objectId: object_id)
|
@@ -60,7 +64,11 @@ module Ferrum
|
|
60
64
|
private
|
61
65
|
|
62
66
|
def call(*args, expression:, wait_time: nil, handle: true, **options)
|
63
|
-
|
67
|
+
errors = [NodeNotFoundError, NoExecutionContextError]
|
68
|
+
max = ENV.fetch("FERRUM_INTERMITTENT_ATTEMPTS", 6).to_i
|
69
|
+
wait = ENV.fetch("FERRUM_INTERMITTENT_SLEEP", 0.1).to_f
|
70
|
+
|
71
|
+
Ferrum.with_attempts(errors: errors, max: max, wait: wait) do
|
64
72
|
arguments = prepare_args(args)
|
65
73
|
params = DEFAULT_OPTIONS.merge(options)
|
66
74
|
expression = [wait_time, expression] if wait_time
|
@@ -174,20 +182,6 @@ module Ferrum
|
|
174
182
|
JS
|
175
183
|
)
|
176
184
|
end
|
177
|
-
|
178
|
-
def rescue_intermittent_error(max = 6)
|
179
|
-
attempts ||= 0
|
180
|
-
yield
|
181
|
-
rescue BrowserError => e
|
182
|
-
case e.message
|
183
|
-
when "No node with given id found", # Node has disappeared while we were trying to get it
|
184
|
-
"Could not find node with given id",
|
185
|
-
"Cannot find context with specified id" # Context is lost, page is reloading
|
186
|
-
sleep 0.1
|
187
|
-
attempts += 1
|
188
|
-
attempts < max ? retry : raise
|
189
|
-
end
|
190
|
-
end
|
191
185
|
end
|
192
186
|
end
|
193
187
|
end
|