selenium-webdriver 4.0.2 → 4.2.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +70 -0
  3. data/LICENSE +1 -1
  4. data/NOTICE +1 -1
  5. data/lib/selenium/server.rb +14 -9
  6. data/lib/selenium/webdriver/bidi/session.rb +38 -0
  7. data/lib/selenium/webdriver/bidi.rb +55 -0
  8. data/lib/selenium/webdriver/chrome/features.rb +5 -0
  9. data/lib/selenium/webdriver/chrome/options.rb +20 -20
  10. data/lib/selenium/webdriver/common/action_builder.rb +108 -21
  11. data/lib/selenium/webdriver/common/driver.rb +2 -2
  12. data/lib/selenium/webdriver/common/driver_extensions/has_bidi.rb +38 -0
  13. data/lib/selenium/webdriver/common/driver_extensions/has_casting.rb +10 -0
  14. data/lib/selenium/webdriver/common/driver_extensions/has_context.rb +1 -2
  15. data/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb +1 -1
  16. data/lib/selenium/webdriver/common/driver_extensions/has_pinned_scripts.rb +1 -1
  17. data/lib/selenium/webdriver/common/element.rb +1 -1
  18. data/lib/selenium/webdriver/common/error.rb +1 -1
  19. data/lib/selenium/webdriver/common/interactions/input_device.rb +10 -4
  20. data/lib/selenium/webdriver/common/interactions/interaction.rb +12 -25
  21. data/lib/selenium/webdriver/common/interactions/interactions.rb +24 -4
  22. data/lib/selenium/webdriver/common/interactions/key_actions.rb +5 -1
  23. data/lib/selenium/webdriver/common/interactions/key_input.rb +11 -27
  24. data/lib/selenium/webdriver/common/interactions/none_input.rb +10 -8
  25. data/lib/selenium/webdriver/common/interactions/pause.rb +49 -0
  26. data/lib/selenium/webdriver/common/interactions/pointer_actions.rb +49 -43
  27. data/lib/selenium/webdriver/common/interactions/pointer_cancel.rb +45 -0
  28. data/lib/selenium/webdriver/common/interactions/pointer_event_properties.rb +63 -0
  29. data/lib/selenium/webdriver/common/interactions/pointer_input.rb +15 -84
  30. data/lib/selenium/webdriver/common/interactions/pointer_move.rb +60 -0
  31. data/lib/selenium/webdriver/common/interactions/pointer_press.rb +85 -0
  32. data/lib/selenium/webdriver/common/interactions/scroll.rb +57 -0
  33. data/lib/selenium/webdriver/common/interactions/scroll_origin.rb +48 -0
  34. data/lib/selenium/webdriver/common/interactions/typing_interaction.rb +54 -0
  35. data/lib/selenium/webdriver/common/interactions/wheel_actions.rb +113 -0
  36. data/lib/selenium/webdriver/common/interactions/wheel_input.rb +42 -0
  37. data/lib/selenium/webdriver/common/keys.rb +1 -0
  38. data/lib/selenium/webdriver/common/options.rb +1 -3
  39. data/lib/selenium/webdriver/common/platform.rb +4 -4
  40. data/lib/selenium/webdriver/common/search_context.rb +0 -6
  41. data/lib/selenium/webdriver/common/service_manager.rb +2 -3
  42. data/lib/selenium/webdriver/common/shadow_root.rb +1 -1
  43. data/lib/selenium/webdriver/common/socket_poller.rb +1 -1
  44. data/lib/selenium/webdriver/common/websocket_connection.rb +149 -0
  45. data/lib/selenium/webdriver/common/zipper.rb +3 -1
  46. data/lib/selenium/webdriver/common.rb +14 -2
  47. data/lib/selenium/webdriver/devtools/request.rb +1 -1
  48. data/lib/selenium/webdriver/devtools/response.rb +1 -1
  49. data/lib/selenium/webdriver/devtools.rb +5 -101
  50. data/lib/selenium/webdriver/edge/features.rb +1 -0
  51. data/lib/selenium/webdriver/firefox/driver.rb +1 -0
  52. data/lib/selenium/webdriver/firefox/features.rb +2 -5
  53. data/lib/selenium/webdriver/firefox/options.rb +5 -3
  54. data/lib/selenium/webdriver/firefox/profile.rb +1 -5
  55. data/lib/selenium/webdriver/firefox/util.rb +46 -0
  56. data/lib/selenium/webdriver/firefox.rb +1 -0
  57. data/lib/selenium/webdriver/ie/options.rb +2 -2
  58. data/lib/selenium/webdriver/remote/bridge.rb +25 -20
  59. data/lib/selenium/webdriver/remote/commands.rb +0 -5
  60. data/lib/selenium/webdriver/remote/http/default.rb +6 -12
  61. data/lib/selenium/webdriver/remote/response.rb +2 -2
  62. data/lib/selenium/webdriver/remote.rb +6 -5
  63. data/lib/selenium/webdriver/support/cdp_client_generator.rb +4 -4
  64. data/lib/selenium/webdriver/support/color.rb +7 -7
  65. data/lib/selenium/webdriver/support/guards/guard_condition.rb +1 -1
  66. data/lib/selenium/webdriver/support/guards.rb +1 -1
  67. data/lib/selenium/webdriver/support/select.rb +1 -1
  68. data/lib/selenium/webdriver/version.rb +1 -1
  69. data/lib/selenium/webdriver.rb +2 -1
  70. data/selenium-webdriver.gemspec +7 -4
  71. metadata +55 -5
