ferrum 0.12 → 0.14

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -22
  3. data/lib/ferrum/browser/client.rb +6 -5
  4. data/lib/ferrum/browser/command.rb +9 -6
  5. data/lib/ferrum/browser/options/base.rb +1 -4
  6. data/lib/ferrum/browser/options/chrome.rb +22 -10
  7. data/lib/ferrum/browser/options/firefox.rb +3 -6
  8. data/lib/ferrum/browser/options.rb +84 -0
  9. data/lib/ferrum/browser/process.rb +6 -7
  10. data/lib/ferrum/browser/version_info.rb +71 -0
  11. data/lib/ferrum/browser/web_socket.rb +1 -1
  12. data/lib/ferrum/browser/xvfb.rb +1 -1
  13. data/lib/ferrum/browser.rb +184 -64
  14. data/lib/ferrum/context.rb +3 -2
  15. data/lib/ferrum/contexts.rb +2 -2
  16. data/lib/ferrum/cookies/cookie.rb +183 -0
  17. data/lib/ferrum/cookies.rb +122 -49
  18. data/lib/ferrum/dialog.rb +30 -0
  19. data/lib/ferrum/frame/dom.rb +177 -0
  20. data/lib/ferrum/frame/runtime.rb +41 -61
  21. data/lib/ferrum/frame.rb +91 -3
  22. data/lib/ferrum/headers.rb +28 -0
  23. data/lib/ferrum/keyboard.rb +45 -2
  24. data/lib/ferrum/mouse.rb +84 -0
  25. data/lib/ferrum/network/exchange.rb +104 -5
  26. data/lib/ferrum/network/intercepted_request.rb +3 -12
  27. data/lib/ferrum/network/request.rb +58 -19
  28. data/lib/ferrum/network/request_params.rb +57 -0
  29. data/lib/ferrum/network/response.rb +106 -4
  30. data/lib/ferrum/network.rb +193 -8
  31. data/lib/ferrum/node.rb +21 -1
  32. data/lib/ferrum/page/animation.rb +16 -0
  33. data/lib/ferrum/page/frames.rb +66 -11
  34. data/lib/ferrum/page/screenshot.rb +97 -0
  35. data/lib/ferrum/page/tracing.rb +26 -0
  36. data/lib/ferrum/page.rb +158 -45
  37. data/lib/ferrum/proxy.rb +91 -2
  38. data/lib/ferrum/target.rb +6 -4
  39. data/lib/ferrum/version.rb +1 -1
  40. metadata +7 -101
data/lib/ferrum/frame.rb CHANGED
@@ -14,8 +14,30 @@ module Ferrum
14
14
  stopped_loading
15
15
  ].freeze
16
16
 
17
- attr_accessor :id, :name
18
- attr_reader :page, :parent_id, :state
17
+ # The Frame's unique id.
18
+ #
19
+ # @return [String]
20
+ attr_accessor :id
21
+
22
+ # If frame was given a name it should be here.
23
+ #
24
+ # @return [String, nil]
25
+ attr_accessor :name
26
+
27
+ # The page the frame belongs to.
28
+ #
29
+ # @return [Page]
30
+ attr_reader :page
31
+
32
+ # Parent frame id if this one is nested in another one.
33
+ #
34
+ # @return [String, nil]
35
+ attr_reader :parent_id
36
+
37
+ # One of the states frame's in.
38
+ #
39
+ # @return [:started_loading, :navigated, :stopped_loading, nil]
40
+ attr_reader :state
19
41
 
20
42
  def initialize(id, page, parent_id = nil)
21
43
  @id = id
@@ -30,18 +52,60 @@ module Ferrum
30
52
  @state = value
31
53
  end
32
54
 
55
+ #
56
+ # Returns current frame's `location.href`.
57
+ #
58
+ # @return [String]
59
+ #
60
+ # @example
61
+ # browser.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
62
+ # frame = browser.frames[1]
63
+ # frame.url # => https://interactive-examples.mdn.mozilla.net/pages/tabbed/iframe.html
64
+ #
33
65
  def url
