ferrum 0.11 → 0.13

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +174 -30
  4. data/lib/ferrum/browser/binary.rb +46 -0
  5. data/lib/ferrum/browser/client.rb +17 -16
  6. data/lib/ferrum/browser/command.rb +10 -12
  7. data/lib/ferrum/browser/options/base.rb +2 -11
  8. data/lib/ferrum/browser/options/chrome.rb +29 -18
  9. data/lib/ferrum/browser/options/firefox.rb +13 -9
  10. data/lib/ferrum/browser/options.rb +84 -0
  11. data/lib/ferrum/browser/process.rb +45 -40
  12. data/lib/ferrum/browser/subscriber.rb +1 -3
  13. data/lib/ferrum/browser/version_info.rb +71 -0
  14. data/lib/ferrum/browser/web_socket.rb +9 -12
  15. data/lib/ferrum/browser/xvfb.rb +4 -8
  16. data/lib/ferrum/browser.rb +193 -47
  17. data/lib/ferrum/context.rb +9 -4
  18. data/lib/ferrum/contexts.rb +12 -10
  19. data/lib/ferrum/cookies/cookie.rb +126 -0
  20. data/lib/ferrum/cookies.rb +93 -55
  21. data/lib/ferrum/dialog.rb +30 -0
  22. data/lib/ferrum/errors.rb +115 -0
  23. data/lib/ferrum/frame/dom.rb +177 -0
  24. data/lib/ferrum/frame/runtime.rb +58 -75
  25. data/lib/ferrum/frame.rb +118 -23
  26. data/lib/ferrum/headers.rb +30 -2
  27. data/lib/ferrum/keyboard.rb +56 -13
  28. data/lib/ferrum/mouse.rb +92 -7
  29. data/lib/ferrum/network/auth_request.rb +7 -2
  30. data/lib/ferrum/network/exchange.rb +97 -12
  31. data/lib/ferrum/network/intercepted_request.rb +10 -8
  32. data/lib/ferrum/network/request.rb +69 -0
  33. data/lib/ferrum/network/response.rb +85 -3
  34. data/lib/ferrum/network.rb +285 -36
  35. data/lib/ferrum/node.rb +69 -23
  36. data/lib/ferrum/page/animation.rb +16 -1
  37. data/lib/ferrum/page/frames.rb +111 -30
  38. data/lib/ferrum/page/screenshot.rb +142 -65
  39. data/lib/ferrum/page/stream.rb +38 -0
  40. data/lib/ferrum/page/tracing.rb +97 -0
  41. data/lib/ferrum/page.rb +224 -60
  42. data/lib/ferrum/proxy.rb +147 -0
  43. data/lib/ferrum/{rbga.rb → rgba.rb} +4 -2
  44. data/lib/ferrum/target.rb +7 -4
  45. data/lib/ferrum/utils/attempt.rb +20 -0
  46. data/lib/ferrum/utils/elapsed_time.rb +27 -0
  47. data/lib/ferrum/utils/platform.rb +28 -0
  48. data/lib/ferrum/version.rb +1 -1
  49. data/lib/ferrum.rb +4 -146
  50. metadata +63 -51
@@ -3,89 +3,208 @@
3
3
  require "base64"
4
4
  require "forwardable"
5
5
  require "ferrum/page"
6
+ require "ferrum/proxy"
6
7
  require "ferrum/contexts"
7
8
  require "ferrum/browser/xvfb"
9
+ require "ferrum/browser/options"
8
10
  require "ferrum/browser/process"
9
11
  require "ferrum/browser/client"
12
+ require "ferrum/browser/binary"
13
+ require "ferrum/browser/version_info"
10
14
 
11
15
  module Ferrum
12
16
  class Browser
13
- DEFAULT_TIMEOUT = ENV.fetch("FERRUM_DEFAULT_TIMEOUT", 5).to_i
14
- WINDOW_SIZE = [1024, 768].freeze
15
- BASE_URL_SCHEMA = %w[http https].freeze
16
-
17
17
  extend Forwardable
