ferrum 0.11 → 0.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +178 -29
  4. data/lib/ferrum/browser/binary.rb +46 -0
  5. data/lib/ferrum/browser/client.rb +13 -12
  6. data/lib/ferrum/browser/command.rb +7 -8
  7. data/lib/ferrum/browser/options/base.rb +1 -7
  8. data/lib/ferrum/browser/options/chrome.rb +17 -11
  9. data/lib/ferrum/browser/options/firefox.rb +11 -4
  10. data/lib/ferrum/browser/process.rb +41 -35
  11. data/lib/ferrum/browser/subscriber.rb +1 -3
  12. data/lib/ferrum/browser/web_socket.rb +9 -12
  13. data/lib/ferrum/browser/xvfb.rb +4 -8
  14. data/lib/ferrum/browser.rb +44 -12
  15. data/lib/ferrum/context.rb +6 -2
  16. data/lib/ferrum/contexts.rb +10 -8
  17. data/lib/ferrum/cookies.rb +10 -9
  18. data/lib/ferrum/errors.rb +115 -0
  19. data/lib/ferrum/frame/runtime.rb +20 -17
  20. data/lib/ferrum/frame.rb +32 -24
  21. data/lib/ferrum/headers.rb +2 -2
  22. data/lib/ferrum/keyboard.rb +11 -11
  23. data/lib/ferrum/mouse.rb +8 -7
  24. data/lib/ferrum/network/auth_request.rb +7 -2
  25. data/lib/ferrum/network/exchange.rb +14 -10
  26. data/lib/ferrum/network/intercepted_request.rb +10 -8
  27. data/lib/ferrum/network/request.rb +5 -0
  28. data/lib/ferrum/network/response.rb +4 -4
  29. data/lib/ferrum/network.rb +124 -35
  30. data/lib/ferrum/node.rb +69 -23
  31. data/lib/ferrum/page/animation.rb +0 -1
  32. data/lib/ferrum/page/frames.rb +46 -20
  33. data/lib/ferrum/page/screenshot.rb +51 -65
  34. data/lib/ferrum/page/stream.rb +38 -0
  35. data/lib/ferrum/page/tracing.rb +71 -0
  36. data/lib/ferrum/page.rb +81 -36
  37. data/lib/ferrum/proxy.rb +58 -0
  38. data/lib/ferrum/{rbga.rb → rgba.rb} +4 -2
  39. data/lib/ferrum/target.rb +1 -0
  40. data/lib/ferrum/utils/attempt.rb +20 -0
  41. data/lib/ferrum/utils/elapsed_time.rb +27 -0
  42. data/lib/ferrum/utils/platform.rb +28 -0
  43. data/lib/ferrum/version.rb +1 -1
  44. data/lib/ferrum.rb +4 -146
  45. metadata +60 -51
@@ -46,7 +46,7 @@ module Ferrum
46
46
  JS
47
47
 
48
48
  def evaluate(expression, *args)
49
- expression = "function() { return %s }" % expression
49
+ expression = format("function() { return %s }", expression)
50
50
  call(expression: expression, arguments: args)
51
51
  end
52
52
 
@@ -66,12 +66,12 @@ module Ferrum
66
66
  }
67
67
  JS
68
68
 
69
- expression = template % [wait * 1000, expression]
69
+ expression = format(template, wait * 1000, expression)
70
70
  call(expression: expression, arguments: args, awaitPromise: true)
71
71
  end
72
72
 
73
73
  def execute(expression, *args)
74
- expression = "function() { %s }" % expression
74
+ expression = format("function() { %s }", expression)
75
75
  call(expression: expression, arguments: args, handle: false, returnByValue: true)
76
76
  true
77
77
  end
@@ -82,7 +82,7 @@ module Ferrum
82
82
 
83
83
  def evaluate_on(node:, expression:, by_value: true, wait: 0)
84
84
  options = { handle: true }
85
- expression = "function() { return %s }" % expression
85
+ expression = format("function() { return %s }", expression)
86
86
  options = { handle: false, returnByValue: true } if by_value
87
87
  call(expression: expression, on: node, wait: wait, **options)
88
88
  end
@@ -119,9 +119,10 @@ module Ferrum
119
119
 
120
120
  def call(expression:, arguments: [], on: nil, wait: 0, handle: true, **options)
121
121
  errors = [NodeNotFoundError, NoExecutionContextError]
