ferrum 0.10.1 → 0.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +261 -28
  4. data/lib/ferrum/browser/binary.rb +46 -0
  5. data/lib/ferrum/browser/client.rb +15 -12
  6. data/lib/ferrum/browser/command.rb +7 -8
  7. data/lib/ferrum/browser/options/base.rb +1 -7
  8. data/lib/ferrum/browser/options/chrome.rb +17 -10
  9. data/lib/ferrum/browser/options/firefox.rb +11 -4
  10. data/lib/ferrum/browser/process.rb +41 -35
  11. data/lib/ferrum/browser/subscriber.rb +1 -3
  12. data/lib/ferrum/browser/web_socket.rb +9 -12
  13. data/lib/ferrum/browser/xvfb.rb +4 -8
  14. data/lib/ferrum/browser.rb +49 -12
  15. data/lib/ferrum/context.rb +12 -4
  16. data/lib/ferrum/contexts.rb +13 -9
  17. data/lib/ferrum/cookies.rb +10 -9
  18. data/lib/ferrum/errors.rb +115 -0
  19. data/lib/ferrum/frame/runtime.rb +20 -17
  20. data/lib/ferrum/frame.rb +32 -24
  21. data/lib/ferrum/headers.rb +2 -2
  22. data/lib/ferrum/keyboard.rb +11 -11
  23. data/lib/ferrum/mouse.rb +8 -7
  24. data/lib/ferrum/network/auth_request.rb +7 -2
  25. data/lib/ferrum/network/exchange.rb +14 -10
  26. data/lib/ferrum/network/intercepted_request.rb +10 -8
  27. data/lib/ferrum/network/request.rb +5 -0
  28. data/lib/ferrum/network/response.rb +4 -4
  29. data/lib/ferrum/network.rb +124 -35
  30. data/lib/ferrum/node.rb +98 -40
  31. data/lib/ferrum/page/animation.rb +15 -0
  32. data/lib/ferrum/page/frames.rb +46 -15
  33. data/lib/ferrum/page/screenshot.rb +53 -67
  34. data/lib/ferrum/page/stream.rb +38 -0
  35. data/lib/ferrum/page/tracing.rb +71 -0
  36. data/lib/ferrum/page.rb +88 -34
  37. data/lib/ferrum/proxy.rb +58 -0
  38. data/lib/ferrum/{rbga.rb → rgba.rb} +4 -2
  39. data/lib/ferrum/target.rb +1 -0
  40. data/lib/ferrum/utils/attempt.rb +20 -0
  41. data/lib/ferrum/utils/elapsed_time.rb +27 -0
  42. data/lib/ferrum/utils/platform.rb +28 -0
  43. data/lib/ferrum/version.rb +1 -1
  44. data/lib/ferrum.rb +4 -140
  45. metadata +65 -50
@@ -36,18 +36,27 @@ module Ferrum
36
36
  "metrics-recording-only" => nil,
37
37
  "safebrowsing-disable-auto-update" => nil,
38
38
  "password-store" => "basic",
39
- # Note: --no-sandbox is not needed if you properly setup a user in the container.
39
+ "no-startup-window" => nil
40
+ # NOTE: --no-sandbox is not needed if you properly setup a user in the container.
40
41
  # https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile#L35-L40
41
42
  # "no-sandbox" => nil,
42
43
  }.freeze
43
44
 
44
45
  MAC_BIN_PATH = [
45
- "/Applications/Chromium.app/Contents/MacOS/Chromium",
46
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
46
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
47
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
47
48
  ].freeze
48
- LINUX_BIN_PATH = %w[chromium google-chrome-unstable google-chrome-beta
49
- google-chrome chrome chromium-browser
50
- google-chrome-stable].freeze
49
+ LINUX_BIN_PATH = %w[chrome google-chrome google-chrome-stable google-chrome-beta
50
+ chromium chromium-browser google-chrome-unstable].freeze
51
+ WINDOWS_BIN_PATH = [
52
+ "C:/Program Files/Google/Chrome/Application/chrome.exe",
53
+ "C:/Program Files/Google/Chrome Dev/Application/chrome.exe"
54
+ ].freeze
55
+ PLATFORM_PATH = {
56
+ mac: MAC_BIN_PATH,
57
+ windows: WINDOWS_BIN_PATH,
58
+ linux: LINUX_BIN_PATH
59
+ }.freeze
51
60
 
52
61
  def merge_required(flags, options, user_data_dir)
53
62
  port = options.fetch(:port, BROWSER_PORT)
