ferrum 0.17 → 0.17.2

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.
@@ -26,11 +26,11 @@ module Ferrum
26
26
  end
27
27
 
28
28
  def except(*keys)
29
- to_h.reject { |n, _| keys.include?(n) }
29
+ to_h.except(*keys)
30
30
  end
31
31
 
32
32
  def detect_path
33
- Binary.find(self.class::PLATFORM_PATH[Utils::Platform.name])
33
+ Binary.find(self.class::PLATFORM_PATH[Utils::Platform.platform_name])
34
34
  end
35
35
 
36
36
  def merge_required(flags, options, user_data_dir)
@@ -5,44 +5,63 @@ module Ferrum
5
5
  class Options
6
6
  class Chrome < Base
7
7
  DEFAULT_OPTIONS = {
8
- "headless" => nil,
9
- "hide-scrollbars" => nil,
10
- "mute-audio" => nil,
11
- "enable-automation" => nil,
12
- "disable-web-security" => nil,
13
- "disable-session-crashed-bubble" => nil,
14
- "disable-breakpad" => nil,
15
- "disable-sync" => nil,
16
- "no-first-run" => nil,
17
- "use-mock-keychain" => nil,
18
- "keep-alive-for-test" => nil,
19
- "disable-popup-blocking" => nil,
20
- "disable-extensions" => nil,
21
- "disable-component-extensions-with-background-pages" => nil,
22
- "disable-hang-monitor" => nil,
23
- "disable-features" => "site-per-process,IsolateOrigins,TranslateUI",
24
- "disable-translate" => nil,
8
+ "allow-pre-commit-input" => nil,
25
9
  "disable-background-networking" => nil,
26
- "enable-features" => "NetworkService,NetworkServiceInProcess",
27
10
  "disable-background-timer-throttling" => nil,
28
11
  "disable-backgrounding-occluded-windows" => nil,
12
+ "disable-blink-features" => "AutomationControlled",
13
+ "disable-breakpad" => nil,
29
14
  "disable-client-side-phishing-detection" => nil,
15
+ "disable-component-extensions-with-background-pages" => nil,
16
+ "disable-component-update" => nil,
17
+ "disable-crash-reporter" => nil,
30
18
  "disable-default-apps" => nil,
31
19
  "disable-dev-shm-usage" => nil,
20
+ "disable-extensions" => nil,
21
+ "disable-features" => %w[
22
+ site-per-process
23
+ IsolateOrigins
24
+ TranslateUI
25
+ Translate
26
+ MacAppCodeSignClone
27
+ InterestFeedContentSuggestion
28
+ OptimizationHints
29
+ AcceptCHFrame
30
+ MediaRouter
31
+ ].join(","),
32
+ "disable-field-trial-config" => nil,
33
+ "disable-hang-monitor" => nil,
34
+ "disable-infobars" => nil,
32
35
  "disable-ipc-flooding-protection" => nil,
36
+ "disable-popup-blocking" => nil,
33
37
  "disable-prompt-on-repost" => nil,
34
38
  "disable-renderer-backgrounding" => nil,
39
+ "disable-search-engine-choice-screen" => nil,
40
+ "disable-session-crashed-bubble" => nil,
35
41
  "disable-site-isolation-trials" => nil,
42
+ "disable-smooth-scrolling" => nil,
43
+ "disable-sync" => nil,
44
+ "disable-translate" => nil,
45
+ "disable-web-security" => nil,
46
+ "enable-automation" => nil,
47
+ "enable-features" => %w[
48
+ NetworkService
49
+ NetworkServiceInProcess
50
+ ].join(","),
36
51
  "force-color-profile" => "srgb",
52
+ "headless" => nil,
53
+ "hide-scrollbars" => nil,
54
+ "keep-alive-for-test" => nil,
37
55
  "metrics-recording-only" => nil,
38
- "safebrowsing-disable-auto-update" => nil,
39
- "password-store" => "basic",
56
+ "mute-audio" => nil,
57
+ "no-crash-upload" => nil,
58
+ "no-default-browser-check" => nil,
59
+ "no-first-run" => nil,
40
60
  "no-startup-window" => nil,
61
+ "password-store" => "basic",
41
62
  "remote-allow-origins" => "*",
42
- "disable-blink-features" => "AutomationControlled"
43
- # NOTE: --no-sandbox is not needed if you properly set up a user in the container.
44
- # https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile#L35-L40
45
- # "no-sandbox" => nil,
63
+ "safebrowsing-disable-auto-update" => nil,
64
+ "use-mock-keychain" => nil
46
65
  }.freeze
