selenium-webdriver 4.35.0 → 4.41.0

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +49 -0
  3. data/Gemfile +1 -1
  4. data/LICENSE +1 -1
  5. data/NOTICE +1 -1
  6. data/bin/linux/selenium-manager +0 -0
  7. data/bin/macos/selenium-manager +0 -0
  8. data/bin/windows/selenium-manager.exe +0 -0
  9. data/lib/selenium/server.rb +29 -4
  10. data/lib/selenium/webdriver/atoms/findElements.js +62 -48
  11. data/lib/selenium/webdriver/atoms/getAttribute.js +17 -6
  12. data/lib/selenium/webdriver/atoms/isDisplayed.js +34 -23
  13. data/lib/selenium/webdriver/bidi/browser.rb +71 -0
  14. data/lib/selenium/webdriver/bidi/browsing_context.rb +3 -2
  15. data/lib/selenium/webdriver/bidi/log_handler.rb +5 -0
  16. data/lib/selenium/webdriver/bidi/network/cookies.rb +13 -9
  17. data/lib/selenium/webdriver/bidi/network/credentials.rb +4 -0
  18. data/lib/selenium/webdriver/bidi/network/headers.rb +4 -0
  19. data/lib/selenium/webdriver/bidi/network/intercepted_auth.rb +4 -0
  20. data/lib/selenium/webdriver/bidi/network/intercepted_item.rb +4 -0
  21. data/lib/selenium/webdriver/bidi/network/intercepted_request.rb +20 -4
  22. data/lib/selenium/webdriver/bidi/network/intercepted_response.rb +24 -6
  23. data/lib/selenium/webdriver/bidi/network/url_pattern.rb +4 -0
  24. data/lib/selenium/webdriver/bidi/network.rb +18 -9
  25. data/lib/selenium/webdriver/bidi/session.rb +4 -0
  26. data/lib/selenium/webdriver/bidi.rb +1 -1
  27. data/lib/selenium/webdriver/chrome/driver.rb +3 -2
  28. data/lib/selenium/webdriver/chrome/service.rb +10 -0
  29. data/lib/selenium/webdriver/common/child_process.rb +2 -1
  30. data/lib/selenium/webdriver/common/driver.rb +0 -5
  31. data/lib/selenium/webdriver/common/driver_extensions/has_session_events.rb +48 -0
  32. data/lib/selenium/webdriver/common/error.rb +10 -3
  33. data/lib/selenium/webdriver/common/local_driver.rb +11 -1
  34. data/lib/selenium/webdriver/common/logger.rb +28 -0
  35. data/lib/selenium/webdriver/common/manager.rb +2 -0
  36. data/lib/selenium/webdriver/common/options.rb +20 -1
  37. data/lib/selenium/webdriver/common/platform.rb +1 -3
  38. data/lib/selenium/webdriver/common/service.rb +6 -0
  39. data/lib/selenium/webdriver/common/service_manager.rb +36 -4
  40. data/lib/selenium/webdriver/common/socket_poller.rb +1 -1
  41. data/lib/selenium/webdriver/common/wait.rb +4 -1
  42. data/lib/selenium/webdriver/common/websocket_connection.rb +73 -37
  43. data/lib/selenium/webdriver/common.rb +1 -0
  44. data/lib/selenium/webdriver/devtools.rb +1 -1
  45. data/lib/selenium/webdriver/edge/driver.rb +3 -2
  46. data/lib/selenium/webdriver/edge/service.rb +11 -0
  47. data/lib/selenium/webdriver/firefox/driver.rb +3 -2
  48. data/lib/selenium/webdriver/firefox/service.rb +21 -2
  49. data/lib/selenium/webdriver/ie/driver.rb +3 -2
  50. data/lib/selenium/webdriver/ie/service.rb +10 -0
  51. data/lib/selenium/webdriver/remote/bidi_bridge.rb +4 -2
  52. data/lib/selenium/webdriver/remote/bridge.rb +12 -4
  53. data/lib/selenium/webdriver/remote/driver.rb +1 -0
  54. data/lib/selenium/webdriver/remote/features.rb +26 -1
  55. data/lib/selenium/webdriver/remote/http/common.rb +32 -0
  56. data/lib/selenium/webdriver/safari/driver.rb +3 -2
  57. data/lib/selenium/webdriver/support/block_event_listener.rb +5 -1
  58. data/lib/selenium/webdriver/support/color.rb +14 -14
  59. data/lib/selenium/webdriver/support/event_firing_bridge.rb +5 -1
  60. data/lib/selenium/webdriver/version.rb +1 -1
  61. data/lib/selenium/webdriver.rb +7 -2
  62. metadata +4 -8
  63. data/lib/selenium/webdriver/bidi/log/base_log_entry.rb +0 -35
  64. data/lib/selenium/webdriver/bidi/log/console_log_entry.rb +0 -35
  65. data/lib/selenium/webdriver/bidi/log/filter_by.rb +0 -40
  66. data/lib/selenium/webdriver/bidi/log/generic_log_entry.rb +0 -33
  67. data/lib/selenium/webdriver/bidi/log/javascript_log_entry.rb +0 -33
  68. data/lib/selenium/webdriver/bidi/log_inspector.rb +0 -147
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Licensed to the Software Freedom Conservancy (SFC) under one
4
+ # or more contributor license agreements. See the NOTICE file
5
+ # distributed with this work for additional information
6
+ # regarding copyright ownership. The SFC licenses this file
7
+ # to you under the Apache License, Version 2.0 (the
8
+ # "License"); you may not use this file except in compliance
9
+ # with the License. You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing,
14
+ # software distributed under the License is distributed on an
15
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
+ # KIND, either express or implied. See the License for the
17
+ # specific language governing permissions and limitations
18
+ # under the License.
19
+
20
+ module Selenium
21
+ module WebDriver
22
+ module DriverExtensions
23
+ module HasSessionEvents
24
+ #
25
+ # Fires a custom session event to the remote server event bus.
26
+ # This allows test code to trigger server-side utilities that subscribe to
27
+ # the event bus.
28
+ #
29
+ # @param [String] event_type The type of event (e.g., "test:failed", "log:collect")
30
+ # @param [Hash] payload Optional data to include with the event
31
+ # @return [Hash] Response data including success status, event type, and timestamp
32
+ #
33
+ # @example Fire a simple event
34
+ # driver.fire_session_event("test:started")
35
+ #
36
+ # @example Fire an event with payload
37
+ # driver.fire_session_event("test:failed", {
38
+ # testName: "LoginTest",
39
+ # error: "Element not found"
40
+ # })
41
+ #
42
+ def fire_session_event(event_type, payload = nil)
43
+ @bridge.fire_session_event(event_type, payload)
44
+ end
45
+ end # HasSessionEvents
46
+ end # DriverExtensions
47
+ end # WebDriver
48
+ end # Selenium
@@ -38,9 +38,9 @@ module Selenium
38
38
  ERROR_URL = 'https://www.selenium.dev/documentation/webdriver/troubleshooting/errors'
