ferrum 0.13 → 0.15

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +288 -154
  4. data/lib/ferrum/browser/command.rb +8 -0
  5. data/lib/ferrum/browser/options/chrome.rb +17 -5
  6. data/lib/ferrum/browser/options.rb +38 -25
  7. data/lib/ferrum/browser/process.rb +44 -17
  8. data/lib/ferrum/browser.rb +34 -52
  9. data/lib/ferrum/client/subscriber.rb +76 -0
  10. data/lib/ferrum/{browser → client}/web_socket.rb +36 -22
  11. data/lib/ferrum/client.rb +169 -0
  12. data/lib/ferrum/context.rb +19 -15
  13. data/lib/ferrum/contexts.rb +46 -12
  14. data/lib/ferrum/cookies/cookie.rb +57 -0
  15. data/lib/ferrum/cookies.rb +40 -4
  16. data/lib/ferrum/downloads.rb +60 -0
  17. data/lib/ferrum/errors.rb +2 -1
  18. data/lib/ferrum/frame.rb +1 -0
  19. data/lib/ferrum/headers.rb +1 -1
  20. data/lib/ferrum/network/exchange.rb +29 -2
  21. data/lib/ferrum/network/intercepted_request.rb +8 -17
  22. data/lib/ferrum/network/request.rb +23 -39
  23. data/lib/ferrum/network/request_params.rb +57 -0
  24. data/lib/ferrum/network/response.rb +25 -5
  25. data/lib/ferrum/network.rb +43 -16
  26. data/lib/ferrum/node.rb +21 -1
  27. data/lib/ferrum/page/frames.rb +5 -5
  28. data/lib/ferrum/page/screenshot.rb +42 -24
  29. data/lib/ferrum/page.rb +183 -131
  30. data/lib/ferrum/proxy.rb +1 -1
  31. data/lib/ferrum/target.rb +25 -5
  32. data/lib/ferrum/utils/elapsed_time.rb +0 -2
  33. data/lib/ferrum/utils/event.rb +19 -0
  34. data/lib/ferrum/utils/platform.rb +4 -0
  35. data/lib/ferrum/utils/thread.rb +18 -0
  36. data/lib/ferrum/version.rb +1 -1
  37. data/lib/ferrum.rb +3 -0
  38. metadata +14 -114
  39. data/lib/ferrum/browser/client.rb +0 -102
  40. data/lib/ferrum/browser/subscriber.rb +0 -36
data/lib/ferrum/page.rb CHANGED
@@ -1,50 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "forwardable"
4
+ require "pathname"
4
5
  require "ferrum/mouse"
5
6
  require "ferrum/keyboard"
6
7
  require "ferrum/headers"
7
8
  require "ferrum/cookies"
8
9
  require "ferrum/dialog"
9
10
  require "ferrum/network"
11
+ require "ferrum/downloads"
10
12
  require "ferrum/page/frames"
11
13
  require "ferrum/page/screenshot"
12
14
  require "ferrum/page/animation"
13
15
  require "ferrum/page/tracing"
14
16
  require "ferrum/page/stream"
15
- require "ferrum/browser/client"
16
17
 
17
18
  module Ferrum
18
19
  class Page
19
20
  GOTO_WAIT = ENV.fetch("FERRUM_GOTO_WAIT", 0.1).to_f
20
21
 
21
- class Event < Concurrent::Event
22
- def iteration
23
- synchronize { @iteration }
24
- end
25
-
26
- def reset
27
- synchronize do
28
- @iteration += 1
29
- @set = false if @set
30
- @iteration
31
- end
32
- end
33
- end
34
-
35
22
  extend Forwardable
36
23
  delegate %i[at_css at_xpath css xpath
37
24
  current_url current_title url title body doctype content=
38
25
  execution_id execution_id! evaluate evaluate_on evaluate_async execute evaluate_func
39
26
  add_script_tag add_style_tag] => :main_frame
27
+ delegate %i[base_url default_user_agent timeout timeout=] => :@options
40
28
 
