ferrum 0.17.2 → 0.19.0

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/lib/ferrum/accessibility/ax_node.rb +108 -0
  4. data/lib/ferrum/accessibility.rb +106 -0
  5. data/lib/ferrum/browser/binary.rb +42 -2
  6. data/lib/ferrum/browser/command.rb +20 -2
  7. data/lib/ferrum/browser/options/base.rb +66 -0
  8. data/lib/ferrum/browser/options/chrome.rb +35 -2
  9. data/lib/ferrum/browser/options.rb +41 -2
  10. data/lib/ferrum/browser/process/killer.rb +188 -0
  11. data/lib/ferrum/browser/process.rb +128 -72
  12. data/lib/ferrum/browser/xvfb.rb +24 -0
  13. data/lib/ferrum/browser.rb +47 -17
  14. data/lib/ferrum/client/subscriber.rb +65 -2
  15. data/lib/ferrum/client/web_socket.rb +126 -13
  16. data/lib/ferrum/client.rb +225 -12
  17. data/lib/ferrum/context.rb +113 -7
  18. data/lib/ferrum/contexts.rb +188 -19
  19. data/lib/ferrum/cookies/cookie.rb +6 -0
  20. data/lib/ferrum/cookies.rb +5 -0
  21. data/lib/ferrum/dialog.rb +18 -2
  22. data/lib/ferrum/downloads.rb +54 -1
  23. data/lib/ferrum/errors.rb +103 -4
  24. data/lib/ferrum/frame/dom.rb +65 -0
  25. data/lib/ferrum/frame/runtime.rb +57 -15
  26. data/lib/ferrum/frame.rb +58 -1
  27. data/lib/ferrum/headers.rb +6 -0
  28. data/lib/ferrum/interceptable.rb +83 -0
  29. data/lib/ferrum/keyboard.rb +25 -0
  30. data/lib/ferrum/mouse.rb +6 -0
  31. data/lib/ferrum/network/auth_request.rb +81 -2
  32. data/lib/ferrum/network/error.rb +15 -0
  33. data/lib/ferrum/network/exchange.rb +10 -0
  34. data/lib/ferrum/network/intercepted_request.rb +94 -2
  35. data/lib/ferrum/network/request.rb +1 -1
  36. data/lib/ferrum/network/response.rb +2 -0
  37. data/lib/ferrum/network.rb +121 -9
  38. data/lib/ferrum/node.rb +305 -15
  39. data/lib/ferrum/page/animation.rb +5 -0
  40. data/lib/ferrum/page/frames.rb +69 -7
  41. data/lib/ferrum/page/screencast.rb +5 -0
  42. data/lib/ferrum/page/screenshot.rb +55 -23
  43. data/lib/ferrum/page/stream.rb +71 -0
  44. data/lib/ferrum/page/tracing.rb +6 -0
  45. data/lib/ferrum/page.rb +148 -45
  46. data/lib/ferrum/proxy.rb +52 -2
  47. data/lib/ferrum/rgba.rb +10 -0
  48. data/lib/ferrum/target.rb +130 -3
  49. data/lib/ferrum/utils/attempt.rb +20 -0
  50. data/lib/ferrum/utils/elapsed_time.rb +38 -0
  51. data/lib/ferrum/utils/event.rb +14 -0
  52. data/lib/ferrum/utils/platform.rb +21 -0
  53. data/lib/ferrum/utils/thread.rb +15 -1
  54. data/lib/ferrum/version.rb +1 -1
  55. data/lib/ferrum/worker.rb +125 -0
  56. data/lib/ferrum.rb +7 -0
  57. metadata +6 -16
  58. data/lib/ferrum/browser/options/firefox.rb +0 -38
data/lib/ferrum/target.rb CHANGED
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ferrum
4
+ #
5
+ # Represents a CDP target, e.g. a page, iframe, dedicated/shared worker or
6
+ # service worker. Wraps the raw `targetInfo` params and lazily connects to
7
+ # the target as a {Page} or {Worker} over its own {Client}/{SessionClient}
8
+ # session, so a `Target` can exist without being connected to yet.
9
+ #
4
10
  class Target