34
66
  evaluate("document.location.href")
35
67
  end
36
68
 
69
+ #
70
+ # Returns current frame's title.
71
+ #
72
+ # @return [String]
73
+ #
74
+ # @example
75
+ # browser.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
76
+ # frame = browser.frames[1]
77
+ # frame.title # => HTML Demo: <iframe>
78
+ #
37
79
  def title
38
80
  evaluate("document.title")
39
81
  end
40
82
 
83
+ #
84
+ # If current frame is the main frame of the page (top of the tree).
85
+ #
86
+ # @return [Boolean]
87
+ #
88
+ # @example
89
+ # browser.go_to("https://www.w3schools.com/tags/tag_frame.asp")
90
+ # frame = browser.frame_by(id: "C09C4E4404314AAEAE85928EAC109A93")
91
+ # frame.main? # => false
92
+ #
41
93
  def main?
42
94
  @parent_id.nil?
43
95
  end
44
96
 
97
+ #
98
+ # Sets a content of a given frame.
99
+ #
100
+ # @param [String] html
101
+ #
102
+ # @example
103
+ # browser.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
104
+ # frame = browser.frames[1]
105
+ # frame.body # <html lang="en"><head><style>body {transition: opacity ease-in 0.2s; }...
106
+ # frame.content = "<html><head></head><body><p>lol</p></body></html>"
107
+ # frame.body # => <html><head></head><body><p>lol</p></body></html>
108
+ #
45
109
  def content=(html)
46
110
  evaluate_async(%(
47
111
  document.open();
@@ -49,16 +113,40 @@ module Ferrum
49
113
  document.close();
50
114
  arguments[1](true);
51
115
  ), @page.timeout, html)
116
+ @page.document_node_id
52
117
  end
53
118
  alias set_content content=
54
119
 
55
- def execution_id
120
+ #
121
+ # Execution context id which is used by JS, each frame has it's own
122
+ # context in which JS evaluates. Locks for a page timeout and raises
123
+ # an error if an execution id hasn't been set yet, if id is set
124
+ # returns immediately.
125
+ #
126
+ # @return [Integer]
127
+ #
128
+ # @raise [NoExecutionContextError]
129
+ #
130
+ def execution_id!
56
131
  value = @execution_id.borrow(@page.timeout, &:itself)
57
132
  raise NoExecutionContextError if value.instance_of?(Object)
58
133
 
59
134
  value
60
135
  end
61
136
 
137
+ #
138
+ # Execution context id which is used by JS, each frame has it's own
139
+ # context in which JS evaluates.
140
+ #
141
+ # @return [Integer, nil]
142
+ #
143
+ def execution_id
144
+ value = @execution_id.value
145
+ return if value.instance_of?(Object)
146
+
147
+ value
148
+ end
149
+
62
150
  def execution_id=(value)
63
151
  if value.nil?
64
152
  @execution_id.try_take!
@@ -7,20 +7,48 @@ module Ferrum
7
7
  @headers = {}
8
8
  end
9
9
 
10
+ #
11
+ # Get all headers.
12
+ #
13
+ # @return [Hash{String => String}]
14
+ #
10
15
  def get
11
16
  @headers
12
17
  end
13
18
 
19
+ #
20
+ # Set given headers. Eventually clear all headers and set given ones.
21
+ #
22
+ # @param [Hash{String => String}] headers
23
+ # key-value pairs for example `"User-Agent" => "Browser"`.
24
+ #
25
+ # @return [true]
26
+ #
14
27
  def set(headers)
15
28
  clear
16
29
  add(headers)
17
30
  end
18
31
 
32
+ #
33
+ # Clear all headers.
34
+ #
35
+ # @return [true]
36
+ #
19
37
  def clear
