ferrum 0.17.1 → 0.18.0
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 +9 -1369
- data/lib/ferrum/accessibility/ax_node.rb +108 -0
- data/lib/ferrum/accessibility.rb +106 -0
- data/lib/ferrum/browser/binary.rb +41 -0
- data/lib/ferrum/browser/command.rb +20 -0
- data/lib/ferrum/browser/options/base.rb +69 -2
- data/lib/ferrum/browser/options/chrome.rb +88 -26
- data/lib/ferrum/browser/options/firefox.rb +32 -0
- data/lib/ferrum/browser/options.rb +43 -3
- data/lib/ferrum/browser/process.rb +56 -4
- data/lib/ferrum/browser/xvfb.rb +24 -0
- data/lib/ferrum/browser.rb +35 -9
- data/lib/ferrum/client/subscriber.rb +58 -0
- data/lib/ferrum/client/web_socket.rb +63 -12
- data/lib/ferrum/client.rb +206 -14
- data/lib/ferrum/context.rb +99 -4
- data/lib/ferrum/contexts.rb +136 -22
- data/lib/ferrum/cookies/cookie.rb +7 -1
- data/lib/ferrum/cookies.rb +5 -0
- data/lib/ferrum/dialog.rb +18 -2
- data/lib/ferrum/downloads.rb +52 -0
- data/lib/ferrum/errors.rb +62 -6
- data/lib/ferrum/frame/dom.rb +17 -0
- data/lib/ferrum/frame/runtime.rb +49 -8
- data/lib/ferrum/frame.rb +58 -1
- data/lib/ferrum/headers.rb +6 -0
- data/lib/ferrum/interceptable.rb +62 -0
- data/lib/ferrum/keyboard.rb +25 -0
- data/lib/ferrum/mouse.rb +6 -0
- data/lib/ferrum/network/auth_request.rb +81 -2
- data/lib/ferrum/network/error.rb +15 -0
- data/lib/ferrum/network/exchange.rb +10 -0
- data/lib/ferrum/network/intercepted_request.rb +94 -2
- data/lib/ferrum/network/request.rb +1 -1
- data/lib/ferrum/network/response.rb +16 -1
- data/lib/ferrum/network.rb +122 -10
- data/lib/ferrum/node.rb +305 -15
- data/lib/ferrum/page/animation.rb +7 -2
- data/lib/ferrum/page/frames.rb +69 -7
- data/lib/ferrum/page/screencast.rb +5 -0
- data/lib/ferrum/page/screenshot.rb +52 -20
- data/lib/ferrum/page/stream.rb +56 -0
- data/lib/ferrum/page/tracing.rb +6 -0
- data/lib/ferrum/page.rb +141 -46
- data/lib/ferrum/proxy.rb +52 -2
- data/lib/ferrum/rgba.rb +10 -0
- data/lib/ferrum/target.rb +124 -1
- data/lib/ferrum/utils/attempt.rb +20 -0
- data/lib/ferrum/utils/elapsed_time.rb +38 -0
- data/lib/ferrum/utils/event.rb +14 -0
- data/lib/ferrum/utils/platform.rb +22 -1
- data/lib/ferrum/utils/thread.rb +12 -0
- data/lib/ferrum/version.rb +1 -1
- data/lib/ferrum/worker.rb +125 -0
- data/lib/ferrum.rb +7 -0
- metadata +8 -21
data/lib/ferrum/node.rb
CHANGED
|
@@ -1,36 +1,92 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Ferrum
|
|
4
|
+
#
|
|
5
|
+
# Represents a DOM node (an element or a text node) found on a {Page} or
|
|
6
|
+
# within a {Frame}. Provides methods to inspect it (`text`, `property`,
|
|
7
|
+
# `attribute`), interact with it (`click`, `focus`, `type`, `select`) and
|
|
8
|
+
# search within it (`at_css`, `at_xpath`, `css`, `xpath`).
|
|
9
|
+
#
|
|
10
|
+
# @note Node identity is tied to the target it was found on; a `Node`
|
|
11
|
+
# fetched before a navigation cannot be used afterwards.
|
|
12
|
+
#
|
|
4
13
|
class Node
|
|
5
14
|
MOVING_WAIT_DELAY = ENV.fetch("FERRUM_NODE_MOVING_WAIT", 0.01).to_f
|
|
6
15
|
MOVING_WAIT_ATTEMPTS = ENV.fetch("FERRUM_NODE_MOVING_ATTEMPTS", 50).to_i
|
|
7
16
|
|
|
8
|
-
attr_reader :page, :target_id, :
|
|
17
|
+
attr_reader :page, :target_id, :description, :tag_name
|
|
9
18
|
|
|
10
|
-
def initialize(frame, target_id,
|
|
19
|
+
def initialize(frame, target_id, description, object_id: nil, node_id: nil)
|
|
11
20
|
@page = frame.page
|
|
12
21
|
@target_id = target_id
|
|
13
|
-
@node_id = node_id
|
|
14
22
|
@description = description
|
|
15
23
|
@tag_name = description["nodeName"].downcase
|
|
24
|
+
@object_id = object_id
|
|
25
|
+
@node_id = node_id
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Frontend node id is resolved lazily, on first actual need (focus, click, scroll_into_view, etc.)
|
|
29
|
+
# We can try to subscribe to `DOM.childNodeRemoved` and `DOM.childNodeInserted` in the future
|
|
30
|
+
# to keep track of nodes.
|
|
31
|
+
def node_id
|
|
32
|
+
@node_id ||= begin
|
|
33
|
+
id = page.command("DOM.requestNode", objectId: @object_id)["nodeId"]
|
|
34
|
+
raise NodeNotFoundError, "node is not trackable" if id.zero?
|
|
35
|
+
|
|
36
|
+
id
|
|
37
|
+
rescue NoExecutionContextError
|
|
38
|
+
raise NodeNotFoundError, "node is not trackable"
|
|
39
|
+
end
|
|
16
40
|
end
|
|
17
41
|
|
|
42
|
+
# Whether this is an element node, as opposed to e.g. a text node.
|
|
43
|
+
#
|
|
44
|
+
# @return [Boolean]
|
|
18
45
|
def node?
|
|
19
46
|
description["nodeType"] == 1 # nodeType: 3, nodeName: "#text" e.g.
|
|
20
47
|
end
|
|
21
48
|
|
|
49
|
+
#
|
|
50
|
+
# The id of the frame this node belongs to.
|
|
51
|
+
#
|
|
52
|
+
# @return [String]
|
|
53
|
+
#
|
|
22
54
|
def frame_id
|
|
23
55
|
description["frameId"]
|
|
24
56
|
end
|
|
25
57
|
|
|
58
|
+
#
|
|
59
|
+
# The {Frame} this node belongs to. Keep using finder methods (`at_css`,
|
|
60
|
+
# `at_xpath`, etc.) on it to search within that frame, e.g. inside an
|
|
61
|
+
# `iframe`.
|
|
62
|
+
#
|
|
63
|
+
# @return [Frame, nil]
|
|
64
|
+
#
|
|
65
|
+
# @example
|
|
66
|
+
# frame = page.at_xpath("//iframe").frame # => Frame
|
|
67
|
+
# frame.at_css("//a[text() = 'Log in']") # => Node
|
|
68
|
+
#
|
|
26
69
|
def frame
|
|
27
70
|
page.frame_by(id: frame_id)
|
|
28
71
|
end
|
|
29
72
|
|
|
73
|
+
#
|
|
74
|
+
# Focuses the node.
|
|
75
|
+
#
|
|
76
|
+
# @return [self]
|
|
77
|
+
#
|
|
78
|
+
# @example
|
|
79
|
+
# input = page.at_css("input[name='q']")
|
|
80
|
+
# input.focus
|
|
81
|
+
#
|
|
30
82
|
def focus
|
|
31
83
|
tap { page.command("DOM.focus", slowmoable: true, nodeId: node_id) }
|
|
32
84
|
end
|
|
33
85
|
|
|
86
|
+
# Whether the node can receive focus. Attempts to {#focus} the node to
|
|
87
|
+
# find out.
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean]
|
|
34
90
|
def focusable?
|
|
35
91
|
focus
|
|
36
92
|
true
|
|
@@ -38,6 +94,20 @@ module Ferrum
|
|
|
38
94
|
e.message == "Element is not focusable" ? false : raise
|
|
39
95
|
end
|
|
40
96
|
|
|
97
|
+
#
|
|
98
|
+
# Waits until the node's position stops changing, retrying up to
|
|
99
|
+
# `attempts` times. Raises {NodeMovingError} if the node is still moving
|
|
100
|
+
# after the last attempt.
|
|
101
|
+
#
|
|
102
|
+
# @param [Float] delay
|
|
103
|
+
# Seconds to wait between two position checks.
|
|
104
|
+
#
|
|
105
|
+
# @param [Integer] attempts
|
|
106
|
+
# Maximum number of attempts before raising.
|
|
107
|
+
#
|
|
108
|
+
# @return [Array]
|
|
109
|
+
# The content quads of the node once it has stopped moving.
|
|
110
|
+
#
|
|
41
111
|
def wait_for_stop_moving(delay: MOVING_WAIT_DELAY, attempts: MOVING_WAIT_ATTEMPTS)
|
|
42
112
|
Utils::Attempt.with_retry(errors: NodeMovingError, max: attempts, wait: 0) do
|
|
43
113
|
previous, current = content_quads_with(delay: delay)
|
|
@@ -47,15 +117,39 @@ module Ferrum
|
|
|
47
117
|
end
|
|
48
118
|
end
|
|
49
119
|
|
|
120
|
+
# Checks whether the node's position has stopped changing, by comparing
|
|
121
|
+
# two content-quad snapshots taken `delay` seconds apart.
|
|
122
|
+
#
|
|
123
|
+
# @param [Float] delay
|
|
124
|
+
# Seconds to wait between the two position checks.
|
|
125
|
+
#
|
|
126
|
+
# @return [Boolean]
|
|
50
127
|
def moving?(delay: MOVING_WAIT_DELAY)
|
|
51
128
|
previous, current = content_quads_with(delay: delay)
|
|
52
129
|
previous == current
|
|
53
130
|
end
|
|
54
131
|
|
|
132
|
+
#
|
|
133
|
+
# Removes focus from the node.
|
|
134
|
+
#
|
|
135
|
+
# @return [self]
|
|
136
|
+
#
|
|
55
137
|
def blur
|
|
56
138
|
tap { evaluate("this.blur()") }
|
|
57
139
|
end
|
|
58
140
|
|
|
141
|
+
#
|
|
142
|
+
# Sends keystrokes to the currently focused element via the page's
|
|
143
|
+
# keyboard. Typically chained after {#focus} or `click`.
|
|
144
|
+
#
|
|
145
|
+
# @param [Array<String, Symbol, (Symbol, String)>] keys
|
|
146
|
+
# The keys to type, e.g. `"Input"`, `[:Shift, "s"], "tring"`.
|
|
147
|
+
#
|
|
148
|
+
# @return [self]
|
|
149
|
+
#
|
|
150
|
+
# @example
|
|
151
|
+
# input.focus.type("Input")
|
|
152
|
+
#
|
|
59
153
|
def type(*keys)
|
|
60
154
|
tap { page.keyboard.type(*keys) }
|
|
61
155
|
end
|
|
@@ -67,32 +161,46 @@ module Ferrum
|
|
|
67
161
|
x, y = find_position(**offset)
|
|
68
162
|
modifiers = page.keyboard.modifiers(keys)
|
|
69
163
|
|
|
164
|
+
# `:right` and `:double` pass `wait: 0` to preserve the historical
|
|
165
|
+
# no-network-wait default of `Mouse#up` and `Mouse#down`
|
|
70
166
|
case mode
|
|
71
167
|
when :right
|
|
72
|
-
page.mouse.
|
|
73
|
-
page.mouse.down(button: :right, modifiers: modifiers)
|
|
74
|
-
sleep(delay)
|
|
75
|
-
page.mouse.up(button: :right, modifiers: modifiers)
|
|
168
|
+
page.mouse.click(x:, y:, modifiers:, delay:, button: :right, wait: 0)
|
|
76
169
|
when :double
|
|
77
|
-
page.mouse.
|
|
78
|
-
page.mouse.down(modifiers: modifiers, count: 2)
|
|
79
|
-
sleep(delay)
|
|
80
|
-
page.mouse.up(modifiers: modifiers, count: 2)
|
|
170
|
+
page.mouse.click(x:, y:, modifiers:, delay:, count: 2, wait: 0)
|
|
81
171
|
when :left
|
|
82
|
-
page.mouse.click(x
|
|
172
|
+
page.mouse.click(x:, y:, modifiers:, delay:)
|
|
83
173
|
end
|
|
84
174
|
|
|
85
175
|
self
|
|
86
176
|
end
|
|
87
177
|
|
|
178
|
+
# Not currently implemented.
|
|
179
|
+
#
|
|
180
|
+
# @raise [NotImplementedError] always
|
|
88
181
|
def hover
|
|
89
182
|
raise NotImplementedError
|
|
90
183
|
end
|
|
91
184
|
|
|
185
|
+
#
|
|
186
|
+
# Scrolls the node into view if it is not already visible.
|
|
187
|
+
#
|
|
188
|
+
# @return [self]
|
|
189
|
+
#
|
|
190
|
+
# @example
|
|
191
|
+
# page.at_css("#footer").scroll_into_view
|
|
192
|
+
#
|
|
92
193
|
def scroll_into_view
|
|
93
194
|
tap { page.command("DOM.scrollIntoViewIfNeeded", nodeId: node_id) }
|
|
94
195
|
end
|
|
95
196
|
|
|
197
|
+
# Whether the node's bounding rect is fully within the viewport (or,
|
|
198
|
+
# when `of:` is given, within that scoping element's bounds).
|
|
199
|
+
#
|
|
200
|
+
# @param [Node, nil] of
|
|
201
|
+
# An element to use as the visible bounds instead of the window.
|
|
202
|
+
#
|
|
203
|
+
# @return [Boolean]
|
|
96
204
|
def in_viewport?(of: nil)
|
|
97
205
|
function = <<~JS
|
|
98
206
|
function(element, scope) {
|
|
@@ -109,26 +217,94 @@ module Ferrum
|
|
|
109
217
|
page.evaluate_func(function, self, of)
|
|
110
218
|
end
|
|
111
219
|
|
|
220
|
+
#
|
|
221
|
+
# Sets files on a file input node.
|
|
222
|
+
#
|
|
223
|
+
# @param [String, Array<String>] value
|
|
224
|
+
# Path or paths to the file(s) to upload.
|
|
225
|
+
#
|
|
226
|
+
# @return [Hash{String => Object}]
|
|
227
|
+
#
|
|
228
|
+
# @example
|
|
229
|
+
# page.at_css("input[type=file]").select_file("/path/to/file.png")
|
|
230
|
+
#
|
|
112
231
|
def select_file(value)
|
|
113
|
-
page.command(
|
|
114
|
-
|
|
115
|
-
|
|
232
|
+
page.command(
|
|
233
|
+
"DOM.setFileInputFiles",
|
|
234
|
+
slowmoable: true,
|
|
235
|
+
backendNodeId: description["backendNodeId"],
|
|
236
|
+
files: Array(value)
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
#
|
|
241
|
+
# Finds a node by xpath, scoped to search within this node. Runs
|
|
242
|
+
# `document.evaluate` within this node.
|
|
243
|
+
#
|
|
244
|
+
# @param [String] selector
|
|
245
|
+
#
|
|
246
|
+
# @return [Node, nil]
|
|
247
|
+
#
|
|
248
|
+
# @example
|
|
249
|
+
# page.at_xpath("//iframe").at_xpath(".//a") # => Node
|
|
250
|
+
#
|
|
116
251
|
def at_xpath(selector)
|
|
117
252
|
page.at_xpath(selector, within: self)
|
|
118
253
|
end
|
|
119
254
|
|
|
255
|
+
#
|
|
256
|
+
# Finds a node by CSS selector, scoped to search within this node. Runs
|
|
257
|
+
# `querySelector` within this node.
|
|
258
|
+
#
|
|
259
|
+
# @param [String] selector
|
|
260
|
+
#
|
|
261
|
+
# @return [Node, nil]
|
|
262
|
+
#
|
|
263
|
+
# @example
|
|
264
|
+
# page.at_css("form").at_css("input[name='q']") # => Node
|
|
265
|
+
#
|
|
120
266
|
def at_css(selector)
|
|
121
267
|
page.at_css(selector, within: self)
|
|
122
268
|
end
|
|
123
269
|
|
|
270
|
+
#
|
|
271
|
+
# Finds nodes by xpath, scoped to search within this node. Runs
|
|
272
|
+
# `document.evaluate` within this node.
|
|
273
|
+
#
|
|
274
|
+
# @param [String] selector
|
|
275
|
+
#
|
|
276
|
+
# @return [Array<Node>]
|
|
277
|
+
#
|
|
278
|
+
# @example
|
|
279
|
+
# page.at_css("ul").xpath(".//li") # => [Node]
|
|
280
|
+
#
|
|
124
281
|
def xpath(selector)
|
|
125
282
|
page.xpath(selector, within: self)
|
|
126
283
|
end
|
|
127
284
|
|
|
285
|
+
#
|
|
286
|
+
# Finds nodes by CSS selector, scoped to search within this node. Runs
|
|
287
|
+
# `querySelectorAll` within this node.
|
|
288
|
+
#
|
|
289
|
+
# @param [String] selector
|
|
290
|
+
#
|
|
291
|
+
# @return [Array<Node>]
|
|
292
|
+
#
|
|
293
|
+
# @example
|
|
294
|
+
# page.at_css("ul").css("li") # => [Node]
|
|
295
|
+
#
|
|
128
296
|
def css(selector)
|
|
129
297
|
page.css(selector, within: self)
|
|
130
298
|
end
|
|
131
299
|
|
|
300
|
+
#
|
|
301
|
+
# The node's text content, i.e. `textContent`.
|
|
302
|
+
#
|
|
303
|
+
# @return [String]
|
|
304
|
+
#
|
|
305
|
+
# @example
|
|
306
|
+
# page.at_css("a > h3").text # => "rubycdp/ferrum: Ruby Chrome/Chromium driver - GitHub"
|
|
307
|
+
#
|
|
132
308
|
def text
|
|
133
309
|
evaluate("this.textContent")
|
|
134
310
|
end
|
|
@@ -138,19 +314,52 @@ module Ferrum
|
|
|
138
314
|
evaluate("this.innerText")
|
|
139
315
|
end
|
|
140
316
|
|
|
317
|
+
#
|
|
318
|
+
# The node's `value` property. Useful for form elements such as `input`,
|
|
319
|
+
# `select` and `textarea`.
|
|
320
|
+
#
|
|
321
|
+
# @return [Object]
|
|
322
|
+
#
|
|
141
323
|
def value
|
|
142
324
|
evaluate("this.value")
|
|
143
325
|
end
|
|
144
326
|
|
|
327
|
+
#
|
|
328
|
+
# Returns the given JavaScript property of the node.
|
|
329
|
+
#
|
|
330
|
+
# @param [String] name
|
|
331
|
+
#
|
|
332
|
+
# @return [Object]
|
|
333
|
+
#
|
|
334
|
+
# @example
|
|
335
|
+
# page.at_css("input").property("value") # => "Foo"
|
|
336
|
+
#
|
|
145
337
|
def property(name)
|
|
146
338
|
evaluate("this['#{name}']")
|
|
147
339
|
end
|
|
148
340
|
alias [] property
|
|
149
341
|
|
|
342
|
+
#
|
|
343
|
+
# Returns the value of the given HTML attribute, i.e.
|
|
344
|
+
# `getAttribute(name)`. Unlike {#property}, it reads the attribute as
|
|
345
|
+
# defined in markup rather than the live DOM property.
|
|
346
|
+
#
|
|
347
|
+
# @param [String] name
|
|
348
|
+
#
|
|
349
|
+
# @return [String, nil]
|
|
350
|
+
#
|
|
351
|
+
# @example
|
|
352
|
+
# page.at_css("input").attribute("value") # => "Foo"
|
|
353
|
+
#
|
|
150
354
|
def attribute(name)
|
|
151
355
|
evaluate("this.getAttribute('#{name}')")
|
|
152
356
|
end
|
|
153
357
|
|
|
358
|
+
#
|
|
359
|
+
# Returns the selected `option` nodes of a `select` element.
|
|
360
|
+
#
|
|
361
|
+
# @return [Array<Node>]
|
|
362
|
+
#
|
|
154
363
|
def selected
|
|
155
364
|
function = <<~JS
|
|
156
365
|
function(element) {
|
|
@@ -163,6 +372,29 @@ module Ferrum
|
|
|
163
372
|
page.evaluate_func(function, self, on: self)
|
|
164
373
|
end
|
|
165
374
|
|
|
375
|
+
#
|
|
376
|
+
# (chainable) Selects options of a `select` element by the given
|
|
377
|
+
# attribute.
|
|
378
|
+
#
|
|
379
|
+
# @param [Array<String>] values
|
|
380
|
+
# The value(s) to select. Accepts a string, multiple strings, or an
|
|
381
|
+
# array of strings.
|
|
382
|
+
#
|
|
383
|
+
# @param [Symbol] by
|
|
384
|
+
# The `option` attribute to match `values` against, e.g. `:value` or
|
|
385
|
+
# `:text`.
|
|
386
|
+
#
|
|
387
|
+
# @return [self]
|
|
388
|
+
#
|
|
389
|
+
# @example
|
|
390
|
+
# page.at_xpath("//*[select]").select(["1"]) # => Node (select)
|
|
391
|
+
# page.at_xpath("//*[select]").select(["text"], by: :text) # => Node (select)
|
|
392
|
+
#
|
|
393
|
+
# @example Accepts a string, multiple strings or an array of strings:
|
|
394
|
+
# page.at_xpath("//*[select]").select("1")
|
|
395
|
+
# page.at_xpath("//*[select]").select("1", "2")
|
|
396
|
+
# page.at_xpath("//*[select]").select(["1", "2"])
|
|
397
|
+
#
|
|
166
398
|
def select(*values, by: :value)
|
|
167
399
|
tap do
|
|
168
400
|
function = <<~JS
|
|
@@ -184,10 +416,29 @@ module Ferrum
|
|
|
184
416
|
end
|
|
185
417
|
end
|
|
186
418
|
|
|
419
|
+
#
|
|
420
|
+
# Evaluates the given JavaScript expression with `this` bound to the
|
|
421
|
+
# node.
|
|
422
|
+
#
|
|
423
|
+
# @param [String] expression
|
|
424
|
+
#
|
|
425
|
+
# @return [Object]
|
|
426
|
+
#
|
|
427
|
+
# @example
|
|
428
|
+
# page.at_css("input").evaluate("this.value")
|
|
429
|
+
#
|
|
187
430
|
def evaluate(expression)
|
|
188
431
|
page.evaluate_on(node: self, expression: expression)
|
|
189
432
|
end
|
|
190
433
|
|
|
434
|
+
#
|
|
435
|
+
# Two nodes are equal when they belong to the same target and share the
|
|
436
|
+
# same backend node id.
|
|
437
|
+
#
|
|
438
|
+
# @param [Object] other
|
|
439
|
+
#
|
|
440
|
+
# @return [Boolean]
|
|
441
|
+
#
|
|
191
442
|
def ==(other)
|
|
192
443
|
return false unless other.is_a?(Node)
|
|
193
444
|
|
|
@@ -197,10 +448,30 @@ module Ferrum
|
|
|
197
448
|
target_id == other.target_id && description["backendNodeId"] == other.description["backendNodeId"]
|
|
198
449
|
end
|
|
199
450
|
|
|
451
|
+
#
|
|
452
|
+
# A developer-friendly string representation of the node.
|
|
453
|
+
#
|
|
454
|
+
# @return [String]
|
|
455
|
+
#
|
|
200
456
|
def inspect
|
|
201
457
|
%(#<#{self.class} @target_id=#{@target_id.inspect} @node_id=#{@node_id} @description=#{@description.inspect}>)
|
|
202
458
|
end
|
|
203
459
|
|
|
460
|
+
#
|
|
461
|
+
# Finds the x, y coordinates to click or hover on the node.
|
|
462
|
+
#
|
|
463
|
+
# @param [Integer, nil] x
|
|
464
|
+
# Horizontal offset from the reference point.
|
|
465
|
+
#
|
|
466
|
+
# @param [Integer, nil] y
|
|
467
|
+
# Vertical offset from the reference point.
|
|
468
|
+
#
|
|
469
|
+
# @param [Symbol] position
|
|
470
|
+
# `:top` to offset from the node's top-left corner, `:center` to offset
|
|
471
|
+
# from its center.
|
|
472
|
+
#
|
|
473
|
+
# @return [(Integer, Integer)]
|
|
474
|
+
#
|
|
204
475
|
def find_position(x: nil, y: nil, position: :top)
|
|
205
476
|
points = wait_for_stop_moving.map { |q| to_points(q) }.first
|
|
206
477
|
get_position(points, x, y, position)
|
|
@@ -218,10 +489,29 @@ module Ferrum
|
|
|
218
489
|
.each_with_object({}) { |style, memo| memo.merge!(style["name"] => style["value"]) }
|
|
219
490
|
end
|
|
220
491
|
|
|
492
|
+
# Returns the computed accessibility node for the element, or nil if the
|
|
493
|
+
# element is ignored by the accessibility tree.
|
|
494
|
+
#
|
|
495
|
+
# @return [Accessibility::AXNode, nil]
|
|
496
|
+
def axnode
|
|
497
|
+
page.accessibility.node_for(self)
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
#
|
|
501
|
+
# Removes the node from the DOM.
|
|
502
|
+
#
|
|
503
|
+
# @return [Hash{String => Object}]
|
|
504
|
+
#
|
|
505
|
+
# @example
|
|
506
|
+
# page.at_css("#ad").remove
|
|
507
|
+
#
|
|
221
508
|
def remove
|
|
222
509
|
page.command("DOM.removeNode", nodeId: node_id)
|
|
223
510
|
end
|
|
224
511
|
|
|
512
|
+
# Whether the node still exists in the DOM.
|
|
513
|
+
#
|
|
514
|
+
# @return [Boolean]
|
|
225
515
|
def exists?
|
|
226
516
|
page.command("DOM.resolveNode", nodeId: node_id)
|
|
227
517
|
true
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Ferrum
|
|
4
4
|
class Page
|
|
5
|
+
#
|
|
6
|
+
# Controls the playback of CSS animations on the page via the CDP
|
|
7
|
+
# [Animation](https://chromedevtools.github.io/devtools-protocol/tot/Animation/)
|
|
8
|
+
# domain, allowing animations to be sped up, slowed down, or paused.
|
|
9
|
+
#
|
|
5
10
|
module Animation
|
|
6
11
|
#
|
|
7
12
|
# Returns playback rate for CSS animations, defaults to `1`.
|
|
8
13
|
#
|
|
9
|
-
# @return [
|
|
14
|
+
# @return [Number]
|
|
10
15
|
#
|
|
11
16
|
def playback_rate
|
|
12
17
|
command("Animation.getPlaybackRate")["playbackRate"]
|
|
@@ -15,7 +20,7 @@ module Ferrum
|
|
|
15
20
|
#
|
|
16
21
|
# Sets playback rate of CSS animations.
|
|
17
22
|
#
|
|
18
|
-
# @param [
|
|
23
|
+
# @param [Number] value
|
|
19
24
|
#
|
|
20
25
|
# @example
|
|
21
26
|
# browser = Ferrum::Browser.new
|
data/lib/ferrum/page/frames.rb
CHANGED
|
@@ -4,6 +4,12 @@ require "ferrum/frame"
|
|
|
4
4
|
|
|
5
5
|
module Ferrum
|
|
6
6
|
class Page
|
|
7
|
+
#
|
|
8
|
+
# Tracks the page's frame tree and keeps it in sync with the browser by
|
|
9
|
+
# subscribing to the relevant `Page.*`, `Network.*`, and `Runtime.*` CDP
|
|
10
|
+
# events. Exposes lookups over the tracked {Frame} objects and reports
|
|
11
|
+
# when the tree has settled into an idle state.
|
|
12
|
+
#
|
|
7
13
|
module Frames
|
|
8
14
|
# The page's main frame, the top of the tree and the parent of all frames.
|
|
9
15
|
#
|
|
@@ -63,12 +69,16 @@ module Ferrum
|
|
|
63
69
|
end
|
|
64
70
|
end
|
|
65
71
|
|
|
72
|
+
private
|
|
73
|
+
|
|
66
74
|
def frames_subscribe
|
|
67
75
|
subscribe_frame_attached
|
|
68
76
|
subscribe_frame_detached
|
|
77
|
+
subscribe_frame_started_navigating
|
|
69
78
|
subscribe_frame_started_loading
|
|
70
79
|
subscribe_frame_navigated
|
|
71
80
|
subscribe_frame_stopped_loading
|
|
81
|
+
subscribe_frame_lifecycle_events
|
|
72
82
|
|
|
73
83
|
subscribe_navigated_within_document
|
|
74
84
|
|
|
@@ -79,8 +89,6 @@ module Ferrum
|
|
|
79
89
|
subscribe_execution_contexts_cleared
|
|
80
90
|
end
|
|
81
91
|
|
|
82
|
-
private
|
|
83
|
-
|
|
84
92
|
def subscribe_frame_attached
|
|
85
93
|
on("Page.frameAttached") do |params|
|
|
86
94
|
parent_frame_id, frame_id = params.values_at("parentFrameId", "frameId")
|
|
@@ -100,6 +108,18 @@ module Ferrum
|
|
|
100
108
|
end
|
|
101
109
|
end
|
|
102
110
|
|
|
111
|
+
# Tracks the +loaderId+ for each navigating frame and clears stale
|
|
112
|
+
# lifecycle events from the previous navigation.
|
|
113
|
+
def subscribe_frame_started_navigating
|
|
114
|
+
on("Page.frameStartedNavigating") do |params|
|
|
115
|
+
frame = @frames[params["frameId"]]
|
|
116
|
+
if frame
|
|
117
|
+
frame.loader_id = params["loaderId"]
|
|
118
|
+
frame.lifecycle_events.clear
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
103
123
|
def subscribe_frame_started_loading
|
|
104
124
|
on("Page.frameStartedLoading") do |params|
|
|
105
125
|
frame = @frames[params["frameId"]]
|
|
@@ -138,6 +158,33 @@ module Ferrum
|
|
|
138
158
|
end
|
|
139
159
|
end
|
|
140
160
|
|
|
161
|
+
# Appends +Page.lifecycleEvent+ events to {Frame#lifecycle_events}.
|
|
162
|
+
# Events from a superseded navigation (+loaderId+ mismatch) are dropped.
|
|
163
|
+
# Transitions +loading="lazy"+ iframes that Chrome never starts loading
|
|
164
|
+
# to +:stopped_loading+ on +networkIdle+, preventing a {Page#go_to} timeout.
|
|
165
|
+
def subscribe_frame_lifecycle_events
|
|
166
|
+
on("Page.lifecycleEvent") do |params|
|
|
167
|
+
frame = @frames[params["frameId"]]
|
|
168
|
+
next unless frame
|
|
169
|
+
|
|
170
|
+
frame.loader_id = params["loaderId"] unless frame.loader_id
|
|
171
|
+
# Reject stale events from a superseded navigation, iframes are not destroyed by Chrome, instead it creates
|
|
172
|
+
# new ones. Main frame stays with the same id, but new events start to flow in.
|
|
173
|
+
next if frame.loader_id != params["loaderId"]
|
|
174
|
+
|
|
175
|
+
event = params.slice("name", "timestamp")
|
|
176
|
+
frame.lifecycle_events << event
|
|
177
|
+
|
|
178
|
+
# This handles iframes with loading="lazy", those that Chrome attaches but parks outside the viewport.
|
|
179
|
+
# They do not trigger any events except `Page.frameAttached` and lifecycle events like:
|
|
180
|
+
# `init` and `networkIdle`. Without it `go_to` would wait for such iframe until it raises timeout.
|
|
181
|
+
if event["name"] == "networkIdle" && !frame.main?
|
|
182
|
+
frame.state = :stopped_loading
|
|
183
|
+
@event.set if idling?
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
141
188
|
def subscribe_navigated_within_document
|
|
142
189
|
on("Page.navigatedWithinDocument") do
|
|
143
190
|
@event.set if idling?
|
|
@@ -177,21 +224,36 @@ module Ferrum
|
|
|
177
224
|
execution_id = params["executionContextId"]
|
|
178
225
|
frame = frame_by(execution_id: execution_id)
|
|
179
226
|
frame&.execution_id = nil
|
|
180
|
-
frame&.state = :
|
|
227
|
+
frame&.state = :canceled
|
|
181
228
|
end
|
|
182
229
|
end
|
|
183
230
|
|
|
231
|
+
# On full navigations/reloads Chrome fires +Page.frameAttached+ with new
|
|
232
|
+
# frame IDs but skips +Page.frameDetached+ for old ones. Removing stale
|
|
233
|
+
# child frames here prevents them from blocking {#idling?} indefinitely.
|
|
234
|
+
# The main frame is reset in-place; child frames are re-added via
|
|
235
|
+
# +Page.frameAttached+.
|
|
184
236
|
def subscribe_execution_contexts_cleared
|
|
185
237
|
on("Runtime.executionContextsCleared") do
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
238
|
+
children = []
|
|
239
|
+
|
|
240
|
+
@frames.each do |frame_id, f|
|
|
241
|
+
if f.main?
|
|
242
|
+
f.execution_id = nil
|
|
243
|
+
f.loader_id = nil
|
|
244
|
+
f.lifecycle_events.clear
|
|
245
|
+
f.state = :canceled
|
|
246
|
+
else
|
|
247
|
+
children << frame_id
|
|
248
|
+
end
|
|
189
249
|
end
|
|
250
|
+
|
|
251
|
+
children.each { |id| @frames.delete(id) }
|
|
190
252
|
end
|
|
191
253
|
end
|
|
192
254
|
|
|
193
255
|
def idling?
|
|
194
|
-
@frames.values.all?
|
|
256
|
+
@frames.values.all?(&:idle?)
|
|
195
257
|
end
|
|
196
258
|
end
|
|
197
259
|
end
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module Ferrum
|
|
4
4
|
class Page
|
|
5
|
+
#
|
|
6
|
+
# Starts and stops a live screencast of the page, streaming a sequence
|
|
7
|
+
# of frame images (a video-like feed) to a given block as the page
|
|
8
|
+
# renders.
|
|
9
|
+
#
|
|
5
10
|
module Screencast
|
|
6
11
|
# Starts sending frames to record screencast to the given block.
|
|
7
12
|
#
|