39
39
 
40
40
  URLS = {
41
- NoSuchElementError: "#{ERROR_URL}#no-such-element-exception",
42
- StaleElementReferenceError: "#{ERROR_URL}#stale-element-reference-exception",
43
- InvalidSelectorError: "#{ERROR_URL}#invalid-selector-exception",
41
+ NoSuchElementError: "#{ERROR_URL}#nosuchelementexception",
42
+ StaleElementReferenceError: "#{ERROR_URL}#staleelementreferenceexception",
43
+ InvalidSelectorError: "#{ERROR_URL}#invalidselectorexception",
44
44
  NoSuchDriverError: "#{ERROR_URL}/driver_location"
45
45
  }.freeze
46
46
 
@@ -119,6 +119,13 @@ module Selenium
119
119
 
120
120
  class NoSuchWindowError < WebDriverError; end
121
121
 
122
+ #
123
+ # A command to find a devtools target could not be satisfied because
124
+ # the target could not be found.
125
+ #
126
+
127
+ class NoSuchTargetError < WebDriverError; end
128
+
122
129
  #
123
130
  # The element does not have a shadow root.
124
131
  #
@@ -27,7 +27,17 @@ module Selenium
27
27
  caps = process_options(options, service)
28
28
  url = service_url(service)
29
29
 
