selenium-webdriver 4.39.0 → 4.40.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +13 -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 -49
  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 +7 -0
  14. data/lib/selenium/webdriver/bidi/browsing_context.rb +2 -1
  15. data/lib/selenium/webdriver/bidi/log_handler.rb +5 -0
  16. data/lib/selenium/webdriver/bidi/network/cookies.rb +4 -0
  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 +4 -0
  22. data/lib/selenium/webdriver/bidi/network/intercepted_response.rb +4 -0
  23. data/lib/selenium/webdriver/bidi/network/url_pattern.rb +4 -0
  24. data/lib/selenium/webdriver/bidi/network.rb +6 -0
  25. data/lib/selenium/webdriver/bidi/session.rb +4 -0
  26. data/lib/selenium/webdriver/chrome/driver.rb +3 -2
  27. data/lib/selenium/webdriver/common/driver.rb +0 -5
  28. data/lib/selenium/webdriver/common/local_driver.rb +11 -1
  29. data/lib/selenium/webdriver/common/options.rb +10 -0
  30. data/lib/selenium/webdriver/common/service_manager.rb +29 -3
  31. data/lib/selenium/webdriver/common/socket_poller.rb +1 -1
  32. data/lib/selenium/webdriver/common/wait.rb +4 -1
  33. data/lib/selenium/webdriver/common/websocket_connection.rb +73 -37
  34. data/lib/selenium/webdriver/edge/driver.rb +3 -2
  35. data/lib/selenium/webdriver/firefox/driver.rb +3 -2
  36. data/lib/selenium/webdriver/ie/driver.rb +3 -2
  37. data/lib/selenium/webdriver/remote/bidi_bridge.rb +4 -2
  38. data/lib/selenium/webdriver/remote/bridge.rb +12 -4
  39. data/lib/selenium/webdriver/remote/http/common.rb +32 -0
  40. data/lib/selenium/webdriver/safari/driver.rb +3 -2
  41. data/lib/selenium/webdriver/version.rb +1 -1
  42. data/lib/selenium/webdriver.rb +1 -1
  43. metadata +2 -2
@@ -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
 
@@ -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
@@ -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
@@ -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
@@ -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
 
@@ -28,6 +28,7 @@ module Selenium
28
28
  'Accept' => CONTENT_TYPE,
29
29
  'Content-Type' => "#{CONTENT_TYPE}; charset=UTF-8"
30
30
  }.freeze
31
+ BINARY_ENCODINGS = [Encoding::BINARY, Encoding::ASCII_8BIT].freeze
31
32
 
32
33
  class << self
33
34
  attr_accessor :extra_headers
@@ -55,6 +56,7 @@ module Selenium
55
56
  headers['Cache-Control'] = 'no-cache' if verb == :get
56
57
 
57
58
  if command_hash
59
+ command_hash = ensure_utf8_encoding(command_hash)
58
60
  payload = JSON.generate(command_hash)
59
61
  headers['Content-Length'] = payload.bytesize.to_s if %i[post put].include?(verb)
60
62
 
@@ -91,6 +93,36 @@ module Selenium
91
93
  raise NotImplementedError, 'subclass responsibility'
92
94
  end
93
95
 
96
+ def ensure_utf8_encoding(obj)
97
+ case obj
98
+ when String
99
+ encode_string_to_utf8(obj)
100
+ when Array
101
+ obj.map { |item| ensure_utf8_encoding(item) }
102
+ when Hash
103
+ obj.each_with_object({}) do |(key, value), result|
104
+ result[ensure_utf8_encoding(key)] = ensure_utf8_encoding(value)
105
+ end
106
+ else
107
+ obj
108
+ end
109
+ end
110
+
111
+ def encode_string_to_utf8(str)
112
+ return str if str.encoding == Encoding::UTF_8 && str.valid_encoding?
113
+
114
+ if BINARY_ENCODINGS.include?(str.encoding)
115
+ result = str.dup.force_encoding(Encoding::UTF_8)
116
+ return result if result.valid_encoding?
117
+ end
118
+
119
+ str.encode(Encoding::UTF_8)
120
+ rescue EncodingError => e
121
+ raise Error::WebDriverError,
122
+ "Unable to encode string to UTF-8: #{e.message}. " \
123
+ "String encoding: #{str.encoding}, content: #{str.inspect}"
124
+ end
125
+
94
126
  def create_response(code, body, content_type)
95
127
  code = code.to_i
96
128
  body = body.to_s.strip
@@ -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
@@ -19,6 +19,6 @@
19
19
 
20
20
  module Selenium
21
21
  module WebDriver
22
- VERSION = '4.39.0'
22
+ VERSION = '4.40.0'
23
23
  end # WebDriver
24
24
  end # Selenium
@@ -95,7 +95,7 @@ module Selenium
95
95
  #
96
96
 
97
97
  def self.logger(**)
98
- level = $DEBUG || ENV.key?('DEBUG') ? :debug : :info
98
+ level = $DEBUG || ENV.key?('DEBUG') || ENV.key?('SE_DEBUG') ? :debug : :info
99
99
  @logger ||= WebDriver::Logger.new('Selenium', default_level: level, **)
100
100
  end
101
101
  end # WebDriver
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: selenium-webdriver
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.39.0
4
+ version: 4.40.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Rodionov
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2025-12-06 00:00:00.000000000 Z
13
+ date: 2026-01-18 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: base64