ferrum 0.12 → 0.13

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.
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,87 @@ 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.nil? || !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
+ # Returns request's URL.
105
+ #
106
+ # @return [String, nil]
107
+ #
108
+ def url
109
+ request&.url
39
110
  end
40
111
 
112
+ #
113
+ # Converts the network exchange into a request, response, and error tuple.
114
+ #
115
+ # @return [Array]
116
+ #
41
117
  def to_a
42
118
  [request, response, error]
43
119
  end
44
120
 
121
+ #
122
+ # Inspects the network exchange.
123
+ #
124
+ # @return [String]
125
+ #
45
126
  def inspect
46
127
  "#<#{self.class} " \
47
128
  "@id=#{@id.inspect} " \
@@ -4,48 +4,112 @@ require "time"
4
4
 
5
5
  module Ferrum
6
6
  class Network
7
+ #
8
+ # Represents a [Network.Request](https://chromedevtools.github.io/devtools-protocol/1-3/Network/#type-Request)
9
+ # object.
10
+ #
7
11
  class Request
12
+ #
13
+ # Initializes the request object.
14
+ #
15
+ # @param [Hash{String => Object}] params
16
+ # The parsed JSON attributes.
17
+ #
8
18
  def initialize(params)
9
19
  @params = params
10
20
  @request = @params["request"]
11
21
  end
12
22
 
23
+ #
24
+ # The request ID.
25
+ #
26
+ # @return [String]
27
+ #
13
28
  def id
14
29
  @params["requestId"]
15
30
  end
16
31
 
32
+ #
33
+ # The request resouce type.
34
+ #
35
+ # @return [String]
36
+ #
17
37
  def type
18
38
  @params["type"]
19
39
  end
20
40
 
41
+ #
42
+ # Determines if the request is of the given type.
43
+ #
44
+ # @param [String, Symbol] value
45
+ # The type value to compare against.
46
+ #
47
+ # @return [Boolean]
48
+ #
21
49
  def type?(value)
22
50
  type.downcase == value.to_s.downcase
23
51
  end
24
52
 
53
+ #
54
+ # The frame ID of the request.
55
+ #
56
+ # @return [String]
57
+ #
25
58
  def frame_id
26
59
  @params["frameId"]
27
60
  end
28
61
 
62
+ #
63
+ # The URL for the request.
64
+ #
65
+ # @return [String]
66
+ #
29
67
  def url
30
68
  @request["url"]
31
69
  end
32
70
 
71
+ #
72
+ # The URL fragment for the request.
73
+ #
74
+ # @return [String, nil]
75
+ #
33
76
  def url_fragment
34
77
  @request["urlFragment"]
35
78
  end
36
79
 
80
+ #
81
+ # The request method.
82
+ #
83
+ # @return [String]
84
+ #
37
85
  def method
38
86
  @request["method"]
39
87
  end
40
88
 
89
+ #
90
+ # The request headers.
91
+ #
92
+ # @return [Hash{String => String}]
93
+ #
41
94
  def headers
42
95
  @request["headers"]
43
96
  end
44
97
 
98
+ #
99
+ # The request timestamp.
100
+ #
101
+ # @return [Time]
102
+ #
45
103
  def time
46
104
  @time ||= Time.strptime(@params["wallTime"].to_s, "%s")
47
105
  end
48
106
 
107
+ #
108
+ # The optional HTTP `POST` form data.
109
+ #
110
+ # @return [String, nil]
111
+ # The HTTP `POST` form data.
112
+ #
49
113
  def post_data
50
114
  @request["postData"]
51
115
  end
@@ -2,43 +2,105 @@
2
2
 
3
3
  module Ferrum
4
4
  class Network
5
+ #
6
+ # Represents a [Network.Response](https://chromedevtools.github.io/devtools-protocol/1-3/Network/#type-Response)
7
+ # object.
8
+ #
5
9
  class Response
6
- attr_reader :body_size, :params
10
+ # The response body size.
11
+ #
12
+ # @return [Integer, nil]
13
+ attr_reader :body_size
7
14
 
