ferrum 0.12 → 0.13

Sign up to get free protection for your applications and to get access to all the features.
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