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.
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,55 +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
- error_text = response["errorText"]
118
- raise StatusError.new(options[:url], "Request to #{options[:url]} failed (#{error_text})") if error_text
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})")
112
+ end
119
113
 
120
114
  response["frameId"]
121
115
  rescue TimeoutError
122
- if @browser.options.pending_connection_errors
116
+ if @options.pending_connection_errors
123
117
  pendings = network.traffic.select(&:pending?).map(&:url).compact
124
118
  raise PendingConnectionsError.new(options[:url], pendings) unless pendings.empty?
125
119
  end
@@ -129,41 +123,76 @@ module Ferrum
129
123
 
130
124
  def close
131
125
  @headers.clear
132
- @browser.command("Target.closeTarget", targetId: @target_id)
133
- @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
+ )
134
158
  end
135
159
 
136
160
  def resize(width: nil, height: nil, fullscreen: false)
137
161
  if fullscreen
138
162
  width, height = document_size
139
- set_window_bounds(windowState: "fullscreen")
163
+ self.window_bounds = { window_state: "fullscreen" }
140
164
  else
141
- set_window_bounds(windowState: "normal")
142
- set_window_bounds(width: width, height: height)
165
+ self.window_bounds = { window_state: "normal" }
166
+ self.window_bounds = { width: width, height: height }
143
167
  end
144
168
 
145
- command("Emulation.setDeviceMetricsOverride", slowmoable: true,
146
- width: width,
147
- height: height,
148
- deviceScaleFactor: 0,
149
- mobile: false)
169
+ set_viewport(width: width, height: height)
170
+ end
171
+
172
+ #
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)
150
179
  end
151
180
 
152
181
  #
153
- # The current position of the browser window.
182
+ # The current position of the window.
154
183
  #
155
184
  # @return [(Integer, Integer)]
156
- # The left, top coordinates of the browser window.
185
+ # The left, top coordinates of the window.
157
186
  #
158
187
  # @example
159
- # browser.position # => [10, 20]
188
+ # page.position # => [10, 20]
160
189
  #
161
190
  def position
162
- @browser.command("Browser.getWindowBounds", windowId: window_id).fetch("bounds").values_at("left", "top")
191
+ window_bounds.values_at("left", "top")
163
192
  end
164
193
 
165
194
  #
166
- # Sets the position of the browser window.
195
+ # Sets the position of the window.
167
196
  #
168
197
  # @param [Hash{Symbol => Object}] options
169
198
  #
@@ -174,20 +203,72 @@ module Ferrum
174
203
  # The number of pixels from the top of the screen.
175
204
  #
176
205
  # @example
177
- # browser.position = { left: 10, top: 20 }
206
+ # page.position = { left: 10, top: 20 }
178
207
  #
179
208
  def position=(options)