@@ -55,14 +64,12 @@ module Ferrum
55
64
  flags.merge("remote-debugging-port" => port,
56
65
  "remote-debugging-address" => host,
57
66
  # Doesn't work on MacOS, so we need to set it by CDP
58
- "window-size" => options[:window_size].join(","),
67
+ "window-size" => options[:window_size]&.join(","),
59
68
  "user-data-dir" => user_data_dir)
60
69
  end
61
70
 
62
71
  def merge_default(flags, options)
63
- unless options.fetch(:headless, true)
64
- defaults = except("headless", "disable-gpu")
65
- end
72
+ defaults = except("headless", "disable-gpu") unless options.fetch(:headless, true)
66
73
 
67
74
  defaults ||= DEFAULT_OPTIONS
68
75
  defaults.merge(flags)
@@ -5,13 +5,22 @@ module Ferrum
5
5
  module Options
6
6
  class Firefox < Base
7
7
  DEFAULT_OPTIONS = {
8
- "headless" => nil,
8
+ "headless" => nil
9
9
  }.freeze
10
10
 
11
11
  MAC_BIN_PATH = [
12
12
  "/Applications/Firefox.app/Contents/MacOS/firefox-bin"
13
13
  ].freeze
14
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
15
24
 
16
25
  def merge_required(flags, options, user_data_dir)
17
26
  port = options.fetch(:port, BROWSER_PORT)
@@ -21,9 +30,7 @@ module Ferrum
21
30
  end
22
31
 
23
32
  def merge_default(flags, options)
24
- unless options.fetch(:headless, true)
25
- defaults = except("headless")
26
- end
33
+ defaults = except("headless") unless options.fetch(:headless, true)
27
34
 
28
35
  defaults ||= DEFAULT_OPTIONS
29
36
  defaults.merge(flags)
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "cliver"
4
3
  require "net/http"
5
4
  require "json"
6
5
  require "addressable"
@@ -22,7 +21,6 @@ module Ferrum
22
21
  :default_user_agent, :browser_version, :protocol_version,
23
22
  :v8_version, :webkit_version, :xvfb
24
23
 
25
-
26
24
  extend Forwardable
27
25
  delegate path: :command
28
26
 
@@ -32,41 +30,50 @@ module Ferrum
32
30
 
33
31
  def self.process_killer(pid)
34
32
  proc do
35
- begin
36
- if Ferrum.windows?
33
+ if Utils::Platform.windows?
34
+ # Process.kill is unreliable on Windows
35
+ ::Process.kill("KILL", pid) unless system("taskkill /f /t /pid #{pid} >NUL 2>NUL")
36
+ else
37
+ ::Process.kill("USR1", pid)
38
+ start = Utils::ElapsedTime.monotonic_time
39
+ while ::Process.wait(pid, ::Process::WNOHANG).nil?
40
+ sleep(WAIT_KILLED)
41
+ next unless Utils::ElapsedTime.timeout?(start, KILL_TIMEOUT)
42
+
37
43
  ::Process.kill("KILL", pid)
38
- else
39
- ::Process.kill("USR1", pid)
40
- start = Ferrum.monotonic_time
41
- while ::Process.wait(pid, ::Process::WNOHANG).nil?
42
- sleep(WAIT_KILLED)
43
- next unless Ferrum.timeout?(start, KILL_TIMEOUT)
44
- ::Process.kill("KILL", pid)
45
- ::Process.wait(pid)
46
- break
47
- end
44
+ ::Process.wait(pid)
45
+ break
48
46
  end
49
- rescue Errno::ESRCH, Errno::ECHILD
50
47
  end
48
+ rescue Errno::ESRCH, Errno::ECHILD
49
+ # nop
51
50
  end
52
51
  end
53
52
 
54
53
  def self.directory_remover(path)
55
- proc { FileUtils.remove_entry(path) rescue Errno::ENOENT }
54
+ proc {
55
+ begin
56
+ FileUtils.remove_entry(path)
57
+ rescue StandardError
58
+ Errno::ENOENT
59
+ end
60
+ }
56
61
  end
57
62
 
58
63
  def initialize(options)
64
+ @pid = @xvfb = @user_data_dir = nil
65
+
59
66
  if options[:url]
60
67
  url = URI.join(options[:url].to_s, "/json/version")
61
68
  response = JSON.parse(::Net::HTTP.get(url))
62
- set_ws_url(response["webSocketDebuggerUrl"])
69
+ self.ws_url = response["webSocketDebuggerUrl"]
63
70
  parse_browser_versions
