ferrum 0.17.2 → 0.19.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 +4 -2
- data/lib/ferrum/accessibility/ax_node.rb +108 -0
- data/lib/ferrum/accessibility.rb +106 -0
- data/lib/ferrum/browser/binary.rb +42 -2
- data/lib/ferrum/browser/command.rb +20 -2
- data/lib/ferrum/browser/options/base.rb +66 -0
- data/lib/ferrum/browser/options/chrome.rb +35 -2
- data/lib/ferrum/browser/options.rb +41 -2
- data/lib/ferrum/browser/process/killer.rb +188 -0
- data/lib/ferrum/browser/process.rb +128 -72
- data/lib/ferrum/browser/xvfb.rb +24 -0
- data/lib/ferrum/browser.rb +47 -17
- data/lib/ferrum/client/subscriber.rb +65 -2
- data/lib/ferrum/client/web_socket.rb +126 -13
- data/lib/ferrum/client.rb +225 -12
- data/lib/ferrum/context.rb +113 -7
- data/lib/ferrum/contexts.rb +188 -19
- 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 +54 -1
- data/lib/ferrum/errors.rb +103 -4
- data/lib/ferrum/frame/dom.rb +65 -0
- data/lib/ferrum/frame/runtime.rb +57 -15
- data/lib/ferrum/frame.rb +58 -1
- data/lib/ferrum/headers.rb +6 -0
- data/lib/ferrum/interceptable.rb +83 -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 +55 -23
- data/lib/ferrum/page/stream.rb +71 -0
- data/lib/ferrum/page/tracing.rb +6 -0
- data/lib/ferrum/page.rb +148 -45
- data/lib/ferrum/proxy.rb +52 -2
- data/lib/ferrum/rgba.rb +10 -0
- data/lib/ferrum/target.rb +130 -3
- 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 +15 -1
- 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/browser/options/firefox.rb +0 -38
|
@@ -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,
|
|
@@ -27,9 +32,9 @@ module Ferrum
|
|
|
27
32
|
A1: { width: 23.40, height: 33.10 },
|
|
28
33
|
A2: { width: 16.54, height: 23.40 },
|
|
29
34
|
A3: { width: 11.70, height: 16.54 },
|
|
30
|
-
A4: { width:
|
|
31
|
-
A5: { width:
|
|
32
|
-
A6: { width:
|
|
35
|
+
A4: { width: 8.27, height: 11.70 },
|
|
36
|
+
A5: { width: 5.83, height: 8.27 },
|
|
37
|
+
A6: { width: 4.13, height: 5.83 }
|
|
33
38
|
}.freeze
|
|
34
39
|
|
|
35
40
|
#
|
|
@@ -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,52 @@ 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, then releases the stream
|
|
74
|
+
# handle on the browser side.
|
|
75
|
+
#
|
|
76
|
+
# @param [#<<] output
|
|
77
|
+
# Anything that responds to `#<<`, e.g. an open `File` or a `String`.
|
|
78
|
+
#
|
|
79
|
+
# @param [String] handle
|
|
80
|
+
# The CDP `IO` stream handle to read from.
|
|
81
|
+
#
|
|
82
|
+
# @return [void]
|
|
83
|
+
#
|
|
27
84
|
def stream(output:, handle:)
|
|
28
85
|
loop do
|
|
29
86
|
result = command("IO.read", handle: handle, size: STREAM_CHUNK)
|
|
@@ -32,6 +89,20 @@ module Ferrum
|
|
|
32
89
|
output << chunk
|
|
33
90
|
break if result["eof"]
|
|
34
91
|
end
|
|
92
|
+
close_stream(handle: handle)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
#
|
|
96
|
+
# Releases a CDP `IO` stream handle, discarding any backing storage
|
|
97
|
+
# the browser is holding for it.
|
|
98
|
+
#
|
|
99
|
+
# @param [String] handle
|
|
100
|
+
# The CDP `IO` stream handle to close.
|
|
101
|
+
#
|
|
102
|
+
# @return [void]
|
|
103
|
+
#
|
|
104
|
+
def close_stream(handle:)
|
|
105
|
+
command("IO.close", handle: handle)
|
|
35
106
|
end
|
|
36
107
|
end
|
|
37
108
|
end
|
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,12 +19,19 @@ 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
|
|
24
33
|
|
|
25
|
-
delegate %i[at_css at_xpath css xpath
|
|
34
|
+
delegate %i[at_css at_xpath css xpath wait_for_selector
|
|
26
35
|
current_url current_title url title body doctype content=
|
|
27
36
|
execution_id execution_id! evaluate evaluate_on evaluate_async execute evaluate_func
|
|
28
37
|
add_script_tag add_style_tag] => :main_frame
|
|
@@ -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
|
|
@@ -147,9 +176,15 @@ module Ferrum
|
|
|
147
176
|
#
|
|
148
177
|
# @param [Float] scale_factor device scale factor value. 0 disables the override
|
|
149
178
|
#
|
|
150
|
-
# @param [Boolean] mobile whether to emulate mobile device
|
|
179
|
+
# @param [Boolean] mobile whether to emulate mobile device and enable touch
|
|
151
180
|
#
|
|
152
|
-
def set_viewport(width
|
|
181
|
+
def set_viewport(width: 0, height: 0, scale_factor: 0, mobile: false)
|
|
182
|
+
command(
|
|
183
|
+
"Emulation.setTouchEmulationEnabled",
|
|
184
|
+
enabled: mobile,
|
|
185
|
+
maxTouchPoints: 1
|
|
186
|
+
)
|
|
187
|
+
|
|
153
188
|
command(
|
|
154
189
|
"Emulation.setDeviceMetricsOverride",
|
|
155
190
|
slowmoable: true,
|
|
@@ -160,6 +195,26 @@ module Ferrum
|
|
|
160
195
|
)
|
|
161
196
|
end
|
|
162
197
|
|
|
198
|
+
#
|
|
199
|
+
# Resizes the window and emulates the viewport accordingly, optionally
|
|
200
|
+
# switching to fullscreen.
|
|
201
|
+
#
|
|
202
|
+
# @param [Integer, nil] width width value in pixels.
|
|
203
|
+
#
|
|
204
|
+
# @param [Integer, nil] height height value in pixels.
|
|
205
|
+
#
|
|
206
|
+
# @param [Boolean] fullscreen whether to put the window into fullscreen
|
|
207
|
+
# mode. When `true`, `width` and `height` are read from
|
|
208
|
+
# {#document_size} instead of the given arguments.
|
|
209
|
+
#
|
|
210
|
+
# @return [Hash{String => Object}]
|
|
211
|
+
#
|
|
212
|
+
# @example
|
|
213
|
+
# page.resize(width: 1024, height: 768)
|
|
214
|
+
#
|
|
215
|
+
# @example
|
|
216
|
+
# page.resize(fullscreen: true)
|
|
217
|
+
#
|
|
163
218
|
def resize(width: nil, height: nil, fullscreen: false)
|
|
164
219
|
if fullscreen
|
|
165
220
|
width, height = document_size
|
|
@@ -314,6 +369,15 @@ module Ferrum
|
|
|
314
369
|
history_navigate(delta: 1)
|
|
315
370
|
end
|
|
316
371
|
|
|
372
|
+
#
|
|
373
|
+
# Blocks until the page reloads or the timeout is reached.
|
|
374
|
+
#
|
|
375
|
+
# @param [Numeric] timeout
|
|
376
|
+
# Maximum time in seconds to wait for a reload event.
|
|
377
|
+
#
|
|
378
|
+
# @example
|
|
379
|
+
# page.wait_for_reload
|
|
380
|
+
#
|
|
317
381
|
def wait_for_reload(timeout = 1)
|
|
318
382
|
@event.reset if @event.set?
|
|
319
383
|
@event.wait(timeout)
|
|
@@ -353,74 +417,102 @@ module Ferrum
|
|
|
353
417
|
true
|
|
354
418
|
end
|
|
355
419
|
|
|
356
|
-
|
|
420
|
+
#
|
|
421
|
+
# Sends a CDP command to the browser and optionally waits for network
|
|
422
|
+
# activity on the main frame to settle before returning.
|
|
423
|
+
#
|
|
424
|
+
# @param [String] method
|
|
425
|
+
# The CDP method name, e.g. `"Page.navigate"`.
|
|
426
|
+
#
|
|
427
|
+
# @param [Numeric] wait
|
|
428
|
+
# How many seconds to wait for a network event on the main frame after
|
|
429
|
+
# the command is sent. `0` disables waiting.
|
|
430
|
+
#
|
|
431
|
+
# @param [Boolean] slowmoable
|
|
432
|
+
# Whether to sleep for `Browser::Options#slowmo` seconds before sending
|
|
433
|
+
# the command.
|
|
434
|
+
#
|
|
435
|
+
# @param [Numeric, nil] timeout
|
|
436
|
+
# Overrides the timeout this command's response is bound by. Defaults
|
|
437
|
+
# to the page's `timeout`. Callers with their own budget (e.g. `#pdf`/
|
|
438
|
+
# `#screenshot`) pass it explicitly.
|
|
439
|
+
#
|
|
440
|
+
# @return [Hash{String => Object}]
|
|
441
|
+
#
|
|
442
|
+
# @example
|
|
443
|
+
# page.command("Page.navigate", url: "https://github.com/")
|
|
444
|
+
#
|
|
445
|
+
def command(method, wait: 0, slowmoable: false, timeout: nil, **params)
|
|
357
446
|
iteration = @event.reset if wait.positive?
|
|
358
447
|
sleep(@options.slowmo) if slowmoable && @options.slowmo.positive?
|
|
359
|
-
result = client.command(method, **params)
|
|
448
|
+
result = client.command(method, timeout: timeout || self.timeout, **params)
|
|
360
449
|
|
|
361
450
|
if wait.positive?
|
|
362
451
|
# Wait a bit after command and check if iteration has
|
|
363
|
-
#
|
|
364
|
-
# the main frame and it started to load new content.
|
|
452
|
+
# changed, which means there was some network event for
|
|
453
|
+
# the main frame, and it started to load new content.
|
|
365
454
|
@event.wait(wait)
|
|
366
455
|
if iteration != @event.iteration
|
|
367
|
-
set = @event.wait(timeout)
|
|
456
|
+
set = @event.wait(self.timeout)
|
|
368
457
|
raise TimeoutError unless set
|
|
369
458
|
end
|
|
370
459
|
end
|
|
371
460
|
result
|
|
372
461
|
end
|
|
373
462
|
|
|
463
|
+
# Subscribes to a CDP event, or to `:dialog`, `:request`, `:response`,
|
|
464
|
+
# `:auth` (the latter three handled by {Interceptable}).
|
|
465
|
+
#
|
|
466
|
+
# @param [Symbol, String] name
|
|
467
|
+
#
|
|
468
|
+
# @return [Integer]
|
|
469
|
+
# The subscription id, used to unsubscribe via {#off}.
|
|
374
470
|
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)
|
|
471
|
+
return super unless name == :dialog
|
|
472
|
+
|
|
473
|
+
client.on("Page.javascriptDialogOpening") do |params, index, total|
|
|
474
|
+
dialog = Dialog.new(self, params)
|
|
475
|
+
block.call(dialog, index, total)
|
|
396
476
|
end
|
|
397
477
|
end
|
|
398
478
|
|
|
479
|
+
# Unsubscribes a listener previously registered via {#on}.
|
|
480
|
+
#
|
|
481
|
+
# @param [Symbol, String] name
|
|
482
|
+
#
|
|
483
|
+
# @param [Integer] id
|
|
484
|
+
# The subscription id returned by {#on}.
|
|
485
|
+
#
|
|
486
|
+
# @return [void]
|
|
399
487
|
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
|
|
488
|
+
return super unless name == :dialog
|
|
411
489
|
|
|
412
|
-
|
|
413
|
-
client.subscribed?(event)
|
|
490
|
+
client.off("Page.javascriptDialogOpening", id)
|
|
414
491
|
end
|
|
415
492
|
|
|
493
|
+
# Whether the page is configured to use a proxy.
|
|
494
|
+
#
|
|
495
|
+
# @return [Boolean]
|
|
416
496
|
def use_proxy?
|
|
417
497
|
@proxy_host && @proxy_port
|
|
418
498
|
end
|
|
419
499
|
|
|
500
|
+
# Whether the page is configured to use a proxy that requires authentication.
|
|
501
|
+
#
|
|
502
|
+
# @return [Boolean]
|
|
420
503
|
def use_authorized_proxy?
|
|
421
504
|
use_proxy? && @proxy_user && @proxy_password
|
|
422
505
|
end
|
|
423
506
|
|
|
507
|
+
#
|
|
508
|
+
# Returns the node id of the document's root element.
|
|
509
|
+
#
|
|
510
|
+
# @param [Boolean] async
|
|
511
|
+
# Whether to send the command without waiting for a response.
|
|
512
|
+
#
|
|
513
|
+
# @return [Integer, Boolean]
|
|
514
|
+
# The root node id, or `true` when sent asynchronously.
|
|
515
|
+
#
|
|
424
516
|
def document_node_id(async: false)
|
|
425
517
|
return client.command("DOM.getDocument", async: true, depth: 0) if async
|
|
426
518
|
|
|
@@ -436,7 +528,7 @@ module Ferrum
|
|
|
436
528
|
|
|
437
529
|
if @options.logger
|
|
438
530
|
on("Runtime.consoleAPICalled") do |params|
|
|
439
|
-
params
|
|
531
|
+
log_console_api(params)
|
|
440
532
|
end
|
|
441
533
|
end
|
|
442
534
|
|
|
@@ -458,8 +550,9 @@ module Ferrum
|
|
|
458
550
|
|
|
459
551
|
def prepare_page
|
|
460
552
|
command("Page.enable")
|
|
553
|
+
command("Page.setLifecycleEventsEnabled", enabled: true)
|
|
461
554
|
command("Runtime.enable")
|
|
462
|
-
command("DOM.enable")
|
|
555
|
+
command("DOM.enable", includeWhitespace: "all")
|
|
463
556
|
command("CSS.enable")
|
|
464
557
|
command("Log.enable")
|
|
465
558
|
command("Network.enable")
|
|
@@ -492,6 +585,16 @@ module Ferrum
|
|
|
492
585
|
document_node_id
|
|
493
586
|
end
|
|
494
587
|
|
|
588
|
+
def log_console_api(params)
|
|
589
|
+
message = params.fetch("args", []).filter_map { |arg| arg["value"] || arg["description"] }.join(" ")
|
|
590
|
+
@options.logger.puts("[#{params['type']}] #{message}")
|
|
591
|
+
|
|
592
|
+
params.dig("stackTrace", "callFrames")&.each do |frame|
|
|
593
|
+
location = "#{frame['url']}:#{frame['lineNumber'].to_i + 1}:#{frame['columnNumber'].to_i + 1}"
|
|
594
|
+
@options.logger.puts(" at #{frame['functionName']} (#{location})")
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
|
|
495
598
|
def inject_extensions
|
|
496
599
|
@options.extensions.each do |extension|
|
|
497
600
|
# 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
|