ferrum 0.11 → 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.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +174 -30
- data/lib/ferrum/browser/binary.rb +46 -0
- data/lib/ferrum/browser/client.rb +17 -16
- data/lib/ferrum/browser/command.rb +10 -12
- data/lib/ferrum/browser/options/base.rb +2 -11
- data/lib/ferrum/browser/options/chrome.rb +29 -18
- data/lib/ferrum/browser/options/firefox.rb +13 -9
- data/lib/ferrum/browser/options.rb +84 -0
- data/lib/ferrum/browser/process.rb +45 -40
- data/lib/ferrum/browser/subscriber.rb +1 -3
- data/lib/ferrum/browser/version_info.rb +71 -0
- data/lib/ferrum/browser/web_socket.rb +9 -12
- data/lib/ferrum/browser/xvfb.rb +4 -8
- data/lib/ferrum/browser.rb +193 -47
- data/lib/ferrum/context.rb +9 -4
- data/lib/ferrum/contexts.rb +12 -10
- data/lib/ferrum/cookies/cookie.rb +126 -0
- data/lib/ferrum/cookies.rb +93 -55
- data/lib/ferrum/dialog.rb +30 -0
- data/lib/ferrum/errors.rb +115 -0
- data/lib/ferrum/frame/dom.rb +177 -0
- data/lib/ferrum/frame/runtime.rb +58 -75
- data/lib/ferrum/frame.rb +118 -23
- data/lib/ferrum/headers.rb +30 -2
- data/lib/ferrum/keyboard.rb +56 -13
- data/lib/ferrum/mouse.rb +92 -7
- data/lib/ferrum/network/auth_request.rb +7 -2
- data/lib/ferrum/network/exchange.rb +97 -12
- data/lib/ferrum/network/intercepted_request.rb +10 -8
- data/lib/ferrum/network/request.rb +69 -0
- data/lib/ferrum/network/response.rb +85 -3
- data/lib/ferrum/network.rb +285 -36
- data/lib/ferrum/node.rb +69 -23
- data/lib/ferrum/page/animation.rb +16 -1
- data/lib/ferrum/page/frames.rb +111 -30
- data/lib/ferrum/page/screenshot.rb +142 -65
- data/lib/ferrum/page/stream.rb +38 -0
- data/lib/ferrum/page/tracing.rb +97 -0
- data/lib/ferrum/page.rb +224 -60
- data/lib/ferrum/proxy.rb +147 -0
- data/lib/ferrum/{rbga.rb → rgba.rb} +4 -2
- data/lib/ferrum/target.rb +7 -4
- data/lib/ferrum/utils/attempt.rb +20 -0
- data/lib/ferrum/utils/elapsed_time.rb +27 -0
- data/lib/ferrum/utils/platform.rb +28 -0
- data/lib/ferrum/version.rb +1 -1
- data/lib/ferrum.rb +4 -146
- metadata +63 -51
data/lib/ferrum/network.rb
CHANGED
@@ -14,20 +14,56 @@ module Ferrum
|
|
14
14
|
RESOURCE_TYPES = %w[Document Stylesheet Image Media Font Script TextTrack
|
15
15
|
XHR Fetch EventSource WebSocket Manifest
|
16
16
|
SignedExchange Ping CSPViolationReport Other].freeze
|
17
|
-
|
17
|
+
AUTHORIZE_BLOCK_MISSING = "Block is missing, call `authorize(...) { |r| r.continue } " \
|
18
|
+
"or subscribe to `on(:request)` events before calling it"
|
19
|
+
AUTHORIZE_TYPE_WRONG = ":type should be in #{AUTHORIZE_TYPE}"
|
20
|
+
ALLOWED_CONNECTION_TYPE = %w[none cellular2g cellular3g cellular4g bluetooth ethernet wifi wimax other].freeze
|
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, ...]
|
18
32
|
attr_reader :traffic
|
19
33
|
|
20
34
|
def initialize(page)
|
21
35
|
@page = page
|
22
36
|
@traffic = []
|
23
37
|
@exchange = nil
|
38
|
+
@blacklist = nil
|
39
|
+
@whitelist = nil
|
24
40
|
end
|
25
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
|
+
#
|
26
61
|
def wait_for_idle(connections: 0, duration: 0.05, timeout: @page.browser.timeout)
|
27
|
-
start =
|
62
|
+
start = Utils::ElapsedTime.monotonic_time
|
28
63
|
|
29
64
|
until idle?(connections)
|
30
|
-
raise TimeoutError if
|
65
|
+
raise TimeoutError if Utils::ElapsedTime.timeout?(start, timeout)
|
66
|
+
|
31
67
|
sleep(duration)
|
32
68
|
end
|
33
69
|
end
|
@@ -48,22 +84,63 @@ module Ferrum
|
|
48
84
|
total_connections - finished_connections
|
49
85
|
end
|
50
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
|
+
#
|
51
96
|
def request
|
52
97
|
@exchange&.request
|
53
98
|
end
|
54
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
|
+
#
|
55
109
|
def response
|
56
110
|
@exchange&.response
|
57
111
|
end
|
58
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
|
+
#
|
59
123
|
def status
|
60
124
|
response&.status
|
61
125
|
end
|
62
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
|
+
#
|
63
142
|
def clear(type)
|
64
|
-
unless CLEAR_TYPE.include?(type)
|
65
|
-
raise ArgumentError, ":type should be in #{CLEAR_TYPE}"
|
66
|
-
end
|
143
|
+
raise ArgumentError, ":type should be in #{CLEAR_TYPE}" unless CLEAR_TYPE.include?(type)
|
67
144
|
|
68
145
|
if type == :traffic
|
69
146
|
@traffic.clear
|
@@ -74,23 +151,77 @@ module Ferrum
|
|
74
151
|
true
|
75
152
|
end
|
76
153
|
|
154
|
+
def blacklist=(patterns)
|
155
|
+
@blacklist = Array(patterns)
|
156
|
+
blacklist_subscribe
|
157
|
+
end
|
158
|
+
alias blocklist= blacklist=
|
159
|
+
|
160
|
+
def whitelist=(patterns)
|
161
|
+
@whitelist = Array(patterns)
|
162
|
+
whitelist_subscribe
|
163
|
+
end
|
164
|
+
alias allowlist= whitelist=
|
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
|
+
#
|
77
190
|
def intercept(pattern: "*", resource_type: nil)
|
78
191
|
pattern = { urlPattern: pattern }
|
79
|
-
if resource_type && RESOURCE_TYPES.include?(resource_type.to_s)
|
80
|
-
pattern[:resourceType] = resource_type
|
81
|
-
end
|
192
|
+
pattern[:resourceType] = resource_type if resource_type && RESOURCE_TYPES.include?(resource_type.to_s)
|
82
193
|
|
83
194
|
@page.command("Fetch.enable", handleAuthRequests: true, patterns: [pattern])
|
84
195
|
end
|
85
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
|
+
#
|
86
222
|
def authorize(user:, password:, type: :server, &block)
|
87
|
-
unless AUTHORIZE_TYPE.include?(type)
|
88
|
-
|
89
|
-
end
|
90
|
-
|
91
|
-
if !block_given? && !@page.subscribed?("Fetch.requestPaused")
|
92
|
-
raise ArgumentError, "Block is missing, call `authorize(...) { |r| r.continue } or subscribe to `on(:request)` events before calling it"
|
93
|
-
end
|
223
|
+
raise ArgumentError, AUTHORIZE_TYPE_WRONG unless AUTHORIZE_TYPE.include?(type)
|
224
|
+
raise ArgumentError, AUTHORIZE_BLOCK_MISSING if !block_given? && !@page.subscribed?("Fetch.requestPaused")
|
94
225
|
|
95
226
|
@authorized_ids ||= {}
|
96
227
|
@authorized_ids[type] ||= []
|
@@ -116,6 +247,92 @@ module Ferrum
|
|
116
247
|
end
|
117
248
|
|
118
249
|
def subscribe
|
250
|
+
subscribe_request_will_be_sent
|
251
|
+
subscribe_response_received
|
252
|
+
subscribe_loading_finished
|
253
|
+
subscribe_loading_failed
|
254
|
+
subscribe_log_entry_added
|
255
|
+
end
|
256
|
+
|
257
|
+
def authorized_response(ids, request_id, username, password)
|
258
|
+
if ids.include?(request_id)
|
259
|
+
{ response: "CancelAuth" }
|
260
|
+
elsif username && password
|
261
|
+
{ response: "ProvideCredentials",
|
262
|
+
username: username,
|
263
|
+
password: password }
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def select(request_id)
|
268
|
+
@traffic.select { |e| e.id == request_id }
|
269
|
+
end
|
270
|
+
|
271
|
+
def build_exchange(id)
|
272
|
+
Network::Exchange.new(@page, id).tap { |e| @traffic << e }
|
273
|
+
end
|
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
|
+
#
|
306
|
+
def emulate_network_conditions(offline: false, latency: 0,
|
307
|
+
download_throughput: -1, upload_throughput: -1,
|
308
|
+
connection_type: nil)
|
309
|
+
params = {
|
310
|
+
offline: offline, latency: latency,
|
311
|
+
downloadThroughput: download_throughput,
|
312
|
+
uploadThroughput: upload_throughput
|
313
|
+
}
|
314
|
+
|
315
|
+
params[:connectionType] = connection_type if connection_type && ALLOWED_CONNECTION_TYPE.include?(connection_type)
|
316
|
+
|
317
|
+
@page.command("Network.emulateNetworkConditions", **params)
|
318
|
+
true
|
319
|
+
end
|
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
|
+
#
|
329
|
+
def offline_mode
|
330
|
+
emulate_network_conditions(offline: true, latency: 0, download_throughput: 0, upload_throughput: 0)
|
331
|
+
end
|
332
|
+
|
333
|
+
private
|
334
|
+
|
335
|
+
def subscribe_request_will_be_sent
|
119
336
|
@page.on("Network.requestWillBeSent") do |params|
|
120
337
|
request = Network::Request.new(params)
|
121
338
|
|
@@ -140,25 +357,29 @@ module Ferrum
|
|
140
357
|
|
141
358
|
exchange.request = request
|
142
359
|
|
143
|
-
if exchange.navigation_request?(@page.main_frame.id)
|
144
|
-
@exchange = exchange
|
145
|
-
end
|
360
|
+
@exchange = exchange if exchange.navigation_request?(@page.main_frame.id)
|
146
361
|
end
|
362
|
+
end
|
147
363
|
|
364
|
+
def subscribe_response_received
|
148
365
|
@page.on("Network.responseReceived") do |params|
|
149
|
-
|
366
|
+
exchange = select(params["requestId"]).last
|
367
|
+
|
368
|
+
if exchange
|
150
369
|
response = Network::Response.new(@page, params)
|
151
370
|
exchange.response = response
|
152
371
|
end
|
153
372
|
end
|
373
|
+
end
|
154
374
|
|
375
|
+
def subscribe_loading_finished
|
155
376
|
@page.on("Network.loadingFinished") do |params|
|
156
377
|
exchange = select(params["requestId"]).last
|
157
|
-
|
158
|
-
exchange.response.body_size = params["encodedDataLength"]
|
159
|
-
end
|
378
|
+
exchange.response.body_size = params["encodedDataLength"] if exchange&.response
|
160
379
|
end
|
380
|
+
end
|
161
381
|
|
382
|
+
def subscribe_loading_failed
|
162
383
|
@page.on("Network.loadingFailed") do |params|
|
163
384
|
exchange = select(params["requestId"]).last
|
164
385
|
exchange.error ||= Network::Error.new
|
@@ -169,7 +390,9 @@ module Ferrum
|
|
169
390
|
exchange.error.monotonic_time = params["timestamp"]
|
170
391
|
exchange.error.canceled = params["canceled"]
|
171
392
|
end
|
393
|
+
end
|
172
394
|
|
395
|
+
def subscribe_log_entry_added
|
173
396
|
@page.on("Log.entryAdded") do |params|
|
174
397
|
entry = params["entry"] || {}
|
175
398
|
if entry["source"] == "network" && entry["level"] == "error"
|
@@ -184,24 +407,50 @@ module Ferrum
|
|
184
407
|
end
|
185
408
|
end
|
186
409
|
|
187
|
-
def
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
410
|
+
def blacklist_subscribe
|
411
|
+
return unless blacklist?
|
412
|
+
raise ArgumentError, "You can't use blacklist along with whitelist" if whitelist?
|
413
|
+
|
414
|
+
@blacklist_subscribe ||= begin
|
415
|
+
intercept
|
416
|
+
|
417
|
+
@page.on(:request) do |request|
|
418
|
+
if @blacklist.any? { |p| request.match?(p) }
|
419
|
+
request.abort
|
420
|
+
else
|
421
|
+
request.continue
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
true
|
196
426
|
end
|
197
427
|
end
|
198
428
|
|
199
|
-
def
|
200
|
-
|
429
|
+
def whitelist_subscribe
|
430
|
+
return unless whitelist?
|
431
|
+
raise ArgumentError, "You can't use whitelist along with blacklist" if blacklist?
|
432
|
+
|
433
|
+
@whitelist_subscribe ||= begin
|
434
|
+
intercept
|
435
|
+
|
436
|
+
@page.on(:request) do |request|
|
437
|
+
if @whitelist.any? { |p| request.match?(p) }
|
438
|
+
request.continue
|
439
|
+
else
|
440
|
+
request.abort
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
true
|
445
|
+
end
|
201
446
|
end
|
202
447
|
|
203
|
-
def
|
204
|
-
|
448
|
+
def blacklist?
|
449
|
+
Array(@blacklist).any?
|
450
|
+
end
|
451
|
+
|
452
|
+
def whitelist?
|
453
|
+
Array(@whitelist).any?
|
205
454
|
end
|
206
455
|
end
|
207
456
|
end
|
data/lib/ferrum/node.rb
CHANGED
@@ -10,7 +10,8 @@ module Ferrum
|
|
10
10
|
def initialize(frame, target_id, node_id, description)
|
11
11
|
@page = frame.page
|
12
12
|
@target_id = target_id
|
13
|
-
@node_id
|
13
|
+
@node_id = node_id
|
14
|
+
@description = description
|
14
15
|
@tag_name = description["nodeName"].downcase
|
15
16
|
end
|
16
17
|
|
@@ -38,15 +39,16 @@ module Ferrum
|
|
38
39
|
end
|
39
40
|
|
40
41
|
def wait_for_stop_moving(delay: MOVING_WAIT_DELAY, attempts: MOVING_WAIT_ATTEMPTS)
|
41
|
-
|
42
|
-
previous, current =
|
42
|
+
Utils::Attempt.with_retry(errors: NodeMovingError, max: attempts, wait: 0) do
|
43
|
+
previous, current = content_quads_with(delay: delay)
|
43
44
|
raise NodeMovingError.new(self, previous, current) if previous != current
|
45
|
+
|
44
46
|
current
|
45
47
|
end
|
46
48
|
end
|
47
49
|
|
48
50
|
def moving?(delay: MOVING_WAIT_DELAY)
|
49
|
-
previous, current =
|
51
|
+
previous, current = content_quads_with(delay: delay)
|
50
52
|
previous == current
|
51
53
|
end
|
52
54
|
|
@@ -122,17 +124,52 @@ module Ferrum
|
|
122
124
|
def property(name)
|
123
125
|
evaluate("this['#{name}']")
|
124
126
|
end
|
127
|
+
alias [] property
|
125
128
|
|
126
129
|
def attribute(name)
|
127
130
|
evaluate("this.getAttribute('#{name}')")
|
128
131
|
end
|
129
132
|
|
133
|
+
def selected
|
134
|
+
function = <<~JS
|
135
|
+
function(element) {
|
136
|
+
if (element.nodeName.toLowerCase() !== 'select') {
|
137
|
+
throw new Error('Element is not a <select> element.');
|
138
|
+
}
|
139
|
+
return Array.from(element).filter(option => option.selected);
|
140
|
+
}
|
141
|
+
JS
|
142
|
+
page.evaluate_func(function, self, on: self)
|
143
|
+
end
|
144
|
+
|
145
|
+
def select(*values, by: :value)
|
146
|
+
tap do
|
147
|
+
function = <<~JS
|
148
|
+
function(element, values, by) {
|
149
|
+
if (element.nodeName.toLowerCase() !== 'select') {
|
150
|
+
throw new Error('Element is not a <select> element.');
|
151
|
+
}
|
152
|
+
const options = Array.from(element.options);
|
153
|
+
element.value = undefined;
|
154
|
+
for (const option of options) {
|
155
|
+
option.selected = values.some((value) => option[by] === value);
|
156
|
+
if (option.selected && !element.multiple) break;
|
157
|
+
}
|
158
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
159
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
160
|
+
}
|
161
|
+
JS
|
162
|
+
page.evaluate_func(function, self, values.flatten, by, on: self)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
130
166
|
def evaluate(expression)
|
131
167
|
page.evaluate_on(node: self, expression: expression)
|
132
168
|
end
|
133
169
|
|
134
170
|
def ==(other)
|
135
171
|
return false unless other.is_a?(Node)
|
172
|
+
|
136
173
|
# We compare backendNodeId because once nodeId is sent to frontend backend
|
137
174
|
# never returns same nodeId sending 0. In other words frontend is
|
138
175
|
# responsible for keeping track of node ids.
|
@@ -147,30 +184,39 @@ module Ferrum
|
|
147
184
|
points = wait_for_stop_moving.map { |q| to_points(q) }.first
|
148
185
|
get_position(points, x, y, position)
|
149
186
|
rescue CoordinatesNotFoundError
|
150
|
-
x, y =
|
151
|
-
raise if x
|
187
|
+
x, y = bounding_rect_coordinates
|
188
|
+
raise if x.zero? && y.zero?
|
189
|
+
|
152
190
|
[x, y]
|
153
191
|
end
|
154
192
|
|
193
|
+
# Returns a hash of the computed styles for the node
|
194
|
+
def computed_style
|
195
|
+
page
|
196
|
+
.command("CSS.getComputedStyleForNode", nodeId: node_id)["computedStyle"]
|
197
|
+
.each_with_object({}) { |style, memo| memo.merge!(style["name"] => style["value"]) }
|
198
|
+
end
|
199
|
+
|
155
200
|
private
|
156
201
|
|
157
|
-
def
|
202
|
+
def bounding_rect_coordinates
|
158
203
|
evaluate <<~JS
|
159
204
|
[this.getBoundingClientRect().left + window.pageXOffset + (this.offsetWidth / 2),
|
160
205
|
this.getBoundingClientRect().top + window.pageYOffset + (this.offsetHeight / 2)]
|
161
206
|
JS
|
162
207
|
end
|
163
208
|
|
164
|
-
def
|
209
|
+
def content_quads
|
165
210
|
quads = page.command("DOM.getContentQuads", nodeId: node_id)["quads"]
|
166
|
-
raise CoordinatesNotFoundError, "Node is either not visible or not an HTMLElement" if quads.size
|
211
|
+
raise CoordinatesNotFoundError, "Node is either not visible or not an HTMLElement" if quads.size.zero?
|
212
|
+
|
167
213
|
quads
|
168
214
|
end
|
169
215
|
|
170
|
-
def
|
171
|
-
previous =
|
216
|
+
def content_quads_with(delay: MOVING_WAIT_DELAY)
|
217
|
+
previous = content_quads
|
172
218
|
sleep(delay)
|
173
|
-
current =
|
219
|
+
current = content_quads
|
174
220
|
[previous, current]
|
175
221
|
end
|
176
222
|
|
@@ -182,28 +228,28 @@ module Ferrum
|
|
182
228
|
x = point[:x] + offset_x.to_i
|
183
229
|
y = point[:y] + offset_y.to_i
|
184
230
|
else
|
185
|
-
x, y = points.inject([0, 0]) do |memo,
|
186
|
-
[memo[0] +
|
187
|
-
memo[1] +
|
231
|
+
x, y = points.inject([0, 0]) do |memo, coordinate|
|
232
|
+
[memo[0] + coordinate[:x],
|
233
|
+
memo[1] + coordinate[:y]]
|
188
234
|
end
|
189
235
|
|
190
|
-
x
|
191
|
-
y
|
236
|
+
x /= 4
|
237
|
+
y /= 4
|
192
238
|
end
|
193
239
|
|
194
240
|
if offset_x && offset_y && position == :center
|
195
|
-
x
|
196
|
-
y
|
241
|
+
x += offset_x.to_i
|
242
|
+
y += offset_y.to_i
|
197
243
|
end
|
198
244
|
|
199
245
|
[x, y]
|
200
246
|
end
|
201
247
|
|
202
248
|
def to_points(quad)
|
203
|
-
[{x: quad[0], y: quad[1]},
|
204
|
-
{x: quad[2], y: quad[3]},
|
205
|
-
{x: quad[4], y: quad[5]},
|
206
|
-
{x: quad[6], y: quad[7]}]
|
249
|
+
[{ x: quad[0], y: quad[1] },
|
250
|
+
{ x: quad[2], y: quad[3] },
|
251
|
+
{ x: quad[4], y: quad[5] },
|
252
|
+
{ x: quad[6], y: quad[7] }]
|
207
253
|
end
|
208
254
|
end
|
209
255
|
end
|
@@ -3,11 +3,26 @@
|
|
3
3
|
module Ferrum
|
4
4
|
class Page
|
5
5
|
module Animation
|
6
|
+
#
|
7
|
+
# Returns playback rate for CSS animations, defaults to `1`.
|
8
|
+
#
|
9
|
+
# @return [Integer]
|
10
|
+
#
|
6
11
|
def playback_rate
|
7
12
|
command("Animation.getPlaybackRate")["playbackRate"]
|
8
13
|
end
|
9
14
|
|
10
|
-
|
15
|
+
#
|
16
|
+
# Sets playback rate of CSS animations.
|
17
|
+
#
|
18
|
+
# @param [Integer] value
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# browser = Ferrum::Browser.new
|
22
|
+
# browser.playback_rate = 2000
|
23
|
+
# browser.go_to("https://google.com")
|
24
|
+
# browser.playback_rate # => 2000
|
25
|
+
#
|
11
26
|
def playback_rate=(value)
|
12
27
|
command("Animation.setPlaybackRate", playbackRate: value)
|
13
28
|
end
|