ferrum 0.14 → 0.16
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 +309 -158
- data/lib/ferrum/browser/command.rb +4 -0
- data/lib/ferrum/browser/options/chrome.rb +8 -3
- data/lib/ferrum/browser/options.rb +38 -25
- data/lib/ferrum/browser/process.rb +42 -17
- data/lib/ferrum/browser.rb +38 -50
- data/lib/ferrum/client/subscriber.rb +76 -0
- data/lib/ferrum/client/web_socket.rb +126 -0
- data/lib/ferrum/client.rb +171 -0
- data/lib/ferrum/context.rb +19 -15
- data/lib/ferrum/contexts.rb +46 -12
- data/lib/ferrum/cookies.rb +28 -1
- data/lib/ferrum/downloads.rb +60 -0
- data/lib/ferrum/errors.rb +10 -3
- data/lib/ferrum/headers.rb +1 -1
- data/lib/ferrum/network/exchange.rb +10 -1
- data/lib/ferrum/network/intercepted_request.rb +5 -5
- data/lib/ferrum/network/request.rb +9 -0
- data/lib/ferrum/network.rb +36 -24
- data/lib/ferrum/node.rb +11 -0
- data/lib/ferrum/page/frames.rb +7 -9
- data/lib/ferrum/page/screenshot.rb +54 -28
- data/lib/ferrum/page.rb +192 -118
- data/lib/ferrum/proxy.rb +1 -1
- data/lib/ferrum/target.rb +25 -5
- data/lib/ferrum/utils/elapsed_time.rb +0 -2
- data/lib/ferrum/utils/event.rb +19 -0
- data/lib/ferrum/utils/platform.rb +4 -0
- data/lib/ferrum/utils/thread.rb +18 -0
- data/lib/ferrum/version.rb +1 -1
- data/lib/ferrum.rb +3 -0
- metadata +28 -17
- data/lib/ferrum/browser/client.rb +0 -103
- data/lib/ferrum/browser/subscriber.rb +0 -36
- data/lib/ferrum/browser/web_socket.rb +0 -91
data/lib/ferrum/network.rb
CHANGED
@@ -41,7 +41,7 @@ module Ferrum
|
|
41
41
|
end
|
42
42
|
|
43
43
|
#
|
44
|
-
# Waits for network idle
|
44
|
+
# Waits for network idle.
|
45
45
|
#
|
46
46
|
# @param [Integer] connections
|
47
47
|
# how many connections are allowed for network to be idling,
|
@@ -52,21 +52,33 @@ module Ferrum
|
|
52
52
|
# @param [Float] timeout
|
53
53
|
# During what time we try to check idle.
|
54
54
|
#
|
55
|
-
# @
|
55
|
+
# @return [Boolean]
|
56
56
|
#
|
57
57
|
# @example
|
58
58
|
# browser.go_to("https://example.com/")
|
59
59
|
# browser.at_xpath("//a[text() = 'No UI changes button']").click
|
60
|
-
# browser.network.wait_for_idle
|
60
|
+
# browser.network.wait_for_idle # => false
|
61
61
|
#
|
62
|
-
def wait_for_idle(connections: 0, duration: 0.05, timeout: @page.
|
62
|
+
def wait_for_idle(connections: 0, duration: 0.05, timeout: @page.timeout)
|
63
63
|
start = Utils::ElapsedTime.monotonic_time
|
64
64
|
|
65
65
|
until idle?(connections)
|
66
|
-
|
66
|
+
return false if Utils::ElapsedTime.timeout?(start, timeout)
|
67
67
|
|
68
68
|
sleep(duration)
|
69
69
|
end
|
70
|
+
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Waits for network idle or raises {Ferrum::TimeoutError} error.
|
76
|
+
# Accepts same arguments as `wait_for_idle`.
|
77
|
+
#
|
78
|
+
# @raise [Ferrum::TimeoutError]
|
79
|
+
def wait_for_idle!(...)
|
80
|
+
result = wait_for_idle(...)
|
81
|
+
raise TimeoutError unless result
|
70
82
|
end
|
71
83
|
|
72
84
|
def idle?(connections = 0)
|
@@ -356,26 +368,24 @@ module Ferrum
|
|
356
368
|
@page.on("Network.requestWillBeSent") do |params|
|
357
369
|
request = Network::Request.new(params)
|
358
370
|
|
359
|
-
# We can build exchange in two places, here on the event or when request
|
360
|
-
# is interrupted. So we have to be careful when to create new one. We
|
361
|
-
# create new exchange only if there's no with such id or there's but
|
362
|
-
# it's filled with request which means this one is new but has response
|
363
|
-
# for a redirect. So we assign response from the params to previous
|
364
|
-
# exchange and build new exchange to assign this request to it.
|
365
|
-
exchange = select(request.id).last
|
366
|
-
exchange = build_exchange(request.id) unless exchange&.blank?
|
367
|
-
|
368
371
|
# On redirects Chrome doesn't change `requestId` and there's no
|
369
372
|
# `Network.responseReceived` event for such request. If there's already
|
370
373
|
# exchange object with this id then we got redirected and params has
|
371
374
|
# `redirectResponse` key which contains the response.
|
372
|
-
if params["redirectResponse"]
|
373
|
-
previous_exchange = select(request.id)[-2]
|
375
|
+
if params["redirectResponse"] && (previous_exchange = select(request.id).last)
|
374
376
|
response = Network::Response.new(@page, params)
|
375
377
|
response.loaded = true
|
376
378
|
previous_exchange.response = response
|
377
379
|
end
|
378
380
|
|
381
|
+
# We can build exchange in two places, here on the event or when request
|
382
|
+
# is interrupted. So we have to be careful when to create new one. We
|
383
|
+
# create new exchange only if there's no with such id or there's but
|
384
|
+
# it's filled with request which means this one is new but has response
|
385
|
+
# for a redirect. So we assign response from the params to previous
|
386
|
+
# exchange and build new exchange to assign this request to it.
|
387
|
+
exchange = select(request.id).last
|
388
|
+
exchange = build_exchange(request.id) unless exchange&.blank?
|
379
389
|
exchange.request = request
|
380
390
|
|
381
391
|
@exchange = exchange if exchange.navigation_request?(@page.main_frame.id)
|
@@ -385,19 +395,19 @@ module Ferrum
|
|
385
395
|
def subscribe_response_received
|
386
396
|
@page.on("Network.responseReceived") do |params|
|
387
397
|
exchange = select(params["requestId"]).last
|
398
|
+
next unless exchange
|
388
399
|
|
389
|
-
|
390
|
-
|
391
|
-
exchange.response = response
|
392
|
-
end
|
400
|
+
response = Network::Response.new(@page, params)
|
401
|
+
exchange.response = response
|
393
402
|
end
|
394
403
|
end
|
395
404
|
|
396
405
|
def subscribe_loading_finished
|
397
406
|
@page.on("Network.loadingFinished") do |params|
|
398
|
-
|
407
|
+
exchange = select(params["requestId"]).last
|
408
|
+
next unless exchange
|
399
409
|
|
400
|
-
if response
|
410
|
+
if (response = exchange.response)
|
401
411
|
response.loaded = true
|
402
412
|
response.body_size = params["encodedDataLength"]
|
403
413
|
end
|
@@ -407,8 +417,9 @@ module Ferrum
|
|
407
417
|
def subscribe_loading_failed
|
408
418
|
@page.on("Network.loadingFailed") do |params|
|
409
419
|
exchange = select(params["requestId"]).last
|
410
|
-
|
420
|
+
next unless exchange
|
411
421
|
|
422
|
+
exchange.error ||= Network::Error.new
|
412
423
|
exchange.error.id = params["requestId"]
|
413
424
|
exchange.error.type = params["type"]
|
414
425
|
exchange.error.error_text = params["errorText"]
|
@@ -422,8 +433,9 @@ module Ferrum
|
|
422
433
|
entry = params["entry"] || {}
|
423
434
|
if entry["source"] == "network" && entry["level"] == "error"
|
424
435
|
exchange = select(entry["networkRequestId"]).last
|
425
|
-
|
436
|
+
next unless exchange
|
426
437
|
|
438
|
+
exchange.error ||= Network::Error.new
|
427
439
|
exchange.error.id = entry["networkRequestId"]
|
428
440
|
exchange.error.url = entry["url"]
|
429
441
|
exchange.error.description = entry["text"]
|
data/lib/ferrum/node.rb
CHANGED
@@ -217,6 +217,17 @@ module Ferrum
|
|
217
217
|
.each_with_object({}) { |style, memo| memo.merge!(style["name"] => style["value"]) }
|
218
218
|
end
|
219
219
|
|
220
|
+
def remove
|
221
|
+
page.command("DOM.removeNode", nodeId: node_id)
|
222
|
+
end
|
223
|
+
|
224
|
+
def exists?
|
225
|
+
page.command("DOM.resolveNode", nodeId: node_id)
|
226
|
+
true
|
227
|
+
rescue Ferrum::NodeNotFoundError
|
228
|
+
false
|
229
|
+
end
|
230
|
+
|
220
231
|
private
|
221
232
|
|
222
233
|
def bounding_rect_coordinates
|
data/lib/ferrum/page/frames.rb
CHANGED
@@ -16,8 +16,8 @@ module Ferrum
|
|
16
16
|
# @return [Array<Frame>]
|
17
17
|
#
|
18
18
|
# @example
|
19
|
-
#
|
20
|
-
#
|
19
|
+
# page.go_to("https://www.w3schools.com/tags/tag_frame.asp")
|
20
|
+
# page.frames # =>
|
21
21
|
# # [
|
22
22
|
# # #<Ferrum::Frame
|
23
23
|
# # @id="C6D104CE454A025FBCF22B98DE612B12"
|
@@ -36,10 +36,10 @@ module Ferrum
|
|
36
36
|
end
|
37
37
|
|
38
38
|
#
|
39
|
-
# Find frame by given
|
39
|
+
# Find a frame by given params.
|
40
40
|
#
|
41
41
|
# @param [String] id
|
42
|
-
# Unique frame's id that
|
42
|
+
# Unique frame's id that page provides.
|
43
43
|
#
|
44
44
|
# @param [String] name
|
45
45
|
# Frame's name if there's one.
|
@@ -51,7 +51,7 @@ module Ferrum
|
|
51
51
|
# The matching frame.
|
52
52
|
#
|
53
53
|
# @example
|
54
|
-
#
|
54
|
+
# page.frame_by(id: "C6D104CE454A025FBCF22B98DE612B12")
|
55
55
|
#
|
56
56
|
def frame_by(id: nil, name: nil, execution_id: nil)
|
57
57
|
if id
|
@@ -60,8 +60,6 @@ module Ferrum
|
|
60
60
|
frames.find { |f| f.name == name }
|
61
61
|
elsif execution_id
|
62
62
|
frames.find { |f| f.execution_id == execution_id }
|
63
|
-
else
|
64
|
-
raise ArgumentError
|
65
63
|
end
|
66
64
|
end
|
67
65
|
|
@@ -130,11 +128,11 @@ module Ferrum
|
|
130
128
|
# node will change the id and all subsequent nodes have to change id too.
|
131
129
|
if @main_frame.id == params["frameId"]
|
132
130
|
@event.set if idling?
|
133
|
-
document_node_id
|
131
|
+
document_node_id(async: true)
|
134
132
|
end
|
135
133
|
|
136
134
|
frame = @frames[params["frameId"]]
|
137
|
-
frame
|
135
|
+
frame&.state = :stopped_loading
|
138
136
|
|
139
137
|
@event.set if idling?
|
140
138
|
end
|
@@ -5,6 +5,12 @@ require "ferrum/rgba"
|
|
5
5
|
module Ferrum
|
6
6
|
class Page
|
7
7
|
module Screenshot
|
8
|
+
FULL_WARNING = "Ignoring :selector or :area in #screenshot since full: true was given at %s"
|
9
|
+
AREA_WARNING = "Ignoring :area in #screenshot since selector: was given at %s"
|
10
|
+
|
11
|
+
DEFAULT_SCREENSHOT_FORMAT = "png"
|
12
|
+
SUPPORTED_SCREENSHOT_FORMAT = %w[png jpeg jpg webp].freeze
|
13
|
+
|
8
14
|
DEFAULT_PDF_OPTIONS = {
|
9
15
|
landscape: false,
|
10
16
|
paper_width: 8.5,
|
@@ -38,7 +44,7 @@ module Ferrum
|
|
38
44
|
# @option opts [:base64, :binary] :encoding
|
39
45
|
# The encoding the image should be returned in.
|
40
46
|
#
|
41
|
-
# @option opts ["jpeg", "png"] :format
|
47
|
+
# @option opts ["jpeg", "jpg", "png", "webp"] :format
|
42
48
|
# The format the image should be returned in.
|
43
49
|
#
|
44
50
|
# @option opts [Integer] :quality
|
@@ -50,6 +56,9 @@ module Ferrum
|
|
50
56
|
# @option opts [String] :selector
|
51
57
|
# CSS selector for the given element.
|
52
58
|
#
|
59
|
+
# @option opts [Hash] :area
|
60
|
+
# x, y, width, height to screenshot an area.
|
61
|
+
#
|
53
62
|
# @option opts [Float] :scale
|
54
63
|
# Zoom in/out.
|
55
64
|
#
|
@@ -57,19 +66,22 @@ module Ferrum
|
|
57
66
|
# Sets the background color.
|
58
67
|
#
|
59
68
|
# @example
|
60
|
-
#
|
69
|
+
# page.go_to("https://google.com/")
|
61
70
|
#
|
62
71
|
# @example Save on the disk in PNG:
|
63
|
-
#
|
72
|
+
# page.screenshot(path: "google.png") # => 134660
|
64
73
|
#
|
65
74
|
# @example Save on the disk in JPG:
|
66
|
-
#
|
75
|
+
# page.screenshot(path: "google.jpg") # => 30902
|
76
|
+
#
|
77
|
+
# @example Save to Base64 in WebP with reduce quality:
|
78
|
+
# page.screenshot(format: "webp", quality: 60) # "iVBORw0KGgoAAAANS...
|
67
79
|
#
|
68
80
|
# @example Save to Base64 the whole page not only viewport and reduce quality:
|
69
|
-
#
|
81
|
+
# page.screenshot(full: true, format: "jpeg", quality: 60) # "iVBORw0KGgoAAAANS...
|
70
82
|
#
|
71
83
|
# @example Save with specific background color:
|
72
|
-
#
|
84
|
+
# page.screenshot(background_color: Ferrum::RGBA.new(0, 0, 0, 0.0))
|
73
85
|
#
|
74
86
|
def screenshot(**opts)
|
75
87
|
path, encoding = common_options(**opts)
|
@@ -113,9 +125,9 @@ module Ferrum
|
|
113
125
|
# can pass.
|
114
126
|
#
|
115
127
|
# @example
|
116
|
-
#
|
128
|
+
# page.go_to("https://google.com/")
|
117
129
|
# # Save to disk as a PDF
|
118
|
-
#
|
130
|
+
# page.pdf(path: "google.pdf", paper_width: 1.0, paper_height: 1.0) # => true
|
119
131
|
#
|
120
132
|
def pdf(**opts)
|
121
133
|
path, encoding = common_options(**opts)
|
@@ -131,8 +143,8 @@ module Ferrum
|
|
131
143
|
# The path to save a file on the disk.
|
132
144
|
#
|
133
145
|
# @example
|
134
|
-
#
|
135
|
-
#
|
146
|
+
# page.go_to("https://google.com/")
|
147
|
+
# page.mhtml(path: "google.mhtml") # => 87742
|
136
148
|
#
|
137
149
|
def mhtml(path: nil)
|
138
150
|
data = command("Page.captureSnapshot", format: :mhtml).fetch("data")
|
@@ -198,45 +210,59 @@ module Ferrum
|
|
198
210
|
screenshot_options.merge!(quality: quality) if quality
|
199
211
|
screenshot_options.merge!(format: format)
|
200
212
|
|
201
|
-
clip = area_options(options[:full], options[:selector], scale)
|
213
|
+
clip = area_options(options[:full], options[:selector], scale, options[:area])
|
202
214
|
screenshot_options.merge!(clip: clip) if clip
|
203
215
|
|
204
216
|
screenshot_options
|
205
217
|
end
|
206
218
|
|
207
219
|
def format_options(format, path, quality)
|
208
|
-
format
|
220
|
+
if !format && path # try to infer from path
|
221
|
+
extension = File.extname(path).delete(".").downcase
|
222
|
+
format = extension unless extension.empty?
|
223
|
+
end
|
224
|
+
|
225
|
+
format ||= DEFAULT_SCREENSHOT_FORMAT
|
226
|
+
format = format.to_s
|
227
|
+
raise Ferrum::InvalidScreenshotFormatError, format unless SUPPORTED_SCREENSHOT_FORMAT.include?(format)
|
228
|
+
|
209
229
|
format = "jpeg" if format == "jpg"
|
210
|
-
raise "Not supported options `:format` #{format}. jpeg | png" if format !~ /jpeg|png/i
|
211
230
|
|
212
|
-
|
231
|
+
# Chrome supports screenshot qualities for JPEG and WebP
|
232
|
+
quality ||= 75 if format != "png"
|
213
233
|
|
214
234
|
[format, quality]
|
215
235
|
end
|
216
236
|
|
217
|
-
def area_options(full, selector, scale)
|
218
|
-
|
219
|
-
warn(
|
237
|
+
def area_options(full, selector, scale, area = nil)
|
238
|
+
warn(FULL_WARNING % caller(1..1).first) if full && (selector || area)
|
239
|
+
warn(AREA_WARNING % caller(1..1).first) if selector && area
|
220
240
|
|
221
241
|
clip = if full
|
222
|
-
|
223
|
-
{ x: 0, y: 0, width: width, height: height, scale: scale } if width.positive? && height.positive?
|
242
|
+
full_window_area || viewport_area
|
224
243
|
elsif selector
|
225
|
-
bounding_rect(selector)
|
244
|
+
bounding_rect(selector)
|
245
|
+
elsif area
|
246
|
+
area
|
247
|
+
else
|
248
|
+
viewport_area
|
226
249
|
end
|
227
250
|
|
228
|
-
|
229
|
-
unless clip
|
230
|
-
width, height = viewport_size
|
231
|
-
clip = { x: 0, y: 0, width: width, height: height }
|
232
|
-
end
|
233
|
-
|
234
|
-
clip.merge!(scale: scale)
|
235
|
-
end
|
251
|
+
clip.merge!(scale: scale)
|
236
252
|
|
237
253
|
clip
|
238
254
|
end
|
239
255
|
|
256
|
+
def full_window_area
|
257
|
+
width, height = document_size
|
258
|
+
{ x: 0, y: 0, width: width, height: height } if width.positive? && height.positive?
|
259
|
+
end
|
260
|
+
|
261
|
+
def viewport_area
|
262
|
+
width, height = viewport_size
|
263
|
+
{ x: 0, y: 0, width: width, height: height }
|
264
|
+
end
|
265
|
+
|
240
266
|
def bounding_rect(selector)
|
241
267
|
rect = evaluate_async(%(
|
242
268
|
const rect = document
|