41
29
  include Animation
42
30
  include Screenshot
43
31
  include Frames
44
32
  include Stream
45
33
 
46
- attr_accessor :referrer, :timeout
47
- attr_reader :target_id, :browser, :event, :tracing
34
+ attr_accessor :referrer
35
+ attr_reader :context_id, :target_id, :event, :tracing
36
+
37
+ # Client connection.
38
+ #
39
+ # @return [Client]
40
+ attr_reader :client
48
41
 
49
42
  # Mouse object.
50
43
  #
@@ -71,61 +64,56 @@ module Ferrum
71
64
  # @return [Cookies]
72
65
  attr_reader :cookies
73
66
 
74
- def initialize(target_id, browser, proxy: nil)
67
+ # Downloads object.
68
+ #
69
+ # @return [Downloads]
70
+ attr_reader :downloads
71
+
72
+ def initialize(client, context_id:, target_id:, proxy: nil)
73
+ @client = client
74
+ @context_id = context_id
75
+ @target_id = target_id
76
+ @options = client.options
77
+
75
78
  @frames = Concurrent::Map.new
76
79
  @main_frame = Frame.new(nil, self)
77
- @browser = browser
78
- @target_id = target_id
79
- @timeout = @browser.timeout
80
- @event = Event.new.tap(&:set)
80
+ @event = Utils::Event.new.tap(&:set)
81
81
  self.proxy = proxy
82
82
 
83
- @client = Browser::Client.new(ws_url, self,
84
- logger: @browser.options.logger,
85
- ws_max_receive_size: @browser.options.ws_max_receive_size,
86
- id_starts_with: 1000)
87
-
88
83
  @mouse = Mouse.new(self)
89
84
  @keyboard = Keyboard.new(self)
90
85
  @headers = Headers.new(self)
91
86
  @cookies = Cookies.new(self)
92
87
  @network = Network.new(self)
93
88
  @tracing = Tracing.new(self)
89
+ @downloads = Downloads.new(self)
94
90
 
95
91
  subscribe
96
92
  prepare_page
97
93
  end
98
94
 
99
- def context
100
- @browser.contexts.find_by(target_id: target_id)
101
- end
102
-
103
95
  #
104
96
  # Navigates the page to a URL.
105
97
  #
106
98
  # @param [String, nil] url
107
99
  # The URL to navigate to. The url should include scheme unless you set
108
- # `{Browser#base_url = url}` when configuring driver.
100
+ # `{Browser#base_url = url}` when configuring.
109
101
  #
110
102
  # @example
111
- # browser.go_to("https://github.com/")
103
+ # page.go_to("https://github.com/")
112
104
  #
113
105
  def go_to(url = nil)
114
106
  options = { url: combine_url!(url) }
115
107
  options.merge!(referrer: referrer) if referrer
116
108
  response = command("Page.navigate", wait: GOTO_WAIT, **options)
117
- # https://cs.chromium.org/chromium/src/net/base/net_error_list.h
118
- if %w[net::ERR_NAME_NOT_RESOLVED
119
- net::ERR_NAME_RESOLUTION_FAILED
120
- net::ERR_INTERNET_DISCONNECTED
121
- net::ERR_CONNECTION_TIMED_OUT
122
- net::ERR_FILE_NOT_FOUND].include?(response["errorText"])
123
- raise StatusError, options[:url]
109
+ error_text = response["errorText"] # https://cs.chromium.org/chromium/src/net/base/net_error_list.h
110
+ if error_text && error_text != "net::ERR_ABORTED" # Request aborted due to user action or download
111
+ raise StatusError.new(options[:url], "Request to #{options[:url]} failed (#{error_text})")
124
112
  end
125
113
 
126
114
  response["frameId"]
127
115
  rescue TimeoutError
128
- if @browser.options.pending_connection_errors
116
+ if @options.pending_connection_errors
129
117
  pendings = network.traffic.select(&:pending?).map(&:url).compact