@@ -45,7 +45,7 @@ module Selenium
45
45
  alias_method :eql?, :==
46
46
 
47
47
  def hash
48
- @id.hash ^ @bridge.hash
48
+ [@id, @bridge].hash
49
49
  end
50
50
 
51
51
  #
@@ -99,7 +99,7 @@ module Selenium
99
99
  end
100
100
 
101
101
  def socket_writable?(sock)
102
- IO.select(nil, [sock], nil, CONNECT_TIMEOUT)
102
+ sock.wait_writable(CONNECT_TIMEOUT)
103
103
  end
104
104
 
105
105
  def conn_completed?(sock)
@@ -0,0 +1,149 @@
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
+ require 'websocket'
21
+
22
+ module Selenium
23
+ module WebDriver
24
+ class WebSocketConnection
25
+ RESPONSE_WAIT_TIMEOUT = 30
26
+ RESPONSE_WAIT_INTERVAL = 0.1
27
+
28
+ def initialize(url:)
29
+ @callback_threads = ThreadGroup.new
30
+
31
+ @messages = []
32
+ @session_id = nil
33
+ @url = url
34
+
35
+ process_handshake
36
+ @socket_thread = attach_socket_listener
37
+ end
38
+
39
+ def close
40
+ @callback_threads.list.each(&:exit)
41
+ @socket_thread.exit
42
+ socket.close
43
+ end
44
+
45
+ def callbacks
46
+ @callbacks ||= Hash.new { |callbacks, event| callbacks[event] = [] }
47
+ end
48
+
49
+ def send_cmd(**payload)
50
+ id = next_id
51
+ data = payload.merge(id: id)
52
+ data = JSON.generate(data)
53
+ WebDriver.logger.debug "WebSocket -> #{data}"
54
+
55
+ out_frame = WebSocket::Frame::Outgoing::Client.new(version: ws.version, data: data, type: 'text')
56
+ socket.write(out_frame.to_s)
57
+
58
+ wait.until { @messages.find { |m| m['id'] == id } }
59
+ end
60
+
61
+ private
62
+
63
+ def process_handshake
64
+ socket.print(ws.to_s)
65
+ ws << socket.readpartial(1024)
66
+ end
67
+
68
+ def attach_socket_listener
69
+ Thread.new do
70
+ Thread.current.abort_on_exception = true
71
+ Thread.current.report_on_exception = false
72
+
73
+ until socket.eof?
74
+ incoming_frame << socket.readpartial(1024)
75
+
76
+ while (frame = incoming_frame.next)
77
+ message = process_frame(frame)
78
+ next unless message['method']
79
+
80
+ params = message['params']
81
+ callbacks[message['method']].each do |callback|
82
+ @callback_threads.add(callback_thread(params, &callback))
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ def incoming_frame
90
+ @incoming_frame ||= WebSocket::Frame::Incoming::Client.new(version: ws.version)
91
+ end
92
+
93
+ def process_frame(frame)
94
+ message = frame.to_s
95
+
96
+ # Firefox will periodically fail on unparsable empty frame
97
+ return {} if message.empty?
98
+
99
+ message = JSON.parse(message)
100
+ @messages << message
101
+ WebDriver.logger.debug "WebSocket <- #{message}"
102
+
103
+ message
104
+ end
105
+
106
+ def callback_thread(params)
107
+ Thread.new do
108
+ Thread.current.abort_on_exception = true
109
+
110
+ # We might end up blocked forever when we have an error in event.
111
+ # For example, if network interception event raises error,
112
+ # the browser will keep waiting for the request to be proceeded
113
+ # before returning back to the original thread. In this case,
114
+ # we should at least print the error.
115
+ Thread.current.report_on_exception = true
116
+
117
+ yield params
118
+ end
119
+ end
120
+
121
+ def wait
122
+ @wait ||= Wait.new(timeout: RESPONSE_WAIT_TIMEOUT, interval: RESPONSE_WAIT_INTERVAL)
123
+ end
124
+
125
+ def socket
126
+ @socket ||= if URI(@url).scheme == 'wss'
127
+ socket = TCPSocket.new(ws.host, ws.port)
128
+ socket = OpenSSL::SSL::SSLSocket.new(socket, OpenSSL::SSL::SSLContext.new)
129
+ socket.sync_close = true
130
+ socket.connect
131
+
132
+ socket
133
+ else
134
+ TCPSocket.new(ws.host, ws.port)
135
+ end
136
+ end
137
+
138
+ def ws
139
+ @ws ||= WebSocket::Handshake::Client.new(url: @url)
140
+ end
141
+
142
+ def next_id
143
+ @id ||= 0
144
+ @id += 1
145
+ end
146
+
147
+ end # BiDi
148
+ end # WebDriver
149
+ end # Selenium
@@ -72,7 +72,9 @@ module Selenium
72
72
  private