5
11
  NEW_WINDOW_WAIT = ENV.fetch("FERRUM_NEW_WINDOW_WAIT", 0.3).to_f
6
12
 
@@ -11,72 +17,193 @@ module Ferrum
11
17
  attr_reader :options
12
18
  attr_accessor :session_id
13
19
 
14
- def initialize(browser_client, session_id = nil, params = nil)
20
+ def initialize(browser_client, session_id = nil, params = nil, implicit: false)
15
21
  @page = nil
22
+ @worker = nil
16
23
  @session_id = session_id
17
24
  @params = params
25
+ @implicit = implicit
18
26
  @browser_client = browser_client
19
27
  @options = browser_client.options
20
28
  end
21
29
 
30
+ # Merges freshly received CDP target info into this target's params,
31
+ # e.g. on `Target.targetInfoChanged`.
32
+ #
33
+ # @param [Hash] params
34
+ #
35
+ # @return [Hash]
22
36
  def update(params)
23
37
  @params.merge!(params)
24
38
  end
25
39
 
40
+ # Whether this target has already been connected to as a {Page} or
41
+ # {Worker}.
42
+ #
43
+ # @return [Boolean]
26
44
  def connected?
27
- !!@page
45
+ !!@page || !!@worker
28
46
  end
29
47
 
48
+ # Connects to and returns this target's {Page}.
49
+ #
50
+ # @return [Page]
30
51
  def page
31
52
  @page ||= build_page
32
53
  end
33
54
 
55
+ # Connects to and returns this target's {Worker}.
56
+ #
57
+ # @return [Worker]
58
+ def worker
59
+ @worker ||= build_worker
60
+ end
61
+
62
+ # The client connected to this target's own session (or a dedicated
63
+ # WebSocket connection if `options.flatten` is disabled).
64
+ #
65
+ # @return [Client, SessionClient]
34
66
  def client
35
67
  @client ||= build_client
36
68
  end
37
69
 
70
+ # Builds a {Page} for this target. Called lazily by `page`, or eagerly
71
+ # by {Context#create_page}.
72
+ #
73
+ # @param [Hash] options
74
+ #
75
+ # @return [Page]
38
76
  def build_page(**options)
39
77
  maybe_sleep_if_new_window
40
78
  Page.new(client, context_id: context_id, target_id: id, **options)
41
79
  end
42
80
 
81
+ # Builds a {Worker} for this target. Called lazily by {#worker}.
82
+ #
83
+ # @return [Worker]
84
+ def build_worker
85
+ Worker.new(client, target_id: id, url: url)
86
+ end
87
+
88
+ # Closes the WebSocket connection, without closing the target itself
89
+ # in the browser.
90
+ #
91
+ # @return [void]
92
+ def close_connection
93
+ @page&.close_connection
94
+ @worker&.close_connection
95
+ end
96
+
97
+ # The target's id.
98
+ #
99
+ # @return [String]
43
100
  def id
44
101
  @params["targetId"]
45
102
  end
46
103
 
104
+ # The target's type, e.g. `"page"`, `"iframe"`, `"worker"`,
105
+ # `"shared_worker"`, `"service_worker"`.
106
+ #
107
+ # @return [String]
47
108
  def type
48
109
  @params["type"]
49
110
  end
50
111
 
112
+ # The target's title.
113
+ #
114
+ # @return [String]
51
115
  def title
52
116
  @params["title"]
53
117
  end
54
118
 
119
+ # The target's URL.
120
+ #
121
+ # @return [String]
55
122
  def url
56
123
  @params["url"]
57
124
  end
58
125
 
126
+ # The id of the target that opened this one, set only for
127
+ # windows/tabs opened via `window.open`/links/etc.
128
+ #
129
+ # @return [String, nil]
59
130
  def opener_id
60
131
  @params["openerId"]
61
132
  end
62
133
 
134
+ # The id of the target that spawned this one, set for iframes and
135
+ # workers. Unlike `opener_id`, which is only set for windows/tabs opened
136
+ # via `window.open`/links/etc.
137
+ def parent_id
138
+ @params["parentId"]
139
+ end
140
+
141
+ # The id of the browser context this target belongs to, `nil` for the
142
+ # browser's implicit context, which Chrome doesn't let us address by id,
143
+ # see {Context#implicit?}. Commands scoped to a browser context target it
144
+ # by omitting `browserContextId`.
145
+ #
146
+ # @return [String, nil]
63
147
  def context_id