15
+ # The parsed JSON attributes for the [Network.Response](https://chromedevtools.github.io/devtools-protocol/1-3/Network/#type-Response)
16
+ # object.
17
+ #
18
+ # @return [Hash{String => Object}]
19
+ attr_reader :params
20
+
21
+ #
22
+ # Initializes the respones object.
23
+ #
24
+ # @param [Page] page
25
+ # The page associated with the network response.
26
+ #
27
+ # @param [Hash{String => Object}] params
28
+ # The parsed JSON attributes for the [Network.Response](https://chromedevtools.github.io/devtools-protocol/1-3/Network/#type-Response)
29
+ #
8
30
  def initialize(page, params)
9
31
  @page = page
10
32
  @params = params
11
33
  @response = params["response"] || params["redirectResponse"]
12
34
  end
13
35
 
36
+ #
37
+ # The request ID associated with the response.
38
+ #
39
+ # @return [String]
40
+ #
14
41
  def id
15
42
  @params["requestId"]
16
43
  end
17
44
 
45
+ #
46
+ # The URL of the response.
47
+ #
48
+ # @return [String]
49
+ #
18
50
  def url
19
51
  @response["url"]
20
52
  end
21
53
 
54
+ #
55
+ # The HTTP status of the response.
56
+ #
57
+ # @return [Integer]
58
+ #
22
59
  def status
23
60
  @response["status"]
24
61
  end
25
62
 
63
+ #
64
+ # The HTTP status text.
65
+ #
66
+ # @return [String]
67
+ #
26
68
  def status_text
27
69
  @response["statusText"]
28
70
  end
29
71
 
72
+ #
73
+ # The headers of the response.
74
+ #
75
+ # @return [Hash{String => String}]
76
+ #
30
77
  def headers
31
78
  @response["headers"]
32
79
  end
33
80
 
81
+ #
82
+ # The total size in bytes of the response.
83
+ #
84
+ # @return [Integer]
85
+ #
34
86
  def headers_size
35
87
  @response["encodedDataLength"]
36
88
  end
37
89
 
90
+ #
91
+ # The resource type of the response.
92
+ #
93
+ # @return [String]
94
+ #
38
95
  def type
39
96
  @params["type"]
40
97
  end
41
98
 
99
+ #
100
+ # The `Content-Type` header value of the response.
101
+ #
102
+ # @return [String, nil]
103
+ #
42
104
  def content_type
43
105
  @content_type ||= headers.find { |k, _| k.downcase == "content-type" }&.last&.sub(/;.*\z/, "")
44
106
  end
@@ -52,6 +114,11 @@ module Ferrum
52
114
  @body_size = size - headers_size
53
115
  end
54
116
 
117
+ #
118
+ # The response body.
119
+ #
120
+ # @return [String]
121
+ #
55
122
  def body
56
123
  @body ||= begin
57
124
  body, encoded = @page
@@ -61,14 +128,29 @@ module Ferrum
61
128
  end
62
129
  end
63
130
 
131
+ #
132
+ # @return [Boolean]
133
+ #
64
134
  def main?
65
135
  @page.network.response == self
66
136
  end
67
137
 
138
+ #
139
+ # Comapres the respones ID to another response's ID.
140
+ #
141
+ # @return [Boolean]
142
+ # Indicates whether the response has the same ID as the other response
143
+ # object.
144
+ #
68
145
  def ==(other)
69
146
  id == other.id
70
147
  end
71
148
 
149
+ #
150
+ # Inspects the response object.
151
+ #
152
+ # @return [String]
153
+ #
72
154
  def inspect