47
66
 
48
67
  MAC_BIN_PATH = [
@@ -76,15 +95,23 @@ module Ferrum
76
95
  end
77
96
 
78
97
  def merge_default(flags, options)
79
- defaults = except("headless", "disable-gpu") if options.headless == false
80
- defaults ||= DEFAULT_OPTIONS
98
+ defaults = options.headless == false ? except("headless", "disable-gpu") : DEFAULT_OPTIONS
81
99
  defaults.delete("no-startup-window") if options.incognito == false
100
+
101
+ if options.dockerize || ENV["FERRUM_CHROME_DOCKERIZE"] == "true"
102
+ # NOTE: --no-sandbox is not needed if you properly set up a user in the container.
103
+ # https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile#L35-L40
104
+ defaults = defaults.merge("no-sandbox" => nil, "disable-setuid-sandbox" => nil)
105
+ end
106
+
82
107
  # On Windows, the --disable-gpu flag is a temporary workaround for a few bugs.
83
108
  # See https://bugs.chromium.org/p/chromium/issues/detail?id=737678 for more information.
84
109
  defaults = defaults.merge("disable-gpu" => nil) if Utils::Platform.windows?
110
+
85
111
  # Use Metal on Apple Silicon
86
112
  # https://github.com/google/angle#platform-support-via-backing-renderers
87
113
  defaults = defaults.merge("use-angle" => "metal") if Utils::Platform.mac_arm?
114
+
88
115
  defaults.merge(flags)
89
116
  end
90
117
  end
@@ -14,7 +14,7 @@ module Ferrum
14
14
  attr_reader :window_size, :logger, :ws_max_receive_size,
15
15
  :js_errors, :base_url, :slowmo, :pending_connection_errors,
16
16
  :url, :ws_url, :env, :process_timeout, :browser_name, :browser_path,
17
- :save_path, :proxy, :port, :host, :headless, :incognito, :browser_options,
17
+ :save_path, :proxy, :port, :host, :headless, :incognito, :dockerize, :browser_options,
18
18
  :ignore_default_browser_options, :xvfb, :flatten
19
19
  attr_accessor :timeout, :default_user_agent
20
20
 
@@ -28,6 +28,7 @@ module Ferrum
28
28
  @js_errors = @options.fetch(:js_errors, false)
29
29
  @headless = @options.fetch(:headless, true)
30
30
  @incognito = @options.fetch(:incognito, true)
31
+ @dockerize = @options.fetch(:dockerize, false)
31
32
  @flatten = @options.fetch(:flatten, true)
32
33
  @pending_connection_errors = @options.fetch(:pending_connection_errors, true)
33
34
  @process_timeout = @options.fetch(:process_timeout, PROCESS_TIMEOUT)
@@ -18,11 +18,8 @@ module Ferrum
18
18
  KILL_TIMEOUT = 2
19
19
  WAIT_KILLED = 0.05
20
20
 
21
- attr_reader :host, :port, :ws_url, :pid, :command,
22
- :default_user_agent, :browser_version, :protocol_version,
23
- :v8_version, :webkit_version, :xvfb
24
-
25
21
  extend Forwardable
22
+
26
23
  delegate path: :command
27
24
 
28
25
  def self.start(*args)
@@ -61,6 +58,10 @@ module Ferrum
61
58
  }
62
59
  end
63
60
 
61
+ attr_reader :host, :port, :ws_url, :pid, :command,
62
+ :default_user_agent, :browser_version, :protocol_version,
63
+ :v8_version, :webkit_version, :xvfb
64
+
64
65
  def initialize(options)
65
66
  @pid = @xvfb = @user_data_dir = nil
66
67
 
@@ -15,6 +15,7 @@ require "ferrum/browser/version_info"
15
15
  module Ferrum
16
16
  class Browser
17
17
  extend Forwardable
18
+
18
19
  delegate %i[default_context] => :contexts
19
20
  delegate %i[targets create_target page pages windows] => :default_context