64
- @params["browserContextId"]
148
+ @params["browserContextId"] unless @implicit
65
149
  end
66
150
 
151
+ # Whether this target is a window/tab, i.e. was opened via
152
+ # `window.open`/a link/etc. and thus has an {#opener_id}.
153
+ #
154
+ # @return [Boolean]
67
155
  def window?
68
156
  !!opener_id
69
157
  end
70
158
 
159
+ # Whether this target is an iframe.
160
+ #
161
+ # @return [Boolean]
71
162
  def iframe?
72
163
  type == "iframe"
73
164
  end
74
165
 
166
+ # Whether this target is a page.
167
+ #
168
+ # @return [Boolean]
169
+ def page?
170
+ type == "page"
171
+ end
172
+
173
+ # Whether this target is a dedicated worker.
174
+ #
175
+ # @return [Boolean]
176
+ def worker?
177
+ type == "worker"
178
+ end
179
+
180
+ # Whether this target is a shared worker.
181
+ #
182
+ # @return [Boolean]
183
+ def shared_worker?
184
+ type == "shared_worker"
185
+ end
186
+
187
+ # Whether this target is a service worker.
188
+ #
189
+ # @return [Boolean]
190
+ def service_worker?
191
+ type == "service_worker"
192
+ end
193
+
194
+ # Chrome fires no events for a newly opened window, so we sleep a bit
195
+ # to give it a chance to load before connecting.
196
+ #
197
+ # @return [void]
75
198
  def maybe_sleep_if_new_window
76
199
  # Dirty hack because new window doesn't have events at all
77
200
  sleep(NEW_WINDOW_WAIT) if window?
78
201
  end
79
202
 
203
+ # Sends a CDP command through {#client}.
204
+ #
205
+ # @return [Boolean, Hash]
206
+ # `true` when sent asynchronously, otherwise the command's result.
80
207
  def command(...)
81
208
  client.command(...)
82
209
  end
@@ -2,9 +2,29 @@
2
2
 
3
3
  module Ferrum
4
4
  module Utils
5
+ #
6
+ # A retry-with-backoff helper for re-running a block a fixed number of
7
+ # times when it raises one of a given set of exceptions, sleeping
8
+ # between attempts.
9
+ #
5
10
  module Attempt
6
11
  module_function
7
12
 
13
+ #
14
+ # Retries the block up to `max` times when one of `errors` is raised,
15
+ # sleeping `wait` seconds between attempts.
16
+ #
17
+ # @param [Array<Class>, Class] errors
18
+ # Exception classes that trigger a retry.
19
+ #
20
+ # @param [Integer] max
21
+ # Maximum number of attempts.
22
+ #
23
+ # @param [Numeric] wait
24
+ # Seconds to sleep between attempts.
25
+ #
26
+ # @return [Object]
27
+ #
8
28
  def with_retry(errors:, max:, wait:)
9
29
  attempts ||= 1
10
30
  yield
@@ -2,25 +2,63 @@
2
2
 
3
3
  module Ferrum
4
4
  module Utils
5
+ #
6
+ # A monotonic-clock helper for tracking elapsed time and checking
7
+ # timeouts, backed by `Concurrent.monotonic_time`.
8
+ #
5
9
  module ElapsedTime
6
10
  module_function
7
11
 
12
+ #
13
+ # Sets the start point to the current monotonic time unless already set.
14
+ #
15
+ # @return [Float]
16
+ #
8
17
  def start
9
18
  @start ||= monotonic_time
10
19
  end
11
20
 
21
+ #
22
+ # Resets the start point to the current monotonic time.
23
+ #
24
+ # @return [Float]
25
+ #
12
26
  def reset
13
27
  @start = monotonic_time
14
28
  end
15
29
 