20
38
  @headers = {}
21
39
  true
22
40
  end
23
41
 
42
+ #
43
+ # Adds given headers to already set ones.
44
+ #
45
+ # @param [Hash{String => String}] headers
46
+ # key-value pairs for example `"Referer" => "http://example.com"`.
47
+ #
48
+ # @param [Boolean] permanent
49
+ #
50
+ # @return [true]
51
+ #
24
52
  def add(headers, permanent: true)
25
53
  if headers["Referer"]
26
54
  @page.referrer = headers["Referer"]
@@ -30,19 +30,44 @@ module Ferrum
30
30
  @page = page
31
31
  end
32
32
 
33
+ #
34
+ # Dispatches a `keydown` event.
35
+ #
36
+ # @param [String, Symbol] key
37
+ # Name of the key, such as `"a"`, `:enter`, or `:backspace`.
38
+ #
39
+ # @return [self]
40
+ #
33
41
  def down(key)
34
- key = normalize_keys(Array(key))
42
+ key = normalize_keys(Array(key)).first
35
43
  type = key[:text] ? "keyDown" : "rawKeyDown"
36
44
  @page.command("Input.dispatchKeyEvent", slowmoable: true, type: type, **key)
37
45
  self
38
46
  end
39
47
 
48
+ #
49
+ # Dispatches a `keyup` event.
50
+ #
51
+ # @param [String, Symbol] key
52
+ # Name of the key, such as `"a"`, `:enter`, or `:backspace`.
53
+ #
54
+ # @return [self]
55
+ #
40
56
  def up(key)
41
- key = normalize_keys(Array(key))
57
+ key = normalize_keys(Array(key)).first
42
58
  @page.command("Input.dispatchKeyEvent", slowmoable: true, type: "keyUp", **key)
43
59
  self
44
60
  end
45
61
 
62
+ #
63
+ # Sends a keydown, keypress/input, and keyup event for each character in
64
+ # the text.
65
+ #
66
+ # @param [Array<String, Symbol, (Symbol, String)>] keys
67
+ # The text to type into a focused element, `[:Shift, "s"], "tring"`.
68
+ #
69
+ # @return [self]
70
+ #
46
71
  def type(*keys)
47
72
  keys = normalize_keys(Array(keys))
48
73
 
@@ -55,15 +80,27 @@ module Ferrum
55
80
  self
56
81
  end
57
82
 
83
+ #
84
+ # Returns bitfield for a given keys.
85
+ #
86
+ # @param [Array<:alt, :ctrl, :command, :shift>] keys
87
+ #
88
+ # @return [Integer]
89
+ #
58
90
  def modifiers(keys)
59
91
  keys.map { |k| MODIFIERS[k.to_s] }.compact.reduce(0, :|)
60
92
  end
61
93
 
62
94
  private
63
95
 
96
+ # TODO: Refactor it, and try to simplify complexity
97
+ # rubocop:disable Metrics/PerceivedComplexity
98
+ # rubocop:disable Metrics/CyclomaticComplexity
64
99
  def normalize_keys(keys, pressed_keys = [], memo = [])
65
100
  case keys
66
101
  when Array
102
+ raise ArgumentError, "empty keys passed" if keys.empty?
103
+
67
104
  pressed_keys.push([])
68
105
  memo += combine_strings(keys).map do |key|
69
106
  normalize_keys(key, pressed_keys, memo)
@@ -82,6 +119,8 @@ module Ferrum
82
119
  to_options(key)
83
120
  end
84
121
  when String
122
+ raise ArgumentError, "empty keys passed" if keys.empty?
123
+
85
124
  pressed = pressed_keys.flatten
86
125
  keys.each_char.map do |char|
87
126
  key = KEYS[char] || {}
@@ -102,8 +141,12 @@ module Ferrum
102
141
  modifiers + [to_options(key)]
103
142
  end.flatten
104
143
  end