30
- [caps, url]
30
+ begin
31
+ yield(caps, url) if block_given?
32
+ rescue Selenium::WebDriver::Error::WebDriverError
33
+ @service_manager&.stop
34
+ raise
35
+ end
36
+ end
37
+
38
+ def service_url(service)
39
+ @service_manager = service.launch
40
+ @service_manager.uri
31
41
  end
32
42
 
33
43
  def process_options(options, service)
@@ -55,9 +55,32 @@ module Selenium
55
55
  @ignored = Array(ignored)
56
56
  @allowed = Array(allowed)
57
57
  @first_warning = false
58
+ @level_forced = false
59
+ @output_forced = false
60
+ end
61
+
62
+ #
63
+ # Forces debug level and prevents it from being overridden.
64
+ #
65
+ def debug!
66
+ @level_forced = true
67
+ @logger.level = :debug
68
+ end
69
+
70
+ #
71
+ # Forces output to stderr and prevents it from being overridden.
72
+ #
73
+ def stderr!
74
+ @output_forced = true
75
+ @logger.reopen($stderr)
58
76
  end
59
77
 
60
78
  def level=(level)
79
+ if @level_forced
80
+ warn('Logger level is forced; ignoring override', id: :logger)
81
+ return
82
+ end
83
+
61
84
  if level == :info && @logger.level == :info
62
85
  info(':info is now the default log level, to see additional logging, set log level to :debug')
63
86
  end
@@ -71,6 +94,11 @@ module Selenium
71
94
  # @param [String] io
72
95
  #
73
96
  def output=(io)
97
+ if @output_forced
98
+ warn('Logger output is forced; ignoring override', id: :logger)
99
+ return
100
+ end
101
+
74
102
  @logger.reopen(io)
75
103
  end
76
104
 
@@ -79,6 +79,8 @@ module Selenium
79
79
  #
80
80
 
81
81
  def delete_cookie(name)
82
+ raise ArgumentError, 'Cookie name cannot be null or empty' if name.nil? || name.to_s.strip.empty?
83
+
82
84
  @bridge.delete_cookie name
83
85
  end
84
86
 
@@ -71,6 +71,8 @@ module Selenium
71
71
  def initialize(**opts)
72
72
  self.class.set_capabilities
73
73
 
74
+ opts[:web_socket_url] = opts.delete(:bidi) if opts.key?(:bidi)
75
+
74
76
  @options = opts
75
77
  @options[:browser_name] = self.class::BROWSER
76
78
  end
@@ -91,6 +93,14 @@ module Selenium
91
93
  @options[name] = value
92
94
  end
93
95
 
96
+ def enable_bidi!
97
+ @options[:web_socket_url] = true
98
+ end
99
+
100
+ def bidi?
101
+ !!@options[:web_socket_url]
102
+ end
103
+
94
104
  def ==(other)
95
105
  return false unless other.is_a? self.class
96
106
 
@@ -131,11 +141,20 @@ module Selenium
131
141
 
132
142
  def process_w3c_options(options)
133
143
  w3c_options = options.select { |key, val| w3c?(key) && !val.nil? }
134
- w3c_options[:unhandled_prompt_behavior] &&= w3c_options[:unhandled_prompt_behavior]&.to_s&.tr('_', ' ')
144
+ w3c_options[:unhandled_prompt_behavior] &&=
145
+ process_unhandled_prompt_behavior_value(w3c_options[:unhandled_prompt_behavior])
135
146
  options.delete_if { |key, _val| w3c?(key) }