30
+ #
31
+ # Returns the time elapsed since `start` (or since {.start}/{.reset} was called).
32
+ #
33
+ # @param [Float, nil] start
34
+ # Monotonic time to measure from, defaults to the stored start point.
35
+ #
36
+ # @return [Float]
37
+ #
16
38
  def elapsed_time(start = nil)
17
39
  monotonic_time - (start || @start)
18
40
  end
19
41
 
42
+ #
43
+ # Returns the current monotonic clock time in seconds.
44
+ #
45
+ # @return [Float]
46
+ #
20
47
  def monotonic_time
21
48
  Concurrent.monotonic_time
22
49
  end
23
50
 
51
+ #
52
+ # Whether more than `timeout` seconds have elapsed since `start`.
53
+ #
54
+ # @param [Float] start
55
+ # Monotonic time to measure from.
56
+ #
57
+ # @param [Float, Integer] timeout
58
+ # The timeout, in seconds.
59
+ #
60
+ # @return [Boolean]
61
+ #
24
62
  def timeout?(start, timeout)
25
63
  elapsed_time(start) > timeout
26
64
  end
@@ -2,11 +2,25 @@
2
2
 
3
3
  module Ferrum
4
4
  module Utils
5
+ #
6
+ # A {Concurrent::Event} subclass that additionally tracks how many
7
+ # times it has been reset, via {#iteration}.
8
+ #
5
9
  class Event < Concurrent::Event
10
+ #
11
+ # Number of times the event has been reset.
12
+ #
13
+ # @return [Integer]
14
+ #
6
15
  def iteration
7
16
  synchronize { @iteration }
8
17
  end
9
18
 
19
+ #
20
+ # Marks the event as unset and increments the iteration counter.
21
+ #
22
+ # @return [Integer]
23
+ #
10
24
  def reset
11
25
  synchronize do
12
26
  @iteration += 1
@@ -2,9 +2,18 @@
2
2
 
3
3
  module Ferrum
4
4
  module Utils
5
+ #
6
+ # OS and Ruby engine detection helpers, used to pick the right browser
7
+ # path/flags and behavior for the current platform.
8
+ #
5
9
  module Platform
6
10
  module_function
7
11
 
12
+ #
13
+ # Detects the current platform.
14
+ #
15
+ # @return [:mac, :windows, :linux]
16
+ #
8
17
  def platform_name
9
18
  return :mac if mac?
10
19
  return :windows if windows?
@@ -12,18 +21,30 @@ module Ferrum
12
21
  :linux
13
22
  end
14
23
 
24
+ # Whether the current platform is Windows.
25
+ #
26
+ # @return [Boolean]
15
27
  def windows?
16
28
  RbConfig::CONFIG["host_os"] =~ /mingw|mswin|cygwin/
17
29
  end
18
30
 
31
+ # Whether the current platform is macOS.
32
+ #
33
+ # @return [Boolean]
19
34
  def mac?
20
35
  RbConfig::CONFIG["host_os"] =~ /darwin/
21
36
  end
22
37
 
38
+ # Whether the current platform is macOS running on Apple Silicon (arm64).
39
+ #
40
+ # @return [Boolean]
23
41
  def mac_arm?
24
42
  mac? && RbConfig::CONFIG["host_cpu"] =~ /arm/
25
43
  end
26
44
 
45
+ # Whether the current Ruby engine is MRI (as opposed to e.g. JRuby, TruffleRuby).
46
+ #
47
+ # @return [Boolean]
27
48
  def mri?
28
49
  defined?(RUBY_ENGINE) && RUBY_ENGINE == "ruby"
29
50
  end
@@ -2,10 +2,24 @@
2
2
 
3
3
  module Ferrum
4
4
  module Utils
5
+ #
6
+ # A helper for spawning threads with consistent exception-handling
7
+ # behavior (abort-on-exception and report-on-exception).
8
+ #
5
9
  module Thread
6
10
  module_function
7
11
 
8
- def spawn(abort_on_exception: true)
12
+ #
13
+ # Spawns a new thread running the given block.
14
+ #
15
+ # @param [Boolean] abort_on_exception
16
+ # Whether the thread aborts the process if it raises an unhandled exception.
17
+ # Off by default: an exception in one of Ferrum's own threads reaches the
18
+ # main thread wherever it happens to be, or kills the process outright.
19
+ #
20
+ # @return [Thread]
21
+ #
22
+ def spawn(abort_on_exception: false)
9
23
  ::Thread.new(abort_on_exception) do |whether_abort_on_exception|
