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
|
@@ -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,10 +19,18 @@ 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
|
|
|
23
32
|
extend Forwardable
|
|
33
|
+
|
|
24
34
|
delegate %i[at_css at_xpath css xpath
|
|
25
35
|
current_url current_title url title body doctype content=
|
|
26
36
|
execution_id execution_id! evaluate evaluate_on evaluate_async execute evaluate_func
|
|
@@ -32,6 +42,7 @@ module Ferrum
|
|
|
32
42
|
include Screenshot
|
|
33
43
|
include Frames
|
|
34
44
|
include Stream
|
|
45
|
+
include Interceptable
|
|
35
46
|
|
|
36
47
|
attr_accessor :referrer
|
|
37
48
|
attr_reader :context_id, :target_id, :event, :tracing
|
|
@@ -56,6 +67,11 @@ module Ferrum
|
|
|
56
67
|
# @return [Network]
|
|
57
68
|
attr_reader :network
|
|
58
69
|
|
|
70
|
+
# Accessibility object.
|
|
71
|
+
#
|
|
72
|
+
# @return [Accessibility]
|
|
73
|
+
attr_reader :accessibility
|
|
74
|
+
|
|
59
75
|
# Headers object.
|
|
60
76
|
#
|
|
61
77
|
# @return [Headers]
|
|
@@ -87,6 +103,7 @@ module Ferrum
|
|
|
87
103
|
@headers = Headers.new(self)
|
|
88
104
|
@cookies = Cookies.new(self)
|
|
89
105
|
@network = Network.new(self)
|
|
106
|
+
@accessibility = Accessibility.new(self)
|
|
90
107
|
@tracing = Tracing.new(self)
|
|
91
108
|
@downloads = Downloads.new(self)
|
|
92
109
|
|
|
@@ -117,12 +134,20 @@ module Ferrum
|
|
|
117
134
|
rescue TimeoutError
|
|
118
135
|
if @options.pending_connection_errors
|
|
119
136
|
pendings = network.traffic.select(&:pending?).map(&:url).compact
|
|
120
|
-
raise PendingConnectionsError.new(options[:url], pendings)
|
|
137
|
+
raise PendingConnectionsError.new(options[:url], Array(pendings))
|
|
121
138
|
end
|
|
122
139
|
end
|
|
123
140
|
alias goto go_to
|
|
124
141
|
alias go go_to
|
|
125
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
|
+
#
|
|
126
151
|
def close
|
|
127
152
|
@headers.clear
|
|
128
153
|
client.command("Target.closeTarget", async: true, targetId: @target_id)
|
|
@@ -131,6 +156,11 @@ module Ferrum
|
|
|
131
156
|
true
|
|
132
157
|
end
|
|
133
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
|
+
#
|
|
134
164
|
def close_connection
|
|
135
165
|
client&.close
|
|
136
166
|
end
|
|
@@ -159,6 +189,26 @@ module Ferrum
|
|
|
159
189
|
)
|
|
160
190
|
end
|
|
161
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
|
+
#
|
|
162
212
|
def resize(width: nil, height: nil, fullscreen: false)
|
|
163
213
|
if fullscreen
|
|
164
214
|
width, height = document_size
|
|
@@ -313,6 +363,15 @@ module Ferrum
|
|
|
313
363
|
history_navigate(delta: 1)
|
|
314
364
|
end
|
|
315
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
|
+
#
|
|
316
375
|
def wait_for_reload(timeout = 1)
|
|
317
376
|
@event.reset if @event.set?
|
|
318
377
|
@event.wait(timeout)
|
|
@@ -352,74 +411,102 @@ module Ferrum
|
|
|
352
411
|
true
|
|
353
412
|
end
|
|
354
413
|
|
|
355
|
-
|
|
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)
|
|
356
440
|
iteration = @event.reset if wait.positive?
|
|
357
441
|
sleep(@options.slowmo) if slowmoable && @options.slowmo.positive?
|
|
358
|
-
result = client.command(method, **params)
|
|
442
|
+
result = client.command(method, timeout: timeout || self.timeout, **params)
|
|
359
443
|
|
|
360
444
|
if wait.positive?
|
|
361
445
|
# Wait a bit after command and check if iteration has
|
|
362
|
-
#
|
|
363
|
-
# 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.
|
|
364
448
|
@event.wait(wait)
|
|
365
449
|
if iteration != @event.iteration
|
|
366
|
-
set = @event.wait(timeout)
|
|
450
|
+
set = @event.wait(self.timeout)
|
|
367
451
|
raise TimeoutError unless set
|
|
368
452
|
end
|
|
369
453
|
end
|
|
370
454
|
result
|
|
371
455
|
end
|
|
372
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}.
|
|
373
464
|
def on(name, &block)
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
end
|
|
380
|
-
when :request
|
|
381
|
-
client.on("Fetch.requestPaused") do |params, index, total|
|
|
382
|
-
request = Network::InterceptedRequest.new(client, params)
|
|
383
|
-
exchange = network.select(request.network_id).last
|
|
384
|
-
exchange ||= network.build_exchange(request.network_id)
|
|
385
|
-
exchange.intercepted_request = request
|
|
386
|
-
block.call(request, index, total)
|
|
387
|
-
end
|
|
388
|
-
when :auth
|
|
389
|
-
client.on("Fetch.authRequired") do |params, index, total|
|
|
390
|
-
request = Network::AuthRequest.new(self, params)
|
|
391
|
-
block.call(request, index, total)
|
|
392
|
-
end
|
|
393
|
-
else
|
|
394
|
-
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)
|
|
395
470
|
end
|
|
396
471
|
end
|
|
397
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]
|
|
398
481
|
def off(name, id)
|
|
399
|
-
|
|
400
|
-
when :dialog
|
|
401
|
-
client.off("Page.javascriptDialogOpening", id)
|
|
402
|
-
when :request
|
|
403
|
-
client.off("Fetch.requestPaused", id)
|
|
404
|
-
when :auth
|
|
405
|
-
client.off("Fetch.authRequired", id)
|
|
406
|
-
else
|
|
407
|
-
client.off(name, id)
|
|
408
|
-
end
|
|
409
|
-
end
|
|
482
|
+
return super unless name == :dialog
|
|
410
483
|
|
|
411
|
-
|
|
412
|
-
client.subscribed?(event)
|
|
484
|
+
client.off("Page.javascriptDialogOpening", id)
|
|
413
485
|
end
|
|
414
486
|
|
|
487
|
+
# Whether the page is configured to use a proxy.
|
|
488
|
+
#
|
|
489
|
+
# @return [Boolean]
|
|
415
490
|
def use_proxy?
|
|
416
491
|
@proxy_host && @proxy_port
|
|
417
492
|
end
|
|
418
493
|
|
|
494
|
+
# Whether the page is configured to use a proxy that requires authentication.
|
|
495
|
+
#
|
|
496
|
+
# @return [Boolean]
|
|
419
497
|
def use_authorized_proxy?
|
|
420
498
|
use_proxy? && @proxy_user && @proxy_password
|
|
421
499
|
end
|
|
422
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
|
+
#
|
|
423
510
|
def document_node_id(async: false)
|
|
424
511
|
return client.command("DOM.getDocument", async: true, depth: 0) if async
|
|
425
512
|
|
|
@@ -435,17 +522,14 @@ module Ferrum
|
|
|
435
522
|
|
|
436
523
|
if @options.logger
|
|
437
524
|
on("Runtime.consoleAPICalled") do |params|
|
|
438
|
-
params
|
|
525
|
+
log_console_api(params)
|
|
439
526
|
end
|
|
440
527
|
end
|
|
441
528
|
|
|
442
529
|
if @options.js_errors
|
|
443
530
|
on("Runtime.exceptionThrown") do |params|
|
|
444
531
|
# FIXME: https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
|
|
445
|
-
Thread.main.raise JavaScriptError
|
|
446
|
-
params.dig("exceptionDetails", "exception"),
|
|
447
|
-
params.dig("exceptionDetails", "stackTrace")
|
|
448
|
-
)
|
|
532
|
+
Thread.main.raise JavaScriptError, params["exceptionDetails"]
|
|
449
533
|
end
|
|
450
534
|
end
|
|
451
535
|
|
|
@@ -460,8 +544,9 @@ module Ferrum
|
|
|
460
544
|
|
|
461
545
|
def prepare_page
|
|
462
546
|
command("Page.enable")
|
|
547
|
+
command("Page.setLifecycleEventsEnabled", enabled: true)
|
|
463
548
|
command("Runtime.enable")
|
|
464
|
-
command("DOM.enable")
|
|
549
|
+
command("DOM.enable", includeWhitespace: "all")
|
|
465
550
|
command("CSS.enable")
|
|
466
551
|
command("Log.enable")
|
|
467
552
|
command("Network.enable")
|
|
@@ -494,6 +579,16 @@ module Ferrum
|
|
|
494
579
|
document_node_id
|
|
495
580
|
end
|
|
496
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
|
+
|
|
497
592
|
def inject_extensions
|
|
498
593
|
@options.extensions.each do |extension|
|
|
499
594
|
# https://github.com/GoogleChrome/puppeteer/issues/1443
|
data/lib/ferrum/proxy.rb
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "tempfile"
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require "webrick"
|
|
7
|
+
require "webrick/httpproxy"
|
|
8
|
+
rescue LoadError
|
|
9
|
+
warn("Please add webrick to your Gemfile to use Ferrum proxy")
|
|
10
|
+
raise
|
|
11
|
+
end
|
|
6
12
|
|
|
7
13
|
module Ferrum
|
|
14
|
+
#
|
|
15
|
+
# A local WEBrick-based proxy server, useful for injecting HTTP Basic-Auth
|
|
16
|
+
# credentials into requests or forwarding to (and rotating between)
|
|
17
|
+
# upstream proxies, cases the browser's own proxy support can't handle
|
|
18
|
+
# directly.
|
|
19
|
+
#
|
|
20
|
+
# @note Requires the `webrick` gem, which isn't a hard dependency of
|
|
21
|
+
# Ferrum; add it to your Gemfile to use this class.
|
|
22
|
+
#
|
|
8
23
|
class Proxy
|
|
24
|
+
#
|
|
25
|
+
# Builds a new proxy server and starts it.
|
|
26
|
+
#
|
|
27
|
+
# @param [Hash] args
|
|
28
|
+
# Keyword arguments forwarded to {#initialize}.
|
|
29
|
+
#
|
|
30
|
+
# @return [Proxy]
|
|
31
|
+
#
|
|
9
32
|
def self.start(**args)
|
|
10
33
|
new(**args).tap(&:start)
|
|
11
34
|
end
|
|
@@ -20,6 +43,11 @@ module Ferrum
|
|
|
20
43
|
@password = password
|
|
21
44
|
end
|
|
22
45
|
|
|
46
|
+
#
|
|
47
|
+
# Starts the WEBrick proxy server.
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
#
|
|
23
51
|
def start
|
|
24
52
|
options = {
|
|
25
53
|
ProxyURI: nil, ServerType: Thread,
|
|
@@ -45,12 +73,34 @@ module Ferrum
|
|
|
45
73
|
@port = @server.config[:Port]
|
|
46
74
|
end
|
|
47
75
|
|
|
76
|
+
#
|
|
77
|
+
# Changes the upstream proxy the server forwards connections to.
|
|
78
|
+
#
|
|
79
|
+
# @param [String] host
|
|
80
|
+
# Address of the upstream proxy.
|
|
81
|
+
#
|
|
82
|
+
# @param [Integer] port
|
|
83
|
+
# Port of the upstream proxy.
|
|
84
|
+
#
|
|
85
|
+
# @param [String, nil] user
|
|
86
|
+
# Username for upstream proxy authentication.
|
|
87
|
+
#
|
|
88
|
+
# @param [String, nil] password
|
|
89
|
+
# Password for upstream proxy authentication.
|
|
90
|
+
#
|
|
91
|
+
# @return [void]
|
|
92
|
+
#
|
|
48
93
|
def rotate(host:, port:, user: nil, password: nil)
|
|
49
94
|
credentials = "#{user}:#{password}@" if user && password
|
|
50
95
|
proxy_uri = "schema://#{credentials}#{host}:#{port}"
|
|
51
96
|
@server.config[:ProxyURI] = URI.parse(proxy_uri)
|
|
52
97
|
end
|
|
53
98
|
|
|
99
|
+
#
|
|
100
|
+
# Stops the proxy server and removes the htpasswd file.
|
|
101
|
+
#
|
|
102
|
+
# @return [void]
|
|
103
|
+
#
|
|
54
104
|
def stop
|
|
55
105
|
@file&.close(true)
|
|
56
106
|
@server.shutdown
|
data/lib/ferrum/rgba.rb
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Ferrum
|
|
4
|
+
#
|
|
5
|
+
# Represents an RGBA color, validating that the red/green/blue components
|
|
6
|
+
# are integers between 0 and 255 and that alpha is a float between 0.0 and
|
|
7
|
+
# 1.0. Used e.g. as the background color option for screenshots.
|
|
8
|
+
#
|
|
4
9
|
class RGBA
|
|
5
10
|
def initialize(red, green, blue, alpha)
|
|
6
11
|
self.red = red
|
|
@@ -11,6 +16,11 @@ module Ferrum
|
|
|
11
16
|
validate
|
|
12
17
|
end
|
|
13
18
|
|
|
19
|
+
#
|
|
20
|
+
# Converts the color to a Hash.
|
|
21
|
+
#
|
|
22
|
+
# @return [Hash{Symbol => Integer, Float}]
|
|
23
|
+
#
|
|
14
24
|
def to_h
|
|
15
25
|
{ r: red, g: green, b: blue, a: alpha }
|
|
16
26
|
end
|