ferrum 0.17.2 → 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/README.md +1 -1
- 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 +67 -0
- data/lib/ferrum/browser/options/chrome.rb +36 -1
- data/lib/ferrum/browser/options/firefox.rb +32 -0
- data/lib/ferrum/browser/options.rb +41 -2
- data/lib/ferrum/browser/process.rb +51 -0
- data/lib/ferrum/browser/xvfb.rb +24 -0
- data/lib/ferrum/browser.rb +32 -10
- data/lib/ferrum/client/subscriber.rb +58 -0
- data/lib/ferrum/client/web_socket.rb +62 -11
- data/lib/ferrum/client.rb +199 -8
- data/lib/ferrum/context.rb +97 -4
- data/lib/ferrum/contexts.rb +119 -9
- data/lib/ferrum/cookies/cookie.rb +6 -0
- 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 +58 -3
- data/lib/ferrum/frame/dom.rb +17 -0
- data/lib/ferrum/frame/runtime.rb +48 -7
- 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 +2 -0
- data/lib/ferrum/network.rb +121 -9
- data/lib/ferrum/node.rb +305 -15
- data/lib/ferrum/page/animation.rb +5 -0
- 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 +139 -42
- 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 +21 -0
- 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 +6 -16
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
|
#
|
|
@@ -4,12 +4,17 @@ require "ferrum/rgba"
|
|
|
4
4
|
|
|
5
5
|
module Ferrum
|
|
6
6
|
class Page
|
|
7
|
+
#
|
|
8
|
+
# Captures screenshots, PDFs, and MHTML snapshots of the page, and
|
|
9
|
+
# exposes viewport/document size helpers used to compute capture areas.
|
|
10
|
+
#
|
|
7
11
|
module Screenshot
|
|
8
12
|
FULL_WARNING = "Ignoring :selector or :area in #screenshot since full: true was given at %s"
|
|
9
13
|
AREA_WARNING = "Ignoring :area in #screenshot since selector: was given at %s"
|
|
10
14
|
|
|
11
15
|
DEFAULT_SCREENSHOT_FORMAT = "png"
|
|
12
16
|
SUPPORTED_SCREENSHOT_FORMAT = %w[png jpeg jpg webp].freeze
|
|
17
|
+
DEFAULT_RENDER_TIMEOUT = 60
|
|
13
18
|
|
|
14
19
|
DEFAULT_PDF_OPTIONS = {
|
|
15
20
|
landscape: false,
|
|
@@ -65,6 +70,11 @@ module Ferrum
|
|
|
65
70
|
# @option opts [Ferrum::RGBA] :background_color
|
|
66
71
|
# Sets the background color.
|
|
67
72
|
#
|
|
73
|
+
# @param [Numeric] timeout
|
|
74
|
+
# How long to wait for the screenshot to be captured. Defaults to
|
|
75
|
+
# {DEFAULT_RENDER_TIMEOUT} since a full-page capture is a known slow
|
|
76
|
+
# outlier among CDP commands.
|
|
77
|
+
#
|
|
68
78
|
# @example
|
|
69
79
|
# page.go_to("https://google.com/")
|
|
70
80
|
#
|
|
@@ -83,10 +93,10 @@ module Ferrum
|
|
|
83
93
|
# @example Save with specific background color:
|
|
84
94
|
# page.screenshot(background_color: Ferrum::RGBA.new(0, 0, 0, 0.0))
|
|
85
95
|
#
|
|
86
|
-
def screenshot(**opts)
|
|
96
|
+
def screenshot(timeout: DEFAULT_RENDER_TIMEOUT, **opts)
|
|
87
97
|
path, encoding = common_options(**opts)
|
|
88
98
|
options = screenshot_options(path, **opts)
|
|
89
|
-
data = capture_screenshot(options, opts[:full], opts[:background_color])
|
|
99
|
+
data = capture_screenshot(options, opts[:full], opts[:background_color], timeout)
|
|
90
100
|
return data if encoding == :base64
|
|
91
101
|
|
|
92
102
|
bin = Base64.decode64(data)
|
|
@@ -124,15 +134,20 @@ module Ferrum
|
|
|
124
134
|
# See other [native options](https://chromedevtools.github.io/devtools-protocol/tot/Page#method-printToPDF) you
|
|
125
135
|
# can pass.
|
|
126
136
|
#
|
|
137
|
+
# @param [Numeric] timeout
|
|
138
|
+
# How long to wait for the PDF to be generated. Defaults to
|
|
139
|
+
# {DEFAULT_RENDER_TIMEOUT} since large documents are a known slow
|
|
140
|
+
# outlier among CDP commands.
|
|
141
|
+
#
|
|
127
142
|
# @example
|
|
128
143
|
# page.go_to("https://google.com/")
|
|
129
144
|
# # Save to disk as a PDF
|
|
130
145
|
# page.pdf(path: "google.pdf", paper_width: 1.0, paper_height: 1.0) # => true
|
|
131
146
|
#
|
|
132
|
-
def pdf(**opts)
|
|
147
|
+
def pdf(timeout: DEFAULT_RENDER_TIMEOUT, **opts)
|
|
133
148
|
path, encoding = common_options(**opts)
|
|
134
149
|
options = pdf_options(**opts).merge(transferMode: "ReturnAsStream")
|
|
135
|
-
handle = command("Page.printToPDF", **options).fetch("stream")
|
|
150
|
+
handle = command("Page.printToPDF", timeout: timeout, **options).fetch("stream")
|
|
136
151
|
stream_to(path: path, encoding: encoding, handle: handle)
|
|
137
152
|
end
|
|
138
153
|
|
|
@@ -153,18 +168,46 @@ module Ferrum
|
|
|
153
168
|
save_file(path, data)
|
|
154
169
|
end
|
|
155
170
|
|
|
171
|
+
#
|
|
172
|
+
# Current viewport size.
|
|
173
|
+
#
|
|
174
|
+
# @return [(Integer, Integer)]
|
|
175
|
+
# The width, height of the viewport.
|
|
176
|
+
#
|
|
177
|
+
# @example
|
|
178
|
+
# page.viewport_size # => [1024, 768]
|
|
179
|
+
#
|
|
156
180
|
def viewport_size
|
|
157
181
|
evaluate <<~JS
|
|
158
182
|
[window.innerWidth, window.innerHeight]
|
|
159
183
|
JS
|
|
160
184
|
end
|
|
161
185
|
|
|
186
|
+
#
|
|
187
|
+
# The ratio of the resolution in physical pixels to the resolution in
|
|
188
|
+
# CSS pixels for the current display device.
|
|
189
|
+
#
|
|
190
|
+
# @return [Float]
|
|
191
|
+
#
|
|
192
|
+
# @example
|
|
193
|
+
# page.device_pixel_ratio # => 1.0
|
|
194
|
+
#
|
|
162
195
|
def device_pixel_ratio
|
|
163
196
|
evaluate <<~JS
|
|
164
197
|
window.devicePixelRatio
|
|
165
198
|
JS
|
|
166
199
|
end
|
|
167
200
|
|
|
201
|
+
#
|
|
202
|
+
# Full size of the document, including the part that is not visible in
|
|
203
|
+
# the viewport.
|
|
204
|
+
#
|
|
205
|
+
# @return [(Integer, Integer)]
|
|
206
|
+
# The scroll width, scroll height of the document.
|
|
207
|
+
#
|
|
208
|
+
# @example
|
|
209
|
+
# page.document_size # => [1024, 4000]
|
|
210
|
+
#
|
|
168
211
|
def document_size
|
|
169
212
|
evaluate <<~JS
|
|
170
213
|
[document.documentElement.scrollWidth,
|
|
@@ -281,23 +324,12 @@ module Ferrum
|
|
|
281
324
|
option.to_s.gsub(%r{(?:_|(/))([a-z\d]*)}) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" }.to_sym
|
|
282
325
|
end
|
|
283
326
|
|
|
284
|
-
def capture_screenshot(options, full, background_color)
|
|
285
|
-
|
|
286
|
-
with_background_color(background_color) do
|
|
287
|
-
command("Page.captureScreenshot", **options)
|
|
288
|
-
end
|
|
289
|
-
end.fetch("data")
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
def maybe_resize_fullscreen(full)
|
|
293
|
-
if full
|
|
294
|
-
width, height = viewport_size.dup
|
|
295
|
-
resize(fullscreen: true)
|
|
296
|
-
end
|
|
327
|
+
def capture_screenshot(options, full, background_color, timeout)
|
|
328
|
+
options = options.merge(captureBeyondViewport: true) if full
|
|
297
329
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
330
|
+
with_background_color(background_color) do
|
|
331
|
+
command("Page.captureScreenshot", timeout: timeout, **options)
|
|
332
|
+
end.fetch("data")
|
|
301
333
|
end
|
|
302
334
|
|
|
303
335
|
def with_background_color(color)
|
data/lib/ferrum/page/stream.rb
CHANGED
|
@@ -2,9 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
module Ferrum
|
|
4
4
|
class Page
|
|
5
|
+
#
|
|
6
|
+
# Reads a CDP `IO` stream handle (e.g. from `Page.printToPDF` or
|
|
7
|
+
# `Tracing.tracingComplete`) in chunks and writes its contents to a file
|
|
8
|
+
# on disk or accumulates it in memory.
|
|
9
|
+
#
|
|
5
10
|
module Stream
|
|
6
11
|
STREAM_CHUNK = 128 * 1024
|
|
7
12
|
|
|
13
|
+
#
|
|
14
|
+
# Reads a CDP `IO` stream to a file on disk, or into memory when no
|
|
15
|
+
# path is given.
|
|
16
|
+
#
|
|
17
|
+
# @param [String, nil] path
|
|
18
|
+
# The path to save the stream's contents to. When `nil` the contents
|
|
19
|
+
# are returned in memory instead.
|
|
20
|
+
#
|
|
21
|
+
# @param [Symbol] encoding
|
|
22
|
+
# `:base64` or `:binary`. Only used when `path` is `nil`.
|
|
23
|
+
#
|
|
24
|
+
# @param [String] handle
|
|
25
|
+
# The CDP `IO` stream handle to read from.
|
|
26
|
+
#
|
|
27
|
+
# @return [Boolean, String]
|
|
28
|
+
# `true` when saved to disk, otherwise the stream's contents.
|
|
29
|
+
#
|
|
8
30
|
def stream_to(path:, encoding:, handle:)
|
|
9
31
|
if path.nil?
|
|
10
32
|
stream_to_memory(encoding: encoding, handle: handle)
|
|
@@ -13,17 +35,51 @@ module Ferrum
|
|
|
13
35
|
end
|
|
14
36
|
end
|
|
15
37
|
|
|
38
|
+
#
|
|
39
|
+
# Reads a CDP `IO` stream and writes its contents to a file on disk.
|
|
40
|
+
#
|
|
41
|
+
# @param [String] path
|
|
42
|
+
# The path to save the stream's contents to.
|
|
43
|
+
#
|
|
44
|
+
# @param [String] handle
|
|
45
|
+
# The CDP `IO` stream handle to read from.
|
|
46
|
+
#
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
#
|
|
16
49
|
def stream_to_file(path:, handle:)
|
|
17
50
|
File.open(path, "wb") { |f| stream(output: f, handle: handle) }
|
|
18
51
|
true
|
|
19
52
|
end
|
|
20
53
|
|
|
54
|
+
#
|
|
55
|
+
# Reads a CDP `IO` stream into memory.
|
|
56
|
+
#
|
|
57
|
+
# @param [Symbol] encoding
|
|
58
|
+
# `:base64` to Base64-encode the result, `:binary` to return it as is.
|
|
59
|
+
#
|
|
60
|
+
# @param [String] handle
|
|
61
|
+
# The CDP `IO` stream handle to read from.
|
|
62
|
+
#
|
|
63
|
+
# @return [String]
|
|
64
|
+
#
|
|
21
65
|
def stream_to_memory(encoding:, handle:)
|
|
22
66
|
data = String.new # Mutable string has << and compatible to File
|
|
23
67
|
stream(output: data, handle: handle)
|
|
24
68
|
encoding == :base64 ? Base64.encode64(data) : data
|
|
25
69
|
end
|
|
26
70
|
|
|
71
|
+
#
|
|
72
|
+
# Reads a CDP `IO` stream in chunks, writing each chunk to the given
|
|
73
|
+
# output until the stream is exhausted.
|
|
74
|
+
#
|
|
75
|
+
# @param [#<<] output
|
|
76
|
+
# Anything that responds to `#<<`, e.g. an open `File` or a `String`.
|
|
77
|
+
#
|
|
78
|
+
# @param [String] handle
|
|
79
|
+
# The CDP `IO` stream handle to read from.
|
|
80
|
+
#
|
|
81
|
+
# @return [void]
|
|
82
|
+
#
|
|
27
83
|
def stream(output:, handle:)
|
|
28
84
|
loop do
|
|
29
85
|
result = command("IO.read", handle: handle, size: STREAM_CHUNK)
|
data/lib/ferrum/page/tracing.rb
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Ferrum
|
|
4
4
|
class Page
|
|
5
|
+
#
|
|
6
|
+
# Records a Chrome performance trace for the page via the CDP
|
|
7
|
+
# [Tracing](https://chromedevtools.github.io/devtools-protocol/tot/Tracing/)
|
|
8
|
+
# domain, optionally including screenshots, and streams the resulting
|
|
9
|
+
# trace data to disk or memory once recording stops.
|
|
10
|
+
#
|
|
5
11
|
class Tracing
|
|
6
12
|
EXCLUDED_CATEGORIES = %w[*].freeze
|
|
7
13
|
SCREENSHOT_CATEGORIES = %w[disabled-by-default-devtools.screenshot].freeze
|
data/lib/ferrum/page.rb
CHANGED
|
@@ -8,6 +8,8 @@ require "ferrum/headers"
|
|
|
8
8
|
require "ferrum/cookies"
|
|
9
9
|
require "ferrum/dialog"
|
|
10
10
|
require "ferrum/network"
|
|
11
|
+
require "ferrum/accessibility"
|
|
12
|
+
require "ferrum/interceptable"
|
|
11
13
|
require "ferrum/downloads"
|
|
12
14
|
require "ferrum/page/frames"
|
|
13
15
|
require "ferrum/page/screencast"
|
|
@@ -17,6 +19,13 @@ require "ferrum/page/tracing"
|
|
|
17
19
|
require "ferrum/page/stream"
|
|
18
20
|
|
|
19
21
|
module Ferrum
|
|
22
|
+
#
|
|
23
|
+
# Represents a single browser tab (a CDP target of type `page`). Owns the
|
|
24
|
+
# tab's {Mouse}, {Keyboard}, {Network}, {Cookies}, {Headers}, {Downloads}
|
|
25
|
+
# and {Accessibility} helpers, as well as its frame tree (see the included
|
|
26
|
+
# {Page::Frames} module), and is the object that navigation, DOM search and
|
|
27
|
+
# JavaScript evaluation methods are ultimately delegated to from {Browser}.
|
|
28
|
+
#
|
|
20
29
|
class Page
|
|
21
30
|
GOTO_WAIT = ENV.fetch("FERRUM_GOTO_WAIT", 0.1).to_f
|
|
22
31
|
|
|
@@ -33,6 +42,7 @@ module Ferrum
|
|
|
33
42
|
include Screenshot
|
|
34
43
|
include Frames
|
|
35
44
|
include Stream
|
|
45
|
+
include Interceptable
|
|
36
46
|
|
|
37
47
|
attr_accessor :referrer
|
|
38
48
|
attr_reader :context_id, :target_id, :event, :tracing
|
|
@@ -57,6 +67,11 @@ module Ferrum
|
|
|
57
67
|
# @return [Network]
|
|
58
68
|
attr_reader :network
|
|
59
69
|
|
|
70
|
+
# Accessibility object.
|
|
71
|
+
#
|
|
72
|
+
# @return [Accessibility]
|
|
73
|
+
attr_reader :accessibility
|
|
74
|
+
|
|
60
75
|
# Headers object.
|
|
61
76
|
#
|
|
62
77
|
# @return [Headers]
|
|
@@ -88,6 +103,7 @@ module Ferrum
|
|
|
88
103
|
@headers = Headers.new(self)
|
|
89
104
|
@cookies = Cookies.new(self)
|
|
90
105
|
@network = Network.new(self)
|
|
106
|
+
@accessibility = Accessibility.new(self)
|
|
91
107
|
@tracing = Tracing.new(self)
|
|
92
108
|
@downloads = Downloads.new(self)
|
|
93
109
|
|
|
@@ -118,12 +134,20 @@ module Ferrum
|
|
|
118
134
|
rescue TimeoutError
|
|
119
135
|
if @options.pending_connection_errors
|
|
120
136
|
pendings = network.traffic.select(&:pending?).map(&:url).compact
|
|
121
|
-
raise PendingConnectionsError.new(options[:url], pendings)
|
|
137
|
+
raise PendingConnectionsError.new(options[:url], Array(pendings))
|
|
122
138
|
end
|
|
123
139
|
end
|
|
124
140
|
alias goto go_to
|
|
125
141
|
alias go go_to
|
|
126
142
|
|
|
143
|
+
#
|
|
144
|
+
# Closes the page's target and its underlying client connection.
|
|
145
|
+
#
|
|
146
|
+
# @return [Boolean]
|
|
147
|
+
#
|
|
148
|
+
# @example
|
|
149
|
+
# page.close # => true
|
|
150
|
+
#
|
|
127
151
|
def close
|
|
128
152
|
@headers.clear
|
|
129
153
|
client.command("Target.closeTarget", async: true, targetId: @target_id)
|
|
@@ -132,6 +156,11 @@ module Ferrum
|
|
|
132
156
|
true
|
|
133
157
|
end
|
|
134
158
|
|
|
159
|
+
#
|
|
160
|
+
# Closes the underlying client connection only, without closing the
|
|
161
|
+
# target itself. Useful when you want to detach from a page without
|
|
162
|
+
# ending the browser tab it represents.
|
|
163
|
+
#
|
|
135
164
|
def close_connection
|
|
136
165
|
client&.close
|
|
137
166
|
end
|
|
@@ -160,6 +189,26 @@ module Ferrum
|
|
|
160
189
|
)
|
|
161
190
|
end
|
|
162
191
|
|
|
192
|
+
#
|
|
193
|
+
# Resizes the window and emulates the viewport accordingly, optionally
|
|
194
|
+
# switching to fullscreen.
|
|
195
|
+
#
|
|
196
|
+
# @param [Integer, nil] width width value in pixels.
|
|
197
|
+
#
|
|
198
|
+
# @param [Integer, nil] height height value in pixels.
|
|
199
|
+
#
|
|
200
|
+
# @param [Boolean] fullscreen whether to put the window into fullscreen
|
|
201
|
+
# mode. When `true`, `width` and `height` are read from
|
|
202
|
+
# {#document_size} instead of the given arguments.
|
|
203
|
+
#
|
|
204
|
+
# @return [Hash{String => Object}]
|
|
205
|
+
#
|
|
206
|
+
# @example
|
|
207
|
+
# page.resize(width: 1024, height: 768)
|
|
208
|
+
#
|
|
209
|
+
# @example
|
|
210
|
+
# page.resize(fullscreen: true)
|
|
211
|
+
#
|
|
163
212
|
def resize(width: nil, height: nil, fullscreen: false)
|
|
164
213
|
if fullscreen
|
|
165
214
|
width, height = document_size
|
|
@@ -314,6 +363,15 @@ module Ferrum
|
|
|
314
363
|
history_navigate(delta: 1)
|
|
315
364
|
end
|
|
316
365
|
|
|
366
|
+
#
|
|
367
|
+
# Blocks until the page reloads or the timeout is reached.
|
|
368
|
+
#
|
|
369
|
+
# @param [Numeric] timeout
|
|
370
|
+
# Maximum time in seconds to wait for a reload event.
|
|
371
|
+
#
|
|
372
|
+
# @example
|
|
373
|
+
# page.wait_for_reload
|
|
374
|
+
#
|
|
317
375
|
def wait_for_reload(timeout = 1)
|
|
318
376
|
@event.reset if @event.set?
|
|
319
377
|
@event.wait(timeout)
|
|
@@ -353,74 +411,102 @@ module Ferrum
|
|
|
353
411
|
true
|
|
354
412
|
end
|
|
355
413
|
|
|
356
|
-
|
|
414
|
+
#
|
|
415
|
+
# Sends a CDP command to the browser and optionally waits for network
|
|
416
|
+
# activity on the main frame to settle before returning.
|
|
417
|
+
#
|
|
418
|
+
# @param [String] method
|
|
419
|
+
# The CDP method name, e.g. `"Page.navigate"`.
|
|
420
|
+
#
|
|
421
|
+
# @param [Numeric] wait
|
|
422
|
+
# How many seconds to wait for a network event on the main frame after
|
|
423
|
+
# the command is sent. `0` disables waiting.
|
|
424
|
+
#
|
|
425
|
+
# @param [Boolean] slowmoable
|
|
426
|
+
# Whether to sleep for `Browser::Options#slowmo` seconds before sending
|
|
427
|
+
# the command.
|
|
428
|
+
#
|
|
429
|
+
# @param [Numeric, nil] timeout
|
|
430
|
+
# Overrides the timeout this command's response is bound by. Defaults
|
|
431
|
+
# to the page's `timeout`. Callers with their own budget (e.g. `#pdf`/
|
|
432
|
+
# `#screenshot`) pass it explicitly.
|
|
433
|
+
#
|
|
434
|
+
# @return [Hash{String => Object}]
|
|
435
|
+
#
|
|
436
|
+
# @example
|
|
437
|
+
# page.command("Page.navigate", url: "https://github.com/")
|
|
438
|
+
#
|
|
439
|
+
def command(method, wait: 0, slowmoable: false, timeout: nil, **params)
|
|
357
440
|
iteration = @event.reset if wait.positive?
|
|
358
441
|
sleep(@options.slowmo) if slowmoable && @options.slowmo.positive?
|
|
359
|
-
result = client.command(method, **params)
|
|
442
|
+
result = client.command(method, timeout: timeout || self.timeout, **params)
|
|
360
443
|
|
|
361
444
|
if wait.positive?
|
|
362
445
|
# Wait a bit after command and check if iteration has
|
|
363
|
-
#
|
|
364
|
-
# the main frame and it started to load new content.
|
|
446
|
+
# changed, which means there was some network event for
|
|
447
|
+
# the main frame, and it started to load new content.
|
|
365
448
|
@event.wait(wait)
|
|
366
449
|
if iteration != @event.iteration
|
|
367
|
-
set = @event.wait(timeout)
|
|
450
|
+
set = @event.wait(self.timeout)
|
|
368
451
|
raise TimeoutError unless set
|
|
369
452
|
end
|
|
370
453
|
end
|
|
371
454
|
result
|
|
372
455
|
end
|
|
373
456
|
|
|
457
|
+
# Subscribes to a CDP event, or to `:dialog`, `:request`, `:auth` (the
|
|
458
|
+
# latter two handled by {Interceptable}).
|
|
459
|
+
#
|
|
460
|
+
# @param [Symbol, String] name
|
|
461
|
+
#
|
|
462
|
+
# @return [Integer]
|
|
463
|
+
# The subscription id, used to unsubscribe via {#off}.
|
|
374
464
|
def on(name, &block)
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
end
|
|
381
|
-
when :request
|
|
382
|
-
client.on("Fetch.requestPaused") do |params, index, total|
|
|
383
|
-
request = Network::InterceptedRequest.new(client, params)
|
|
384
|
-
exchange = network.select(request.network_id).last
|
|
385
|
-
exchange ||= network.build_exchange(request.network_id)
|
|
386
|
-
exchange.intercepted_request = request
|
|
387
|
-
block.call(request, index, total)
|
|
388
|
-
end
|
|
389
|
-
when :auth
|
|
390
|
-
client.on("Fetch.authRequired") do |params, index, total|
|
|
391
|
-
request = Network::AuthRequest.new(self, params)
|
|
392
|
-
block.call(request, index, total)
|
|
393
|
-
end
|
|
394
|
-
else
|
|
395
|
-
client.on(name, &block)
|
|
465
|
+
return super unless name == :dialog
|
|
466
|
+
|
|
467
|
+
client.on("Page.javascriptDialogOpening") do |params, index, total|
|
|
468
|
+
dialog = Dialog.new(self, params)
|
|
469
|
+
block.call(dialog, index, total)
|
|
396
470
|
end
|
|
397
471
|
end
|
|
398
472
|
|
|
473
|
+
# Unsubscribes a listener previously registered via {#on}.
|
|
474
|
+
#
|
|
475
|
+
# @param [Symbol, String] name
|
|
476
|
+
#
|
|
477
|
+
# @param [Integer] id
|
|
478
|
+
# The subscription id returned by {#on}.
|
|
479
|
+
#
|
|
480
|
+
# @return [void]
|
|
399
481
|
def off(name, id)
|
|
400
|
-
|
|
401
|
-
when :dialog
|
|
402
|
-
client.off("Page.javascriptDialogOpening", id)
|
|
403
|
-
when :request
|
|
404
|
-
client.off("Fetch.requestPaused", id)
|
|
405
|
-
when :auth
|
|
406
|
-
client.off("Fetch.authRequired", id)
|
|
407
|
-
else
|
|
408
|
-
client.off(name, id)
|
|
409
|
-
end
|
|
410
|
-
end
|
|
482
|
+
return super unless name == :dialog
|
|
411
483
|
|
|
412
|
-
|
|
413
|
-
client.subscribed?(event)
|
|
484
|
+
client.off("Page.javascriptDialogOpening", id)
|
|
414
485
|
end
|
|
415
486
|
|
|
487
|
+
# Whether the page is configured to use a proxy.
|
|
488
|
+
#
|
|
489
|
+
# @return [Boolean]
|
|
416
490
|
def use_proxy?
|
|
417
491
|
@proxy_host && @proxy_port
|
|
418
492
|
end
|
|
419
493
|
|
|
494
|
+
# Whether the page is configured to use a proxy that requires authentication.
|
|
495
|
+
#
|
|
496
|
+
# @return [Boolean]
|
|
420
497
|
def use_authorized_proxy?
|
|
421
498
|
use_proxy? && @proxy_user && @proxy_password
|
|
422
499
|
end
|
|
423
500
|
|
|
501
|
+
#
|
|
502
|
+
# Returns the node id of the document's root element.
|
|
503
|
+
#
|
|
504
|
+
# @param [Boolean] async
|
|
505
|
+
# Whether to send the command without waiting for a response.
|
|
506
|
+
#
|
|
507
|
+
# @return [Integer, Boolean]
|
|
508
|
+
# The root node id, or `true` when sent asynchronously.
|
|
509
|
+
#
|
|
424
510
|
def document_node_id(async: false)
|
|
425
511
|
return client.command("DOM.getDocument", async: true, depth: 0) if async
|
|
426
512
|
|
|
@@ -436,7 +522,7 @@ module Ferrum
|
|
|
436
522
|
|
|
437
523
|
if @options.logger
|
|
438
524
|
on("Runtime.consoleAPICalled") do |params|
|
|
439
|
-
params
|
|
525
|
+
log_console_api(params)
|
|
440
526
|
end
|
|
441
527
|
end
|
|
442
528
|
|
|
@@ -458,8 +544,9 @@ module Ferrum
|
|
|
458
544
|
|
|
459
545
|
def prepare_page
|
|
460
546
|
command("Page.enable")
|
|
547
|
+
command("Page.setLifecycleEventsEnabled", enabled: true)
|
|
461
548
|
command("Runtime.enable")
|
|
462
|
-
command("DOM.enable")
|
|
549
|
+
command("DOM.enable", includeWhitespace: "all")
|
|
463
550
|
command("CSS.enable")
|
|
464
551
|
command("Log.enable")
|
|
465
552
|
command("Network.enable")
|
|
@@ -492,6 +579,16 @@ module Ferrum
|
|
|
492
579
|
document_node_id
|
|
493
580
|
end
|
|
494
581
|
|
|
582
|
+
def log_console_api(params)
|
|
583
|
+
message = params.fetch("args", []).filter_map { |arg| arg["value"] || arg["description"] }.join(" ")
|
|
584
|
+
@options.logger.puts("[#{params['type']}] #{message}")
|
|
585
|
+
|
|
586
|
+
params.dig("stackTrace", "callFrames")&.each do |frame|
|
|
587
|
+
location = "#{frame['url']}:#{frame['lineNumber'].to_i + 1}:#{frame['columnNumber'].to_i + 1}"
|
|
588
|
+
@options.logger.puts(" at #{frame['functionName']} (#{location})")
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
|
|
495
592
|
def inject_extensions
|
|
496
593
|
@options.extensions.each do |extension|
|
|
497
594
|
# https://github.com/GoogleChrome/puppeteer/issues/1443
|