20
21
  delegate %i[go_to goto go back forward refresh reload stop wait_for_reload
@@ -48,6 +49,9 @@ module Ferrum
48
49
  # @option options [Boolean] :incognito (true)
49
50
  # Create an incognito profile for the browser startup window.
50
51
  #
52
+ # @option options [Boolean] :dockerize (false)
53
+ # Add CLI flags to a browser to run in a container.
54
+ #
51
55
  # @option options [Boolean] :xvfb (false)
52
56
  # Run browser in a virtual framebuffer.
53
57
  #
@@ -77,7 +81,7 @@ module Ferrum
77
81
  # @option options [Boolean] :js_errors
78
82
  # When true, JavaScript errors get re-raised in Ruby.
79
83
  #
80
- # @option options [Boolean] :pending_connection_errors (true)
84
+ # @option options [Boolean] :pending_connection_errors (false)
81
85
  # When main frame is still waiting for slow responses while timeout is
82
86
  # reached {PendingConnectionsError} is raised. It's better to figure out
83
87
  # why you have slow responses and fix or block them rather than turn this
@@ -295,6 +299,8 @@ module Ferrum
295
299
  end
296
300
 
297
301
  def build_remote_debug_url(path:)
302
+ return path if Addressable::URI.parse(path).absolute?
303
+
298
304
  "http://#{process.host}:#{process.port}#{path}"
299
305
  end
300
306
  end
@@ -19,7 +19,7 @@ module Ferrum
19
19
  uri = URI.parse(@url)
20
20
  port = uri.port || DEFAULT_PORTS[uri.scheme]
21
21
 
22
- if port == 443
22
+ if port == 443 || url.scheme == "wss"
23
23
  tcp = TCPSocket.new(uri.host, port)
24
24
  ssl_context = OpenSSL::SSL::SSLContext.new
25
25
  @sock = OpenSSL::SSL::SSLSocket.new(tcp, ssl_context)
data/lib/ferrum/client.rb CHANGED
@@ -24,8 +24,8 @@ module Ferrum
24
24
  @client.send_message(message, async: async)
25
25
  end
26
26
 
27
- def on(event, &block)
28
- @client.on(event_name(event), &block)
27
+ def on(event, &)
28
+ @client.on(event_name(event), &)
29
29
  end
30
30
 
31
31
  def off(event, id)
@@ -40,8 +40,8 @@ module Ferrum
40
40
  @client.respond_to?(name, include_private)
41
41
  end
42
42
 
43
- def method_missing(name, *args, **opts, &block)
44
- @client.send(name, *args, **opts, &block)
43
+ def method_missing(name, ...)
44
+ @client.send(name, ...)
45
45
  end
46
46
 
47
47
  def close
@@ -61,6 +61,7 @@ module Ferrum
61
61
 
62
62
  class Client
63
63
  extend Forwardable
64
+
64
65
  delegate %i[timeout timeout=] => :options
65
66
 
66
67
  attr_reader :ws_url, :options, :subscriber
@@ -101,8 +102,8 @@ module Ferrum
101
102
  end
102
103
  end
103
104
 
104
- def on(event, &block)
105
- @subscriber.on(event, &block)
105
+ def on(event, &)
106
+ @subscriber.on(event, &)
106
107
  end
107
108
 
108
109
  def off(event, id)
@@ -61,6 +61,8 @@ module Ferrum
61
61
  new_target = Target.new(@client, session_id, params)
62
62
  # `put_if_absent` returns nil if added a new value or existing if there was one already
63
63
  target = @targets.put_if_absent(new_target.id, new_target) || new_target
64
+ # on first iteration session_id may be nil, then if session is present here we must set it to the target
65
+ target.session_id = session_id if session_id && target.session_id.nil?
64
66
  @default_target ||= target
65
67
 
66
68
  new_pending = Concurrent::IVar.new
@@ -22,10 +22,10 @@ module Ferrum
22
22
  @default_context ||= create
23
23
  end
24
24
 
25
- def each(&block)
25
+ def each(&)
26
26
  return enum_for(__method__) unless block_given?
27
27
 
28
- @contexts.each(&block)
28
+ @contexts.each(&)
29
29
  end
30
30
 
31
31
  def [](id)
@@ -48,6 +48,8 @@ module Ferrum
48
48
 
49
49
  def dispose(context_id)
50
50
  context = @contexts[context_id]
51
+ return unless context
52
+
51
53
  context.close_targets_connection
52
54
  @client.command("Target.disposeBrowserContext", browserContextId: context.id)
53
55
  @contexts.delete(context_id)
@@ -59,8 +61,9 @@ module Ferrum
59
61
  end
60
62
 
61
63
  def reset
62
- @default_context = nil
63
- @contexts.each_key { |id| dispose(id) }
64
+ context_ids = @client.command("Target.getBrowserContexts")["browserContextIds"]
65
+ @default_context = nil if context_ids.include?(@default_context&.id)
66
+ @contexts.each_key { |id| dispose(id) if context_ids.include?(id) }
64
67
  end
65
68
 
66
69
  def size
@@ -69,18 +72,13 @@ module Ferrum
69
72
 
70
73
  private
71
74
 
72
- # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
73
- def subscribe
75
+ def subscribe # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
74
76
  @client.on("Target.attachedToTarget") do |params|
75
77
  info, session_id = params.values_at("targetInfo", "sessionId")
76
78
  next unless ALLOWED_TARGET_TYPES.include?(info["type"])
77
79
 
78
80
  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
81
+ add_context(context_id)
84
82
 
85
83
  @contexts[context_id]&.add_target(session_id: session_id, params: info)
86
84
  if params["waitingForDebugger"]
@@ -93,9 +91,10 @@ module Ferrum
93
91
  next unless ALLOWED_TARGET_TYPES.include?(info["type"])
94
92
 
95
93
  context_id = info["browserContextId"]
94
+ add_context(context_id)
96
95
 
97
96
  if info["type"] == "iframe" &&
98
- (target = @contexts[context_id].find_target { |t| t.connected? && t.page.frame_by(id: info["targetId"]) })
97
+ (target = @contexts[context_id]&.find_target { |t| t.connected? && t.page.frame_by(id: info["targetId"]) })
99
98
  @contexts[context_id]&.add_target(session_id: target.page.client.session_id, params: info)
100
99
  else
101
100
  @contexts[context_id]&.add_target(params: info)
@@ -120,16 +119,21 @@ module Ferrum
120
119
  context&.delete_target(params["targetId"])
121
120
  end
122
121
  end
123
- # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
124
122
 
125
123
  def discover
126
124
  @client.command("Target.setDiscoverTargets", discover: true)
127
125
  end
128
126
 
129
127
  def auto_attach
130
- return unless @client.options.flatten
131
-
132
128
  @client.command("Target.setAutoAttach", autoAttach: true, waitForDebuggerOnStart: true, flatten: true)
133
129
  end
130
+
131
+ def add_context(context_id)
132
+ return if @contexts[context_id]
133
+
134
+ context = Context.new(@client, self, context_id)
135
+ @contexts[context_id] = context
136
+ @default_context ||= context # rubocop:disable Naming/MemoizedInstanceVariableName
137
+ end
134
138
  end
135
139
  end
@@ -161,7 +161,7 @@ module Ferrum
161
161
  # The raw cookie string.
162
162
  #
163
163
  def to_s
164
- string = String.new("#{@attributes['name']}=#{@attributes['value']}")
164
+ string = "#{@attributes['name']}=#{@attributes['value']}"
165
165
 
166
166
  @attributes.each do |key, value|
167
167
  case key
data/lib/ferrum/errors.rb CHANGED
@@ -113,9 +113,10 @@ module Ferrum
113
113
  class JavaScriptError < BrowserError
114
114
  attr_reader :class_name, :message, :stack_trace
115
115
 
116
- def initialize(response, stack_trace = nil)
117
- @class_name, @message = response.values_at("className", "description")
118
- @stack_trace = stack_trace
116
+ def initialize(response)
117
+ @class_name, @message = response["exception"]&.values_at("className", "description")
118
+ @message ||= response["text"]
119
+ @stack_trace = response["stackTrace"]
119
120
  super(response.merge("message" => @message))
120
121
  end
121
122
  end
@@ -135,7 +135,7 @@ module Ferrum
135
135
  when /\AError: timed out promise/
136
136
  raise ScriptTimeoutError
137
137
  else
138
- raise JavaScriptError.new(result, response.dig("exceptionDetails", "stackTrace"))
138
+ raise JavaScriptError, response["exceptionDetails"]
139
139
  end
140
140
  end
141
141
 
@@ -124,7 +124,9 @@ module Ferrum
124
124
  #
125
125
  # @return [String]
126
126
  #
127
- def body
127
+ # @raise [Ferrum::BrowserError]
128
+ #
129
+ def body!
128
130
  @body ||= begin
129
131
  body, encoded = @page.command("Network.getResponseBody", requestId: id)
130
132
  .values_at("body", "base64Encoded")
@@ -132,6 +134,17 @@ module Ferrum
132
134
  end
133
135
  end
134
136
 
137
+ #
138
+ # The response body.
139
+ #
140
+ # @return [String, nil]
141
+ #
142
+ def body
143
+ body!
144
+ rescue Ferrum::BrowserError
145
+ # nop
146
+ end
147
+
135
148
  #
136
149
  # @return [Boolean]
137
150
  #
@@ -17,7 +17,7 @@ module Ferrum
17
17
  SignedExchange Ping CSPViolationReport Preflight Other].freeze
