selenium-webdriver 4.1.0 → 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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +46 -1
  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 +19 -19
  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/platform.rb +4 -4
  39. data/lib/selenium/webdriver/common/search_context.rb +0 -6
  40. data/lib/selenium/webdriver/common/service_manager.rb +2 -3
  41. data/lib/selenium/webdriver/common/shadow_root.rb +1 -1
  42. data/lib/selenium/webdriver/common/socket_poller.rb +1 -1
  43. data/lib/selenium/webdriver/common/websocket_connection.rb +149 -0
  44. data/lib/selenium/webdriver/common.rb +14 -2
  45. data/lib/selenium/webdriver/devtools/request.rb +1 -1
  46. data/lib/selenium/webdriver/devtools/response.rb +1 -1
  47. data/lib/selenium/webdriver/devtools.rb +5 -112
  48. data/lib/selenium/webdriver/edge/features.rb +1 -0
  49. data/lib/selenium/webdriver/firefox/driver.rb +1 -0
  50. data/lib/selenium/webdriver/firefox/features.rb +2 -5
  51. data/lib/selenium/webdriver/firefox/options.rb +3 -1
  52. data/lib/selenium/webdriver/firefox/profile.rb +1 -5
  53. data/lib/selenium/webdriver/firefox/util.rb +46 -0
  54. data/lib/selenium/webdriver/firefox.rb +1 -0
  55. data/lib/selenium/webdriver/remote/bridge.rb +21 -19
  56. data/lib/selenium/webdriver/remote/commands.rb +0 -5
  57. data/lib/selenium/webdriver/remote/http/default.rb +6 -12
  58. data/lib/selenium/webdriver/remote/response.rb +2 -2
  59. data/lib/selenium/webdriver/support/cdp_client_generator.rb +4 -4
  60. data/lib/selenium/webdriver/support/color.rb +7 -7
  61. data/lib/selenium/webdriver/support/guards/guard_condition.rb +1 -1
  62. data/lib/selenium/webdriver/support/guards.rb +1 -1
  63. data/lib/selenium/webdriver/version.rb +1 -1
  64. data/lib/selenium/webdriver.rb +1 -0
  65. data/selenium-webdriver.gemspec +7 -4
  66. metadata +55 -5
@@ -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
@@ -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,66 +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 ||= begin
165
- if URI(@url).scheme == 'wss'
166
- socket = TCPSocket.new(ws.host, ws.port)
167
- socket = OpenSSL::SSL::SSLSocket.new(socket, OpenSSL::SSL::SSLContext.new)
168
- socket.sync_close = true
169
- socket.connect
170
-
171
- socket
172
- else
173
- TCPSocket.new(ws.host, ws.port)
174
- end
175
- end
176
- end
177
-
178
- def ws
179
- @ws ||= WebSocket::Handshake::Client.new(url: @url)
180
- end
181
-
182
- def next_id
183
- @id ||= 0
184
- @id += 1
185
- end
186
-
187
80
  def error_message(error)
188
81
  [error['code'], error['message'], error['data']].join(': ')
189
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',
@@ -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))
@@ -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'
@@ -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
 
@@ -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
  #
@@ -639,8 +643,6 @@ module Selenium
639
643
  when 'name'
640
644
  how = 'css selector'
641
645
  what = "*[name='#{escape_css(what.to_s)}']"
642
- when 'tag name'
643
- how = 'css selector'
644
646
  end
645
647
 
646
648
  if what.is_a?(Hash)
@@ -60,10 +60,6 @@ module Selenium
60
60
  fullscreen_window: [:post, 'session/:session_id/window/fullscreen'],
61
61
  minimize_window: [:post, 'session/:session_id/window/minimize'],
62
62
  maximize_window: [:post, 'session/:session_id/window/maximize'],
63
- set_window_size: [:post, 'session/:session_id/window/size'],
64
- get_window_size: [:get, 'session/:session_id/window/size'],
65
- set_window_position: [:post, 'session/:session_id/window/position'],
66
- get_window_position: [:get, 'session/:session_id/window/position'],
67
63
  set_window_rect: [:post, 'session/:session_id/window/rect'],
68
64
  get_window_rect: [:get, 'session/:session_id/window/rect'],
69
65
  switch_to_frame: [:post, 'session/:session_id/frame'],
@@ -130,7 +126,6 @@ module Selenium
130
126
  #
131
127
 
132
128
  element_click: [:post, 'session/:session_id/element/:id/click'],
133
- element_tap: [:post, 'session/:session_id/element/:id/tap'],
134
129
  element_clear: [:post, 'session/:session_id/element/:id/clear'],
135
130
  element_send_keys: [:post, 'session/:session_id/element/:id/value'],
136
131
 
@@ -75,9 +75,10 @@ module Selenium
75
75
  begin
76
76
  request = new_request_for(verb, url, headers, payload)
77
77
  response = response_for(request)
78
- rescue Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EADDRINUSE
79
- # a retry is sometimes needed on Windows XP where we may quickly
80
- # run out of ephemeral ports
78
+ rescue Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EADDRINUSE, Errno::EADDRNOTAVAIL
79
+ # a retry is sometimes needed:
80
+ # on Windows XP where we may quickly run out of ephemeral ports
81
+ # when the port becomes temporarily unavailable
81
82
  #
82
83
  # A more robust solution is bumping the MaxUserPort setting
83
84
  # as described here:
@@ -85,13 +86,6 @@ module Selenium
85
86
  # http://msdn.microsoft.com/en-us/library/aa560610%28v=bts.20%29.aspx
86
87
  raise if retries >= MAX_RETRIES
87
88
 
88
- retries += 1
89
- sleep 2
90
- retry
91
- rescue Errno::EADDRNOTAVAIL => e
92
- # a retry is sometimes needed when the port becomes temporarily unavailable
93
- raise if retries >= MAX_RETRIES
94
-
95
89
  retries += 1
96
90
  sleep 2
97
91
  retry
@@ -142,8 +136,8 @@ module Selenium
142
136
 
143
137
  def proxy
144
138
  @proxy ||= begin
145
- proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']
146
- no_proxy = ENV['no_proxy'] || ENV['NO_PROXY']
139
+ proxy = ENV.fetch('http_proxy', nil) || ENV.fetch('HTTP_PROXY', nil)
140
+ no_proxy = ENV.fetch('no_proxy', nil) || ENV.fetch('NO_PROXY', nil)
147
141
 
148
142
  if proxy
149
143
  proxy = "http://#{proxy}" unless proxy.start_with?('http://')
@@ -74,7 +74,7 @@ module Selenium
74
74
  end
75
75
 
76
76
  def backtrace_from_remote(server_trace)
77
- server_trace.map { |frame|
77
+ server_trace.filter_map do |frame|
78
78
  next unless frame.is_a?(Hash)
79
79
 
80
80
  file = frame['fileName']
@@ -87,7 +87,7 @@ module Selenium
87
87
  meth = 'unknown' if meth.nil? || meth.empty?
88
88
 
89
89
  "[remote server] #{file}:#{line}:in `#{meth}'"
90
- }.compact
90
+ end
91
91
  end
92
92
 
93
93
  def process_error