136
147
  w3c_options
137
148
  end
138
149
 
150
+ def process_unhandled_prompt_behavior_value(value)
151
+ if value.is_a?(Hash)
152
+ value.transform_values { |v| process_unhandled_prompt_behavior_value(v) }
153
+ else
154
+ value&.to_s&.tr('_', ' ')
155
+ end
156
+ end
157
+
139
158
  def process_browser_options(_browser_options)
140
159
  nil
141
160
  end
@@ -51,9 +51,7 @@ module Selenium
51
51
  end
52
52
 
53
53
  def ci
54
- if ENV['TRAVIS']
55
- :travis
56
- elsif ENV['JENKINS']
54
+ if ENV['JENKINS']
57
55
  :jenkins
58
56
  elsif ENV['APPVEYOR']
59
57
  :appveyor
@@ -104,6 +104,12 @@ module Selenium
104
104
  def env_path
105
105
  ENV.fetch(self.class::DRIVER_PATH_ENV_KEY, nil)
106
106
  end
107
+
108
+ private
109
+
110
+ def warn_driver_log_override
111
+ WebDriver.logger.warn('SE_DEBUG is set; overriding user-specified driver logging settings', id: :se_debug)
112
+ end
107
113
  end # Service
108
114
  end # WebDriver
109
115
  end # Selenium
@@ -80,7 +80,13 @@ module Selenium
80
80
  def build_process(*command)
81
81
  WebDriver.logger.debug("Executing Process #{command}", id: :driver_service)
82
82
  @process = ChildProcess.build(*command)
83
- @io ||= WebDriver.logger.io if WebDriver.logger.debug?
83
+ if ENV.key?('SE_DEBUG')
84
+ if @io && @io != WebDriver.logger.io
85
+ WebDriver.logger.warn('SE_DEBUG is set; overriding user-specified driver log output to use stderr',
86
+ id: :se_debug)
87
+ end
88
+ @io = WebDriver.logger.io
89
+ end
84
90
  @process.io = @io if @io
85
91
 
86
92
  @process
@@ -127,10 +133,36 @@ module Selenium
127
133
  end
128
134
 
129
135
  def connect_until_stable
130
- socket_poller = SocketPoller.new @host, @port, START_TIMEOUT
131
- return if socket_poller.connected?
136
+ deadline = current_time + START_TIMEOUT
137
+
138
+ loop do
139
+ error = check_connection_error
140
+ return unless error
141
+
142
+ raise Error::WebDriverError, "#{cannot_connect_error_text}: #{error}" if current_time > deadline
143
+
144
+ sleep 0.1
145
+ end
146
+ end
147
+
148
+ def check_connection_error
149
+ response = Net::HTTP.start(@host, @port, open_timeout: 0.5, read_timeout: 1) do |http|
150
+ http.get('/status', {'Connection' => 'close'})
151
+ end
152
+
153
+ return "status returned #{response.code}\n#{response.body}" unless response.is_a?(Net::HTTPSuccess)
154
+
155
+ status = JSON.parse(response.body)
156
+ ready = status['ready'] || status.dig('value', 'ready')
157
+ "driver not ready: #{response.body}" unless ready
158
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE, Errno::ETIMEDOUT,
159
+ Errno::EADDRNOTAVAIL, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout,
160
+ EOFError, SocketError, Net::HTTPBadResponse, JSON::ParserError => e
161
+ "#{e.class}: #{e.message}"
162
+ end
132
163
 
133
- raise Error::WebDriverError, cannot_connect_error_text
164
+ def current_time
165
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
134
166
  end
135
167
 
136
168
  def cannot_connect_error_text
@@ -93,7 +93,7 @@ module Selenium
93
93
  true
94
94
  rescue *NOT_CONNECTED_ERRORS
95
95
  sock&.close