64
71
  return
65
72
  end
66
73
 
67
- @pid = @xvfb = @user_data_dir = nil
68
74
  @logger = options[:logger]
69
75
  @process_timeout = options.fetch(:process_timeout, PROCESS_TIMEOUT)
76
+ @env = Hash(options[:env])
70
77
 
71
78
  tmpdir = Dir.mktmpdir("ferrum_user_data_dir_")
72
79
  ObjectSpace.define_finalizer(self, self.class.directory_remover(tmpdir))
@@ -81,7 +88,7 @@ module Ferrum
81
88
  begin
82
89
  read_io, write_io = IO.pipe
83
90
  process_options = { in: File::NULL }
84
- process_options[:pgroup] = true unless Ferrum.windows?
91
+ process_options[:pgroup] = true unless Utils::Platform.windows?
85
92
  process_options[:out] = process_options[:err] = write_io
86
93
 
87
94
  if @command.xvfb?
@@ -89,7 +96,8 @@ module Ferrum
89
96
  ObjectSpace.define_finalizer(self, self.class.process_killer(@xvfb.pid))
90
97
  end
91
98
 
92
- @pid = ::Process.spawn(Hash(@xvfb&.to_env), *@command.to_a, process_options)
99
+ env = Hash(@xvfb&.to_env).merge(@env)
100
+ @pid = ::Process.spawn(env, *@command.to_a, process_options)
93
101
  ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))
94
102
 
95
103
  parse_ws_url(read_io, @process_timeout)
@@ -128,29 +136,29 @@ module Ferrum
128
136
 
129
137
  def parse_ws_url(read_io, timeout)
130
138
  output = ""
131
- start = Ferrum.monotonic_time
139
+ start = Utils::ElapsedTime.monotonic_time
132
140
  max_time = start + timeout