73
73
 
74
74
  def with_tmp_zip(&blk)
75
- Tempfile.create do |zip_path|
75
+ # Don't use Tempfile since it lacks rb_file_s_rename permission on Windows.
76
+ Dir.mktmpdir do |tmp_dir|
77
+ zip_path = File.join(tmp_dir, 'webdriver-zip')
76
78
  Zip::File.open(zip_path, Zip::File::CREATE, &blk)
77
79
  end
78
80
  end
@@ -38,14 +38,24 @@ require 'selenium/webdriver/common/logger'
38
38
  require 'selenium/webdriver/common/logs'
39
39
  require 'selenium/webdriver/common/manager'
40
40
  require 'selenium/webdriver/common/search_context'
41
+ require 'selenium/webdriver/common/interactions/interaction'
42
+ require 'selenium/webdriver/common/interactions/interactions'
43
+ require 'selenium/webdriver/common/interactions/pointer_event_properties'
44
+ require 'selenium/webdriver/common/interactions/pointer_cancel'
45
+ require 'selenium/webdriver/common/interactions/pointer_move'
46
+ require 'selenium/webdriver/common/interactions/pointer_press'
47
+ require 'selenium/webdriver/common/interactions/typing_interaction'
48
+ require 'selenium/webdriver/common/interactions/pause'
41
49
  require 'selenium/webdriver/common/interactions/key_actions'