10
24
  ::Thread.current.abort_on_exception = whether_abort_on_exception
11
25
  ::Thread.current.report_on_exception = true if ::Thread.current.respond_to?(:report_on_exception=)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ferrum
4
- VERSION = "0.17.2"
4
+ VERSION = "0.19.0"
5
5
  end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ferrum/frame/runtime"
4
+ require "ferrum/interceptable"
5
+
6
+ module Ferrum
7
+ # A dedicated or shared Worker spawned by a page. Unlike {Page} it has no
8
+ # DOM, frames, mouse/keyboard, or navigation history -- just a single
9
+ # global execution context and its own network activity.
10
+ class Worker
11
+ include Frame::Runtime
12
+ include Interceptable
13
+
14
+ # Client connection.
15
+ #
16
+ # @return [Client, SessionClient]
17
+ attr_reader :client
18
+
19
+ attr_reader :target_id, :url
20
+
21
+ def initialize(client, target_id:, url:)
22
+ @client = client
23
+ @target_id = target_id
24
+ @url = url
25
+ @options = client.options
26
+ @page = self
27
+ @execution_id = Concurrent::MVar.new
28
+ @network = Network.new(self)
29
+
30
+ subscribe
31
+ prepare
32
+ end
33
+
34
+ # Network object.
35
+ #
36
+ # @return [Network]
37
+ attr_reader :network
38
+
39
+ # How long to wait for CDP responses and JS evaluation to complete.
40
+ #
41
+ # @return [Numeric]
42
+ def timeout
43
+ @options.timeout
44
+ end
45
+
46
+ # Sends a CDP command through {#client}.
47
+ #
48
+ # @return [Boolean, Hash]
49
+ # `true` when sent asynchronously, otherwise the command's result.
50
+ def command(...)
51
+ client.command(...)
52
+ end
53
+
54
+ # Workers have a single execution context and no navigable document, so
55
+ # there's nothing for {Network} to check requests against.
56
+ def main_frame
57
+ nil
58
+ end
59
+
60
+ # Closes the target in the browser, and its connection.
61
+ #
62
+ # @return [Boolean]
63
+ def close
64
+ client.command("Target.closeTarget", async: true, targetId: target_id)
65
+ close_connection
66
+
67
+ true
68
+ end
69
+
70
+ # Closes the WebSocket connection only, without asking the browser to
71
+ # close the underlying target. Kept separate from {#close} so a whole
72
+ # context can be dropped (the browser closes its targets anyway) without
73
+ # every worker having to be closed one by one.
74
+ #
75
+ # @return [void]
76
+ def close_connection
77
+ client&.close
78
+ end
79
+
80
+ # Debug representation of the worker.
81
+ #
82
+ # @return [String]
83
+ def inspect
84
+ "#<#{self.class} @target_id=#{@target_id.inspect} @url=#{@url.inspect}>"
85
+ end
86
+
87
+ private
88
+
89
+ def subscribe
90
+ network.subscribe
91
+
92
+ on("Runtime.executionContextCreated") do |params|
93
+ self.execution_id = params.dig("context", "id")
94
+ end
95
+
96
+ return unless @options.js_errors
97
+
98
+ on("Runtime.exceptionThrown") do |params|
99
+ # FIXME: https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
100
+ Thread.main.raise JavaScriptError, params["exceptionDetails"]
101
+ end
102
+ end
103
+
104
+ def prepare
105
+ command("Runtime.enable")
106
+ command("Network.enable")
107
+ command("Runtime.runIfWaitingForDebugger")
108
+ end
109
+
110
+ def execution_id!
111
+ value = @execution_id.borrow(timeout, &:itself)
112
+ raise NoExecutionContextError if value.instance_of?(Object)
113
+
114
+ value
115
+ end
116
+
117
+ def execution_id=(value)
118
+ if value.nil?
119
+ @execution_id.try_take!
120
+ else
121
+ @execution_id.try_put!(value)
122
+ end
123
+ end
124
+ end
125
+ end
data/lib/ferrum.rb CHANGED
@@ -10,5 +10,12 @@ require "ferrum/errors"
10
10
  require "ferrum/browser"