122
- attempts, sleep = INTERMITTENT_ATTEMPTS, INTERMITTENT_SLEEP
122
+ sleep = INTERMITTENT_SLEEP
123
+ attempts = INTERMITTENT_ATTEMPTS
123
124
 
124
- Ferrum.with_attempts(errors: errors, max: attempts, wait: sleep) do
125
+ Utils::Attempt.with_retry(errors: errors, max: attempts, wait: sleep) do
125
126
  params = options.dup
126
127
 
127
128
  if on
@@ -141,7 +142,7 @@ module Ferrum
141
142
  handle_error(response)
142
143
  response = response["result"]
143
144
 
144
- handle ? handle_response(response) : response.dig("value")
145
+ handle ? handle_response(response) : response["value"]
145
146
  end
146
147
  end
147
148
 
@@ -154,7 +155,7 @@ module Ferrum
154
155
  when /\AError: timed out promise/
155
156
  raise ScriptTimeoutError
156
157
  else
157
- raise JavaScriptError.new(result)
158
+ raise JavaScriptError.new(result, response.dig("exceptionDetails", "stackTrace"))
158
159
  end
159
160
  end
160
161
 
@@ -171,16 +172,17 @@ module Ferrum
171
172
 
172
173
  case response["subtype"]
173
174
  when "node"
174
- # We cannot store object_id in the node because page can be reloaded
175
- # and node destroyed so we need to retrieve it each time for given id.
176
- # Though we can try to subscribe to `DOM.childNodeRemoved` and
177
- # `DOM.childNodeInserted` in the future.
178
- node_id = @page.command("DOM.requestNode", objectId: object_id)["nodeId"]
179
- description = @page.command("DOM.describeNode", nodeId: node_id)["node"]
180
- Node.new(self, @page.target_id, node_id, description)
175
+ # We cannot store object_id in the node because page can be reloaded
176
+ # and node destroyed so we need to retrieve it each time for given id.
177
+ # Though we can try to subscribe to `DOM.childNodeRemoved` and
178
+ # `DOM.childNodeInserted` in the future.
179
+ node_id = @page.command("DOM.requestNode", objectId: object_id)["nodeId"]
180
+ description = @page.command("DOM.describeNode", nodeId: node_id)["node"]
181
+ Node.new(self, @page.target_id, node_id, description)
181
182
  when "array"
182
183
  reduce_props(object_id, []) do |memo, key, value|
183
- next(memo) unless (Integer(key) rescue nil)
184
+ next(memo) unless Integer(key, exception: false)
185
+
184
186
  value = value["objectId"] ? handle_response(value) : value["value"]
185
187
  memo.insert(key.to_i, value)
186
188
  end.compact
@@ -212,11 +214,12 @@ module Ferrum
212
214
 
213
215
  def reduce_props(object_id, to)
214
216
  if cyclic?(object_id).dig("result", "value")
215
- return to.is_a?(Array) ? [cyclic_object] : cyclic_object
217
+ to.is_a?(Array) ? [cyclic_object] : cyclic_object
216
218
  else
217
219
  props = @page.command("Runtime.getProperties", ownProperties: true, objectId: object_id)
218
220
  props["result"].reduce(to) do |memo, prop|
219
221
  next(memo) unless prop["enumerable"]
222
+
220
223
  yield(memo, prop["name"], prop["value"])
221
224
  end
222
225
  end
data/lib/ferrum/frame.rb CHANGED
@@ -5,21 +5,28 @@ require "ferrum/frame/runtime"
5
5
 
6
6
  module Ferrum
7
7
  class Frame
8
- include DOM, Runtime
8
+ include DOM
9
+ include Runtime
10
+
11
+ STATE_VALUES = %i[
12
+ started_loading
13
+ navigated
14
+ stopped_loading
15
+ ].freeze
9
16
 
10
- attr_reader :page, :parent_id, :state
11
17
  attr_accessor :id, :name
18
+ attr_reader :page, :parent_id, :state
12
19
 
13
20
  def initialize(id, page, parent_id = nil)
14
- @execution_id = nil
15
- @id, @page, @parent_id = id, page, parent_id
21
+ @id = id
22
+ @page = page
23
+ @parent_id = parent_id
24
+ @execution_id = Concurrent::MVar.new
16
25
  end
17
26
 
18
- # Can be one of:
19
- # * started_loading
20
- # * navigated
21
- # * stopped_loading
22
27
  def state=(value)
28
+ raise ArgumentError unless STATE_VALUES.include?(value)
29
+
23
30
  @state = value
24
31
  end
25
32
 
@@ -35,7 +42,7 @@ module Ferrum
35
42
  @parent_id.nil?