73
155
  %(#<#{self.class} @params=#{@params.inspect} @response=#{@response.inspect}>)
74
156
  end
@@ -19,6 +19,16 @@ module Ferrum
19
19
  AUTHORIZE_TYPE_WRONG = ":type should be in #{AUTHORIZE_TYPE}"
20
20
  ALLOWED_CONNECTION_TYPE = %w[none cellular2g cellular3g cellular4g bluetooth ethernet wifi wimax other].freeze
21
21
 
22
+ # Network traffic.
23
+ #
24
+ # @return [Array<Exchange>]
25
+ # Returns all information about network traffic as {Exchange}
26
+ # instance which in general is a wrapper around `request`, `response` and
27
+ # `error`.
28
+ #
29
+ # @example
30
+ # browser.go_to("https://github.com/")
31
+ # browser.network.traffic # => [#<Ferrum::Network::Exchange, ...]
22
32
  attr_reader :traffic
23
33
 
24
34
  def initialize(page)
@@ -29,6 +39,25 @@ module Ferrum
29
39
  @whitelist = nil
30
40
  end
31
41
 
42
+ #
43
+ # Waits for network idle or raises {Ferrum::TimeoutError} error.
44
+ #
45
+ # @param [Integer] connections
46
+ # how many connections are allowed for network to be idling,
47
+ #
48
+ # @param [Float] duration
49
+ # Sleep for given amount of time and check again.
50
+ #
51
+ # @param [Float] timeout
52
+ # During what time we try to check idle.
53
+ #
54
+ # @raise [Ferrum::TimeoutError]
55
+ #
56
+ # @example
57
+ # browser.go_to("https://example.com/")
58
+ # browser.at_xpath("//a[text() = 'No UI changes button']").click
59
+ # browser.network.wait_for_idle
60
+ #
32
61
  def wait_for_idle(connections: 0, duration: 0.05, timeout: @page.browser.timeout)
33
62
  start = Utils::ElapsedTime.monotonic_time
34
63
 
@@ -55,18 +84,61 @@ module Ferrum
55
84
  total_connections - finished_connections
56
85
  end
57
86
 
87
+ #
88
+ # Page request of the main frame.
89
+ #
90
+ # @return [Request]
91
+ #
92
+ # @example
93
+ # browser.go_to("https://github.com/")
94
+ # browser.network.request # => #<Ferrum::Network::Request...
95
+ #
58
96
  def request
59
97
  @exchange&.request
60
98
  end
61
99
 
100
+ #
101
+ # Page response of the main frame.
102
+ #
103
+ # @return [Response, nil]
104
+ #
105
+ # @example
106
+ # browser.go_to("https://github.com/")
107
+ # browser.network.response # => #<Ferrum::Network::Response...
108
+ #
62
109
  def response
63
110
  @exchange&.response
64
111
  end
65
112
 
113
+ #
114
+ # Contains the status code of the main page response (e.g., 200 for a
115
+ # success). This is just a shortcut for `response.status`.
116
+ #
117
+ # @return [Integer, nil]
118
+ #
119
+ # @example
120
+ # browser.go_to("https://github.com/")
121
+ # browser.network.status # => 200
122
+ #
66
123
  def status
67
124
  response&.status
68
125
  end
69
126
 
127
+ #
128
+ # Clear browser's cache or collected traffic.
129
+ #
130
+ # @param [:traffic, :cache] type
131
+ # The type of traffic to clear.
132
+ #
133
+ # @return [true]
134
+ #
135
+ # @example
136
+ # traffic = browser.network.traffic # => []
137
+ # browser.go_to("https://github.com/")
138
+ # traffic.size # => 51
139
+ # browser.network.clear(:traffic)
140
+ # traffic.size # => 0
141
+ #
70
142
  def clear(type)
71
143
  raise ArgumentError, ":type should be in #{CLEAR_TYPE}" unless CLEAR_TYPE.include?(type)
72
144
 
@@ -91,6 +163,30 @@ module Ferrum
91
163
  end
92
164
  alias allowlist= whitelist=
93
165
 
166
+ #
167
+ # Set request interception for given options. This method is only sets
168
+ # request interception, you should use `on` callback to catch requests and
169
+ # abort or continue them.
170
+ #
171
+ # @param [String] pattern
172
+ #
173
+ # @param [Symbol, nil] resource_type
174
+ # One of the [resource types](https://chromedevtools.github.io/devtools-protocol/tot/Network#type-ResourceType)
175
+ #
176
+ # @example
177
+ # browser = Ferrum::Browser.new
178
+ # browser.network.intercept
179
+ # browser.on(:request) do |request|
180
+ # if request.match?(/bla-bla/)
181
+ # request.abort
182
+ # elsif request.match?(/lorem/)
183
+ # request.respond(body: "Lorem ipsum")
184
+ # else
185
+ # request.continue
186
+ # end
187
+ # end
188
+ # browser.go_to("https://google.com")
189
+ #
94
190
  def intercept(pattern: "*", resource_type: nil)
95
191
  pattern = { urlPattern: pattern }
96
192
  pattern[:resourceType] = resource_type if resource_type && RESOURCE_TYPES.include?(resource_type.to_s)
@@ -98,6 +194,31 @@ module Ferrum
98
194
  @page.command("Fetch.enable", handleAuthRequests: true, patterns: [pattern])
99
195
  end
100
196
 
197
+ #
198
+ # Sets HTTP Basic-Auth credentials.
199
+ #
200
+ # @param [String] user
201
+ # The username to send.
202
+ #
203
+ # @param [String] password
204
+ # The password to send.
205
+ #
206
+ # @param [:server, :proxy] type
207
+ # Specifies whether the credentials are for a website or a proxy.
208
+ #
209
+ # @yield [request]
210
+ # The given block will be passed each authenticated request and can allow
211
+ # or deny the request.
212
+ #
213
+ # @yieldparam [Request] request
214
+ # An HTTP request.
215
+ #
216
+ # @example
217
+ # browser.network.authorize(user: "login", password: "pass") { |req| req.continue }
218
+ # browser.go_to("http://example.com/authenticated")
219
+ # puts browser.network.status # => 200
220
+ # puts browser.body # => Welcome, authenticated client
221
+ #
101
222
  def authorize(user:, password:, type: :server, &block)
102
223
  raise ArgumentError, AUTHORIZE_TYPE_WRONG unless AUTHORIZE_TYPE.include?(type)
103
224
  raise ArgumentError, AUTHORIZE_BLOCK_MISSING if !block_given? && !@page.subscribed?("Fetch.requestPaused")
@@ -151,6 +272,37 @@ module Ferrum
151
272
  Network::Exchange.new(@page, id).tap { |e| @traffic << e }
152
273
  end
153
274
 
275
+ #
276
+ # Activates emulation of network conditions.
277
+ #
278
+ # @param [Boolean] offline
279
+ # Emulate internet disconnection,
280
+ #
281
+ # @param [Integer] latency
282
+ # Minimum latency from request sent to response headers received (ms).
283
+ #
284
+ # @param [Integer] download_throughput
285
+ # Maximal aggregated download throughput (bytes/sec).
286
+ #
287
+ # @param [Integer] upload_throughput
288
+ # Maximal aggregated upload throughput (bytes/sec).
289
+ #
290
+ # @param [String, nil] connection_type
291
+ # Connection type if known:
292
+ # * `"none"`
293
+ # * `"cellular2g"`
294
+ # * `"cellular3g"`
295
+ # * `"cellular4g"`
296
+ # * `"bluetooth"`
297
+ # * `"ethernet"`
298
+ # * `"wifi"`
299
+ # * `"wimax"`
300
+ # * `"other"`
301
+ #
302
+ # @example
303
+ # browser.network.emulate_network_conditions(connection_type: "cellular2g")
304
+ # browser.go_to("https://github.com/")
305
+ #
154
306
  def emulate_network_conditions(offline: false, latency: 0,
155
307
  download_throughput: -1, upload_throughput: -1,
156
308
  connection_type: nil)
@@ -166,6 +318,14 @@ module Ferrum
166
318
  true
167
319
  end
168
320
 
321
+ #
322
+ # Activates offline mode for a page.
323
+ #
324
+ # @example
325
+ # browser.network.offline_mode
326
+ # browser.go_to("https://github.com/") # => Ferrum::StatusError (Request to https://github.com/ failed to reach
327
+ # server, check DNS and server status)
328
+ #
169
329
  def offline_mode
170
330
  emulate_network_conditions(offline: true, latency: 0, download_throughput: 0, upload_throughput: 0)
171
331
  end