42
50
  require 'selenium/webdriver/common/interactions/pointer_actions'
43
- require 'selenium/webdriver/common/interactions/interactions'
44
51
  require 'selenium/webdriver/common/interactions/input_device'
45
- require 'selenium/webdriver/common/interactions/interaction'
46
52
  require 'selenium/webdriver/common/interactions/none_input'
47
53
  require 'selenium/webdriver/common/interactions/key_input'
48
54
  require 'selenium/webdriver/common/interactions/pointer_input'
55
+ require 'selenium/webdriver/common/interactions/scroll'
56
+ require 'selenium/webdriver/common/interactions/wheel_input'
57
+ require 'selenium/webdriver/common/interactions/scroll_origin'
58
+ require 'selenium/webdriver/common/interactions/wheel_actions'
49
59
  require 'selenium/webdriver/common/action_builder'
50
60
  require 'selenium/webdriver/common/html5/shared_web_storage'
51
61
  require 'selenium/webdriver/common/html5/local_storage'
@@ -66,6 +76,7 @@ require 'selenium/webdriver/common/driver_extensions/prints_page'
66
76
  require 'selenium/webdriver/common/driver_extensions/uploads_files'
67
77
  require 'selenium/webdriver/common/driver_extensions/full_page_screenshot'
68
78
  require 'selenium/webdriver/common/driver_extensions/has_addons'
79
+ require 'selenium/webdriver/common/driver_extensions/has_bidi'
69
80
  require 'selenium/webdriver/common/driver_extensions/has_devtools'
70
81
  require 'selenium/webdriver/common/driver_extensions/has_authentication'
71
82
  require 'selenium/webdriver/common/driver_extensions/has_logs'
@@ -81,3 +92,4 @@ require 'selenium/webdriver/common/takes_screenshot'
81
92
  require 'selenium/webdriver/common/driver'
82
93
  require 'selenium/webdriver/common/element'
83
94
  require 'selenium/webdriver/common/shadow_root'
95
+ require 'selenium/webdriver/common/websocket_connection'
@@ -35,7 +35,7 @@ module Selenium
35
35
  id: id,
36
36
  url: params.dig('request', 'url'),
37
37
  method: params.dig('request', 'method'),
38
- headers: params.dig('request', 'headers'),
38
+ headers: params.dig('request', 'headers').dup,
39
39
  post_data: params.dig('request', 'postData')
40
40
  )
41
41
  end
@@ -35,7 +35,7 @@ module Selenium
35
35
  id: id,
36
36
  code: params['responseStatusCode'],
37
37
  body: (Base64.strict_decode64(encoded_body) if encoded_body),
38
- headers: params['responseHeaders'].each_with_object({}) do |header, hash|
38
+ headers: params.fetch('responseHeaders', []).each_with_object({}) do |header, hash|
39
39
  hash[header['name']] = header['value']
40
40
  end
41
41
  )
@@ -20,9 +20,6 @@
20
20
  module Selenium
21
21
  module WebDriver
22
22
  class DevTools
23
- RESPONSE_WAIT_TIMEOUT = 30
24
- RESPONSE_WAIT_INTERVAL = 0.1
25
-
26
23
  autoload :ConsoleEvent, 'selenium/webdriver/devtools/console_event'
27
24
  autoload :ExceptionEvent, 'selenium/webdriver/devtools/exception_event'
28
25
  autoload :MutationEvent, 'selenium/webdriver/devtools/mutation_event'
@@ -31,41 +28,23 @@ module Selenium
31
28
  autoload :Response, 'selenium/webdriver/devtools/response'
32
29
 
33
30
  def initialize(url:)
34
- @callback_threads = ThreadGroup.new
35
-
36
- @messages = []
31
+ @ws = WebSocketConnection.new(url: url)
37
32
  @session_id = nil