36
43
  end
37
44
 
38
- def set_content(html)
45
+ def content=(html)
39
46
  evaluate_async(%(
40
47
  document.open();
41
48
  document.write(arguments[0]);
@@ -43,29 +50,30 @@ module Ferrum
43
50
  arguments[1](true);
44
51
  ), @page.timeout, html)
45
52
  end
46
-
47
- def execution_id?(execution_id)
48
- @execution_id == execution_id
49
- end
53
+ alias set_content content=
50
54
 
51
55
  def execution_id
52
- raise NoExecutionContextError unless @execution_id
53
- @execution_id
54
- rescue NoExecutionContextError
55
- @page.event.reset
56
- @page.event.wait(@page.timeout) ? retry : raise
57
- end
56
+ value = @execution_id.borrow(@page.timeout, &:itself)
57
+ raise NoExecutionContextError if value.instance_of?(Object)
58
58
 
59
- def set_execution_id(value)
60
- @execution_id ||= value
59
+ value
61
60
  end
62
61
 
63
- def reset_execution_id
64
- @execution_id = nil
62
+ def execution_id=(value)
63
+ if value.nil?
64
+ @execution_id.try_take!
65
+ else
66
+ @execution_id.try_put!(value)
67
+ end
65
68
  end
66
69
 
67
70
  def inspect
68
- %(#<#{self.class} @id=#{@id.inspect} @parent_id=#{@parent_id.inspect} @name=#{@name.inspect} @state=#{@state.inspect} @execution_id=#{@execution_id.inspect}>)
71
+ "#<#{self.class} " \
72
+ "@id=#{@id.inspect} " \
73
+ "@parent_id=#{@parent_id.inspect} " \
74
+ "@name=#{@name.inspect} " \
75
+ "@state=#{@state.inspect} " \
76
+ "@execution_id=#{@execution_id.inspect}>"
69
77
  end
70
78
  end
71
79
  end
@@ -39,12 +39,12 @@ module Ferrum
39
39
  private
40
40
 
41
41
  def set_overrides(user_agent: nil, accept_language: nil, platform: nil)
42
- options = Hash.new
42
+ options = {}
43
43
  options[:userAgent] = user_agent || @page.browser.default_user_agent
44
44
  options[:acceptLanguage] = accept_language if accept_language
45
45
  options[:platform] if platform
46
46
 
47
- @page.command("Network.setUserAgentOverride", **options) if !options.empty?
47
+ @page.command("Network.setUserAgentOverride", **options) unless options.empty?
48
48
  end
49
49
  end
50
50
  end
@@ -4,14 +4,14 @@ require "json"
4
4
 
5
5
  module Ferrum
6
6
  class Keyboard
7
- KEYS = JSON.parse(File.read(File.expand_path("../keyboard.json", __FILE__)))
7
+ KEYS = JSON.parse(File.read(File.expand_path("keyboard.json", __dir__)))
8
8
  MODIFIERS = { "alt" => 1, "ctrl" => 2, "control" => 2,
9
- "meta" => 4, "command" => 4, "shift" => 8 }
9
+ "meta" => 4, "command" => 4, "shift" => 8 }.freeze
10
10
  KEYS_MAPPING = {
11
11
  cancel: "Cancel", help: "Help", backspace: "Backspace", tab: "Tab",
12
12
  clear: "Clear", return: "Enter", enter: "Enter", shift: "Shift",
13
13
  ctrl: "Control", control: "Control", alt: "Alt", pause: "Pause",
14
- escape: "Escape", space: "Space", pageup: "PageUp", page_up: "PageUp",
14
+ escape: "Escape", space: "Space", pageup: "PageUp", page_up: "PageUp",
15
15
  pagedown: "PageDown", page_down: "PageDown", end: "End", home: "Home",
16
16
  left: "ArrowLeft", up: "ArrowUp", right: "ArrowRight",
17
17
  down: "ArrowDown", insert: "Insert", delete: "Delete",
@@ -23,8 +23,8 @@ module Ferrum
23
23
  separator: "NumpadDecimal", subtract: "NumpadSubtract",
24
24
  decimal: "NumpadDecimal", divide: "NumpadDivide", f1: "F1", f2: "F2",
25
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
- }
26
+ f10: "F10", f11: "F11", f12: "F12", meta: "Meta", command: "Meta"
27
+ }.freeze
28
28
 
29
29
  def initialize(page)
30
30
  @page = page