18
18
  delegate %i[default_context] => :contexts
19
- delegate %i[targets create_target create_page page pages windows] => :default_context
20
- delegate %i[go_to back forward refresh reload stop wait_for_reload
19
+ delegate %i[targets create_target page pages windows] => :default_context
20
+ delegate %i[go_to goto go back forward refresh reload stop wait_for_reload
21
21
  at_css at_xpath css xpath current_url current_title url title
22
- body doctype set_content
22
+ body doctype content=
23
23
  headers cookies network
24
24
  mouse keyboard
25
25
  screenshot pdf mhtml viewport_size
26
26
  frames frame_by main_frame
27
27
  evaluate evaluate_on evaluate_async execute evaluate_func
28
28
  add_script_tag add_style_tag bypass_csp
29
- on goto position position=
29
+ on position position=
30
30
  playback_rate playback_rate=] => :page
31
31
  delegate %i[default_user_agent] => :process
32
32
 
33
- attr_reader :client, :process, :contexts, :logger, :js_errors, :pending_connection_errors,
34
- :slowmo, :base_url, :options, :window_size, :ws_max_receive_size
35
- attr_writer :timeout
33
+ attr_reader :client, :process, :contexts, :options, :window_size, :base_url
34
+ attr_accessor :timeout
36
35
 
36
+ #
37
+ # Initializes the browser.
38
+ #
39
+ # @param [Hash{Symbol => Object}, nil] options
40
+ # Additional browser options.
41
+ #
42
+ # @option options [Boolean] :headless (true)
43
+ # Set browser as headless or not.
44
+ #
45
+ # @option options [Boolean] :xvfb (false)
46
+ # Run browser in a virtual framebuffer.
47
+ #
48
+ # @option options [(Integer, Integer)] :window_size ([1024, 768])
49
+ # The dimensions of the browser window in which to test, expressed as a
50
+ # 2-element array, e.g. `[1024, 768]`.
51
+ #
52
+ # @option options [Array<String, Hash>] :extensions
53
+ # An array of paths to files or JS source code to be preloaded into the
54
+ # browser e.g.: `["/path/to/script.js", { source: "window.secret = 'top'" }]`
55
+ #
56
+ # @option options [#puts] :logger
57
+ # When present, debug output is written to this object.
58
+ #
59
+ # @option options [Integer, Float] :slowmo
60
+ # Set a delay in seconds to wait before sending command.
61
+ # Useful companion of headless option, so that you have time to see
62
+ # changes.
63
+ #
64
+ # @option options [Numeric] :timeout (5)
65
+ # The number of seconds we'll wait for a response when communicating with
66
+ # browser.
67
+ #
68
+ # @option options [Boolean] :js_errors
69
+ # When true, JavaScript errors get re-raised in Ruby.
70
+ #
71
+ # @option options [Boolean] :pending_connection_errors (true)
72
+ # When main frame is still waiting for slow responses while timeout is
73
+ # reached {PendingConnectionsError} is raised. It's better to figure out
74
+ # why you have slow responses and fix or block them rather than turn this
75
+ # setting off.
76
+ #
77
+ # @option options [:chrome, :firefox] :browser_name (:chrome)
78
+ # Sets the browser's name. **Note:** only experimental support for
79
+ # `:firefox` for now.
80
+ #
81
+ # @option options [String] :browser_path
82
+ # Path to Chrome binary, you can also set ENV variable as
83
+ # `BROWSER_PATH=some/path/chrome bundle exec rspec`.
84
+ #
85
+ # @option options [Hash] :browser_options
86
+ # Additional command line options, [see them all](https://peter.sh/experiments/chromium-command-line-switches/)
87
+ # e.g. `{ "ignore-certificate-errors" => nil }`
88
+ #
89
+ # @option options [Boolean] :ignore_default_browser_options
90
+ # Ferrum has a number of default options it passes to the browser,
91
+ # if you set this to `true` then only options you put in
92
+ # `:browser_options` will be passed to the browser, except required ones
93
+ # of course.
94
+ #
95
+ # @option options [Integer] :port
96
+ # Remote debugging port for headless Chrome.
97
+ #
98
+ # @option options [String] :host
99
+ # Remote debugging address for headless Chrome.
100
+ #
101
+ # @option options [String] :url
102
+ # URL for a running instance of Chrome. If this is set, a browser process
103
+ # will not be spawned.
104
+ #
105
+ # @option options [Integer] :process_timeout
106
+ # How long to wait for the Chrome process to respond on startup.
107
+ #
108
+ # @option options [Integer] :ws_max_receive_size
109
+ # How big messages to accept from Chrome over the web socket, in bytes.
110
+ # Defaults to 64MB. Incoming messages larger this will cause a
111
+ # {Ferrum::DeadBrowserError}.
112
+ #
113
+ # @option options [Hash] :proxy
114
+ # Specify proxy settings, [read more](https://github.com/rubycdp/ferrum#proxy).
115
+ #
116
+ # @option options [String] :save_path
117
+ # Path to save attachments with [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition)
118
+ # header.
119
+ #
120
+ # @option options [Hash] :env
121
+ # Environment variables you'd like to pass through to the process.
122
+ #
37
123
  def initialize(options = nil)
38
- options ||= {}
124
+ @options = Options.new(options)
125
+ @client = @process = @contexts = nil
39
126
 
40
- @client = nil
41
- @window_size = options.fetch(:window_size, WINDOW_SIZE)
42
- @original_window_size = @window_size
127
+ @timeout = @options.timeout
128
+ @window_size = @options.window_size
129
+ @base_url = @options.base_url if @options.base_url
43
130
 
44
- @options = Hash(options.merge(window_size: @window_size))
45
- @logger, @timeout, @ws_max_receive_size =
46
- @options.values_at(:logger, :timeout, :ws_max_receive_size)
47
- @js_errors = @options.fetch(:js_errors, false)
48
- @pending_connection_errors = @options.fetch(:pending_connection_errors, true)
49
- @slowmo = @options[:slowmo].to_f
131
+ start
132
+ end
50
133
 
51
- if @options.key?(:base_url)
52
- self.base_url = @options[:base_url]
53
- end
134
+ #
135
+ # Sets the base URL.
136
+ #
137
+ # @param [String] value
138
+ # The new base URL value.
139
+ #
140
+ # @raise [ArgumentError] when path is not absolute or doesn't include schema
141
+ #
142
+ # @return [Addressable::URI]
143
+ # The parsed base URI value.
144
+ #
145
+ def base_url=(value)
146
+ @base_url = options.parse_base_url(value)
147
+ end
54
148
 
55
- if ENV["FERRUM_DEBUG"] && !@logger
56
- STDOUT.sync = true
57
- @logger = STDOUT
58
- @options[:logger] = @logger
59
- end
149
+ #
150
+ # Creates a new page.
151
+ #
152
+ # @param [Boolean] new_context
153
+ # Whether to create a page in a new context or not.
154
+ #
155
+ # @param [Hash] proxy
156
+ # Whether to use proxy for a page. The page will be created in a new context if so.
157
+ #
158
+ # @return [Ferrum::Page]
159
+ # Created page.
160
+ #
161
+ def create_page(new_context: false, proxy: nil)
162
+ page = if new_context || proxy
163
+ params = {}
60
164
 
61
- @options.freeze
165
+ if proxy
166
+ options.parse_proxy(proxy)
167
+ params.merge!(proxyServer: "#{proxy[:host]}:#{proxy[:port]}")
168
+ params.merge!(proxyBypassList: proxy[:bypass]) if proxy[:bypass]
169
+ end
62
170
 
63
- start
64
- end
171
+ context = contexts.create(**params)
172
+ context.create_page(proxy: proxy)
173
+ else
174
+ default_context.create_page
175
+ end
65
176
 
66
- def base_url=(value)
67
- parsed = Addressable::URI.parse(value)
68
- unless BASE_URL_SCHEMA.include?(parsed.normalized_scheme)
69
- raise "Set `base_url` should be absolute and include schema: #{BASE_URL_SCHEMA}"
177
+ block_given? ? yield(page) : page
178
+ ensure
179
+ if block_given?
180
+ page.close
181
+ context.dispose if new_context
70
182
  end
71
-
72
- @base_url = parsed
73
183
  end
74
184
 
75
185
  def extensions
76
- @extensions ||= Array(@options[:extensions]).map do |ext|
186
+ @extensions ||= Array(options.extensions).map do |ext|
77
187
  (ext.is_a?(Hash) && ext[:source]) || File.read(ext)
78
188
  end
79
189
  end
80
190
 
191
+ #
192
+ # Evaluate JavaScript to modify things before a page load.
193
+ #
194
+ # @param [String] expression
195
+ # The JavaScript to add to each new document.
196
+ #
197
+ # @example
198
+ # browser.evaluate_on_new_document <<~JS
199
+ # Object.defineProperty(navigator, "languages", {
200
+ # get: function() { return ["tlh"]; }
201
+ # });
202
+ # JS
203
+ #
81
204
  def evaluate_on_new_document(expression)
82
205
  extensions << expression
83
206
  end
84
207
 
85
- def timeout
86
- @timeout || DEFAULT_TIMEOUT
87
- end
88
-
89
208
  def command(*args)
90
209
  @client.command(*args)
91
210
  rescue DeadBrowserError
@@ -93,8 +212,22 @@ module Ferrum
93
212
  raise
94
213
  end
95
214
 
215
+ #
216
+ # Closes browser tabs opened by the `Browser` instance.
217
+ #
218
+ # @example
219
+ # # connect to a long-running Chrome process
220
+ # browser = Ferrum::Browser.new(url: 'http://localhost:9222')
221
+ #
222
+ # browser.go_to("https://github.com/")
223
+ #
224
+ # # clean up, lest the tab stays there hanging forever
225
+ # browser.reset
226
+ #
227
+ # browser.quit
228
+ #
96
229
  def reset
97
- @window_size = @original_window_size
230
+ @window_size = options.window_size
98
231
  contexts.reset
99
232
  end
100
233
 
@@ -118,12 +251,25 @@ module Ferrum
118
251
  command("Browser.crash")
119
252
  end
120
253
 
254
+ #
255
+ # Gets the version information from the browser.
256
+ #
257
+ # @return [VersionInfo]
258
+ #
259
+ # @since 0.13
260
+ #
261
+ def version
262
+ VersionInfo.new(command("Browser.getVersion"))
263
+ end
264
+
121
265
  private
122
266
 
123
267
  def start
124
- Ferrum.started
125
- @process = Process.start(@options)
126
- @client = Client.new(self, @process.ws_url)
268
+ Utils::ElapsedTime.start
269
+ @process = Process.start(options)
270
+ @client = Client.new(@process.ws_url, self,
271
+ logger: options.logger,
272
+ ws_max_receive_size: options.ws_max_receive_size)
127
273
  @contexts = Contexts.new(self)
128
274
  end
129
275
  end
@@ -9,7 +9,9 @@ module Ferrum
9
9
  attr_reader :id, :targets
10
10
 
11
11
  def initialize(browser, contexts, id)
12
- @browser, @contexts, @id = browser, contexts, id
12
+ @id = id
13
+ @browser = browser
14
+ @contexts = contexts
13
15
  @targets = Concurrent::Map.new
14
16
  @pendings = Concurrent::MVar.new
15
17
  end
@@ -32,13 +34,15 @@ module Ferrum
32
34
  # usually is the last one.
33
35
  def windows(pos = nil, size = 1)
34
36
  raise ArgumentError if pos && !POSITION.include?(pos)
37
+
35
38
  windows = @targets.values.select(&:window?)
36
39
  windows = windows.send(pos, size) if pos
37
40
  windows.map(&:page)
38
41
  end
39
42
 
40
- def create_page
41
- create_target.page
43
+ def create_page(**options)
44
+ target = create_target
45
+ target.page = target.build_page(**options)
42
46
  end
43
47
 
44
48
  def create_target
@@ -47,6 +51,7 @@ module Ferrum
47
51
  url: "about:blank")