96
- WebDriver.logger.debug("polling for socket on #{[@host, @port].inspect}", id: :driver_service)
96
+ WebDriver.logger.debug("polling for socket on #{[@host, @port].inspect}", id: :socket_poller)
97
97
  false
98
98
  end
99
99
  end
@@ -37,7 +37,8 @@ module Selenium
37
37
  @timeout = opts.fetch(:timeout, DEFAULT_TIMEOUT)
38
38
  @interval = opts.fetch(:interval, DEFAULT_INTERVAL)
39
39
  @message = opts[:message]
40
- @ignored = Array(opts[:ignore] || Error::NoSuchElementError)
40
+ @message_provider = opts[:message_provider]
41
+ @ignored = Array(opts[:ignore] || Error::NoSuchElementError)
41
42
  end
42
43
 
43
44
  #
@@ -64,6 +65,8 @@ module Selenium
64
65
 
65
66
  msg = if @message
66
67
  @message.dup
68
+ elsif @message_provider
69
+ @message_provider.call
67
70
  else
68
71
  "timed out after #{@timeout} seconds"
69
72
  end
@@ -24,7 +24,10 @@ module Selenium
24
24
  class WebSocketConnection
25
25
  CONNECTION_ERRORS = [
26
26
  Errno::ECONNRESET, # connection is aborted (browser process was killed)
27
- Errno::EPIPE # broken pipe (browser process was killed)
27
+ Errno::EPIPE, # broken pipe (browser process was killed)
28
+ Errno::EBADF, # file descriptor already closed (double-close or GC)
29
+ IOError, # Ruby socket read/write after close
30
+ EOFError # socket reached EOF after remote closed cleanly
28
31
  ].freeze
29
32
 
30
33
  RESPONSE_WAIT_TIMEOUT = 30
@@ -35,6 +38,11 @@ module Selenium
35
38
  def initialize(url:)
36
39
  @callback_threads = ThreadGroup.new
37
40
 
41
+ @callbacks_mtx = Mutex.new
42
+ @messages_mtx = Mutex.new
43
+ @closing_mtx = Mutex.new
44
+
45
+ @closing = false
38
46
  @session_id = nil
39
47
  @url = url
40
48
 
@@ -43,9 +51,26 @@ module Selenium
43
51
  end
44
52
 
45
53
  def close
46
- @callback_threads.list.each(&:exit)
47
- @socket_thread.exit
48
- socket.close
54
+ @closing_mtx.synchronize do
55
+ return if @closing
56
+
57
+ @closing = true
58
+ end
59
+
60
+ begin
61
+ socket.close
62
+ rescue *CONNECTION_ERRORS => e
63
+ WebDriver.logger.debug "WebSocket listener closed: #{e.class}: #{e.message}", id: :ws
64
+ # already closed
65
+ end
66
+
67
+ # Let threads unwind instead of calling exit
68
+ @socket_thread&.join(0.5)
69
+ @callback_threads.list.each do |thread|
70
+ thread.join(0.5)
71
+ rescue StandardError => e
72
+ WebDriver.logger.debug "Failed to join thread during close: #{e.class}: #{e.message}", id: :ws
73
+ end
49
74
  end
50
75
 
51
76
  def callbacks
@@ -53,62 +78,73 @@ module Selenium
53
78
  end
54
79
 
55
80
  def add_callback(event, &block)
56
- callbacks[event] << block
57
- block.object_id
81
+ @callbacks_mtx.synchronize do
82
+ callbacks[event] << block
83
+ block.object_id
84
+ end
58
85
  end
59
86
 
60
87
  def remove_callback(event, id)
61
- return if callbacks[event].reject! { |callback| callback.object_id == id }
88
+ @callbacks_mtx.synchronize do
89
+ return if @closing
90
+
91
+ callbacks_for_event = callbacks[event]
92
+ return if callbacks_for_event.reject! { |cb| cb.object_id == id }
62
93
 