144
+ else
145
+ raise ArgumentError, "unexpected argument"
105
146
  end
106
147
  end
148
+ # rubocop:enable Metrics/PerceivedComplexity
149
+ # rubocop:enable Metrics/CyclomaticComplexity
107
150
 
108
151
  def combine_strings(keys)
109
152
  keys
data/lib/ferrum/mouse.rb CHANGED
@@ -10,10 +10,50 @@ module Ferrum
10
10
  @x = @y = 0
11
11
  end
12
12
 
13
+ #
14
+ # Scroll page to a given x, y coordinates.
15
+ #
16
+ # @param [Integer] top
17
+ # The pixel along the horizontal axis of the document that you want
18
+ # displayed in the upper left.
19
+ #
20
+ # @param [Integer] left
21
+ # The pixel along the vertical axis of the document that you want
22
+ # displayed in the upper left.
23
+ #
24
+ # @example
25
+ # browser.go_to("https://www.google.com/search?q=Ruby+headless+driver+for+Capybara")
26
+ # browser.mouse.scroll_to(0, 400)
27
+ #
13
28
  def scroll_to(top, left)
14
29
  tap { @page.execute("window.scrollTo(#{top}, #{left})") }
15
30
  end
16
31
 
32
+ #
33
+ # Click given coordinates, fires mouse move, down and up events.
34
+ #
35
+ # @param [Integer] x
36
+ #
37
+ # @param [Integer] y
38
+ #
39
+ # @param [Float] delay
40
+ # Delay between mouse down and mouse up events.
41
+ #
42
+ # @param [Float] wait
43
+ #
44
+ # @param [Hash{Symbol => Object}] options
45
+ # Additional keyword arguments.
46
+ #
47
+ # @option options [:left, :right] :button (:left)
48
+ # The mouse button to click.
49
+ #
50
+ # @option options [Integer] :count (1)
51
+ #
52
+ # @option options [Integer] :modifiers
53
+ # Bitfield for key modifiers. See`keyboard.modifiers`.
54
+ #
55
+ # @return [self]
56
+ #
17
57
  def click(x:, y:, delay: 0, wait: CLICK_WAIT, **options)
18
58
  move(x: x, y: y)
19
59
  down(**options)
@@ -24,14 +64,58 @@ module Ferrum
24
64
  self
25
65
  end
26
66
 
67
+ #
68
+ # Mouse down for given coordinates.
69
+ #
70
+ # @param [Hash{Symbol => Object}] options
71
+ # Additional keyword arguments.
72
+ #
73
+ # @option options [:left, :right] :button (:left)
74
+ # The mouse button to click.
75
+ #
76
+ # @option options [Integer] :count (1)
77
+ #
78
+ # @option options [Integer] :modifiers
79
+ # Bitfield for key modifiers. See`keyboard.modifiers`.
80
+ #
81
+ # @return [self]
82
+ #
27
83
  def down(**options)
28
84
  tap { mouse_event(type: "mousePressed", **options) }
29
85
  end
30
86
 
87
+ #
88
+ # Mouse up for given coordinates.
89
+ #
90
+ # @param [Hash{Symbol => Object}] options
91
+ # Additional keyword arguments.
92
+ #
93
+ # @option options [:left, :right] :button (:left)
94
+ # The mouse button to click.
95
+ #
96
+ # @option options [Integer] :count (1)
97
+ #
98
+ # @option options [Integer] :modifiers
99
+ # Bitfield for key modifiers. See`keyboard.modifiers`.
100
+ #
101
+ # @return [self]
102
+ #
31
103
  def up(**options)
32
104
  tap { mouse_event(type: "mouseReleased", **options) }
33
105
  end
34
106
 
107
+ #
108
+ # Mouse move to given x and y.
109
+ #
110
+ # @param [Integer] x
111
+ #
112
+ # @param [Integer] y
113
+ #
114
+ # @param [Integer] steps
115
+ # Sends intermediate mousemove events.
116
+ #
117
+ # @return [self]
118
+ #
35
119
  def move(x:, y:, steps: 1)