18
18
  AUTHORIZE_BLOCK_MISSING = "Block is missing, call `authorize(...) { |r| r.continue } " \
19
19
  "or subscribe to `on(:request)` events before calling it"
20
- AUTHORIZE_TYPE_WRONG = ":type should be in #{AUTHORIZE_TYPE}"
20
+ AUTHORIZE_TYPE_WRONG = ":type should be in #{AUTHORIZE_TYPE}".freeze
21
21
  ALLOWED_CONNECTION_TYPE = %w[none cellular2g cellular3g cellular4g bluetooth ethernet wifi wimax other].freeze
22
22
 
23
23
  # Network traffic.
@@ -6,7 +6,7 @@ module Ferrum
6
6
  #
7
7
  # Returns playback rate for CSS animations, defaults to `1`.
8
8
  #
9
- # @return [Integer]
9
+ # @return [Number]
10
10
  #
11
11
  def playback_rate
12
12
  command("Animation.getPlaybackRate")["playbackRate"]
@@ -15,7 +15,7 @@ module Ferrum
15
15
  #
16
16
  # Sets playback rate of CSS animations.
17
17
  #
18
- # @param [Integer] value
18
+ # @param [Number] value
19
19
  #
20
20
  # @example
21
21
  # browser = Ferrum::Browser.new