63
- ids = callbacks[event]&.map(&:object_id)
64
- raise Error::WebDriverError, "Callback with ID #{id} does not exist for event #{event}: #{ids}"
94
+ ids = callbacks_for_event.map(&:object_id)
95
+ raise Error::WebDriverError, "Callback with ID #{id} does not exist for event #{event}: #{ids}"
96
+ end
65
97
  end
66
98
 
67
99
  def send_cmd(**payload)
68
100
  id = next_id
69
101
  data = payload.merge(id: id)
70
- WebDriver.logger.debug "WebSocket -> #{data}"[...MAX_LOG_MESSAGE_SIZE], id: :bidi
102
+ WebDriver.logger.debug "WebSocket -> #{data}"[...MAX_LOG_MESSAGE_SIZE], id: :ws
71
103
  data = JSON.generate(data)
72
104
  out_frame = WebSocket::Frame::Outgoing::Client.new(version: ws.version, data: data, type: 'text')
73
- socket.write(out_frame.to_s)
74
105
 
75
- wait.until { messages.delete(id) }
106
+ begin
107
+ socket.write(out_frame.to_s)
108
+ rescue *CONNECTION_ERRORS => e
109
+ raise e, "WebSocket is closed (#{e.class}: #{e.message})"
110
+ end
111
+
112
+ wait.until { @messages_mtx.synchronize { messages.delete(id) } }
76
113
  end
77
114
 
78
115
  private
79
116
 
80
- # We should be thread-safe to use the hash without synchronization
81
- # because its keys are WebSocket message identifiers and they should be
82
- # unique within a devtools session.
83
117
  def messages
84
118
  @messages ||= {}
85
119
  end
86
120
 
87
121
  def process_handshake
88
122
  socket.print(ws.to_s)
89
- ws << socket.readpartial(1024)
123
+ ws << socket.readpartial(1024) until ws.finished?
90
124
  end
91
125
 
92
126
  def attach_socket_listener
93
127
  Thread.new do
94
- Thread.current.abort_on_exception = true
95
128
  Thread.current.report_on_exception = false
96
129
 
97
- until socket.eof?
130
+ loop do
131
+ break if @closing
132
+
98
133
  incoming_frame << socket.readpartial(1024)
99
134
 
100
135
  while (frame = incoming_frame.next)
136
+ break if @closing
137
+
101
138
  message = process_frame(frame)
102
139
  next unless message['method']
103
140
 
104
- params = message['params']
105
- callbacks[message['method']].each do |callback|
106
- @callback_threads.add(callback_thread(params, &callback))
141
+ @messages_mtx.synchronize { callbacks[message['method']].dup }.each do |callback|
142
+ @callback_threads.add(callback_thread(message['params'], &callback))
107
143
  end
108
144
  end
109
145
  end
110
- rescue *CONNECTION_ERRORS
111
- Thread.stop
146
+ rescue *CONNECTION_ERRORS, WebSocket::Error => e
147
+ WebDriver.logger.debug "WebSocket listener closed: #{e.class}: #{e.message}", id: :ws
112
148
  end
113
149
  end
114
150
 
@@ -122,27 +158,27 @@ module Selenium
122
158
  # Firefox will periodically fail on unparsable empty frame
123
159
  return {} if message.empty?
124
160
 
125
- message = JSON.parse(message)
126
- messages[message['id']] = message
127
- WebDriver.logger.debug "WebSocket <- #{message}"[...MAX_LOG_MESSAGE_SIZE], id: :bidi
161
+ msg = JSON.parse(message)
162
+ @messages_mtx.synchronize { messages[msg['id']] = msg if msg.key?('id') }
128
163
 
129
- message
164
+ WebDriver.logger.debug "WebSocket <- #{msg}"[...MAX_LOG_MESSAGE_SIZE], id: :ws
165
+ msg
130
166
  end
131
167
 
132
168
  def callback_thread(params)
133
169
  Thread.new do