36
120
  from_x = @x
37
121
  from_y = @y
@@ -3,9 +3,38 @@
3
3
  module Ferrum
4
4
  class Network
5
5
  class Exchange
6
+ # ID of the request.
7
+ #
8
+ # @return String
6
9
  attr_reader :id
7
- attr_accessor :intercepted_request, :request, :response, :error
8
10
 
11
+ # The intercepted request.
12
+ #
13
+ # @return [InterceptedRequest, nil]
14
+ attr_accessor :intercepted_request
15
+
16
+ # The request object.
17
+ #
18
+ # @return [Request, nil]
19
+ attr_accessor :request
20
+
21
+ # The response object.
22
+ #
23
+ # @return [Response, nil]
24
+ attr_accessor :response
25
+
26
+ # The error object.
27
+ #
28
+ # @return [Error, nil]
29
+ attr_accessor :error
30
+
31
+ #
32
+ # Initializes the network exchange.
33
+ #
34
+ # @param [Page] page
35
+ #
36
+ # @param [String] id
37
+ #
9
38
  def initialize(page, id)
10
39
  @id = id
11
40
  @page = page
@@ -13,35 +42,105 @@ module Ferrum
13
42
  @request = @response = @error = nil
14
43
  end
15
44
 
45
+ #
46
+ # Determines if the network exchange was caused by a page navigation
47
+ # event.
48
+ #
49
+ # @param [String] frame_id
50
+ #
51
+ # @return [Boolean]
52
+ #
16
53
  def navigation_request?(frame_id)
17
- request.type?(:document) &&
18
- request.frame_id == frame_id
54
+ request&.type?(:document) && request&.frame_id == frame_id
19
55
  end
20
56
 
57
+ #
58
+ # Determines if the network exchange has a request.
59
+ #
60
+ # @return [Boolean]
61
+ #
21
62
  def blank?
22
63
  !request
23
64
  end
24
65
 
66
+ #
67
+ # Determines if the request was intercepted and blocked.
68
+ #
69
+ # @return [Boolean]
70
+ #
25
71
  def blocked?
26
72
  intercepted? && intercepted_request.status?(:aborted)
27
73
  end
28
74
 
75
+ #
76
+ # Determines if the request was blocked, a response was returned, or if an
77
+ # error occurred.
78
+ #
79
+ # @return [Boolean]
80
+ #
29
81
  def finished?
30
- blocked? || response || error
82
+ blocked? || response&.loaded? || !error.nil?
31
83
  end
32
84
 
85
+ #
86
+ # Determines if the network exchange is still not finished.
87
+ #
88
+ # @return [Boolean]
89
+ #
33
90
  def pending?
34
91
  !finished?
35
92
  end
36
93
 
94
+ #
95
+ # Determines if the exchange's request was intercepted.
96
+ #
97
+ # @return [Boolean]
98
+ #
37
99
  def intercepted?
38
- intercepted_request
100
+ !intercepted_request.nil?
101
+ end
102
+
103
+ #
104
+ # Determines if the exchange is XHR.
105
+ #
106
+ # @return [Boolean]
107
+ #
108
+ def xhr?
109
+ !!request&.xhr?
110
+ end
111
+
112
+ #
113
+ # Determines if the exchange is a redirect.
114
+ #
115
+ # @return [Boolean]
116
+ #
117
+ def redirect?
118
+ response&.redirect?
119
+ end
120
+
121
+ #
122
+ # Returns request's URL.
123
+ #
124
+ # @return [String, nil]
125
+ #
126
+ def url
127
+ request&.url
39
128
  end
40
129
 
130
+ #
131
+ # Converts the network exchange into a request, response, and error tuple.
132
+ #
133
+ # @return [Array]
134
+ #
41
135
  def to_a