38
- @url = url
39
-
40
- process_handshake
41
- @socket_thread = attach_socket_listener
42
33
  start_session
43
34
  end
44
35
 
45
36
  def close
46
- @callback_threads.list.each(&:exit)
47
- @socket_thread.exit
48
- socket.close
37
+ @ws.close
49
38
  end
50
39
 
51
40
  def callbacks
52
- @callbacks ||= Hash.new { |callbacks, event| callbacks[event] = [] }
41
+ @ws.callbacks
53
42
  end
54
43
 
55
44
  def send_cmd(method, **params)
56
- id = next_id
57
- data = {id: id, method: method, params: params.reject { |_, v| v.nil? }}
45
+ data = {method: method, params: params.compact}
58
46
  data[:sessionId] = @session_id if @session_id
59
- data = JSON.generate(data)
60
- WebDriver.logger.debug "DevTools -> #{data}"
61
-
62
- out_frame = WebSocket::Frame::Outgoing::Client.new(version: ws.version, data: data, type: 'text')
63
- socket.write(out_frame.to_s)
64
-
65
- message = wait.until do
66
- @messages.find { |m| m['id'] == id }
67
- end
68
-
47
+ message = @ws.send_cmd(**data)
69
48
  raise Error::WebDriverError, error_message(message['error']) if message['error']
70
49
 
71
50
  message
@@ -91,32 +70,6 @@ module Selenium
91
70
 
92
71
  private
93
72
 
94
- def process_handshake
95
- socket.print(ws.to_s)
96
- ws << socket.readpartial(1024)
97
- end
98
-
99
- def attach_socket_listener
100
- Thread.new do
101
- Thread.current.abort_on_exception = true
102
- Thread.current.report_on_exception = false
103
-
104
- until socket.eof?
105
- incoming_frame << socket.readpartial(1024)
106
-
107
- while (frame = incoming_frame.next)
108
- message = process_frame(frame)
109
- next unless message['method']
110
-
111
- params = message['params']
112
- callbacks[message['method']].each do |callback|
113
- @callback_threads.add(callback_thread(params, &callback))
114
- end
115
- end
116
- end
117
- end
118
- end
119
-
120
73
  def start_session
121
74
  targets = target.get_targets.dig('result', 'targetInfos')
122
75
  page_target = targets.find { |target| target['type'] == 'page' }
@@ -124,55 +77,6 @@ module Selenium
124
77
  @session_id = session.dig('result', 'sessionId')
125
78
  end
126
79
 
127
- def incoming_frame
128
- @incoming_frame ||= WebSocket::Frame::Incoming::Client.new(version: ws.version)
129
- end
130
-
131
- def process_frame(frame)
132
- message = frame.to_s
133
-
134
- # Firefox will periodically fail on unparsable empty frame
135
- return {} if message.empty?
136
-
137
- message = JSON.parse(message)
138
- @messages << message
139
- WebDriver.logger.debug "DevTools <- #{message}"
140
-
141
- message
142
- end
143
-
144
- def callback_thread(params)
145
- Thread.new do
146
- Thread.current.abort_on_exception = true
147
-
148
- # We might end up blocked forever when we have an error in event.
149
- # For example, if network interception event raises error,
150
- # the browser will keep waiting for the request to be proceeded
151
- # before returning back to the original thread. In this case,
152
- # we should at least print the error.
153
- Thread.current.report_on_exception = true
154
-
155
- yield params
156
- end
157
- end
158
-
159
- def wait
160
- @wait ||= Wait.new(timeout: RESPONSE_WAIT_TIMEOUT, interval: RESPONSE_WAIT_INTERVAL)
161
- end
162
-
163
- def socket
164
- @socket ||= TCPSocket.new(ws.host, ws.port)
165
- end
166
-
167
- def ws
168
- @ws ||= WebSocket::Handshake::Client.new(url: @url)
169
- end
170
-
171
- def next_id
172
- @id ||= 0
173
- @id += 1
174
- end
175
-
176
80
  def error_message(error)
