ferrum 0.1.1 → 0.1.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 +2 -3
- data/lib/ferrum/node.rb +8 -8
- data/lib/ferrum/page.rb +2 -1
- data/lib/ferrum/page/dom.rb +5 -5
- data/lib/ferrum/page/runtime.rb +68 -69
- data/lib/ferrum/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 858608216b6e3a287e7d47621bf9c44415ea2e5411cdc53e34c2e18c3fbd8153
|
4
|
+
data.tar.gz: 74d290a312092c690b77ef6afac4ae65839417559749188da22e34b655789dc5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fe68b7a02885080677a38cc347f7bf79ed094ed09dcd7f111aee613b345e3a6bfdaf1b744b266e3b1de11770bcc37f4cb5408a499b1ad4640e370e3cd0887f49
|
7
|
+
data.tar.gz: 721e01402142f201cf21aa3e91d9ac21e698a277dc276b32a72c93e044abe9c81c7707e4606ec82780b89214e4108bbd0730d4d47a9509510186aa4b856e8ebf
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Ferrum - fearless Ruby Chrome/Chromium driver
|
1
|
+
# Ferrum - fearless Ruby Chrome/Chromium driver
|
2
2
|
|
3
3
|
As simple as Puppeteer, though even simpler. It is Ruby clean and high-level API
|
4
4
|
to Chrome/Chromium through the DevTools Protocol. Runs headless by default,
|
@@ -19,8 +19,7 @@ Interact with a page:
|
|
19
19
|
browser = Ferrum::Browser.new
|
20
20
|
browser.goto("https://google.com")
|
21
21
|
input = browser.at_css("input[title='Search']")
|
22
|
-
input.send_keys("Ruby headless driver for Capybara")
|
23
|
-
input.send_keys(:Enter)
|
22
|
+
input.send_keys("Ruby headless driver for Capybara", :Enter)
|
24
23
|
browser.at_css("a > h3").text # => "machinio/cuprite: Headless Chrome driver for Capybara - GitHub"
|
25
24
|
browser.quit
|
26
25
|
```
|
data/lib/ferrum/node.rb
CHANGED
@@ -2,15 +2,15 @@
|
|
2
2
|
|
3
3
|
module Ferrum
|
4
4
|
class Node
|
5
|
-
attr_reader :page, :target_id, :node_id, :
|
5
|
+
attr_reader :page, :target_id, :node_id, :description
|
6
6
|
|
7
|
-
def initialize(page, target_id, node_id,
|
8
|
-
@page, @target_id, @node_id, @
|
9
|
-
page, target_id, node_id,
|
7
|
+
def initialize(page, target_id, node_id, description)
|
8
|
+
@page, @target_id, @node_id, @description =
|
9
|
+
page, target_id, node_id, description
|
10
10
|
end
|
11
11
|
|
12
12
|
def node?
|
13
|
-
|
13
|
+
description["nodeType"] == 1 # nodeType: 3, nodeName: "#text" e.g.
|
14
14
|
end
|
15
15
|
|
16
16
|
def page_send(name, *args)
|
@@ -101,7 +101,7 @@ module Ferrum
|
|
101
101
|
end
|
102
102
|
|
103
103
|
def tag_name
|
104
|
-
@tag_name ||=
|
104
|
+
@tag_name ||= description["nodeName"].downcase
|
105
105
|
end
|
106
106
|
|
107
107
|
def visible?
|
@@ -156,7 +156,7 @@ module Ferrum
|
|
156
156
|
# We compare backendNodeId because once nodeId is sent to frontend backend
|
157
157
|
# never returns same nodeId sending 0. In other words frontend is
|
158
158
|
# responsible for keeping track of node ids.
|
159
|
-
target_id == other.target_id &&
|
159
|
+
target_id == other.target_id && description["backendNodeId"] == other.description["backendNodeId"]
|
160
160
|
end
|
161
161
|
|
162
162
|
def send_keys(*keys)
|
@@ -169,7 +169,7 @@ module Ferrum
|
|
169
169
|
end
|
170
170
|
|
171
171
|
def inspect
|
172
|
-
%(#<#{self.class} @target_id=#{@target_id.inspect} @node_id=#{@node_id} @
|
172
|
+
%(#<#{self.class} @target_id=#{@target_id.inspect} @node_id=#{@node_id} @description=#{@description.inspect}>)
|
173
173
|
end
|
174
174
|
end
|
175
175
|
end
|
data/lib/ferrum/page.rb
CHANGED
@@ -53,7 +53,8 @@ module Ferrum
|
|
53
53
|
begin
|
54
54
|
@session_id = @browser.command("Target.attachToTarget", targetId: @target_id)["sessionId"]
|
55
55
|
rescue BrowserError => e
|
56
|
-
|
56
|
+
case e.message
|
57
|
+
when "No target with given id found"
|
57
58
|
raise NoSuchWindowError
|
58
59
|
else
|
59
60
|
raise
|
data/lib/ferrum/page/dom.rb
CHANGED
@@ -4,11 +4,11 @@ module Ferrum
|
|
4
4
|
class Page
|
5
5
|
module DOM
|
6
6
|
def current_url
|
7
|
-
|
7
|
+
evaluate("window.top.location.href")
|
8
8
|
end
|
9
9
|
|
10
10
|
def title
|
11
|
-
|
11
|
+
evaluate("window.top.document.title")
|
12
12
|
end
|
13
13
|
|
14
14
|
def body
|
@@ -37,7 +37,7 @@ module Ferrum
|
|
37
37
|
ids = command("DOM.querySelectorAll",
|
38
38
|
nodeId: node_id,
|
39
39
|
selector: selector)["nodeIds"]
|
40
|
-
ids.map { |id|
|
40
|
+
ids.map { |id| build_node(id) }.compact
|
41
41
|
end
|
42
42
|
|
43
43
|
def at_css(selector, within: nil)
|
@@ -46,12 +46,12 @@ module Ferrum
|
|
46
46
|
id = command("DOM.querySelector",
|
47
47
|
nodeId: node_id,
|
48
48
|
selector: selector)["nodeId"]
|
49
|
-
|
49
|
+
build_node(id)
|
50
50
|
end
|
51
51
|
|
52
52
|
private
|
53
53
|
|
54
|
-
def
|
54
|
+
def build_node(node_id)
|
55
55
|
description = command("DOM.describeNode", nodeId: node_id)
|
56
56
|
Node.new(self, target_id, node_id, description["node"])
|
57
57
|
rescue BrowserError => e
|
data/lib/ferrum/page/runtime.rb
CHANGED
@@ -29,65 +29,51 @@ module Ferrum
|
|
29
29
|
}.freeze
|
30
30
|
|
31
31
|
def evaluate(expression, *args)
|
32
|
-
|
33
|
-
handle(response)
|
34
|
-
end
|
35
|
-
|
36
|
-
def evaluate_in(context_id, expression)
|
37
|
-
response = call(expression, nil, { executionContextId: context_id })
|
38
|
-
handle(response)
|
39
|
-
end
|
40
|
-
|
41
|
-
def evaluate_on(node:, expression:, by_value: true, timeout: 0)
|
42
|
-
object_id = command("DOM.resolveNode", nodeId: node.node_id).dig("object", "objectId")
|
43
|
-
options = DEFAULT_OPTIONS.merge(objectId: object_id)
|
44
|
-
options[:functionDeclaration] = options[:functionDeclaration] % expression
|
45
|
-
options.merge!(returnByValue: by_value)
|
46
|
-
|
47
|
-
response = command("Runtime.callFunctionOn", timeout: timeout, **options)
|
48
|
-
.dig("result").tap { |r| handle_error(r) }
|
49
|
-
|
50
|
-
by_value ? response.dig("value") : handle(response)
|
32
|
+
call(*args, expression: expression)
|
51
33
|
end
|
52
34
|
|
53
35
|
def evaluate_async(expression, wait_time, *args)
|
54
|
-
|
55
|
-
handle(response)
|
36
|
+
call(*args, expression: expression, wait_time: wait_time * 1000, **EVALUATE_ASYNC_OPTIONS)
|
56
37
|
end
|
57
38
|
|
58
39
|
def execute(expression, *args)
|
59
|
-
call(expression,
|
40
|
+
call(*args, expression: expression, handle: false, **EXECUTE_OPTIONS)
|
60
41
|
true
|
61
42
|
end
|
62
43
|
|
44
|
+
def evaluate_on(node:, expression:, by_value: true, timeout: 0)
|
45
|
+
rescue_intermittent_error do
|
46
|
+
response = command("DOM.resolveNode", nodeId: node.node_id)
|
47
|
+
object_id = response.dig("object", "objectId")
|
48
|
+
options = DEFAULT_OPTIONS.merge(objectId: object_id)
|
49
|
+
options[:functionDeclaration] = options[:functionDeclaration] % expression
|
50
|
+
options.merge!(returnByValue: by_value)
|
51
|
+
|
52
|
+
response = command("Runtime.callFunctionOn",
|
53
|
+
timeout: timeout,
|
54
|
+
**options)["result"].tap { |r| handle_error(r) }
|
55
|
+
|
56
|
+
by_value ? response.dig("value") : handle_response(response)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
63
60
|
private
|
64
61
|
|
65
|
-
def call(
|
66
|
-
|
67
|
-
|
62
|
+
def call(*args, expression:, wait_time: nil, handle: true, **options)
|
63
|
+
rescue_intermittent_error do
|
64
|
+
arguments = prepare_args(args)
|
65
|
+
params = DEFAULT_OPTIONS.merge(options)
|
66
|
+
expression = [wait_time, expression] if wait_time
|
67
|
+
params[:functionDeclaration] = params[:functionDeclaration] % expression
|
68
|
+
params = params.merge(arguments: arguments)
|
69
|
+
unless params[:executionContextId]
|
70
|
+
params = params.merge(executionContextId: execution_context_id)
|
71
|
+
end
|
68
72
|
|
69
|
-
|
70
|
-
|
71
|
-
options[:functionDeclaration] = options[:functionDeclaration] % expression
|
72
|
-
options = options.merge(arguments: args)
|
73
|
-
unless options[:executionContextId]
|
74
|
-
options = options.merge(executionContextId: execution_context_id)
|
75
|
-
end
|
73
|
+
response = command("Runtime.callFunctionOn",
|
74
|
+
**params)["result"].tap { |r| handle_error(r) }
|
76
75
|
|
77
|
-
|
78
|
-
attempts ||= 1
|
79
|
-
response = command("Runtime.callFunctionOn", **options)
|
80
|
-
response.dig("result").tap { |r| handle_error(r) }
|
81
|
-
rescue BrowserError => e
|
82
|
-
case e.message
|
83
|
-
when "No node with given id found",
|
84
|
-
"Could not find node with given id",
|
85
|
-
"Cannot find context with specified id"
|
86
|
-
sleep 0.1
|
87
|
-
attempts += 1
|
88
|
-
options = options.merge(executionContextId: execution_context_id)
|
89
|
-
retry if attempts <= 3
|
90
|
-
end
|
76
|
+
handle ? handle_response(response) : response
|
91
77
|
end
|
92
78
|
end
|
93
79
|
|
@@ -103,20 +89,7 @@ module Ferrum
|
|
103
89
|
end
|
104
90
|
end
|
105
91
|
|
106
|
-
def
|
107
|
-
args.map do |arg|
|
108
|
-
if arg.is_a?(Node)
|
109
|
-
resolved = command("DOM.resolveNode", nodeId: arg.node_id)
|
110
|
-
{ objectId: resolved["object"]["objectId"] }
|
111
|
-
elsif arg.is_a?(Hash) && arg["objectId"]
|
112
|
-
{ objectId: arg["objectId"] }
|
113
|
-
else
|
114
|
-
{ value: arg }
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
def handle(response)
|
92
|
+
def handle_response(response)
|
120
93
|
case response["type"]
|
121
94
|
when "boolean", "number", "string"
|
122
95
|
response["value"]
|
@@ -129,18 +102,17 @@ module Ferrum
|
|
129
102
|
|
130
103
|
case response["subtype"]
|
131
104
|
when "node"
|
132
|
-
|
105
|
+
# We cannot store object_id in the node because page can be reloaded
|
106
|
+
# and node destroyed so we need to retrieve it each time for given id.
|
107
|
+
# Though we can try to subscribe to `DOM.childNodeRemoved` and
|
108
|
+
# `DOM.childNodeInserted` in the future.
|
133
109
|
node_id = command("DOM.requestNode", objectId: object_id)["nodeId"]
|
134
|
-
|
135
|
-
Node.new(self, target_id, node_id,
|
136
|
-
rescue BrowserError => e
|
137
|
-
# Node has disappeared while we were trying to get it
|
138
|
-
raise if e.message != "Could not find node with given id"
|
139
|
-
end
|
110
|
+
description = command("DOM.describeNode", nodeId: node_id)["node"]
|
111
|
+
Node.new(self, target_id, node_id, description)
|
140
112
|
when "array"
|
141
113
|
reduce_props(object_id, []) do |memo, key, value|
|
142
114
|
next(memo) unless (Integer(key) rescue nil)
|
143
|
-
value = value["objectId"] ?
|
115
|
+
value = value["objectId"] ? handle_response(value) : value["value"]
|
144
116
|
memo.insert(key.to_i, value)
|
145
117
|
end.compact
|
146
118
|
when "date"
|
@@ -149,13 +121,26 @@ module Ferrum
|
|
149
121
|
nil
|
150
122
|
else
|
151
123
|
reduce_props(object_id, {}) do |memo, key, value|
|
152
|
-
value = value["objectId"] ?
|
124
|
+
value = value["objectId"] ? handle_response(value) : value["value"]
|
153
125
|
memo.merge(key => value)
|
154
126
|
end
|
155
127
|
end
|
156
128
|
end
|
157
129
|
end
|
158
130
|
|
131
|
+
def prepare_args(args)
|
132
|
+
args.map do |arg|
|
133
|
+
if arg.is_a?(Node)
|
134
|
+
resolved = command("DOM.resolveNode", nodeId: arg.node_id)
|
135
|
+
{ objectId: resolved["object"]["objectId"] }
|
136
|
+
elsif arg.is_a?(Hash) && arg["objectId"]
|
137
|
+
{ objectId: arg["objectId"] }
|
138
|
+
else
|
139
|
+
{ value: arg }
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
159
144
|
def reduce_props(object_id, to)
|
160
145
|
if cyclic?(object_id).dig("result", "value")
|
161
146
|
return "(cyclic structure)"
|
@@ -189,6 +174,20 @@ module Ferrum
|
|
189
174
|
JS
|
190
175
|
)
|
191
176
|
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
|
192
191
|
end
|
193
192
|
end
|
194
193
|
end
|
data/lib/ferrum/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ferrum
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dmitry Vorotilin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-08-
|
11
|
+
date: 2019-08-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: websocket-driver
|