42
136
  [request, response, error]
43
137
  end
44
138
 
139
+ #
140
+ # Inspects the network exchange.
141
+ #
142
+ # @return [String]
143
+ #
45
144
  def inspect
46
145
  "#<#{self.class} " \
47
146
  "@id=#{@id.inspect} " \
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ferrum/network/request_params"
3
4
  require "base64"
4
5
 
5
6
  module Ferrum
6
7
  class Network
7
8
  class InterceptedRequest
9
+ include RequestParams
10
+
8
11
  attr_accessor :request_id, :frame_id, :resource_type, :network_id, :status
9
12
 
10
13
  def initialize(page, params)
@@ -54,18 +57,6 @@ module Ferrum
54
57
  @page.command("Fetch.failRequest", requestId: request_id, errorReason: "BlockedByClient")
55
58
  end
56
59
 
57
- def url
58
- @request["url"]
59
- end
60
-
61
- def method
62
- @request["method"]
63
- end
64
-
65
- def headers
66
- @request["headers"]
67
- end
68
-
69
60
  def initial_priority
70
61
  @request["initialPriority"]
71
62
  end
@@ -1,55 +1,94 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ferrum/network/request_params"
3
4
  require "time"
4
5
 
5
6
  module Ferrum
6
7
  class Network
8
+ #
9
+ # Represents a [Network.Request](https://chromedevtools.github.io/devtools-protocol/1-3/Network/#type-Request)
10
+ # object.
11
+ #
7
12
  class Request
13
+ include RequestParams
14
+
15
+ #
16
+ # Initializes the request object.
17
+ #
18
+ # @param [Hash{String => Object}] params
19
+ # The parsed JSON attributes.
20
+ #
8
21
  def initialize(params)
9
22
  @params = params
10
23
  @request = @params["request"]
11
24
  end
12
25
 
26
+ #
27
+ # The request ID.
28
+ #
29
+ # @return [String]
30
+ #
13
31
  def id
14
32
  @params["requestId"]
15
33
  end
16
34
 
35
+ #
36
+ # The request resouce type.
37
+ #
38
+ # @return [String]
39
+ #
17
40
  def type
18
41
  @params["type"]
19
42
  end
20
43
 
44
+ #
45
+ # Determines if the request is of the given type.
46
+ #
47
+ # @param [String, Symbol] value
48
+ # The type value to compare against.
49
+ #
50
+ # @return [Boolean]
51
+ #
21
52
  def type?(value)
22
53
  type.downcase == value.to_s.downcase
23
54
  end
24
55
 
25
- def frame_id
26
- @params["frameId"]
27
- end
28
-
29
- def url
30
- @request["url"]
56
+ #
57
+ # Determines if the request is XHR.
58
+ #
59
+ # @return [Boolean]
60
+ #
61
+ def xhr?
62
+ type?("xhr")
31
63
  end
32
64
 
33
- def url_fragment
34
- @request["urlFragment"]
35
- end
36
-
37
- def method
38
- @request["method"]
39
- end
40
-
41
- def headers
42
- @request["headers"]
65
+ #
66
+ # The frame ID of the request.
67
+ #
68
+ # @return [String]
69
+ #
70
+ def frame_id
71
+ @params["frameId"]
43
72
  end
44
73
 
74
+ #
75
+ # The request timestamp.
76
+ #
77
+ # @return [Time]
78
+ #
45
79
  def time
46
80
  @time ||= Time.strptime(@params["wallTime"].to_s, "%s")
47
81
  end
48
82
 
49
- def post_data
50
- @request["postData"]
83
+ #
84
+ # Converts the request to a Hash.
85
+ #
86
+ # @return [Hash{String => Object}]
87
+ # The params of the request.
88
+ #
89
+ def to_h
90
+ @params
51
91
  end
52
- alias body post_data
53
92
  end
54
93
  end
55
94
  end