48
52
  target = @pendings.take(@browser.timeout)
49
53
  raise NoSuchTargetError unless target.is_a?(Target)
54
+
50
55
  @targets.put_if_absent(target.id, target)
51
56
  target
52
57
  end
@@ -72,7 +77,7 @@ module Ferrum
72
77
  @contexts.dispose(@id)
73
78
  end
74
79
 
75
- def has_target?(target_id)
80
+ def target?(target_id)
76
81
  !!@targets[target_id]
77
82
  end
78
83
 
@@ -19,12 +19,12 @@ module Ferrum
19
19
 
20
20
  def find_by(target_id:)
21
21
  context = nil
22
- @contexts.each_value { |c| context = c if c.has_target?(target_id) }
22
+ @contexts.each_value { |c| context = c if c.target?(target_id) }
23
23
  context
24
24
  end
25
25
 
26
- def create
27
- response = @browser.command("Target.createBrowserContext")
26
+ def create(**options)
27
+ response = @browser.command("Target.createBrowserContext", **options)
28
28
  context_id = response["browserContextId"]
29
29
  context = Context.new(@browser, self, context_id)
30
30
  @contexts[context_id] = context
@@ -41,7 +41,11 @@ module Ferrum
41
41
 
42
42
  def reset
43
43
  @default_context = nil