11
11
  require "ferrum/node"
12
12
 
13
+ #
14
+ # Ferrum is a pure Ruby driver for headless Chrome. It talks
15
+ # directly to the browser over the Chrome DevTools Protocol (CDP), so it
16
+ # doesn't depend on Selenium, WebDriver or any other third party gem.
17
+ #
18
+ # {Ferrum::Browser} is the entry point most applications start from.
19
+ #
13
20
  module Ferrum
14
21
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ferrum
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.2
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Vorotilin
@@ -51,20 +51,6 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '1.1'
54
- - !ruby/object:Gem::Dependency
55
- name: webrick
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '1.7'
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: '1.7'
68
54
  - !ruby/object:Gem::Dependency
69
55
  name: websocket-driver
70
56
  requirement: !ruby/object:Gem::Requirement
@@ -89,14 +75,16 @@ files:
89
75
  - LICENSE
90
76
  - README.md
91
77
  - lib/ferrum.rb
78
+ - lib/ferrum/accessibility.rb
79
+ - lib/ferrum/accessibility/ax_node.rb
92
80
  - lib/ferrum/browser.rb
93
81
  - lib/ferrum/browser/binary.rb
94
82
  - lib/ferrum/browser/command.rb
95
83
  - lib/ferrum/browser/options.rb
96
84
  - lib/ferrum/browser/options/base.rb
97
85
  - lib/ferrum/browser/options/chrome.rb
98
- - lib/ferrum/browser/options/firefox.rb
99
86
  - lib/ferrum/browser/process.rb
87
+ - lib/ferrum/browser/process/killer.rb
100
88
  - lib/ferrum/browser/version_info.rb
101
89
  - lib/ferrum/browser/xvfb.rb
102
90
  - lib/ferrum/client.rb
@@ -113,6 +101,7 @@ files:
113
101
  - lib/ferrum/frame/dom.rb
114
102
  - lib/ferrum/frame/runtime.rb
115
103
  - lib/ferrum/headers.rb
104
+ - lib/ferrum/interceptable.rb
116
105
  - lib/ferrum/keyboard.json
117
106
  - lib/ferrum/keyboard.rb
118
107
  - lib/ferrum/mouse.rb
@@ -141,6 +130,7 @@ files:
141
130
  - lib/ferrum/utils/platform.rb
142
131
  - lib/ferrum/utils/thread.rb
143
132
  - lib/ferrum/version.rb
133
+ - lib/ferrum/worker.rb
144
134
  homepage: https://github.com/rubycdp/ferrum
145
135
  licenses:
146
136
  - MIT
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ferrum
4
- class Browser
5
- class Options
6
- class Firefox < Base
7
- DEFAULT_OPTIONS = {
8
- "headless" => nil
9
- }.freeze
10
-
11
- MAC_BIN_PATH = [
12
- "/Applications/Firefox.app/Contents/MacOS/firefox-bin"
13
- ].freeze
14
- LINUX_BIN_PATH = %w[firefox].freeze
15
- WINDOWS_BIN_PATH = [
16
- "C:/Program Files/Firefox Developer Edition/firefox.exe",
17
- "C:/Program Files/Mozilla Firefox/firefox.exe"
18
- ].freeze
19
- PLATFORM_PATH = {
20
- mac: MAC_BIN_PATH,
21
- windows: WINDOWS_BIN_PATH,
22
- linux: LINUX_BIN_PATH
23
- }.freeze
24
-
25
- def merge_required(flags, options, user_data_dir)
26
- flags.merge("remote-debugger" => "#{options.host}:#{options.port}", "profile" => user_data_dir)
27
- end
28
-
29
- def merge_default(flags, options)
30
- defaults = except("headless") unless options.headless
31
-
32
- defaults ||= DEFAULT_OPTIONS
33
- defaults.merge(flags)
34
- end
35
- end
36
- end
37
- end
38
- end