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 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