130
118
  raise PendingConnectionsError.new(options[:url], pendings) unless pendings.empty?
131
119
  end
@@ -135,42 +123,76 @@ module Ferrum
135
123
 
136
124
  def close
137
125
  @headers.clear
138
- @browser.command("Target.closeTarget", targetId: @target_id)
139
- @client.close
126
+ client.command("Target.closeTarget", async: true, targetId: @target_id)
127
+ close_connection
128
+
129
+ true
130
+ end
131
+
132
+ def close_connection
133
+ client&.close
134
+ end
135
+
136
+ #
137
+ # Overrides device screen dimensions and emulates viewport according to parameters
138
+ #
139
+ # Read more [here](https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setDeviceMetricsOverride).
140
+ #
141
+ # @param [Integer] width width value in pixels. 0 disables the override
142
+ #
143
+ # @param [Integer] height height value in pixels. 0 disables the override
144
+ #
145
+ # @param [Float] scale_factor device scale factor value. 0 disables the override
146
+ #
147
+ # @param [Boolean] mobile whether to emulate mobile device
148
+ #
149
+ def set_viewport(width:, height:, scale_factor: 0, mobile: false)
150
+ command(
151
+ "Emulation.setDeviceMetricsOverride",
152
+ slowmoable: true,
153
+ width: width,
154
+ height: height,
155
+ deviceScaleFactor: scale_factor,
156
+ mobile: mobile
157
+ )
140
158
  end
141
159
 
142
160
  def resize(width: nil, height: nil, fullscreen: false)
143
161
  if fullscreen
144
162
  width, height = document_size
145
- set_window_bounds(windowState: "fullscreen")
163
+ self.window_bounds = { window_state: "fullscreen" }
146
164
  else
147
- set_window_bounds(windowState: "normal")
148
- set_window_bounds(width: width, height: height)
165
+ self.window_bounds = { window_state: "normal" }
166
+ self.window_bounds = { width: width, height: height }
149
167
  end
150
168
 
151
- command("Emulation.setDeviceMetricsOverride", slowmoable: true,
152
- width: width,
153
- height: height,
154
- deviceScaleFactor: 1,
155
- mobile: false,
156
- fitWindow: false)
169
+ set_viewport(width: width, height: height)
157
170
  end
158
171
 
159
172
  #
160
- # The current position of the browser window.
173
+ # Disables JavaScript execution from the HTML source for the page.
174
+ #
175
+ # This doesn't prevent users evaluate JavaScript with Ferrum.
176
+ #
177
+ def disable_javascript
178
+ command("Emulation.setScriptExecutionDisabled", value: true)
179
+ end
180
+
181
+ #
182
+ # The current position of the window.
161
183
  #
162
184
  # @return [(Integer, Integer)]
163
- # The left, top coordinates of the browser window.
185
+ # The left, top coordinates of the window.
164
186
  #
165
187
  # @example
166
- # browser.position # => [10, 20]
188
+ # page.position # => [10, 20]
167
189
  #
168
190
  def position
169
- @browser.command("Browser.getWindowBounds", windowId: window_id).fetch("bounds").values_at("left", "top")
191
+ window_bounds.values_at("left", "top")
170
192
  end
171
193
 
172
194
  #
173
- # Sets the position of the browser window.
195
+ # Sets the position of the window.
174
196
  #
175
197
  # @param [Hash{Symbol => Object}] options
176
198
  #
@@ -181,20 +203,72 @@ module Ferrum
181
203
  # The number of pixels from the top of the screen.
182
204
  #
183
205
  # @example
184
- # browser.position = { left: 10, top: 20 }
206
+ # page.position = { left: 10, top: 20 }
185
207
  #
186
208
  def position=(options)
