ferrum 0.10.1 → 0.12

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 (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