ferrum 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5cb7e946aa6600d6ee5beb78a3d00d77d4a4ebc58cf7b72951327dede08d4803
4
- data.tar.gz: 3e5755f718fb2faede6e8fb675fa69d5c1ea2d7934205d0a56bb445c8b9d0a42
3
+ metadata.gz: 858608216b6e3a287e7d47621bf9c44415ea2e5411cdc53e34c2e18c3fbd8153
4
+ data.tar.gz: 74d290a312092c690b77ef6afac4ae65839417559749188da22e34b655789dc5
5
5
  SHA512:
6
- metadata.gz: 4a2536b9335c8784c2d4f315262e38735e3567560912f9cbedd725619e65417f90b84a0b00e0b8391f4afae225ff04d09159e8062c07d3622200839abadd499a
7
- data.tar.gz: 9ca06d5a73e1bd4bfa11e8cbca9def9b800e0740ce5cf8037b49907b845a88fddbb5c30296fccd3f5aba5c12d8c31e85559c1e1bd839ad74c6e6c8cd0cbc7e6f
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, :desc
5
+ attr_reader :page, :target_id, :node_id, :description
6
6
 
7
- def initialize(page, target_id, node_id, desc)
8
- @page, @target_id, @node_id, @desc =
9
- page, target_id, node_id, desc
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
- desc["nodeType"] == 1 # nodeType: 3, nodeName: "#text" e.g.
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 ||= desc["nodeName"].downcase
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 && desc["backendNodeId"] == other.desc["backendNodeId"]
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} @desc=#{@desc.inspect}>)
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
- if e.message == "No target with given id found"
56
+ case e.message
57
+ when "No target with given id found"
57
58
  raise NoSuchWindowError
58
59
  else
59
60
  raise
@@ -4,11 +4,11 @@ module Ferrum
4
4
  class Page
5
5
  module DOM
6
6
  def current_url
7
- evaluate_in(execution_context_id, "window.top.location.href")
7
+ evaluate("window.top.location.href")
8
8
  end
9
9
 
10
10
  def title
11
- evaluate_in(execution_context_id, "window.top.document.title")
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| _build_node(id) }.compact
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
- _build_node(id)
49
+ build_node(id)
50
50
  end
51
51
 
52
52
  private
53
53
 
54
- def _build_node(node_id)
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
@@ -29,65 +29,51 @@ module Ferrum
29
29
  }.freeze
30
30
 
31
31
  def evaluate(expression, *args)
32
- response = call(expression, nil, nil, *args)
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
- response = call(expression, wait_time * 1000, EVALUATE_ASYNC_OPTIONS, *args)
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, nil, EXECUTE_OPTIONS, *args)
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(expression, wait_time, options = nil, *args)
66
- options ||= {}
67
- args = prepare_args(args)
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
- options = DEFAULT_OPTIONS.merge(options)
70
- expression = [wait_time, expression] if wait_time
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
- begin
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 prepare_args(args)
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
- begin
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
- desc = command("DOM.describeNode", nodeId: node_id)["node"]
135
- Node.new(self, target_id, node_id, desc)
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"] ? handle(value) : value["value"]
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"] ? handle(value) : value["value"]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ferrum
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
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.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-26 00:00:00.000000000 Z
11
+ date: 2019-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: websocket-driver