ferrum 0.11 → 0.13

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