ferrum 0.11 → 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 +178 -29
  4. data/lib/ferrum/browser/binary.rb +46 -0
  5. data/lib/ferrum/browser/client.rb +13 -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 -11
  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 +44 -12
  15. data/lib/ferrum/context.rb +6 -2
  16. data/lib/ferrum/contexts.rb +10 -8
  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 +69 -23
  31. data/lib/ferrum/page/animation.rb +0 -1
  32. data/lib/ferrum/page/frames.rb +46 -20
  33. data/lib/ferrum/page/screenshot.rb +51 -65
  34. data/lib/ferrum/page/stream.rb +38 -0
  35. data/lib/ferrum/page/tracing.rb +71 -0
  36. data/lib/ferrum/page.rb +81 -36
  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 -146
  45. metadata +60 -51
@@ -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,22 +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 position position=
31
+ on position position=
30
32
  playback_rate playback_rate=] => :page
31
33
  delegate %i[default_user_agent] => :process
32
34
 
33
35
  attr_reader :client, :process, :contexts, :logger, :js_errors, :pending_connection_errors,
34
- :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
35
38
  attr_writer :timeout
36
39
 
37
40
  def initialize(options = nil)
@@ -45,16 +48,29 @@ module Ferrum
45
48
  @logger, @timeout, @ws_max_receive_size =
46
49
  @options.values_at(:logger, :timeout, :ws_max_receive_size)
47
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
+
48
66
  @pending_connection_errors = @options.fetch(:pending_connection_errors, true)
49
67
  @slowmo = @options[:slowmo].to_f
50
68
 
51
- if @options.key?(:base_url)
52
- self.base_url = @options[:base_url]
53
- end
69
+ self.base_url = @options[:base_url] if @options.key?(:base_url)
54
70
 
55
- if ENV["FERRUM_DEBUG"] && !@logger
56
- STDOUT.sync = true
57
- @logger = STDOUT
71
+ if ENV.fetch("FERRUM_DEBUG", nil) && !@logger
72
+ $stdout.sync = true
73
+ @logger = $stdout
58
74
  @options[:logger] = @logger
59
75
  end
60
76
 
@@ -72,6 +88,22 @@ module Ferrum
72
88
  @base_url = parsed
73
89
  end
74
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
+
75
107
  def extensions
76
108
  @extensions ||= Array(@options[:extensions]).map do |ext|
77
109
  (ext.is_a?(Hash) && ext[:source]) || File.read(ext)
@@ -121,7 +153,7 @@ module Ferrum
121
153
  private
122
154
 
123
155
  def start
124
- Ferrum.started
156
+ Utils::ElapsedTime.start
125
157
  @process = Process.start(@options)
126
158
  @client = Client.new(self, @process.ws_url)
127
159
  @contexts = Contexts.new(self)
@@ -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,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,6 +50,7 @@ module Ferrum
47
50
  url: "about:blank")
48
51
  target = @pendings.take(@browser.timeout)
49
52
  raise NoSuchTargetError unless target.is_a?(Target)
53
+
50
54
  @targets.put_if_absent(target.id, target)
51
55
  target
52
56
  end
@@ -72,7 +76,7 @@ module Ferrum
72
76
  @contexts.dispose(@id)
73
77
  end
74
78
 
75
- def has_target?(target_id)
79
+ def target?(target_id)
76
80
  !!@targets[target_id]
77
81
  end
78
82
 
@@ -19,7 +19,7 @@ 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
 
@@ -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
 
@@ -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
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ferrum
4
+ class Error < StandardError; end
5
+ class NoSuchPageError < Error; end
6
+ class NoSuchTargetError < Error; end
7
+ class NotImplementedError < Error; end
8
+ class BinaryNotFoundError < Error; end
9
+ class EmptyPathError < Error; end
10
+
11
+ class StatusError < Error
12
+ def initialize(url, message = nil)
13
+ super(message || "Request to #{url} failed to reach server, check DNS and server status")
14
+ end
15
+ end
16
+
17
+ class PendingConnectionsError < StatusError
18
+ attr_reader :pendings
19
+
20
+ def initialize(url, pendings = [])
21
+ @pendings = pendings
22
+
23
+ message = "Request to #{url} reached server, but there are still pending connections: #{pendings.join(', ')}"
24
+
25
+ super(url, message)
26
+ end
27
+ end
28
+
29
+ class TimeoutError < Error
30
+ def message
31
+ "Timed out waiting for response. It's possible that this happened " \
32
+ "because something took a very long time (for example a page load " \
33
+ "was slow). If so, setting the :timeout option to a higher value might " \
34
+ "help."
35
+ end
36
+ end
37
+
38
+ class ScriptTimeoutError < Error
39
+ def message
40
+ "Timed out waiting for evaluated script to return a value"
41
+ end
42
+ end
43
+
44
+ class ProcessTimeoutError < Error
45
+ attr_reader :output
46
+
47
+ def initialize(timeout, output)
48
+ @output = output
49
+ super("Browser did not produce websocket url within #{timeout} seconds, try to increase `:process_timeout`. See https://github.com/rubycdp/ferrum#customization")
50
+ end
51
+ end
52
+
53
+ class DeadBrowserError < Error
54
+ def initialize(message = "Browser is dead or given window is closed")
55
+ super
56
+ end
57
+ end
58
+
59
+ class NodeMovingError < Error
60
+ def initialize(node, prev, current)
61
+ @node = node
62
+ @prev = prev
63
+ @current = current
64
+ super(message)
65
+ end
66
+
67
+ def message
68
+ "#{@node.inspect} that you're trying to click is moving, hence " \
69
+ "we cannot. Previously it was at #{@prev.inspect} but now at " \
70
+ "#{@current.inspect}."
71
+ end
72
+ end
73
+
74
+ class CoordinatesNotFoundError < Error
75
+ def initialize(message = "Could not compute content quads")
76
+ super
77
+ end
78
+ end
79
+
80
+ class BrowserError < Error
81
+ attr_reader :response
82
+
83
+ def initialize(response)
84
+ @response = response
85
+ super(response["message"])
86
+ end
87
+
88
+ def code
89
+ response["code"]
90
+ end
91
+
92
+ def data
93
+ response["data"]
94
+ end
95
+ end
96
+
97
+ class NodeNotFoundError < BrowserError; end
98
+
99
+ class NoExecutionContextError < BrowserError
100
+ def initialize(response = nil)
101
+ response ||= { "message" => "There's no context available" }
102
+ super(response)
103
+ end
104
+ end
105
+
106
+ class JavaScriptError < BrowserError
107
+ attr_reader :class_name, :message, :stack_trace
108
+
109
+ def initialize(response, stack_trace = nil)
110
+ @class_name, @message = response.values_at("className", "description")
111
+ @stack_trace = stack_trace
112
+ super(response.merge("message" => @message))
113
+ end
114
+ end
115
+ end