133
- regexp = /DevTools listening on (ws:\/\/.*)/
134
- while (now = Ferrum.monotonic_time) < max_time
141
+ regexp = %r{DevTools listening on (ws://.*)}
142
+ while (now = Utils::ElapsedTime.monotonic_time) < max_time
135
143
  begin
136
144
  output += read_io.read_nonblock(512)
137
145
  rescue IO::WaitReadable
138
- IO.select([read_io], nil, nil, max_time - now)
146
+ read_io.wait_readable(max_time - now)
139
147
  else
140
148
  if output.match(regexp)
141
- set_ws_url(output.match(regexp)[1].strip)
149
+ self.ws_url = output.match(regexp)[1].strip
142
150
  break
143
151
  end
144
152
  end
145
153
  end
146
154
 
147
- unless ws_url
148
- @logger.puts(output) if @logger
149
- raise ProcessTimeoutError.new(timeout, output)
150
- end
155
+ return if ws_url
156
+
157
+ @logger&.puts(output)
158
+ raise ProcessTimeoutError.new(timeout, output)
151
159
  end
152
160
 
153
- def set_ws_url(url)
161
+ def ws_url=(url)
154
162
  @ws_url = Addressable::URI.parse(url)
155
163
  @host = @ws_url.host
156
164
  @port = @ws_url.port
@@ -171,11 +179,9 @@ module Ferrum
171
179
 
172
180
  def close_io(*ios)
173
181
  ios.each do |io|
174
- begin
175
- io.close unless io.closed?
176
- rescue IOError
177
- raise unless RUBY_ENGINE == "jruby"
178
- end
182
+ io.close unless io.closed?
183
+ rescue IOError
184
+ raise unless RUBY_ENGINE == "jruby"
179
185
  end
180
186
  end
181
187
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "concurrent-ruby"
4
-
5
3
  module Ferrum
6
4
  class Browser
7
5
  class Subscriber
@@ -29,7 +27,7 @@ module Ferrum
29
27
  method, params = message.values_at("method", "params")
30
28
  total = @on[method].size
31
29
  @on[method].each_with_index do |block, index|
32
- # If there are a few callback we provide current index and total
30
+ # In case of multiple callbacks we provide current index and total
33
31
  block.call(params, index, total)
34
32
  end
35
33
  end
@@ -21,9 +21,7 @@ module Ferrum
21
21
  @driver = ::WebSocket::Driver.client(self, max_length: max_receive_size)
22
22
  @messages = Queue.new
23
23
 
24
- if SKIP_LOGGING_SCREENSHOTS
25
- @screenshot_commands = Concurrent::Hash.new
26
- end
24
+ @screenshot_commands = Concurrent::Hash.new if SKIP_LOGGING_SCREENSHOTS
27
25
 
28
26
  @driver.on(:open, &method(:on_open))
29
27
  @driver.on(:message, &method(:on_message))
@@ -31,12 +29,13 @@ module Ferrum
31
29
 
32
30
  @thread = Thread.new do
33
31
  Thread.current.abort_on_exception = true
34
- if Thread.current.respond_to?(:report_on_exception=)
35
- Thread.current.report_on_exception = true
36
- end
32
+ Thread.current.report_on_exception = true if Thread.current.respond_to?(:report_on_exception=)
37
33
 
38
34
  begin
39
- while data = @sock.readpartial(512)
35
+ loop do
36
+ data = @sock.readpartial(512)
37
+ break unless data
38
+
40
39
  @driver.parse(data)
41
40
  end
42
41
  rescue EOFError, Errno::ECONNRESET, Errno::EPIPE
@@ -62,7 +61,7 @@ module Ferrum
62
61
  output.sub!(/{"data":"(.*)"}/, %("Set FERRUM_LOGGING_SCREENSHOTS=true to see screenshots in Base64"))
63
62
  end
64
63
 
65
- @logger&.puts(" ◀ #{Ferrum.elapsed_time} #{output}\n")
64
+ @logger&.puts(" ◀ #{Utils::ElapsedTime.elapsed_time} #{output}\n")
66
65
  end
67
66
 
68
67
  def on_close(_event)
@@ -71,13 +70,11 @@ module Ferrum
71
70
  end
72
71
 
73
72
  def send_message(data)
74
- if SKIP_LOGGING_SCREENSHOTS
75
- @screenshot_commands[data[:id]] = true
76
- end
73
+ @screenshot_commands[data[:id]] = true if SKIP_LOGGING_SCREENSHOTS
77
74
 
78
75
  json = data.to_json
79
76
  @driver.text(json)
80
- @logger&.puts("\n\n▶ #{Ferrum.elapsed_time} #{json}")
77
+ @logger&.puts("\n\n▶ #{Utils::ElapsedTime.elapsed_time} #{json}")
81
78
  end
82
79
 
83
80
  def write(data)
@@ -4,23 +4,19 @@ module Ferrum
4
4
  class Browser
5
5
  class Xvfb
6
6
  NOT_FOUND = "Could not find an executable for the Xvfb. Try to install " \
7
- "it with your package manager".freeze
7
+ "it with your package manager"
8
8
 
9
9
  def self.start(*args)
10
10
  new(*args).tap(&:start)
11
11
  end
12
12
 
13
- def self.xvfb_path
14
- Cliver.detect("Xvfb")
15
- end
16
-
17
13
  attr_reader :screen_size, :display_id, :pid
18
14
 
19
15
  def initialize(options)
20
- @path = self.class.xvfb_path
21
- raise Cliver::Dependency::NotFound.new(NOT_FOUND) unless @path
16
+ @path = Binary.find("Xvfb")
17
+ raise BinaryNotFoundError, NOT_FOUND unless @path
22
18
 
23
- @screen_size = options.fetch(:window_size, [1024, 768]).join("x") + "x24"
19
+ @screen_size = "#{options.fetch(:window_size, [1024, 768]).join('x')}x24"
24
20
  @display_id = (Time.now.to_f * 1000).to_i % 100_000_000
25
21
  end
26
22
 
@@ -3,10 +3,12 @@
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"
8
9
  require "ferrum/browser/process"
9
10
  require "ferrum/browser/client"
11
+ require "ferrum/browser/binary"
10
12
 
11
13
  module Ferrum
12
14
  class Browser
@@ -16,21 +18,23 @@ module Ferrum
16
18
 
17
19
  extend Forwardable
18
20
  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
21
+ delegate %i[targets create_target page pages windows] => :default_context
22
+ delegate %i[go_to goto go back forward refresh reload stop wait_for_reload
21
23
  at_css at_xpath css xpath current_url current_title url title
22
- body doctype set_content
24
+ body doctype content=
23
25
  headers cookies network
24
26
  mouse keyboard
25
27
  screenshot pdf mhtml viewport_size
26
28
  frames frame_by main_frame
27
29
  evaluate evaluate_on evaluate_async execute evaluate_func
28
30
  add_script_tag add_style_tag bypass_csp
29
- on goto] => :page
31
+ on position position=
32
+ playback_rate playback_rate=] => :page
30
33
  delegate %i[default_user_agent] => :process
31
34
 
32
35
  attr_reader :client, :process, :contexts, :logger, :js_errors, :pending_connection_errors,
33
- :slowmo, :base_url, :options, :window_size, :ws_max_receive_size
36
+ :slowmo, :base_url, :options, :window_size, :ws_max_receive_size, :proxy_options,
37
+ :proxy_server
34
38
  attr_writer :timeout
35
39
 
36
40
  def initialize(options = nil)
@@ -44,16 +48,29 @@ module Ferrum
44
48
  @logger, @timeout, @ws_max_receive_size =
45
49
  @options.values_at(:logger, :timeout, :ws_max_receive_size)
46
50
  @js_errors = @options.fetch(:js_errors, false)
51
+
52
+ if @options[:proxy]
53
+ @proxy_options = @options[:proxy]
54
+
55
+ if @proxy_options[:server]
56
+ @proxy_server = Proxy.start(**@proxy_options.slice(:host, :port, :user, :password))
57
+ @proxy_options.merge!(host: @proxy_server.host, port: @proxy_server.port)
58
+ end
59
+
60
+ @options[:browser_options] ||= {}
61
+ address = "#{@proxy_options[:host]}:#{@proxy_options[:port]}"
62
+ @options[:browser_options].merge!("proxy-server" => address)
63
+ @options[:browser_options].merge!("proxy-bypass-list" => @proxy_options[:bypass]) if @proxy_options[:bypass]
64
+ end
65
+
47
66
  @pending_connection_errors = @options.fetch(:pending_connection_errors, true)
48
67
  @slowmo = @options[:slowmo].to_f
49
68
 
50
- if @options.key?(:base_url)
51
- self.base_url = @options[:base_url]
52
- end
69
+ self.base_url = @options[:base_url] if @options.key?(:base_url)
53
70
 
54
- if ENV["FERRUM_DEBUG"] && !@logger
55
- STDOUT.sync = true
56
- @logger = STDOUT
71
+ if ENV.fetch("FERRUM_DEBUG", nil) && !@logger
72
+ $stdout.sync = true
73
+ @logger = $stdout
57
74
  @options[:logger] = @logger
58
75
  end
59
76
 
@@ -71,12 +88,32 @@ module Ferrum
71
88
  @base_url = parsed
72
89
  end
73
90
 
91
+ def create_page(new_context: false)
92
+ page = if new_context
93
+ context = contexts.create
94
+ context.create_page
95
+ else
96
+ default_context.create_page
97
+ end
98
+
99
+ block_given? ? yield(page) : page
100
+ ensure
101
+ if block_given?
102
+ page.close
103
+ context.dispose if new_context
104
+ end
105
+ end
106
+
74
107
  def extensions
75
108
  @extensions ||= Array(@options[:extensions]).map do |ext|
76
109
  (ext.is_a?(Hash) && ext[:source]) || File.read(ext)
77
110
  end
78
111
  end
79
112
 
113
+ def evaluate_on_new_document(expression)
114
+ extensions << expression
115
+ end
116
+
80
117
  def timeout
81
118
  @timeout || DEFAULT_TIMEOUT
82
119
  end
@@ -116,7 +153,7 @@ module Ferrum
116
153
  private
117
154
 
118
155
  def start
119
- Ferrum.started
156
+ Utils::ElapsedTime.start
120
157
  @process = Process.start(@options)
121
158
  @client = Client.new(self, @process.ws_url)
122
159
  @contexts = Contexts.new(self)
@@ -9,8 +9,10 @@ module Ferrum
9
9
  attr_reader :id, :targets
10
10
 
11
11
  def initialize(browser, contexts, id)
12
- @browser, @contexts, @id = browser, contexts, id
13
- @targets = Concurrent::Hash.new
12
+ @id = id
13
+ @browser = browser
14
+ @contexts = contexts
15
+ @targets = Concurrent::Map.new
14
16
  @pendings = Concurrent::MVar.new
15
17
  end
16
18
 
@@ -32,6 +34,7 @@ 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)
@@ -47,14 +50,15 @@ module Ferrum
47
50
  url: "about:blank")