187
- @browser.command("Browser.setWindowBounds",
188
- windowId: window_id,
189
- bounds: { left: options[:left], top: options[:top] })
209
+ self.window_bounds = { left: options[:left], top: options[:top] }
210
+ end
211
+
212
+ # Sets the position of the window.
213
+ #
214
+ # @param [Hash{Symbol => Object}] bounds
215
+ #
216
+ # @option options [Integer] :left
217
+ # The number of pixels from the left-hand side of the screen.
218
+ #
219
+ # @option options [Integer] :top
220
+ # The number of pixels from the top of the screen.
221
+ #
222
+ # @option options [Integer] :width
223
+ # The window width in pixels.
224
+ #
225
+ # @option options [Integer] :height
226
+ # The window height in pixels.
227
+ #
228
+ # @option options [String] :window_state
229
+ # The window state. Default to normal. Allowed Values: normal, minimized, maximized, fullscreen
230
+ #
231
+ # @example
232
+ # page.window_bounds = { left: 10, top: 20, width: 1024, height: 768, window_state: "normal" }
233
+ #
234
+ def window_bounds=(bounds)
235
+ options = bounds.dup
236
+ window_state = options.delete(:window_state)
237
+ bounds = { windowState: window_state, **options }.compact
238
+
239
+ client.command("Browser.setWindowBounds", windowId: window_id, bounds: bounds)
240
+ end
241
+
242
+ #
243
+ # Current window bounds.
244
+ #
245
+ # @return [Hash{String => (Integer, String)}]
246
+ #
247
+ # @example
248
+ # page.window_bounds # => { "left": 0, "top": 1286, "width": 10, "height": 10, "windowState": "normal" }
249
+ #
250
+ def window_bounds
251
+ client.command("Browser.getWindowBounds", windowId: window_id).fetch("bounds")
252
+ end
253
+
254
+ #
255
+ # Current window id.
256
+ #
257
+ # @return [Integer]
258
+ #
259
+ # @example
260
+ # page.window_id # => 1
261
+ #
262
+ def window_id
263
+ client.command("Browser.getWindowForTarget", targetId: target_id)["windowId"]
190
264
  end
191
265
 
192
266
  #
193
267
  # Reloads the current page.
194
268
  #
195
269
  # @example
196
- # browser.go_to("https://github.com/")
197
- # browser.refresh
270
+ # page.go_to("https://github.com/")
271
+ # page.refresh
198
272
  #
199
273
  def refresh
200
274
  command("Page.reload", wait: timeout, slowmoable: true)
@@ -205,41 +279,41 @@ module Ferrum
205
279
  # Stop all navigations and loading pending resources on the page.
206
280
  #
207
281
  # @example
208
- # browser.go_to("https://github.com/")
209
- # browser.stop
282
+ # page.go_to("https://github.com/")
283
+ # page.stop
210
284
  #
211
285
  def stop
212
286
  command("Page.stopLoading", slowmoable: true)
213
287
  end
214
288
 
215
289
  #
216
- # Navigates to the previous URL in the browser's history.
290
+ # Navigates to the previous URL in the history.
217
291
  #
218
292
  # @example
219
- # browser.go_to("https://github.com/")
220
- # browser.at_xpath("//a").click
221
- # browser.back
293
+ # page.go_to("https://github.com/")
294
+ # page.at_xpath("//a").click
295
+ # page.back
222
296
  #
223
297
  def back
224
298
  history_navigate(delta: -1)
225
299
  end
226
300
 
227
301
  #
228
- # Navigates to the next URL in the browser's history.
302
+ # Navigates to the next URL in the history.
229
303
  #
230
304
  # @example
231
- # browser.go_to("https://github.com/")
232
- # browser.at_xpath("//a").click
233
- # browser.back
234
- # browser.forward
305
+ # page.go_to("https://github.com/")
306
+ # page.at_xpath("//a").click
307
+ # page.back
308
+ # page.forward
235
309
  #
236
310
  def forward
237
311
  history_navigate(delta: 1)
238
312
  end
239
313
 
240
- def wait_for_reload(sec = 1)
314
+ def wait_for_reload(timeout = 1)
241
315
  @event.reset if @event.set?
242
- @event.wait(sec)
316
+ @event.wait(timeout)
243
317
  @event.set
