ferrum 0.15 → 0.17

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.
@@ -4,6 +4,8 @@ require "ferrum/context"
4
4
 
5
5
  module Ferrum
6
6
  class Contexts
7
+ ALLOWED_TARGET_TYPES = %w[page iframe].freeze
8
+
7
9
  include Enumerable
8
10
 
9
11
  attr_reader :contexts
@@ -67,12 +69,19 @@ module Ferrum
67
69
 
68
70
  private
69
71
 
72
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
70
73
  def subscribe
71
74
  @client.on("Target.attachedToTarget") do |params|
72
75
  info, session_id = params.values_at("targetInfo", "sessionId")
73
- next unless info["type"] == "page"
76
+ next unless ALLOWED_TARGET_TYPES.include?(info["type"])
74
77
 
75
78
  context_id = info["browserContextId"]
79
+ unless @contexts[context_id]
80
+ context = Context.new(@client, self, context_id)
81
+ @contexts[context_id] = context
82
+ @default_context ||= context
83
+ end
84
+
76
85
  @contexts[context_id]&.add_target(session_id: session_id, params: info)
77
86
  if params["waitingForDebugger"]
78
87
  @client.session(session_id).command("Runtime.runIfWaitingForDebugger", async: true)
@@ -81,15 +90,21 @@ module Ferrum
81
90
 
82
91
  @client.on("Target.targetCreated") do |params|
83
92
  info = params["targetInfo"]
84
- next unless info["type"] == "page"
93
+ next unless ALLOWED_TARGET_TYPES.include?(info["type"])
85
94
 
86
95
  context_id = info["browserContextId"]
87
- @contexts[context_id]&.add_target(params: info)
96
+
97
+ if info["type"] == "iframe" &&
98
+ (target = @contexts[context_id].find_target { |t| t.connected? && t.page.frame_by(id: info["targetId"]) })
99
+ @contexts[context_id]&.add_target(session_id: target.page.client.session_id, params: info)
100
+ else
101
+ @contexts[context_id]&.add_target(params: info)
102
+ end
88
103
  end
89
104
 
90
105
  @client.on("Target.targetInfoChanged") do |params|
91
106
  info = params["targetInfo"]
92
- next unless info["type"] == "page"
107
+ next unless ALLOWED_TARGET_TYPES.include?(info["type"])
93
108
 
94
109
  context_id, target_id = info.values_at("browserContextId", "targetId")
95
110
  @contexts[context_id]&.update_target(target_id, info)
@@ -105,6 +120,7 @@ module Ferrum
105
120
  context&.delete_target(params["targetId"])
106
121
  end
107
122
  end
123
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
108
124
 
109
125
  def discover
110
126
  @client.command("Target.setDiscoverTargets", discover: true)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "yaml"
3
4
  require "ferrum/cookies/cookie"
4
5
 
5
6
  module Ferrum
@@ -169,6 +170,32 @@ module Ferrum
169
170
  true
170
171
  end
171
172
 
173
+ #
174
+ # Stores all cookies of current page in a file.
175
+ #
176
+ # @return [Integer]
177
+ #
178
+ # @example
179
+ # browser.cookies.store # => Integer
180
+ #
181
+ def store(path = "cookies.yml")
182
+ File.write(path, map(&:to_h).to_yaml)
183
+ end
184
+
185
+ #
186
+ # Loads all cookies from the file and sets them for current page.
187
+ #
188
+ # @return [true]
189
+ #
190
+ # @example
191
+ # browser.cookies.load # => true
192
+ #
193
+ def load(path = "cookies.yml")
194
+ cookies = YAML.load_file(path)
195
+ cookies.each { |c| set(c) }
196
+ true
197
+ end
198
+
172
199
  private
173
200
 
174
201
  def default_domain
data/lib/ferrum/errors.rb CHANGED
@@ -78,6 +78,13 @@ module Ferrum
78
78
  end
79
79
  end
80
80
 
81
+ class InvalidScreenshotFormatError < Error
82
+ def initialize(format)
83
+ valid_formats = Page::Screenshot::SUPPORTED_SCREENSHOT_FORMAT.join(" | ")
84
+ super("Invalid value #{format} for option `:format` (#{valid_formats})")
85
+ end
86
+ end
87
+
81
88
  class BrowserError < Error
82
89
  attr_reader :response
83
90
 
@@ -99,8 +106,7 @@ module Ferrum
99
106
 
100
107
  class NoExecutionContextError < BrowserError
