ferrum 0.13 → 0.15

Sign up to get free protection for your applications and to get access to all the features.
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