244
318
  end
245
319
 
@@ -251,29 +325,21 @@ module Ferrum
251
325
  # @return [Boolean]
252
326
  #
253
327
  # @example
254
- # browser.bypass_csp # => true
255
- # browser.go_to("https://github.com/ruby-concurrency/concurrent-ruby/blob/master/docs-source/promises.in.md")
256
- # browser.refresh
257
- # browser.add_script_tag(content: "window.__injected = 42")
258
- # browser.evaluate("window.__injected") # => 42
328
+ # page.bypass_csp # => true
329
+ # page.go_to("https://github.com/ruby-concurrency/concurrent-ruby/blob/master/docs-source/promises.in.md")
330
+ # page.refresh
331
+ # page.add_script_tag(content: "window.__injected = 42")
332
+ # page.evaluate("window.__injected") # => 42
259
333
  #
260
334
  def bypass_csp(enabled: true)
261
335
  command("Page.setBypassCSP", enabled: enabled)
262
336
  enabled
263
337
  end
264
338
 
265
- def window_id
266
- @browser.command("Browser.getWindowForTarget", targetId: @target_id)["windowId"]
267
- end
268
-
269
- def set_window_bounds(bounds = {})
270
- @browser.command("Browser.setWindowBounds", windowId: window_id, bounds: bounds)
271
- end
272
-
273
339
  def command(method, wait: 0, slowmoable: false, **params)
274
340
  iteration = @event.reset if wait.positive?
275
- sleep(@browser.options.slowmo) if slowmoable && @browser.options.slowmo.positive?
276
- result = @client.command(method, params)
341
+ sleep(@options.slowmo) if slowmoable && @options.slowmo.positive?
342
+ result = client.command(method, **params)
277
343
 
278
344
  if wait.positive?
279
345
  # Wait a bit after command and check if iteration has
@@ -291,30 +357,30 @@ module Ferrum
291
357
  def on(name, &block)
292
358
  case name
293
359
  when :dialog
294
- @client.on("Page.javascriptDialogOpening") do |params, index, total|
360
+ client.on("Page.javascriptDialogOpening") do |params, index, total|
295
361
  dialog = Dialog.new(self, params)
296
362
  block.call(dialog, index, total)
297
363
  end
298
364
  when :request
299
- @client.on("Fetch.requestPaused") do |params, index, total|
300
- request = Network::InterceptedRequest.new(self, params)
365
+ client.on("Fetch.requestPaused") do |params, index, total|
366
+ request = Network::InterceptedRequest.new(client, params)
301
367
  exchange = network.select(request.network_id).last
302
368
  exchange ||= network.build_exchange(request.network_id)
303
369
  exchange.intercepted_request = request
304
370
  block.call(request, index, total)
305
371
  end
306
372
  when :auth
307
- @client.on("Fetch.authRequired") do |params, index, total|
373
+ client.on("Fetch.authRequired") do |params, index, total|
308
374
  request = Network::AuthRequest.new(self, params)
309
375
  block.call(request, index, total)
310
376
  end
311
377
  else
312
- @client.on(name, &block)
378
+ client.on(name, &block)
313
379
  end
314
380
  end
315
381
 
316
382
  def subscribed?(event)
317
- @client.subscribed?(event)
383
+ client.subscribed?(event)
318
384
  end
319
385
 
320
386
  def use_proxy?
@@ -325,19 +391,24 @@ module Ferrum
325
391
  use_proxy? && @proxy_user && @proxy_password
326
392
  end
327
393
 
394
+ def document_node_id
395
+ command("DOM.getDocument", depth: 0).dig("root", "nodeId")
396
+ end
397
+
328
398
  private
329
399
 
330
400
  def subscribe
331
401
  frames_subscribe
332
402
  network.subscribe
403
+ downloads.subscribe
333
404
 
334
- if @browser.options.logger
405
+ if @options.logger
335
406
  on("Runtime.consoleAPICalled") do |params|