44
- @contexts.keys.each { |id| dispose(id) }
44
+ @contexts.each_key { |id| dispose(id) }
45
+ end
46
+
47
+ def size
48
+ @contexts.size
45
49
  end
46
50
 
47
51
  private
@@ -64,15 +68,13 @@ module Ferrum
64
68
  end
65
69
 
66
70
  @browser.client.on("Target.targetDestroyed") do |params|
67
- if context = find_by(target_id: params["targetId"])
68
- context.delete_target(params["targetId"])
69
- end
71
+ context = find_by(target_id: params["targetId"])
72
+ context&.delete_target(params["targetId"])
70
73
  end
71
74
 
72
75
  @browser.client.on("Target.targetCrashed") do |params|
73
- if context = find_by(target_id: params["targetId"])
74
- context.delete_target(params["targetId"])
75
- end
76
+ context = find_by(target_id: params["targetId"])
77
+ context&.delete_target(params["targetId"])
76
78
  end
77
79
  end
78
80
 
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ferrum
4
+ class Cookies
5
+ #
6
+ # Represents a [cookie value](https://chromedevtools.github.io/devtools-protocol/1-3/Network/#type-Cookie).
7
+ #
8
+ class Cookie
9
+ # The parsed JSON attributes.
10
+ #
11
+ # @return [Hash{String => [String, Boolean, nil]}]
12
+ attr_reader :attributes
13
+
14
+ #
15
+ # Initializes the cookie.
16
+ #
17
+ # @param [Hash{String => String}] attributes
18
+ # The parsed JSON attributes.
19
+ #
20
+ def initialize(attributes)
21
+ @attributes = attributes
22
+ end
23
+
24
+ #
25
+ # The cookie's name.
26
+ #
27
+ # @return [String]
28
+ #
29
+ def name
30
+ attributes["name"]
31
+ end
32
+
33
+ #
34
+ # The cookie's value.
35
+ #
36
+ # @return [String]
37
+ #
38
+ def value
39
+ attributes["value"]
40
+ end
41
+
42
+ #
43
+ # The cookie's domain.
44
+ #
45
+ # @return [String]
46
+ #
47
+ def domain
48
+ attributes["domain"]
49
+ end
50
+
51
+ #
52
+ # The cookie's path.
53
+ #
54
+ # @return [String]
55
+ #
56
+ def path
57
+ attributes["path"]
58
+ end
59
+
60
+ #
61
+ # The `sameSite` configuration.
62
+ #
63
+ # @return ["Strict", "Lax", "None", nil]
64
+ #
65
+ def samesite
66
+ attributes["sameSite"]
67
+ end
68
+ alias same_site samesite
69
+
70
+ #
71
+ # The cookie's size.
72
+ #
73
+ # @return [Integer]
74
+ #
75
+ def size
76
+ attributes["size"]
77
+ end
78
+
79
+ #
80
+ # Specifies whether the cookie is secure or not.
81
+ #
82
+ # @return [Boolean]
83
+ #
84
+ def secure?
85
+ attributes["secure"]
86
+ end
87
+
88
+ #
89
+ # Specifies whether the cookie is HTTP-only or not.
90
+ #
91
+ # @return [Boolean]
92
+ #
93
+ def httponly?
94
+ attributes["httpOnly"]
95
+ end
96
+ alias http_only? httponly?
97
+
98
+ #
99
+ # Specifies whether the cookie is a session cookie or not.
100
+ #
101
+ # @return [Boolean]
102
+ #
103
+ def session?
104
+ attributes["session"]
105
+ end
106
+
107
+ #
108
+ # Specifies when the cookie will expire.
109
+ #
110
+ # @return [Time, nil]
111
+ #
112
+ def expires
113
+ Time.at(attributes["expires"]) if attributes["expires"].positive?
114
+ end
115
+
116
+ #
117
+ # Compares different cookie objects.
118
+ #
119
+ # @return [Boolean]
120
+ #
121
+ def ==(other)
122
+ other.class == self.class && other.attributes == attributes
123
+ end
124
+ end
125
+ end
126
+ end
@@ -1,84 +1,114 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ferrum/cookies/cookie"
4
+
3
5
  module Ferrum