134
- Thread.current.abort_on_exception = true
135
-
136
- # We might end up blocked forever when we have an error in event.
137
- # For example, if network interception event raises error,
138
- # the browser will keep waiting for the request to be proceeded
139
- # before returning back to the original thread. In this case,
140
- # we should at least print the error.
141
- Thread.current.report_on_exception = true
170
+ Thread.current.abort_on_exception = false
171
+ Thread.current.report_on_exception = false
172
+ next if @closing
142
173
 
143
174
  yield params
144
- rescue Error::WebDriverError, *CONNECTION_ERRORS
145
- Thread.stop
175
+ rescue Error::WebDriverError, *CONNECTION_ERRORS => e
176
+ WebDriver.logger.debug "Callback aborted: #{e.class}: #{e.message}", id: :ws
177
+ rescue StandardError => e
178
+ next if @closing
179
+
180
+ bt = Array(e.backtrace).first(5).join("\n")
181
+ WebDriver.logger.error "Callback error: #{e.class}: #{e.message}\n#{bt}", id: :ws
146
182
  end
147
183
  end
148
184
 
@@ -78,6 +78,7 @@ require 'selenium/webdriver/common/driver_extensions/has_addons'
78
78
  require 'selenium/webdriver/common/driver_extensions/has_bidi'
79
79
  require 'selenium/webdriver/common/driver_extensions/has_devtools'
80
80
  require 'selenium/webdriver/common/driver_extensions/has_file_downloads'
81
+ require 'selenium/webdriver/common/driver_extensions/has_session_events'
81
82
  require 'selenium/webdriver/common/driver_extensions/has_authentication'
82
83
  require 'selenium/webdriver/common/driver_extensions/has_logs'
83
84
  require 'selenium/webdriver/common/driver_extensions/has_log_events'
@@ -84,7 +84,7 @@ module Selenium
84
84
  def start_session(target_type:)
85
85
  targets = target.get_targets.dig('result', 'targetInfos')
86
86
  found_target = targets.find { |target| target['type'] == target_type }
87
- raise Error::WebDriverError, "Target type '#{target_type}' not found" unless found_target
87
+ raise Error::NoSuchTargetError, "Target type '#{target_type}' not found" unless found_target
88
88
 
89
89
  session = target.attach_to_target(target_id: found_target['targetId'], flatten: true)
90
90
  @session_id = session.dig('result', 'sessionId')
@@ -31,8 +31,9 @@ module Selenium
31
31
  include LocalDriver
32
32
 
33
33
  def initialize(options: nil, service: nil, url: nil, **)
34
- caps, url = initialize_local_driver(options, service, url)
35
- super(caps: caps, url: url, **)
34
+ initialize_local_driver(options, service, url) do |caps, driver_url|
35
+ super(caps: caps, url: driver_url, **)
36
+ end
36
37
  end
37
38
 
38
39
  def browser
@@ -25,6 +25,17 @@ module Selenium
25
25
  EXECUTABLE = 'msedgedriver'
26
26
  SHUTDOWN_SUPPORTED = true
27
27
  DRIVER_PATH_ENV_KEY = 'SE_EDGEDRIVER'
28
+
29
+ def initialize(args: nil, **)
30
+ if ENV.key?('SE_DEBUG')
31
+ args = Array(args.dup)
32
+ warn_driver_log_override if args.reject! { |arg| arg.include?('log-level') || arg.include?('silent') }
33
+ args << '--verbose'
34
+ end
35
+
36
+ super
37
+ end
38
+
28
39
  def log
29
40
  return @log unless @log.is_a? String
30
41
 
@@ -37,8 +37,9 @@ module Selenium
37
37
  include LocalDriver
38
38
 
39
39
  def initialize(options: nil, service: nil, url: nil, **)
40
- caps, url = initialize_local_driver(options, service, url)
41
- super(caps: caps, url: url, **)
40
+ initialize_local_driver(options, service, url) do |caps, driver_url|
41
+ super(caps: caps, url: driver_url, **)
42
+ end
42
43
  end