180
- @browser.command("Browser.setWindowBounds",
181
- windowId: window_id,
182
- 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"]
183
264
  end
184
265
 
185
266
  #
186
267
  # Reloads the current page.
187
268
  #
188
269
  # @example
189
- # browser.go_to("https://github.com/")
190
- # browser.refresh
270
+ # page.go_to("https://github.com/")
271
+ # page.refresh
191
272
  #
192
273
  def refresh
193
274
  command("Page.reload", wait: timeout, slowmoable: true)
@@ -198,41 +279,41 @@ module Ferrum
198
279
  # Stop all navigations and loading pending resources on the page.
199
280
  #
200
281
  # @example
201
- # browser.go_to("https://github.com/")
202
- # browser.stop
282
+ # page.go_to("https://github.com/")
283
+ # page.stop
203
284
  #
204
285
  def stop
205
286
  command("Page.stopLoading", slowmoable: true)
206
287
  end
207
288
 
208
289
  #
209
- # Navigates to the previous URL in the browser's history.
290
+ # Navigates to the previous URL in the history.
210
291
  #
211
292
  # @example
212
- # browser.go_to("https://github.com/")
213
- # browser.at_xpath("//a").click
214
- # browser.back
293
+ # page.go_to("https://github.com/")
294
+ # page.at_xpath("//a").click
295
+ # page.back
215
296
  #
216
297
  def back
217
298
  history_navigate(delta: -1)
218
299
  end
219
300
 
220
301
  #
221
- # Navigates to the next URL in the browser's history.
302
+ # Navigates to the next URL in the history.
222
303
  #
223
304
  # @example
224
- # browser.go_to("https://github.com/")
225
- # browser.at_xpath("//a").click
226
- # browser.back
227
- # browser.forward
305
+ # page.go_to("https://github.com/")
306
+ # page.at_xpath("//a").click
307
+ # page.back
308
+ # page.forward
228
309
  #
229
310
  def forward
230
311
  history_navigate(delta: 1)
231
312
  end
232
313
 
233
- def wait_for_reload(sec = 1)
314
+ def wait_for_reload(timeout = 1)
234
315
  @event.reset if @event.set?
235
- @event.wait(sec)
316
+ @event.wait(timeout)
236
317
  @event.set
237
318
  end
238
319
 
@@ -244,29 +325,35 @@ module Ferrum
244
325
  # @return [Boolean]
245
326
  #
246
327
  # @example
247
- # browser.bypass_csp # => true
248
- # browser.go_to("https://github.com/ruby-concurrency/concurrent-ruby/blob/master/docs-source/promises.in.md")
249
- # browser.refresh
250
- # browser.add_script_tag(content: "window.__injected = 42")
251
- # 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
252
333
  #
253
334
  def bypass_csp(enabled: true)
254
335
  command("Page.setBypassCSP", enabled: enabled)
255
336
  enabled
256
337
  end
257
338
 
258
- def window_id
259
- @browser.command("Browser.getWindowForTarget", targetId: @target_id)["windowId"]
260
- end
261
-
262
- def set_window_bounds(bounds = {})
263
- @browser.command("Browser.setWindowBounds", windowId: window_id, bounds: bounds)
339
+ #
340
+ # Activates (focuses) the target for the given page.
341
+ # When you have multiple tabs you work with, and you need to switch a given one.
342
+ #
343
+ # @return [Boolean]
344
+ #
345
+ # @example
346
+ # page.activate # => true
347
+ #
348
+ def activate
349
+ command("Target.activateTarget", targetId: target_id)
350
+ true
264
351
  end
265
352
 
266
353
  def command(method, wait: 0, slowmoable: false, **params)
267
354
  iteration = @event.reset if wait.positive?
268
- sleep(@browser.options.slowmo) if slowmoable && @browser.options.slowmo.positive?
269
- result = @client.command(method, params)
355
+ sleep(@options.slowmo) if slowmoable && @options.slowmo.positive?
356
+ result = client.command(method, **params)
270
357
 
271
358
  if wait.positive?
272
359
  # Wait a bit after command and check if iteration has
@@ -284,30 +371,30 @@ module Ferrum
284
371
  def on(name, &block)
285
372
  case name
286
373
  when :dialog
287
- @client.on("Page.javascriptDialogOpening") do |params, index, total|
374
+ client.on("Page.javascriptDialogOpening") do |params, index, total|
288
375
  dialog = Dialog.new(self, params)
289
376
  block.call(dialog, index, total)
290
377
  end
291
378
  when :request
292
- @client.on("Fetch.requestPaused") do |params, index, total|
293
- request = Network::InterceptedRequest.new(self, params)
379
+ client.on("Fetch.requestPaused") do |params, index, total|
380
+ request = Network::InterceptedRequest.new(client, params)
294
381
  exchange = network.select(request.network_id).last
295
382
  exchange ||= network.build_exchange(request.network_id)
296
383
  exchange.intercepted_request = request
297
384
  block.call(request, index, total)
298
385
  end
299
386
  when :auth
300
- @client.on("Fetch.authRequired") do |params, index, total|
387
+ client.on("Fetch.authRequired") do |params, index, total|
301
388
  request = Network::AuthRequest.new(self, params)
302
389
  block.call(request, index, total)
303
390
  end
304
391
  else
305
- @client.on(name, &block)
392
+ client.on(name, &block)
306
393
  end
307
394
  end
308
395
 
309
396
  def subscribed?(event)
310
- @client.subscribed?(event)
397
+ client.subscribed?(event)
311
398
  end
312
399
 
313
400
  def use_proxy?
@@ -318,7 +405,9 @@ module Ferrum
318
405
  use_proxy? && @proxy_user && @proxy_password
319
406
  end
320
407
 
321
- def document_node_id
408
+ def document_node_id(async: false)
409
+ return client.command("DOM.getDocument", async: true, depth: 0) if async
410
+
322
411
  command("DOM.getDocument", depth: 0).dig("root", "nodeId")
323
412
  end
324
413
 
@@ -327,14 +416,15 @@ module Ferrum
327
416
  def subscribe
328
417
  frames_subscribe
329
418
  network.subscribe
419
+ downloads.subscribe
330
420
 
331
- if @browser.options.logger
421
+ if @options.logger
332
422
  on("Runtime.consoleAPICalled") do |params|
333
- params["args"].each { |r| @browser.options.logger.puts(r["value"]) }
423
+ params["args"].each { |r| @options.logger.puts(r["value"]) }
334
424
  end
335
425
  end
336
426
 
337
- if @browser.options.js_errors
427
+ if @options.js_errors
338
428
  on("Runtime.exceptionThrown") do |params|
339
429
  # FIXME: https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
340
430
  Thread.main.raise JavaScriptError.new(
@@ -369,26 +459,14 @@ module Ferrum
369
459
  end
370
460
  end
371
461
 
372
- if @browser.options.save_path
373
- unless Pathname.new(@browser.options.save_path).absolute?
374
- raise Error, "supply absolute path for `:save_path` option"
375
- end
376
-
377
- @browser.command("Browser.setDownloadBehavior",
378
- browserContextId: context.id,
379
- downloadPath: @browser.options.save_path,
380
- behavior: "allow", eventsEnabled: true)
381
- end
462
+ downloads.set_behavior(save_path: @options.save_path) if @options.save_path
382
463
 
383
- @browser.extensions.each do |extension|
464
+ @options.extensions.each do |extension|
384
465
  command("Page.addScriptToEvaluateOnNewDocument", source: extension)
385
466
  end
386
467
 
387
468
  inject_extensions
388
469
 
389
- width, height = @browser.window_size
390
- resize(width: width, height: height)
391
-
392
470
  response = command("Page.getNavigationHistory")
393
471
  transition_type = response.dig("entries", 0, "transitionType")
394
472
  return if transition_type == "auto_toplevel"
@@ -402,7 +480,7 @@ module Ferrum
402
480
  end
403
481
 
404
482
  def inject_extensions
405
- @browser.extensions.each do |extension|
483
+ @options.extensions.each do |extension|
406
484
  # https://github.com/GoogleChrome/puppeteer/issues/1443
407
485
  # https://github.com/ChromeDevTools/devtools-protocol/issues/77
408
486
  # https://github.com/cyrus-and/chrome-remote-interface/issues/319
@@ -432,22 +510,18 @@ module Ferrum
432
510
  url = Addressable::URI.parse(url_or_path)
433
511
  nil_or_relative = url.nil? || url.relative?
434
512
 
435
- if nil_or_relative && !@browser.base_url
513
+ if nil_or_relative && !@options.base_url
436
514
  raise "Set :base_url browser's option or use absolute url in `go_to`, you passed: #{url_or_path}"
437
515
  end
438
516
 
439
- (nil_or_relative ? @browser.base_url.join(url.to_s) : url).to_s
440
- end
441
-
442
- def ws_url
443
- "ws://#{@browser.process.host}:#{@browser.process.port}/devtools/page/#{@target_id}"
517
+ (nil_or_relative ? @options.base_url.join(url.to_s) : url).to_s
444
518
  end
445
519
 
446
520
  def proxy=(options)
447
- @proxy_host = options&.[](:host) || @browser.options.proxy&.[](:host)
448
- @proxy_port = options&.[](:port) || @browser.options.proxy&.[](:port)
449
- @proxy_user = options&.[](:user) || @browser.options.proxy&.[](:user)
450
- @proxy_password = options&.[](:password) || @browser.options.proxy&.[](:password)
521
+ @proxy_host = options&.[](:host) || @options.proxy&.[](:host)
522
+ @proxy_port = options&.[](:port) || @options.proxy&.[](:port)
523
+ @proxy_user = options&.[](:user) || @options.proxy&.[](:user)
524
+ @proxy_password = options&.[](:password) || @options.proxy&.[](:password)
451
525
  end
452
526
  end
453
527
  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.14"
4
+ VERSION = "0.16"
5
5
  end
data/lib/ferrum.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent-ruby"
4
+ require "ferrum/utils/event"
5
+ require "ferrum/utils/thread"
3
6
  require "ferrum/utils/platform"
4
7
  require "ferrum/utils/elapsed_time"
5
8
  require "ferrum/utils/attempt"