4
6
  class Cookies
5
- class Cookie
6
- def initialize(attributes)
7
- @attributes = attributes
8
- end
9
-
10
- def name
11
- @attributes["name"]
12
- end
13
-
14
- def value
15
- @attributes["value"]
16
- end
17
-
18
- def domain
19
- @attributes["domain"]
20
- end
21
-
22
- def path
23
- @attributes["path"]
24
- end
25
-
26
- def samesite
27
- @attributes["sameSite"]
28
- end
29
-
30
- def size
31
- @attributes["size"]
32
- end
33
-
34
- def secure?
35
- @attributes["secure"]
36
- end
37
-
38
- def httponly?
39
- @attributes["httpOnly"]
40
- end
41
-
42
- def session?
43
- @attributes["session"]
44
- end
45
-
46
- def expires
47
- if @attributes["expires"] > 0
48
- Time.at(@attributes["expires"])
49
- end
50
- end
51
- end
52
-
53
7
  def initialize(page)
54
8
  @page = page
55
9
  end
56
10
 
11
+ #
12
+ # Returns cookies hash.
13
+ #
14
+ # @return [Hash{String => Cookie}]
15
+ #
16
+ # @example
17
+ # browser.cookies.all # => {
18
+ # # "NID" => #<Ferrum::Cookies::Cookie:0x0000558624b37a40 @attributes={
19
+ # # "name"=>"NID", "value"=>"...", "domain"=>".google.com", "path"=>"/",
20
+ # # "expires"=>1583211046.575681, "size"=>178, "httpOnly"=>true, "secure"=>false, "session"=>false
21
+ # # }>
22
+ # # }
23
+ #
57
24
  def all