43
44
 
44
45
  def browser
@@ -26,14 +26,33 @@ module Selenium
26
26
  SHUTDOWN_SUPPORTED = false
27
27
  DRIVER_PATH_ENV_KEY = 'SE_GECKODRIVER'
28
28
 
29
- def initialize(path: nil, port: nil, log: nil, args: nil)
30
- args ||= []
29
+ def initialize(args: nil, **)
30
+ args = Array(args.dup)
31
31
  unless args.any? { |arg| arg.include?('--connect-existing') || arg.include?('--websocket-port') }
32
32
  args << '--websocket-port'
33
33
  args << '0'
34
34
  end
35
+
36
+ if ENV.key?('SE_DEBUG')
37
+ remove_log_args(args)
38
+ args << '-v'
39
+ end
40
+
35
41
  super
36
42
  end
43
+
44
+ private
45
+
46
+ def remove_log_args(args)
47
+ if (index = args.index('--log'))
48
+ args.delete_at(index) # delete '--log'
49
+ args.delete_at(index) if args[index] && !args[index].start_with?('-') # delete value if present
50
+ warn_driver_log_override
51
+ elsif (index = args.index { |arg| arg.start_with?('--log=') })
52
+ args.delete_at(index)
53
+ warn_driver_log_override
54
+ end
55
+ end
37
56
  end # Service
38
57
  end # Firefox
39
58
  end # WebDriver
@@ -32,8 +32,9 @@ module Selenium
32
32
  include LocalDriver
33
33
 
34
34
  def initialize(options: nil, service: nil, url: nil, **)
35
- caps, url = initialize_local_driver(options, service, url)
36
- super(caps: caps, url: url, **)
35
+ initialize_local_driver(options, service, url) do |caps, driver_url|
36
+ super(caps: caps, url: driver_url, **)
37
+ end
37
38
  end
38
39
 
39
40
  def browser
@@ -25,6 +25,16 @@ module Selenium
25
25
  EXECUTABLE = 'IEDriverServer'
26
26
  SHUTDOWN_SUPPORTED = true
27
27
  DRIVER_PATH_ENV_KEY = 'SE_IEDRIVER'
28
+
29
+ def initialize(args: nil, **)
30
+ if ENV.key?('SE_DEBUG')
31
+ args = Array(args.dup)
32
+ warn_driver_log_override if args.reject! { |arg| arg.include?('log-level') || arg.include?('silent') }
33
+ args << '--log-level=DEBUG'
34
+ end
35
+
36
+ super
37
+ end
28
38
  end # Server
29
39
  end # IE
30
40
  end # WebDriver
@@ -46,9 +46,11 @@ module Selenium
46
46
  end
47
47
 
48
48
  def quit
49
- super
50
- ensure
51
49
  bidi.close
50
+ rescue *QUIT_ERRORS
51
+ nil
52
+ ensure
53
+ super
52
54
  end
53
55
 
54
56
  def close
@@ -206,12 +206,20 @@ module Selenium
206
206
  switch_to_frame nil
207
207
  end
208
208
 
209
- QUIT_ERRORS = [IOError].freeze
209
+ QUIT_ERRORS = [IOError, EOFError, WebSocket::Error].freeze
210
210
 
211
211
  def quit
212
- execute :delete_session
213
- http.close
214
- rescue *QUIT_ERRORS
212
+ begin
213
+ execute :delete_session
214
+ rescue *QUIT_ERRORS => e
215
+ WebDriver.logger.debug "delete_session failed during quit: #{e.class}: #{e.message}", id: :quit
216
+ ensure
217
+ begin
218
+ http.close
219
+ rescue *QUIT_ERRORS => e
220
+ WebDriver.logger.debug "http.close failed during quit: #{e.class}: #{e.message}", id: :quit
221
+ end
222
+ end
215
223
  nil
216
224
  end
217
225