101
108
  def initialize(response = nil)
102
- response ||= { "message" => "There's no context available" }
103
- super(response)
109
+ super(response || { "message" => "There's no context available" })
104
110
  end
105
111
  end
106
112
 
@@ -14,7 +14,7 @@
14
14
  # any updates, for example, the node may be destroyed without any notification.
15
15
  # This is a way to keep a reference to the Node, when you don't necessarily want
16
16
  # to keep track of it. One example would be linking to the node from performance
17
- # data (e.g. relayout root node). BackendNodeId may be either resolved to
17
+ # data (e.g. re-layout root node). BackendNodeId may be either resolved to
18
18
  # inspected node (DOM.pushNodesByBackendIdsToFrontend) or described in more
19
19
  # details (DOM.describeNode).
20
20
  module Ferrum
@@ -92,7 +92,23 @@ module Ferrum
92
92
  # browser.body # => '<html itemscope="" itemtype="http://schema.org/WebPage" lang="ru"><head>...
93
93
  #
94
94
  def body
95
- evaluate("document.documentElement.outerHTML")
95
+ evaluate("document.documentElement?.outerHTML") || ""
96
+ end
97
+
98
+ #
99
+ # Returns the element in which the window is embedded.
100
+ #
101
+ # @return [Node, nil]
102
+ # The element in which the window is embedded.
103
+ #
104
+ # @example
105
+ # browser.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
106
+ # frame = browser.frames.last
107
+ # frame.frame_element # => [Node]
108
+ # frame.parent.parent.parent.frame_element # => nil
109
+ #
110
+ def frame_element
111
+ evaluate("window.frameElement")
96
112
  end
97
113
 
98
114
  #
data/lib/ferrum/frame.rb CHANGED
@@ -94,6 +94,23 @@ module Ferrum
94
94
  @parent_id.nil?
95
95
  end
96
96
 
97
+ #
98
+ # Returns the parent frame if this frame is nested in another one.
99
+ #
100
+ # @return [Frame, nil]
101
+ #
102
+ # @example
103
+ # browser.go_to("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
104
+ # frame = browser.frames.last
105
+ # frame.url # => "https://www.openstreetmap.org/export/embed.html?bbox=-0.004017949104309083%2C51.47612752641776%2C0.00030577182769775396%2C51.478569861898606&layer=mapnik"
106
+ # frame.parent.main? # => false
107
+ # frame.parent.parent.main? # => false
108
+ # frame.parent.parent.parent.main? # => true
109
+ #
110
+ def parent
111
+ @page.frame_by(id: @parent_id) if @parent_id
112
+ end
113
+
97
114
  #
98
115
  # Sets a content of a given frame.
99
116
  #
@@ -93,7 +93,6 @@ module Ferrum
93
93
 
94
94
  private
95
95
 
96
- # TODO: Refactor it, and try to simplify complexity
97
96
  # rubocop:disable Metrics/PerceivedComplexity
98
97
  # rubocop:disable Metrics/CyclomaticComplexity
99
98
  def normalize_keys(keys, pressed_keys = [], memo = [])
data/lib/ferrum/mouse.rb CHANGED
@@ -3,11 +3,36 @@
3
3
  module Ferrum
4
4
  class Mouse
5
5
  CLICK_WAIT = ENV.fetch("FERRUM_CLICK_WAIT", 0.1).to_f
6
- VALID_BUTTONS = %w[none left middle right back forward].freeze
6
+ BUTTON_MASKS = {
7
+ "none" => 0,
8
+ "left" => 1,
9
+ "right" => 2,
10
+ "middle" => 4,
11
+ "back" => 8,
12
+ "forward" => 16
13
+ }.freeze
7
14
 
8
15
  def initialize(page)
9
16
  @page = page
10
17
  @x = @y = 0
18
+ @buttons = 0
19
+ end
20
+
21
+ #
22
+ # Scroll page by the given amount x, y.
23
+ #
24
+ # @param [Integer] x
25
+ # The horizontal pixel value that you want to scroll by.
26
+ #
27
+ # @param [Integer] y
28
+ # The vertical pixel value that you want to scroll by.
29
+ #
30
+ # @example
31
+ # browser.go_to("https://www.google.com/search?q=Ruby+headless+driver+for+Capybara")
32
+ # browser.mouse.scroll_by(0, 400)
33
+ #
34
+ def scroll_by(x, y)
35
+ tap { @page.execute("window.scrollBy(#{x}, #{y})") }
11
36
  end