58
25
  cookies = @page.command("Network.getAllCookies")["cookies"]
59
- cookies.map { |c| [c["name"], Cookie.new(c)] }.to_h
26
+ cookies.to_h { |c| [c["name"], Cookie.new(c)] }
60
27
  end
61
28
 
29
+ #
30
+ # Returns cookie.
31
+ #
32
+ # @param [String] name
33
+ # The cookie name to fetch.
34
+ #
35
+ # @return [Cookie, nil]
36
+ # The cookie with the matching name.
37
+ #
38
+ # @example
39
+ # browser.cookies["NID"] # =>
40
+ # # <Ferrum::Cookies::Cookie:0x0000558624b67a88 @attributes={
41
+ # # "name"=>"NID", "value"=>"...", "domain"=>".google.com",
42
+ # # "path"=>"/", "expires"=>1583211046.575681, "size"=>178,
43
+ # # "httpOnly"=>true, "secure"=>false, "session"=>false
44
+ # # }>
45
+ #
62
46
  def [](name)
63
47
  all[name]
64
48
  end
65
49
 
66
- def set(name: nil, value: nil, **options)
67
- cookie = options.dup
68
- cookie[:name] ||= name
69
- cookie[:value] ||= value
50
+ #
51
+ # Sets a cookie.
52
+ #
53
+ # @param [Hash{Symbol => Object}, Cookie] options
54
+ #
55
+ # @option options [String] :name
56
+ #
57
+ # @option options [String] :value
58
+ #
59
+ # @option options [String] :domain
60
+ #
61
+ # @option options [String] :path
62
+ #
63
+ # @option options [Integer] :expires
64
+ #
65
+ # @option options [Integer] :size
66
+ #
67
+ # @option options [Boolean] :httponly
68
+ #
69
+ # @option options [Boolean] :secure
70
+ #
71
+ # @option options [String] :samesite
72
+ #
73
+ #
74
+ # @example
75
+ # browser.cookies.set(name: "stealth", value: "omg", domain: "google.com") # => true
76
+ #
77
+ # @example
78
+ # nid_cookie = browser.cookies["NID"] # => <Ferrum::Cookies::Cookie:0x0000558624b67a88>
79
+ # browser.cookies.set(nid_cookie) # => true
80
+ #
81
+ def set(options)
82
+ cookie = (
83
+ options.is_a?(Cookie) ? options.attributes : options
84
+ ).dup.transform_keys(&:to_sym)
85
+
70
86
  cookie[:domain] ||= default_domain