48
51
  target = @pendings.take(@browser.timeout)
49
52
  raise NoSuchTargetError unless target.is_a?(Target)
50
- @targets[target.id] = target
53
+
54
+ @targets.put_if_absent(target.id, target)
51
55
  target
52
56
  end
53
57
 
54
58
  def add_target(params)
55
59
  target = Target.new(@browser, params)
56
60
  if target.window?
57
- @targets[target.id] = target
61
+ @targets.put_if_absent(target.id, target)
58
62
  else
59
63
  @pendings.put(target, @browser.timeout)
60
64
  end
@@ -72,6 +76,10 @@ module Ferrum
72
76
  @contexts.dispose(@id)
73
77
  end
74
78
 
79
+ def target?(target_id)
80
+ !!@targets[target_id]
81
+ end
82
+
75
83
  def inspect
76
84
  %(#<#{self.class} @id=#{@id.inspect} @targets=#{@targets.inspect} @default_target=#{@default_target.inspect}>)
77
85
  end
@@ -7,7 +7,7 @@ module Ferrum
7
7
  attr_reader :contexts
8
8
 
9
9
  def initialize(browser)
10
- @contexts = Concurrent::Hash.new
10
+ @contexts = Concurrent::Map.new
11
11
  @browser = browser
12
12
  subscribe
13
13
  discover
@@ -18,7 +18,9 @@ module Ferrum
18
18
  end
19
19
 
20
20
  def find_by(target_id:)
21
- @contexts.find { |_, c| c.targets.keys.include?(target_id) }&.last
21
+ context = nil
22
+ @contexts.each_value { |c| context = c if c.target?(target_id) }
23
+ context
22
24
  end
23
25
 
24
26
  def create
@@ -39,7 +41,11 @@ module Ferrum
39
41
 
40
42
  def reset
41
43
  @default_context = nil
42
- @contexts.keys.each { |id| dispose(id) }
44
+ @contexts.each_key { |id| dispose(id) }
45
+ end
46
+
47
+ def size
48
+ @contexts.size
43
49
  end
44
50
 
45
51
  private
@@ -62,15 +68,13 @@ module Ferrum
62
68
  end
63
69
 
64
70
  @browser.client.on("Target.targetDestroyed") do |params|
65
- if context = find_by(target_id: params["targetId"])
66
- context.delete_target(params["targetId"])
67
- end
71
+ context = find_by(target_id: params["targetId"])
72
+ context&.delete_target(params["targetId"])
68
73
  end
69
74
 
70
75
  @browser.client.on("Target.targetCrashed") do |params|
71
- if context = find_by(target_id: params["targetId"])
72
- context.delete_target(params["targetId"])
73
- end
76
+ context = find_by(target_id: params["targetId"])
77
+ context&.delete_target(params["targetId"])
74
78
  end
75
79
  end
76
80
 
@@ -3,6 +3,8 @@
3
3
  module Ferrum
4
4
  class Cookies
5
5
  class Cookie
6
+ attr_reader :attributes
7
+
6
8
  def initialize(attributes)
7
9
  @attributes = attributes
8
10
  end
@@ -44,9 +46,7 @@ module Ferrum
44
46
  end
45
47
 
46
48
  def expires
47
- if @attributes["expires"] > 0
48
- Time.at(@attributes["expires"])
49
- end
49
+ Time.at(@attributes["expires"]) if @attributes["expires"].positive?
50
50
  end
51
51
  end
52
52
 
@@ -56,24 +56,25 @@ module Ferrum
56
56
 
57
57
  def all
58
58
  cookies = @page.command("Network.getAllCookies")["cookies"]
59
- cookies.map { |c| [c["name"], Cookie.new(c)] }.to_h
59
+ cookies.to_h { |c| [c["name"], Cookie.new(c)] }
60
60
  end
61
61
 
62
62
  def [](name)
63
63
  all[name]
64
64
  end
65
65
 
66
- def set(name: nil, value: nil, **options)
67
- cookie = options.dup
68
- cookie[:name] ||= name
69
- cookie[:value] ||= value
66
+ def set(options)
67
+ cookie = (
68
+ options.is_a?(Cookie) ? options.attributes : options
69
+ ).dup.transform_keys(&:to_sym)
70
+
70
71
  cookie[:domain] ||= default_domain
71
72
 
72
73
  cookie[:httpOnly] = cookie.delete(:httponly) if cookie.key?(:httponly)
73
74
  cookie[:sameSite] = cookie.delete(:samesite) if cookie.key?(:samesite)
74
75
 
75
76
  expires = cookie.delete(:expires).to_i
76
- cookie[:expires] = expires if expires > 0
77
+ cookie[:expires] = expires if expires.positive?
77
78
 
78
79
  @page.command("Network.setCookie", **cookie)["success"]
79
80
  end