12
37
 
13
38
  #
@@ -107,9 +132,9 @@ module Ferrum
107
132
  #
108
133
  # Mouse move to given x and y.
109
134
  #
110
- # @param [Integer] x
135
+ # @param [Number] x
111
136
  #
112
- # @param [Integer] y
137
+ # @param [Number] y
113
138
  #
114
139
  # @param [Integer] steps
115
140
  # Sends intermediate mousemove events.
@@ -129,8 +154,9 @@ module Ferrum
129
154
  @page.command("Input.dispatchMouseEvent",
130
155
  slowmoable: true,
131
156
  type: "mouseMoved",
132
- x: new_x.to_i,
133
- y: new_y.to_i)
157
+ x: new_x,
158
+ y: new_y,
159
+ buttons: @buttons)
134
160
  end
135
161
 
136
162
  self
@@ -140,16 +166,26 @@ module Ferrum
140
166
 
141
167
  def mouse_event(type:, button: :left, count: 1, modifiers: nil, wait: 0)
142
168
  button = validate_button(button)
143
- options = { x: @x, y: @y, type: type, button: button, clickCount: count }
169
+ register_event_button(type, button)
170
+ options = { x: @x, y: @y, type: type, button: button, buttons: @buttons, clickCount: count }
144
171
  options.merge!(modifiers: modifiers) if modifiers
145
172
  @page.command("Input.dispatchMouseEvent", wait: wait, slowmoable: true, **options)
146
173
  end
147
174
 
148
175
  def validate_button(button)
149
176
  button = button.to_s
150
- raise "Invalid button: #{button}" unless VALID_BUTTONS.include?(button)
177
+ raise "Invalid button: #{button}" unless BUTTON_MASKS.key?(button)
151
178
 
152
179
  button
153
180
  end
181
+
182
+ def register_event_button(type, button)
183
+ case type
184
+ when "mousePressed"
185
+ @buttons |= BUTTON_MASKS[button]
186
+ when "mouseReleased"
187
+ @buttons &= ~BUTTON_MASKS[button]
188
+ end
189
+ end
154
190
  end
155
191
  end
@@ -28,6 +28,15 @@ module Ferrum
28
28
  # @return [Error, nil]
29
29
  attr_accessor :error
30
30
 
31
+ # Determines if the network exchange is unknown due to
32
+ # a lost of its context
33
+ #
34
+ # @return Boolean
35
+ attr_accessor :unknown
36
+
37
+ # @api private
38
+ attr_accessor :request_extra_info
39
+
31
40
  #
32
41
  # Initializes the network exchange.
33
42
  #
@@ -40,6 +49,8 @@ module Ferrum
40
49
  @page = page
41
50
  @intercepted_request = nil
42
51
  @request = @response = @error = nil
52
+ @request_extra_info = nil
53
+ @unknown = false
43
54
  end
44
55
 
45
56
  #
@@ -54,6 +65,15 @@ module Ferrum
54
65
  request&.type?(:document) && request&.frame_id == frame_id
55
66
  end
56
67
 
68
+ #
69
+ # The loader ID of the request.
70
+ #
71
+ # @return [String, nil]
72
+ #
73
+ def loader_id
74
+ request&.loader_id
75
+ end
76
+
57
77
  #
58
78
  # Determines if the network exchange has a request.
59
79
  #
@@ -74,12 +94,12 @@ module Ferrum
74
94
 
75
95
  #
76
96
  # Determines if the request was blocked, a response was returned, or if an
77
- # error occurred.
97
+ # error occurred or the exchange is unknown and cannot be inferred.
78
98
  #
79
99
  # @return [Boolean]
80
100
  #
81
101
  def finished?
82
- blocked? || response&.loaded? || !error.nil? || ping?
102
+ blocked? || response&.loaded? || !error.nil? || ping? || blob? || unknown
83
103
  end
84
104
 
85
105
  #
@@ -127,6 +147,15 @@ module Ferrum
127
147
  !!request&.ping?
128
148
  end
129
149
 
150
+ #
151
+ # Determines if the exchange is blob.
152
+ #
153
+ # @return [Boolean]
154
+ #
155
+ def blob?
156
+ !!url&.start_with?("blob:")
157
+ end
158
+
130
159
  #
131
160
  # Returns request's URL.
132
161
  #
@@ -156,7 +185,8 @@ module Ferrum
156
185
  "@intercepted_request=#{@intercepted_request.inspect} " \
157
186
  "@request=#{@request.inspect} " \