71
87
 
72
88
  cookie[:httpOnly] = cookie.delete(:httponly) if cookie.key?(:httponly)
73
89
  cookie[:sameSite] = cookie.delete(:samesite) if cookie.key?(:samesite)
74
90
 
75
91
  expires = cookie.delete(:expires).to_i
76
- cookie[:expires] = expires if expires > 0
92
+ cookie[:expires] = expires if expires.positive?
77
93
 
78
94
  @page.command("Network.setCookie", **cookie)["success"]
79
95
  end
80
96
 
81
- # Supports :url, :domain and :path options
97
+ #
98
+ # Removes given cookie.
99
+ #
100
+ # @param [String] name
101
+ #
102
+ # @param [Hash{Symbol => Object}] options
103
+ # Additional keyword arguments.
104
+ #
105
+ # @option options [String] :domain
106
+ #
107
+ # @option options [String] :url
108
+ #
109
+ # @example
110
+ # browser.cookies.remove(name: "stealth", domain: "google.com") # => true
111
+ #
82
112
  def remove(name:, **options)
83
113
  raise "Specify :domain or :url option" if !options[:domain] && !options[:url] && !default_domain
84
114
 
@@ -90,6 +120,14 @@ module Ferrum
90
120
  true
91
121
  end
92
122
 
123
+ #
124
+ # Removes all cookies for current page.
125
+ #
126
+ # @return [true]
127
+ #
128
+ # @example
129
+ # browser.cookies.clear # => true
130
+ #
93
131
  def clear
94
132
  @page.command("Network.clearBrowserCookies")
95
133
  true