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.
@@ -41,7 +41,7 @@ module Ferrum
41
41
  end
42
42
 
43
43
  #
44
- # Waits for network idle or raises {Ferrum::TimeoutError} error.
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
- # @raise [Ferrum::TimeoutError]
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.browser.timeout)
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
- raise TimeoutError if Utils::ElapsedTime.timeout?(start, timeout)
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
- if exchange
390
- response = Network::Response.new(@page, params)
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
- response = select(params["requestId"]).last&.response
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
- exchange.error ||= Network::Error.new
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
- exchange.error ||= Network::Error.new
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
@@ -16,8 +16,8 @@ module Ferrum
16
16
  # @return [Array<Frame>]
17
17
  #
18
18
  # @example
19
- # browser.go_to("https://www.w3schools.com/tags/tag_frame.asp")
20
- # browser.frames # =>
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 options.
39
+ # Find a frame by given params.
40
40
  #
41
41
  # @param [String] id
42
- # Unique frame's id that browser provides.
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
- # browser.frame_by(id: "C6D104CE454A025FBCF22B98DE612B12")
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.state = :stopped_loading
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
- # browser.go_to("https://google.com/")
69
+ # page.go_to("https://google.com/")
61
70
  #
62
71
  # @example Save on the disk in PNG:
63
- # browser.screenshot(path: "google.png") # => 134660
72
+ # page.screenshot(path: "google.png") # => 134660
64
73
  #
65
74
  # @example Save on the disk in JPG:
66
- # browser.screenshot(path: "google.jpg") # => 30902
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
- # browser.screenshot(full: true, quality: 60) # "iVBORw0KGgoAAAANS...
81
+ # page.screenshot(full: true, format: "jpeg", quality: 60) # "iVBORw0KGgoAAAANS...
70
82
  #
71
83
  # @example Save with specific background color:
72
- # browser.screenshot(background_color: Ferrum::RGBA.new(0, 0, 0, 0.0))
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
- # browser.go_to("https://google.com/")
128
+ # page.go_to("https://google.com/")
117
129
  # # Save to disk as a PDF
118
- # browser.pdf(path: "google.pdf", paper_width: 1.0, paper_height: 1.0) # => true
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
- # browser.go_to("https://google.com/")
135
- # browser.mhtml(path: "google.mhtml") # => 87742
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 ||= path ? File.extname(path).delete(".") : "png"
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
- quality ||= 75 if format == "jpeg"
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
- message = "Ignoring :selector in #screenshot since full: true was given at #{caller(1..1).first}"
219
- warn(message) if full && selector
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
- width, height = document_size
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).merge(scale: scale)
244
+ bounding_rect(selector)
245
+ elsif area
246
+ area
247
+ else
248
+ viewport_area
226
249
  end
227
250
 
228
- if scale != 1
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