@@ -77,25 +77,25 @@ module Ferrum
77
77
  pressed_keys.last.push(key)
78
78
  nil
79
79
  else
80
- _key = KEYS.fetch(KEYS_MAPPING[key.to_sym] || key.to_sym)
81
- _key[:modifiers] = pressed_keys.flatten.map { |k| MODIFIERS[k] }.reduce(0, :|)
82
- to_options(_key)
80
+ key = KEYS.fetch(KEYS_MAPPING[key.to_sym] || key.to_sym)
81
+ key[:modifiers] = pressed_keys.flatten.map { |k| MODIFIERS[k] }.reduce(0, :|)
82
+ to_options(key)
83
83
  end
84
84
  when String
85
85
  pressed = pressed_keys.flatten
86
86
  keys.each_char.map do |char|
87
+ key = KEYS[char] || {}
88
+
87
89
  if pressed.empty?
88
- key = KEYS[char] || {}
89
90
  key = key.merge(text: char, unmodifiedText: char)
90
91
  [to_options(key)]
91
92
  else
92
- key = KEYS[char] || {}
93
93
  text = pressed == ["shift"] ? char.upcase : char
94
94
  key = key.merge(
95
95
  text: text,
96
96
  unmodifiedText: text,
97
97
  isKeypad: key["location"] == 3,
98
- modifiers: pressed.map { |k| MODIFIERS[k] }.reduce(0, :|),
98
+ modifiers: pressed.map { |k| MODIFIERS[k] }.reduce(0, :|)
99
99
  )
100
100
 
101
101
  modifiers = pressed.map { |k| to_options(KEYS.fetch(KEYS_MAPPING[k.to_sym])) }
data/lib/ferrum/mouse.rb CHANGED
@@ -33,12 +33,14 @@ module Ferrum
33
33
  end
34
34
 
35
35
  def move(x:, y:, steps: 1)
36
- from_x, from_y = @x, @y
37
- @x, @y = x, y
36
+ from_x = @x
37
+ from_y = @y
38
+ @x = x
39
+ @y = y
38
40
 
39
41
  steps.times do |i|
40
- new_x = from_x + (@x - from_x) * ((i + 1) / steps.to_f)
41
- new_y = from_y + (@y - from_y) * ((i + 1) / steps.to_f)
42
+ new_x = from_x + ((@x - from_x) * ((i + 1) / steps.to_f))
43
+ new_y = from_y + ((@y - from_y) * ((i + 1) / steps.to_f))
42
44
 