158
187
  "@response=#{@response.inspect} " \
159
- "@error=#{@error.inspect}>"
188
+ "@error=#{@error.inspect}> " \
189
+ "@unknown=#{@unknown.inspect}>"
160
190
  end
161
191
  end
162
192
  end
@@ -71,6 +71,15 @@ module Ferrum
71
71
  @params["frameId"]
72
72
  end
73
73
 
74
+ #
75
+ # The loader ID of the request.
76
+ #
77
+ # @return [String]
78
+ #
79
+ def loader_id
80
+ @params["loaderId"]
81
+ end
82
+
74
83
  #
75
84
  # The request timestamp.
76
85
  #
@@ -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
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)
@@ -88,7 +100,7 @@ module Ferrum
88
100
  #
89
101
  # Page request of the main frame.
90
102
  #
91
- # @return [Request]
103
+ # @return [Request, nil]
92
104
  #
93
105
  # @example
94
106
  # browser.go_to("https://github.com/")
@@ -356,29 +368,38 @@ 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) if exchange.nil? || !exchange.blank?
389
+ request.headers.merge!(Hash(exchange.request_extra_info&.dig("headers")))
379
390
  exchange.request = request
380
391
 
381
- @exchange = exchange if exchange.navigation_request?(@page.main_frame.id)
392
+ if exchange.navigation_request?(@page.main_frame.id)
393
+ @exchange = exchange
394
+ classify_pending_exchanges(exchange.loader_id)
395
+ end
396
+ end
397
+
398
+ @page.on("Network.requestWillBeSentExtraInfo") do |params|
399
+ exchange = select(params["requestId"]).last
400
+ exchange ||= build_exchange(params["requestId"])
401
+ exchange.request_extra_info = params
402
+ exchange.request&.headers&.merge!(params["headers"])
382
403
  end
383
404
  end
384
405
 
@@ -397,6 +418,8 @@ module Ferrum
397
418
  exchange = select(params["requestId"]).last
398
419
  next unless exchange
399
420
 
421
+ exchange.unknown = false
422
+
400
423
  if (response = exchange.response)
401
424
  response.loaded = true
402
425
  response.body_size = params["encodedDataLength"]
@@ -479,5 +502,17 @@ module Ferrum
479
502
  def whitelist?
480
503
  Array(@whitelist).any?
481
504
  end
505
+
506
+ # When the main frame navigates Chrome doesn't send `Network.loadingFailed`
507
+ # for pending async requests. Therefore, we mark pending connections as unknown since
508
+ # they are not relevant to the current navigation.
509
+ def classify_pending_exchanges(new_loader_id)
510
+ @traffic.each do |exchange|
511
+ break if exchange.loader_id == new_loader_id
512
+ next unless exchange.pending?
513
+
514
+ exchange.unknown = true
515
+ end
516
+ end
482
517
  end
483
518
  end
data/lib/ferrum/node.rb CHANGED
@@ -76,6 +76,7 @@ module Ferrum
76
76
  when :double
77
77
  page.mouse.move(x: x, y: y)
78
78
  page.mouse.down(modifiers: modifiers, count: 2)
79
+ sleep(delay)
79
80
  page.mouse.up(modifiers: modifiers, count: 2)
80
81
  when :left
81
82
  page.mouse.click(x: x, y: y, modifiers: modifiers, delay: delay)
@@ -217,6 +218,17 @@ module Ferrum
217
218
  .each_with_object({}) { |style, memo| memo.merge!(style["name"] => style["value"]) }
218
219
  end
219
220
 
221
+ def remove
222
+ page.command("DOM.removeNode", nodeId: node_id)
223
+ end
224
+
225
+ def exists?
226
+ page.command("DOM.resolveNode", nodeId: node_id)
227
+ true
228
+ rescue Ferrum::NodeNotFoundError
229
+ false
230
+ end
231
+
220
232
  private
221
233
 
222
234
  def bounding_rect_coordinates
@@ -36,7 +36,7 @@ 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
42
  # Unique frame's id that page provides.
@@ -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
 
@@ -126,11 +124,11 @@ module Ferrum
126
124
  on("Page.frameStoppedLoading") do |params|
127
125
  # `DOM.performSearch` doesn't work without getting #document node first.
128
126
  # It returns node with nodeId 1 and nodeType 9 from which descend the
129
- # tree and we save it in a variable because if we call that again root
127
+ # tree, and we save it in a variable because if we call that again root
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"]]
@@ -179,12 +177,16 @@ module Ferrum
179
177
  execution_id = params["executionContextId"]