336
- params["args"].each { |r| @browser.options.logger.puts(r["value"]) }
407
+ params["args"].each { |r| @options.logger.puts(r["value"]) }
337
408
  end
338
409
  end
339
410
 
340
- if @browser.options.js_errors
411
+ if @options.js_errors
341
412
  on("Runtime.exceptionThrown") do |params|
342
413
  # FIXME: https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
343
414
  Thread.main.raise JavaScriptError.new(
@@ -350,7 +421,7 @@ module Ferrum
350
421
  on(:dialog) do |dialog, _index, total|
351
422
  if total == 1
352
423
  warn "Dialog was shown but you didn't provide `on(:dialog)` callback, accepting it by default. " \
353
- "Please take a look at https://github.com/rubycdp/ferrum#dialog"
424
+ "Please take a look at https://github.com/rubycdp/ferrum#dialogs"
354
425
  dialog.accept
355
426
  end
356
427
  end
@@ -372,28 +443,17 @@ module Ferrum
372
443
  end
373
444
  end
374
445
 
375
- if @browser.options.save_path
376
- unless Pathname.new(@browser.options.save_path).absolute?
377
- raise Error, "supply absolute path for `:save_path` option"
378
- end
379
-
380
- @browser.command("Browser.setDownloadBehavior",
381
- browserContextId: context.id,
382
- downloadPath: @browser.options.save_path,
383
- behavior: "allow", eventsEnabled: true)
384
- end
446
+ downloads.set_behavior(save_path: @options.save_path) if @options.save_path
385
447
 
386
- @browser.extensions.each do |extension|
448
+ @options.extensions.each do |extension|
387
449
  command("Page.addScriptToEvaluateOnNewDocument", source: extension)
388
450
  end
389
451
 
390
452
  inject_extensions
391
453
 
392
- width, height = @browser.window_size
393
- resize(width: width, height: height)
394
-
395
454
  response = command("Page.getNavigationHistory")
396
- return unless response.dig("entries", 0, "transitionType") != "typed"
455
+ transition_type = response.dig("entries", 0, "transitionType")
456
+ return if transition_type == "auto_toplevel"
397
457
 
398
458
  # If we create page by clicking links, submitting forms and so on it
399
459
  # opens a new window for which `frameStoppedLoading` event never
@@ -404,7 +464,7 @@ module Ferrum
404
464
  end
405
465
 
406
466
  def inject_extensions
407
- @browser.extensions.each do |extension|
467
+ @options.extensions.each do |extension|
408
468
  # https://github.com/GoogleChrome/puppeteer/issues/1443
409
469
  # https://github.com/ChromeDevTools/devtools-protocol/issues/77
410
470
  # https://github.com/cyrus-and/chrome-remote-interface/issues/319
@@ -434,26 +494,18 @@ module Ferrum
434
494
  url = Addressable::URI.parse(url_or_path)
435
495
  nil_or_relative = url.nil? || url.relative?
436
496
 
437
- if nil_or_relative && !@browser.base_url
497
+ if nil_or_relative && !@options.base_url
438
498
  raise "Set :base_url browser's option or use absolute url in `go_to`, you passed: #{url_or_path}"
439
499
  end
440
500
 
441
- (nil_or_relative ? @browser.base_url.join(url.to_s) : url).to_s
442
- end
443
-
444
- def document_node_id
445
- command("DOM.getDocument", depth: 0).dig("root", "nodeId")
446
- end
447
-
448
- def ws_url
449
- "ws://#{@browser.process.host}:#{@browser.process.port}/devtools/page/#{@target_id}"
501
+ (nil_or_relative ? @options.base_url.join(url.to_s) : url).to_s
450
502
  end
451
503
 
452
504
  def proxy=(options)
453
- @proxy_host = options&.[](:host) || @browser.options.proxy&.[](:host)
454
- @proxy_port = options&.[](:port) || @browser.options.proxy&.[](:port)
455
- @proxy_user = options&.[](:user) || @browser.options.proxy&.[](:user)
456
- @proxy_password = options&.[](:password) || @browser.options.proxy&.[](:password)
505
+ @proxy_host = options&.[](:host) || @options.proxy&.[](:host)
506
+ @proxy_port = options&.[](:port) || @options.proxy&.[](:port)
507
+ @proxy_user = options&.[](:user) || @options.proxy&.[](:user)
508
+ @proxy_password = options&.[](:password) || @options.proxy&.[](:password)
457
509
  end
458
510
  end
459
511
  end
data/lib/ferrum/proxy.rb CHANGED
@@ -52,7 +52,7 @@ module Ferrum
52
52
  end
53
53
 
54
54
  def stop
55
- @file&.unlink
55
+ @file&.close(true)
56
56
  @server.shutdown
57
57
  end
58
58
 
data/lib/ferrum/target.rb CHANGED
@@ -8,17 +8,21 @@ module Ferrum
8
8
  # where we enhance page class and build page ourselves.
9
9
  attr_writer :page
10
10
 
11
- def initialize(browser, params = nil)
11
+ attr_reader :session_id, :options
12
+
13
+ def initialize(browser_client, session_id = nil, params = nil)
12
14
  @page = nil
13
- @browser = browser
15
+ @session_id = session_id
14
16
  @params = params
17
+ @browser_client = browser_client
18
+ @options = browser_client.options
15
19
  end
16
20
 
17
21
  def update(params)
18
- @params = params
22
+ @params.merge!(params)
19
23
  end
20
24
 
21
- def attached?
25
+ def connected?
22
26
  !!@page
23
27
  end
24
28
 
@@ -26,9 +30,13 @@ module Ferrum
26
30
  @page ||= build_page
27
31
  end
28
32
 
33
+ def client
34
+ @client ||= build_client
35
+ end
36
+
29
37
  def build_page(**options)
30
38
  maybe_sleep_if_new_window
31
- Page.new(id, @browser, **options)
39
+ Page.new(client, context_id: context_id, target_id: id, **options)
32
40
  end
33
41
 
34
42
  def id
@@ -63,5 +71,17 @@ module Ferrum
63
71
  # Dirty hack because new window doesn't have events at all
64
72
  sleep(NEW_WINDOW_WAIT) if window?
65
73
  end
74
+
75
+ private
76
+
77
+ def build_client
78
+ return @browser_client.session(session_id) if options.flatten
79
+
80
+ Client.new(ws_url, options)
81
+ end
82
+
83
+ def ws_url
84
+ @browser_client.ws_url.merge(path: "/devtools/page/#{id}")
85
+ end
66
86
  end
67
87
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "concurrent-ruby"
4
-
5
3
  module Ferrum
6
4
  module Utils
7
5
  module ElapsedTime
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ferrum
4
+ module Utils
5
+ class Event < Concurrent::Event
6
+ def iteration
7
+ synchronize { @iteration }
8
+ end
9
+
10
+ def reset
11
+ synchronize do
12
+ @iteration += 1
13
+ @set = false if @set
14
+ @iteration
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -20,6 +20,10 @@ module Ferrum
20
20
  RbConfig::CONFIG["host_os"] =~ /darwin/
21
21
  end
22
22
 
23
+ def mac_arm?
24
+ mac? && RbConfig::CONFIG["host_cpu"] =~ /arm/
25
+ end
26
+
23
27
  def mri?
24
28
  defined?(RUBY_ENGINE) && RUBY_ENGINE == "ruby"
25
29
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ferrum
4
+ module Utils
5
+ module Thread
6
+ module_function
7
+
8
+ def spawn(abort_on_exception: true)
9
+ ::Thread.new(abort_on_exception) do |whether_abort_on_exception|
10
+ ::Thread.current.abort_on_exception = whether_abort_on_exception
11
+ ::Thread.current.report_on_exception = true if ::Thread.current.respond_to?(:report_on_exception=)
12
+
13
+ yield
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ferrum
4
- VERSION = "0.13"
4
+ VERSION = "0.15"
5
5
  end