177
81
  [error['code'], error['message'], error['data']].join(': ')
178
82
  end
@@ -30,6 +30,7 @@ module Selenium
30
30
  get_cast_sinks: [:get, 'session/:session_id/ms/cast/get_sinks'],
31
31
  set_cast_sink_to_use: [:post, 'session/:session_id/ms/cast/set_sink_to_use'],
32
32
  start_cast_tab_mirroring: [:post, 'session/:session_id/ms/cast/start_tab_mirroring'],
33
+ start_cast_desktop_mirroring: [:post, 'session/:session_id/ms/cast/start_desktop_mirroring'],
33
34
  get_cast_issue_message: [:get, 'session/:session_id/ms/cast/get_issue_message'],
34
35
  stop_casting: [:post, 'session/:session_id/ms/cast/stop_casting'],
35
36
  send_command: [:post, 'session/:session_id/ms/cdp/execute']
@@ -30,6 +30,7 @@ module Selenium
30
30
  EXTENSIONS = [DriverExtensions::HasAddons,
31
31
  DriverExtensions::FullPageScreenshot,
32
32
  DriverExtensions::HasContext,
33
+ DriverExtensions::HasBiDi,
33
34
  DriverExtensions::HasDevTools,
34
35
  DriverExtensions::HasLogEvents,
35
36
  DriverExtensions::HasNetworkInterception,
@@ -35,12 +35,9 @@ module Selenium
35
35
  end
36
36
 
37
37
  def install_addon(path, temporary)
38
- if @file_detector
39
- local_file = @file_detector.call(path)
40
- path = upload(local_file) if local_file
41
- end
38
+ addon = File.open(path, 'rb') { |crx_file| Base64.strict_encode64 crx_file.read }
42
39
 
43
- payload = {path: path}
40
+ payload = {addon: addon}
44
41
  payload[:temporary] = temporary unless temporary.nil?
45
42
  execute :install_addon, {}, payload
46
43
  end
@@ -25,11 +25,12 @@ module Selenium
25
25
 
26
26
  KEY = 'moz:firefoxOptions'
27
27
 
28
- # see: https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html
28
+ # see: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions
29
29
  CAPABILITIES = {binary: 'binary',
30
30
  args: 'args',
31
31
  log: 'log',
32
32
  prefs: 'prefs',
33
+ env: 'env',
33
34
  android_package: 'androidPackage',
34
35
  android_activity: 'androidActivity',
35
36
  android_device_serial: 'androidDeviceSerial',
@@ -44,7 +45,7 @@ module Selenium
44
45
  #
45
46
  # @example
46
47
  # options = Selenium::WebDriver::Firefox::Options.new(args: ['--host=127.0.0.1'])
47
- # driver = Selenium::WebDriver.for :firefox, options: options
48
+ # driver = Selenium::WebDriver.for :firefox, capabilities: options
48
49
  #
49
50
  # @param [Hash] opts the pre-defined options to create the Firefox::Options with
50
51
  # @option opts [String] :binary Path to the Firefox executable to use
@@ -62,6 +63,7 @@ module Selenium
62
63
 
63
64
  @options[:args] ||= []
64
65
  @options[:prefs] ||= {}
66
+ @options[:env] ||= {}
65
67
  @options[:log] ||= {level: log_level} if log_level
66
68
 
67
69
  process_profile(@options.delete(:profile))
@@ -175,7 +177,7 @@ module Selenium
175
177
  end
176
178
 
177
179
  def camelize?(key)
178
- key != :prefs
180
+ key != "prefs"
179
181
  end
180
182
  end # Options
181
183
  end # Firefox
@@ -96,7 +96,7 @@ module Selenium
96
96
  raise TypeError, "expected one of #{VALID_PREFERENCE_TYPES.inspect}, got #{value.inspect}:#{value.class}"
97
97
  end
98
98
 
99
- if value.is_a?(String) && stringified?(value)
99
+ if value.is_a?(String) && Util.stringified?(value)
100
100
  raise ArgumentError, "preference values must be plain strings: #{key.inspect} => #{value.inspect}"
101
101
  end
102
102
 
@@ -221,10 +221,6 @@ module Selenium
221
221
  end
222
222
  end
223
223
  end
224
-
225
- def stringified?(str)
226
- /^".*"$/.match?(str)
227
- end
228
224
  end # Profile
229
225
  end # Firefox
230
226
  end # WebDriver
@@ -0,0 +1,46 @@
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 Firefox
23
+ # @api private
24
+ module Util
25
+ module_function
26
+
27
+ def app_data_path
28
+ case Platform.os
29
+ when :windows
30
+ "#{ENV.fetch('APPDATA')}\\Mozilla\\Firefox"
31
+ when :macosx
32
+ "#{Platform.home}/Library/Application Support/Firefox"
33
+ when :unix, :linux
34
+ "#{Platform.home}/.mozilla/firefox"
35
+ else
36
+ raise "Unknown os: #{Platform.os}"
37
+ end
38
+ end
39
+
40
+ def stringified?(str)
41
+ str =~ /^".*"$/
42
+ end
43
+ end # Util
44
+ end # Firefox
45
+ end # WebDriver
46
+ end # Selenium
@@ -24,6 +24,7 @@ require 'rexml/document'
24
24
  module Selenium
25
25
  module WebDriver
26
26
  module Firefox
27
+ autoload :Util, 'selenium/webdriver/firefox/util'
27
28
  autoload :Extension, 'selenium/webdriver/firefox/extension'
28
29
  autoload :ProfilesIni, 'selenium/webdriver/firefox/profiles_ini'
29
30
  autoload :Profile, 'selenium/webdriver/firefox/profile'
@@ -52,12 +52,12 @@ module Selenium
52
52
  #
53
53
  # @example
54
54
  # options = Selenium::WebDriver::IE::Options.new(args: ['--host=127.0.0.1'])
55
- # driver = Selenium::WebDriver.for(:ie, options: options)
55
+ # driver = Selenium::WebDriver.for(:ie, capabilities: options)
56
56
  #
57
57
  # @example
58
58
  # options = Selenium::WebDriver::IE::Options.new
59
59
  # options.element_scroll_behavior = Selenium::WebDriver::IE::Options::SCROLL_BOTTOM
60
- # driver = Selenium::WebDriver.for(:ie, options: options)
60
+ # driver = Selenium::WebDriver.for(:ie, capabilities: options)
61
61
  #
62
62
  # @param [Hash] opts the pre-defined options
63
63
  # @option opts [Array<String>] args
@@ -30,8 +30,8 @@ module Selenium
30
30
 
31
31
  #
32
32
  # Initializes the bridge with the given server URL
33
- # @param [String, URI] :url url for the remote server
34
- # @param [Object] :http_client an HTTP client instance that implements the same protocol as Http::Default
33
+ # @param [String, URI] url url for the remote server
34
+ # @param [Object] http_client an HTTP client instance that implements the same protocol as Http::Default
35
35
  # @api private
36
36
  #
37
37
 
@@ -118,7 +118,7 @@ module Selenium
118
118
  end
119
119
 
120
120
  def alert=(keys)
121
- execute :send_alert_text, {}, {value: keys.split(//), text: keys}
121
+ execute :send_alert_text, {}, {value: keys.chars, text: keys}
122
122
  end
123
123
 
124
124
  def alert_text
@@ -146,9 +146,7 @@ module Selenium
146
146
  end
147
147
 
148
148
  def page_source
149
- execute_script('var source = document.documentElement.outerHTML;' \
150
- 'if (!source) { source = new XMLSerializer().serializeToString(document); }' \
151
- 'return source;')
149
+ execute :get_page_source
152
150
  end
153
151
 
154
152
  #
@@ -369,11 +367,8 @@ module Selenium
369
367
  # actions
370
368
  #
371
369
 
372
- def action(async = false)
373
- ActionBuilder.new self,
374
- Interactions.pointer(:mouse, name: 'mouse'),
375
- Interactions.key('keyboard'),
376
- async
370
+ def action(deprecated_async = nil, async: false, devices: [], duration: 250)
371
+ ActionBuilder.new self, nil, nil, deprecated_async, async: async, devices: devices, duration: duration
377
372
  end
378
373
  alias_method :actions, :action
379
374
 
@@ -404,7 +399,7 @@ module Selenium
404
399
  def send_keys_to_element(element, keys)
405
400
  # TODO: rework file detectors before Selenium 4.0
406
401
  if @file_detector
407
- local_files = keys.first.split("\n").map { |key| @file_detector.call(Array(key)) }.compact
402
+ local_files = keys.first&.split("\n")&.map { |key| @file_detector.call(Array(key)) }&.compact
408
403
  if local_files.any?
409
404
  keys = local_files.map { |local_file| upload(local_file) }
410
405
  keys = Array(keys.join("\n"))
@@ -412,8 +407,8 @@ module Selenium
412
407
  end
413
408
 
414
409
  # Keep .split(//) for backward compatibility for now
415
- text = keys.join('')
416
- execute :element_send_keys, {id: element}, {value: text.split(//), text: text}
410
+ text = keys.join
411
+ execute :element_send_keys, {id: element}, {value: text.chars, text: text}
417
412
  end
418
413
 
419
414
  def upload(local_file)
@@ -430,10 +425,19 @@ module Selenium
430
425
  end
431
426
 
432
427
  def submit_element(element)
433
- form = find_element_by('xpath', "./ancestor-or-self::form", [:element, element])
434
- execute_script("var e = arguments[0].ownerDocument.createEvent('Event');" \
435
- "e.initEvent('submit', true, true);" \
436
- 'if (arguments[0].dispatchEvent(e)) { arguments[0].submit() }', form.as_json)
428
+ script = "var form = arguments[0];\n" \
429
+ "while (form.nodeName != \"FORM\" && form.parentNode) {\n" \
430
+ " form = form.parentNode;\n" \
431
+ "}\n" \
432
+ "if (!form) { throw Error('Unable to find containing form element'); }\n" \
433
+ "if (!form.ownerDocument) { throw Error('Unable to find owning document'); }\n" \
434
+ "var e = form.ownerDocument.createEvent('Event');\n" \
435
+ "e.initEvent('submit', true, true);\n" \
436
+ "if (form.dispatchEvent(e)) { HTMLFormElement.prototype.submit.call(form) }\n"
437
+
438
+ execute_script(script, Element::ELEMENT_KEY => element)
439
+ rescue Error::JavascriptError
440
+ raise Error::UnsupportedOperationError, "To submit an element, it must be nested inside a form element"
437
441
  end
438
442
 
439
443
  #
@@ -604,6 +608,9 @@ module Selenium
604
608
  element_id = element_id_from(arg)
605
609
  return Element.new(self, element_id) if element_id
606
610
 
611
+ shadow_root_id = shadow_root_id_from(arg)
612
+ return ShadowRoot.new self, shadow_root_id if shadow_root_id
613
+
607
614
  arg.each { |k, v| arg[k] = unwrap_script_result(v) }
608
615
  else
609
616
  arg
@@ -636,8 +643,6 @@ module Selenium
636
643
  when 'name'
637
644
  how = 'css selector'
638
645
  what = "*[name='#{escape_css(what.to_s)}']"
639
- when 'tag name'
640
- how = 'css selector'
641
646
  end
642
647
 
643
648
  if what.is_a?(Hash)