180
178
  frame = frame_by(execution_id: execution_id)
181
179
  frame&.execution_id = nil
180
+ frame&.state = :stopped_loading
182
181
  end
183
182
  end
184
183
 
185
184
  def subscribe_execution_contexts_cleared
186
185
  on("Runtime.executionContextsCleared") do
187
- @frames.each_value { |f| f.execution_id = nil }
186
+ @frames.each_value do |f|
187
+ f.execution_id = nil
188
+ f.state = :stopped_loading
189
+ end
188
190
  end
189
191
  end
190
192
 
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ferrum
4
+ class Page
5
+ module Screencast
6
+ # Starts sending frames to record screencast to the given block.
7
+ #
8
+ # @param [Hash{Symbol => Object}] opts
9
+ #
10
+ # @option opts [:jpeg, :png] :format
11
+ # The format the image should be returned in.
12
+ #
13
+ # @option opts [Integer] :quality
14
+ # The image quality. **Note:** 0-100 works for JPEG only.
15
+ #
16
+ # @option opts [Integer] :max_width
17
+ # Maximum screencast frame width.
18
+ #
19
+ # @option opts [Integer] :max_height
20
+ # Maximum screencast frame height.
21
+ #
22
+ # @option opts [Integer] :every_nth_frame
23
+ # Send every n-th frame.
24
+ #
25
+ # @yield [data, metadata, session_id]
26
+ # The given block receives the screencast frame along with metadata
27
+ # about the frame and the screencast session ID.
28
+ #
29
+ # @yieldparam data [String]
30
+ # Base64-encoded compressed image.
31
+ #
32
+ # @yieldparam metadata [Hash{String => Object}]
33
+ # Screencast frame metadata.
34
+ #
35
+ # @option metadata [Integer] 'offsetTop'
36
+ # Top offset in DIP.
37
+ #
38
+ # @option metadata [Integer] 'pageScaleFactor'
39
+ # Page scale factor.
40
+ #
41
+ # @option metadata [Integer] 'deviceWidth'
42
+ # Device screen width in DIP.
43
+ #
44
+ # @option metadata [Integer] 'deviceHeight'
45
+ # Device screen height in DIP.
46
+ #
47
+ # @option metadata [Integer] 'scrollOffsetX'
48
+ # Position of horizontal scroll in CSS pixels.
49
+ #
50
+ # @option metadata [Integer] 'scrollOffsetY'
51
+ # Position of vertical scroll in CSS pixels.
52
+ #
53
+ # @option metadata [Float] 'timestamp'
54
+ # (optional) Frame swap timestamp in seconds since Unix epoch.
55
+ #
56
+ # @yieldparam session_id [Integer]
57
+ # Frame number.
58
+ #
59
+ # @example
60
+ # require "base64"
61
+ #
62
+ # page.go_to("https://apple.com/ipad")
63
+ #
64
+ # page.start_screencast(format: :jpeg, quality: 75) do |data, metadata|
65
+ # timestamp = (metadata['timestamp'] * 1000).to_i
66
+ # File.binwrite("image_#{timestamp}.jpg", Base64.decode64(data))
67
+ # end
68
+ #
69
+ # sleep 10
70
+ #
71
+ # page.stop_screencast
72
+ #
73
+ def start_screencast(**opts)
74
+ options = opts.transform_keys { START_SCREENCAST_KEY_CONV.fetch(_1, _1) }
75
+ response = command("Page.startScreencast", **options)
76
+
77
+ if (error_text = response["errorText"]) # https://cs.chromium.org/chromium/src/net/base/net_error_list.h
78
+ raise "Starting screencast failed (#{error_text})"
79
+ end
80
+
81
+ on("Page.screencastFrame") do |params|
82
+ data, metadata, session_id = params.values_at("data", "metadata", "sessionId")
83
+
84
+ command("Page.screencastFrameAck", sessionId: session_id)
85
+
86
+ yield data, metadata, session_id
87
+ end
88
+ end
89
+
90
+ # Stops sending frames.
91
+ def stop_screencast
92
+ command("Page.stopScreencast")
93
+ end
94
+
95
+ START_SCREENCAST_KEY_CONV = {
96
+ max_width: :maxWidth,
97
+ max_height: :maxHeight,
98
+ every_nth_frame: :everyNthFrame
99
+ }.freeze
100
+ end
101
+ end
102
+ end