@@ -266,11 +266,11 @@ module Ferrum
266
266
  def bounding_rect(selector)
267
267
  rect = evaluate_async(%(
268
268
  const rect = document
269
- .querySelector('#{selector}')
269
+ .querySelector(arguments[0])
270
270
  .getBoundingClientRect();
271
271
  const {x, y, width, height} = rect;
272
- arguments[0]([x, y, width, height])
273
- ), timeout)
272
+ arguments[1]([x, y, width, height])
273
+ ), timeout, selector)
274
274
 
275
275
  { x: rect[0], y: rect[1], width: rect[2], height: rect[3] }
276
276
  end
data/lib/ferrum/page.rb CHANGED
@@ -21,6 +21,7 @@ module Ferrum
21
21
  GOTO_WAIT = ENV.fetch("FERRUM_GOTO_WAIT", 0.1).to_f
22
22
 
23
23
  extend Forwardable
24
+
24
25
  delegate %i[at_css at_xpath css xpath
25
26
  current_url current_title url title body doctype content=
26
27
  execution_id execution_id! evaluate evaluate_on evaluate_async execute evaluate_func
@@ -442,10 +443,7 @@ module Ferrum
442
443
  if @options.js_errors
443
444
  on("Runtime.exceptionThrown") do |params|
444
445
  # FIXME: https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
445
- Thread.main.raise JavaScriptError.new(
446
- params.dig("exceptionDetails", "exception"),
447
- params.dig("exceptionDetails", "stackTrace")
448
- )
446
+ Thread.main.raise JavaScriptError, params["exceptionDetails"]
449
447
  end
450
448
  end
451
449
 
@@ -5,7 +5,7 @@ module Ferrum
5
5
  module Platform
6
6
  module_function
7
7
 
8
- def name
8
+ def platform_name
9
9
  return :mac if mac?
10
10
  return :windows if windows?
11
11
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ferrum
4
- VERSION = "0.17"
4
+ VERSION = "0.17.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ferrum
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.17'
4
+ version: 0.17.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Vorotilin
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-05-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: addressable
@@ -152,7 +151,6 @@ metadata:
152
151
  changelog_uri: https://github.com/rubycdp/ferrum/blob/main/CHANGELOG.md
153
152
  source_code_uri: https://github.com/rubycdp/ferrum
154
153
  rubygems_mfa_required: 'true'
155
- post_install_message:
156
154
  rdoc_options: []
157
155
  require_paths:
158
156
  - lib
@@ -160,15 +158,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
160
158
  requirements:
161
159
  - - ">="
162
160
  - !ruby/object:Gem::Version
163
- version: 2.7.0
161
+ version: '3.1'
164
162
  required_rubygems_version: !ruby/object:Gem::Requirement
165
163
  requirements:
166
164
  - - ">="
167
165
  - !ruby/object:Gem::Version
168
166
  version: '0'
169
167
  requirements: []
170
- rubygems_version: 3.5.22
171
- signing_key:
168
+ rubygems_version: 4.0.6
172
169
  specification_version: 4
173
170
  summary: Ruby headless Chrome driver
174
171
  test_files: []