43
45
  @page.command("Input.dispatchMouseEvent",
44
46
  slowmoable: true,
@@ -61,9 +63,8 @@ module Ferrum
61
63
 
62
64
  def validate_button(button)
63
65
  button = button.to_s
64
- unless VALID_BUTTONS.include?(button)
65
- raise "Invalid button: #{button}"
66
- end
66
+ raise "Invalid button: #{button}" unless VALID_BUTTONS.include?(button)
67
+
67
68
  button
68
69
  end
69
70
  end
@@ -6,7 +6,8 @@ module Ferrum
6
6
  attr_accessor :request_id, :frame_id, :resource_type
7
7
 
8
8
  def initialize(page, params)
9
- @page, @params = page, params
9
+ @page = page
10
+ @params = params
10
11
  @request_id = params["requestId"]
11
12
  @frame_id = params["frameId"]
12
13
  @resource_type = params["resourceType"]
@@ -55,7 +56,11 @@ module Ferrum
55
56
  end
56
57
 
57
58
  def inspect
58
- %(#<#{self.class} @request_id=#{@request_id.inspect} @frame_id=#{@frame_id.inspect} @resource_type=#{@resource_type.inspect} @request=#{@request.inspect}>)
59
+ "#<#{self.class} " \
60
+ "@request_id=#{@request_id.inspect} " \
61
+ "@frame_id=#{@frame_id.inspect} " \
62
+ "@resource_type=#{@resource_type.inspect} " \
63
+ "@request=#{@request.inspect}>"
59
64
  end
60
65
  end
61
66
  end
@@ -4,11 +4,11 @@ module Ferrum
4
4
  class Network
5
5
  class Exchange
6
6
  attr_reader :id
7
- attr_accessor :intercepted_request
8
- attr_accessor :request, :response, :error
7
+ attr_accessor :intercepted_request, :request, :response, :error
9
8
 
10
9
  def initialize(page, id)
11
- @page, @id = page, id
10
+ @id = id
11
+ @page = page
12
12
  @intercepted_request = nil
13
13
  @request = @response = @error = nil
14
14
  end
@@ -23,7 +23,7 @@ module Ferrum
23
23
  end
24
24
 
25
25
  def blocked?
26
- intercepted_request && intercepted_request.status?(:aborted)
26
+ intercepted? && intercepted_request.status?(:aborted)
27
27
  end
28
28
 
29
29
  def finished?
@@ -34,17 +34,21 @@ module Ferrum
34
34
  !finished?
35
35
  end
36
36
 
37
+ def intercepted?
38
+ intercepted_request
39
+ end
40
+
37
41
  def to_a
38
42
  [request, response, error]
39
43
  end
40
44
 
41
45
  def inspect
42
- "#<#{self.class} "\
43
- "@id=#{@id.inspect} "\
44
- "@intercepted_request=#{@intercepted_request.inspect} "\
45
- "@request=#{@request.inspect} "\
46
- "@response=#{@response.inspect} "\
47
- "@error=#{@error.inspect}>"
46
+ "#<#{self.class} " \
47
+ "@id=#{@id.inspect} " \
48
+ "@intercepted_request=#{@intercepted_request.inspect} " \
49
+ "@request=#{@request.inspect} " \
50
+ "@response=#{@response.inspect} " \
51
+ "@error=#{@error.inspect}>"
48
52
  end
49
53
  end
50
54
  end
@@ -9,7 +9,8 @@ module Ferrum
9
9
 
10
10
  def initialize(page, params)
11
11
  @status = nil
12
- @page, @params = page, params
12
+ @page = page
13
+ @params = params
13
14
  @request_id = params["requestId"]
14
15
  @frame_id = params["frameId"]
15
16
  @resource_type = params["resourceType"]
@@ -30,15 +31,12 @@ module Ferrum
30
31
  end
31
32
 
32
33
  def respond(**options)
33
- has_body = options.has_key?(:body)
34
+ has_body = options.key?(:body)
34
35
  headers = has_body ? { "content-length" => options.fetch(:body, "").length } : {}
35
36
  headers = headers.merge(options.fetch(:responseHeaders, {}))
36
37
 
37
- options = {responseCode: 200}.merge(options)
38
- options = options.merge({
39
- requestId: request_id,
40
- responseHeaders: header_array(headers),
41
- })
38
+ options = { responseCode: 200 }.merge(options)
39
+ options = options.merge(requestId: request_id, responseHeaders: header_array(headers))
42
40
  options = options.merge(body: Base64.strict_encode64(options.fetch(:body, ""))) if has_body
43
41
 
44
42
  @status = :responded
@@ -77,7 +75,11 @@ module Ferrum
77
75
  end
78
76
 
79
77
  def inspect
80
- %(#<#{self.class} @request_id=#{@request_id.inspect} @frame_id=#{@frame_id.inspect} @resource_type=#{@resource_type.inspect} @request=#{@request.inspect}>)
78
+ "#<#{self.class} " \
79
+ "@request_id=#{@request_id.inspect} " \
80
+ "@frame_id=#{@frame_id.inspect} " \
81
+ "@resource_type=#{@resource_type.inspect} " \
82
+ "@request=#{@request.inspect}>"
81
83
  end
82
84
 
83
85
  private
@@ -45,6 +45,11 @@ module Ferrum
45
45
  def time
46
46
  @time ||= Time.strptime(@params["wallTime"].to_s, "%s")
47
47
  end
48
+
49
+ def post_data
50
+ @request["postData"]
51
+ end
52
+ alias body post_data
48
53
  end
49
54
  end
50
55
  end
@@ -3,7 +3,7 @@
3
3
  module Ferrum
4
4
  class Network
5
5
  class Response
6
- attr_reader :body_size
6
+ attr_reader :body_size, :params
7
7
 
8
8
  def initialize(page, params)
9
9
  @page = page
@@ -34,7 +34,7 @@ module Ferrum
34
34
  def headers_size
35
35
  @response["encodedDataLength"]
36
36
  end
37
-
37
+
38
38
  def type
39
39
  @params["type"]
40
40
  end
@@ -55,8 +55,8 @@ module Ferrum
55
55
  def body
56
56
  @body ||= begin
57
57
  body, encoded = @page
58
- .command("Network.getResponseBody", requestId: id)
59
- .values_at("body", "base64Encoded")
58
+ .command("Network.getResponseBody", requestId: id)
59
+ .values_at("body", "base64Encoded")
60
60
  encoded ? Base64.decode64(body) : body
61
61
  end
62
62
  end