selenium-webdriver 3.142.7 → 4.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +21 -43
  3. data/lib/selenium/webdriver.rb +2 -4
  4. data/lib/selenium/webdriver/chrome/bridge.rb +3 -21
  5. data/lib/selenium/webdriver/chrome/driver.rb +12 -39
  6. data/lib/selenium/webdriver/chrome/options.rb +3 -7
  7. data/lib/selenium/webdriver/chrome/profile.rb +2 -2
  8. data/lib/selenium/webdriver/chrome/service.rb +4 -9
  9. data/lib/selenium/webdriver/common.rb +7 -16
  10. data/lib/selenium/webdriver/common/action_builder.rb +97 -249
  11. data/lib/selenium/webdriver/common/driver.rb +2 -4
  12. data/lib/selenium/webdriver/common/driver_extensions/takes_screenshot.rb +1 -1
  13. data/lib/selenium/webdriver/common/element.rb +3 -6
  14. data/lib/selenium/webdriver/common/error.rb +27 -203
  15. data/lib/selenium/webdriver/common/interactions/key_actions.rb +5 -5
  16. data/lib/selenium/webdriver/common/interactions/pointer_actions.rb +13 -13
  17. data/lib/selenium/webdriver/common/manager.rb +1 -1
  18. data/lib/selenium/webdriver/common/options.rb +148 -24
  19. data/lib/selenium/webdriver/common/service.rb +16 -34
  20. data/lib/selenium/webdriver/common/socket_poller.rb +2 -2
  21. data/lib/selenium/webdriver/common/w3c_options.rb +45 -0
  22. data/lib/selenium/webdriver/edge.rb +0 -1
  23. data/lib/selenium/webdriver/edge/driver.rb +14 -10
  24. data/lib/selenium/webdriver/edge/service.rb +6 -7
  25. data/lib/selenium/webdriver/firefox.rb +2 -6
  26. data/lib/selenium/webdriver/firefox/binary.rb +3 -80
  27. data/lib/selenium/webdriver/firefox/bridge.rb +47 -0
  28. data/lib/selenium/webdriver/firefox/driver.rb +44 -22
  29. data/lib/selenium/webdriver/firefox/marionette/driver.rb +1 -1
  30. data/lib/selenium/webdriver/firefox/options.rb +2 -2
  31. data/lib/selenium/webdriver/firefox/profile.rb +25 -14
  32. data/lib/selenium/webdriver/firefox/service.rb +4 -4
  33. data/lib/selenium/webdriver/ie/driver.rb +5 -18
  34. data/lib/selenium/webdriver/ie/options.rb +2 -2
  35. data/lib/selenium/webdriver/ie/service.rb +4 -4
  36. data/lib/selenium/webdriver/remote.rb +2 -6
  37. data/lib/selenium/webdriver/remote/bridge.rb +515 -69
  38. data/lib/selenium/webdriver/remote/capabilities.rb +77 -99
  39. data/lib/selenium/webdriver/remote/commands.rb +156 -0
  40. data/lib/selenium/webdriver/remote/driver.rb +12 -5
  41. data/lib/selenium/webdriver/remote/http/default.rb +0 -9
  42. data/lib/selenium/webdriver/remote/response.rb +16 -47
  43. data/lib/selenium/webdriver/safari.rb +1 -1
  44. data/lib/selenium/webdriver/safari/bridge.rb +3 -3
  45. data/lib/selenium/webdriver/safari/driver.rb +4 -1
  46. data/lib/selenium/webdriver/safari/service.rb +4 -4
  47. data/lib/selenium/webdriver/support/select.rb +1 -1
  48. data/lib/selenium/webdriver/version.rb +1 -1
  49. data/selenium-webdriver.gemspec +3 -3
  50. metadata +14 -5
@@ -0,0 +1,47 @@
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
+ module Bridge
24
+
25
+ COMMANDS = {
26
+ install_addon: [:post, 'session/:session_id/moz/addon/install'],
27
+ uninstall_addon: [:post, 'session/:session_id/moz/addon/uninstall']
28
+ }.freeze
29
+
30
+ def commands(command)
31
+ COMMANDS[command] || super
32
+ end
33
+
34
+ def install_addon(path, temporary)
35
+ payload = {path: path}
36
+ payload[:temporary] = temporary unless temporary.nil?
37
+ execute :install_addon, {}, payload
38
+ end
39
+
40
+ def uninstall_addon(id)
41
+ execute :uninstall_addon, {}, {id: id}
42
+ end
43
+
44
+ end # Bridge
45
+ end # Firefox
46
+ end # WebDriver
47
+ end # Selenium
@@ -20,30 +20,52 @@
20
20
  module Selenium
21
21
  module WebDriver
22
22
  module Firefox
23
- module Driver
24
- class << self
25
-
26
- #
27
- # Instantiates correct Firefox driver implementation
28
- # @return [Marionette::Driver, Legacy::Driver]
29
- #
30
-
31
- def new(**opts)
32
- if marionette?(opts)
33
- Firefox::Marionette::Driver.new(opts)
34
- else
35
- Firefox::Legacy::Driver.new(opts)
36
- end
37
- end
38
-
39
- private
40
-
41
- def marionette?(opts)
42
- opts.delete(:marionette) != false &&
43
- (!opts[:desired_capabilities] || opts[:desired_capabilities][:marionette] != false)
44
- end
23
+
24
+ #
25
+ # Driver implementation for Firefox using GeckoDriver.
26
+ # @api private
27
+ #
28
+
29
+ class Driver < WebDriver::Driver
30
+ include DriverExtensions::HasAddons
31
+ include DriverExtensions::HasWebStorage
32
+ include DriverExtensions::TakesScreenshot
33
+
34
+ def initialize(opts = {})
35
+ opts[:desired_capabilities] = create_capabilities(opts)
36
+
37
+ opts[:url] ||= service_url(opts)
38
+
39
+ listener = opts.delete(:listener)
40
+ desired_capabilities = opts.delete(:desired_capabilities)
41
+
42
+ @bridge = Remote::Bridge.new(opts)
43
+ @bridge.extend Bridge
44
+ @bridge.create_session(desired_capabilities)
45
+
46
+ super(@bridge, listener: listener)
45
47
  end
46
48
 
49
+ def browser
50
+ :firefox
51
+ end
52
+
53
+ def quit
54
+ super
55
+ ensure
56
+ @service&.stop
57
+ end
58
+
59
+ private
60
+
61
+ def create_capabilities(opts)
62
+ caps = opts.delete(:desired_capabilities) { Remote::Capabilities.firefox }
63
+ options = opts.delete(:options) { Options.new }
64
+ options = options.as_json
65
+ caps.merge!(options) unless options.empty?
66
+
67
+ caps
68
+ end
47
69
  end # Driver
48
70
  end # Firefox
49
71
  end # WebDriver
@@ -42,7 +42,7 @@ module Selenium
42
42
  desired_capabilities = opts.delete(:desired_capabilities)
43
43
  bridge = Remote::Bridge.new(opts)
44
44
  capabilities = bridge.create_session(desired_capabilities)
45
- @bridge = Remote::W3C::Bridge.new(capabilities, bridge.session_id, **opts)
45
+ @bridge = Remote::W3C::Bridge.new(capabilities, bridge.session_id, opts)
46
46
  @bridge.extend Marionette::Bridge
47
47
 
48
48
  super(@bridge, listener: listener)
@@ -20,7 +20,7 @@
20
20
  module Selenium
21
21
  module WebDriver
22
22
  module Firefox
23
- class Options < WebDriver::Common::Options
23
+ class Options
24
24
  attr_reader :args, :prefs, :options, :profile
25
25
  attr_accessor :binary, :log_level
26
26
 
@@ -139,7 +139,7 @@ module Selenium
139
139
  opts[:prefs] = @prefs unless @prefs.empty?
140
140
  opts[:log] = {level: @log_level} if @log_level
141
141
 
142
- {KEY => generate_as_json(opts)}
142
+ {KEY => opts}
143
143
  end
144
144
 
145
145
  private
@@ -73,20 +73,9 @@ module Selenium
73
73
  model_prefs = read_model_prefs
74
74
 
75
75
  if model_prefs.empty?
76
- @native_events = DEFAULT_ENABLE_NATIVE_EVENTS
77
- @secure_ssl = DEFAULT_SECURE_SSL
78
- @untrusted_issuer = DEFAULT_ASSUME_UNTRUSTED_ISSUER
79
- @load_no_focus_lib = DEFAULT_LOAD_NO_FOCUS_LIB
80
-
81
- @additional_prefs = {}
76
+ assign_default_preferences
82
77
  else
83
- # TODO: clean this up
84
- @native_events = model_prefs.delete(WEBDRIVER_PREFS[:native_events]) == 'true'
85
- @secure_ssl = model_prefs.delete(WEBDRIVER_PREFS[:untrusted_certs]) != 'true'
86
- @untrusted_issuer = model_prefs.delete(WEBDRIVER_PREFS[:untrusted_issuer]) == 'true'
87
- # not stored in profile atm, so will always be false.
88
- @load_no_focus_lib = model_prefs.delete(WEBDRIVER_PREFS[:load_no_focus_lib]) == 'true'
89
- @additional_prefs = model_prefs
78
+ assign_updated_preferences(model_prefs)
90
79
  end
91
80
 
92
81
  @extensions = {}
@@ -116,7 +105,7 @@ module Selenium
116
105
  raise TypeError, "expected one of #{VALID_PREFERENCE_TYPES.inspect}, got #{value.inspect}:#{value.class}"
117
106
  end
118
107
 
119
- if value.is_a?(String) && Util.stringified?(value)
108
+ if value.is_a?(String) && stringified?(value)
120
109
  raise ArgumentError, "preference values must be plain strings: #{key.inspect} => #{value.inspect}"
121
110
  end
122
111
 
@@ -195,6 +184,24 @@ module Selenium
195
184
 
196
185
  private
197
186
 
187
+ def assign_default_preferences
188
+ @native_events = DEFAULT_ENABLE_NATIVE_EVENTS
189
+ @secure_ssl = DEFAULT_SECURE_SSL
190
+ @untrusted_issuer = DEFAULT_ASSUME_UNTRUSTED_ISSUER
191
+ @load_no_focus_lib = DEFAULT_LOAD_NO_FOCUS_LIB
192
+
193
+ @additional_prefs = {}
194
+ end
195
+
196
+ def assign_updated_preferences(model_prefs)
197
+ @native_events = model_prefs.delete(WEBDRIVER_PREFS[:native_events]) == 'true'
198
+ @secure_ssl = model_prefs.delete(WEBDRIVER_PREFS[:untrusted_certs]) != 'true'
199
+ @untrusted_issuer = model_prefs.delete(WEBDRIVER_PREFS[:untrusted_issuer]) == 'true'
200
+ # not stored in profile atm, so will always be false.
201
+ @load_no_focus_lib = model_prefs.delete(WEBDRIVER_PREFS[:load_no_focus_lib]) == 'true'
202
+ @additional_prefs = model_prefs
203
+ end
204
+
198
205
  def set_manual_proxy_preference(key, value)
199
206
  return unless value
200
207
 
@@ -275,6 +282,10 @@ module Selenium
275
282
  end
276
283
  end
277
284
  end
285
+
286
+ def stringified?(str)
287
+ /^".*"$/.match?(str)
288
+ end
278
289
  end # Profile
279
290
  end # Firefox
280
291
  end # WebDriver
@@ -25,14 +25,14 @@ module Selenium
25
25
  #
26
26
 
27
27
  class Service < WebDriver::Service
28
- @default_port = 4444
29
- @executable = 'geckodriver'
30
- @missing_text = <<~ERROR
28
+ DEFAULT_PORT = 4444
29
+ EXECUTABLE = 'geckodriver'
30
+ MISSING_TEXT = <<~ERROR
31
31
  Unable to find Mozilla geckodriver. Please download the server from
32
32
  https://github.com/mozilla/geckodriver/releases and place it somewhere on your PATH.
33
33
  More info at https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/WebDriver.
34
34
  ERROR
35
- @shutdown_supported = false
35
+ SHUTDOWN_SUPPORTED = false
36
36
 
37
37
  private
38
38
 
@@ -37,7 +37,11 @@ module Selenium
37
37
  opts[:url] ||= service_url(opts)
38
38
 
39
39
  listener = opts.delete(:listener)
40
- @bridge = Remote::Bridge.handshake(**opts)
40
+ desired_capabilities = opts.delete(:desired_capabilities)
41
+
42
+ @bridge = Remote::Bridge.new(opts)
43
+ @bridge.create_session(desired_capabilities)
44
+
41
45
  super(@bridge, listener: listener)
42
46
  end
43
47
 
@@ -56,23 +60,6 @@ module Selenium
56
60
  def create_capabilities(opts)
57
61
  caps = opts.delete(:desired_capabilities) { Remote::Capabilities.internet_explorer }
58
62
  options = opts.delete(:options) { Options.new }
59
-
60
- if opts.delete(:introduce_flakiness_by_ignoring_security_domains)
61
- WebDriver.logger.deprecate ':introduce_flakiness_by_ignoring_security_domains',
62
- 'Selenium::WebDriver::IE::Options#ignore_protected_mode_settings='
63
- options.ignore_protected_mode_settings = true
64
- end
65
-
66
- native_events = opts.delete(:native_events)
67
- unless native_events.nil?
68
- WebDriver.logger.deprecate ':native_events', 'Selenium::WebDriver::IE::Options#native_events='
69
- options.native_events = native_events
70
- end
71
-
72
- # Backward compatibility with older IEDriverServer versions
73
- caps[:ignore_protected_mode_settings] = options.ignore_protected_mode_settings
74
- caps[:native_events] = options.native_events
75
-
76
63
  options = options.as_json
77
64
  caps.merge!(options) unless options.empty?
78
65
 
@@ -20,7 +20,7 @@
20
20
  module Selenium
21
21
  module WebDriver
22
22
  module IE
23
- class Options < WebDriver::Common::Options
23
+ class Options
24
24
  KEY = 'se:ieOptions'
25
25
  SCROLL_TOP = 0
26
26
  SCROLL_BOTTOM = 1
@@ -130,7 +130,7 @@ module Selenium
130
130
  opts['ie.browserCommandLineSwitches'] = @args.to_a.join(' ') if @args.any?
131
131
  opts.merge!(@options)
132
132
 
133
- {KEY => generate_as_json(opts)}
133
+ {KEY => opts}
134
134
  end
135
135
  end # Options
136
136
  end # IE
@@ -25,14 +25,14 @@ module Selenium
25
25
  #
26
26
 
27
27
  class Service < WebDriver::Service
28
- @default_port = 5555
29
- @executable = 'IEDriverServer'
30
- @missing_text = <<~ERROR
28
+ DEFAULT_PORT = 5555
29
+ EXECUTABLE = 'IEDriverServer'
30
+ MISSING_TEXT = <<~ERROR
31
31
  Unable to find IEDriverServer. Please download the server from
32
32
  http://selenium-release.storage.googleapis.com/index.html and place it somewhere on your PATH.
33
33
  More info at https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver.
34
34
  ERROR
35
- @shutdown_supported = true
35
+ SHUTDOWN_SUPPORTED = true
36
36
 
37
37
  private
38
38
 
@@ -26,10 +26,6 @@ require 'selenium/webdriver/remote/server_error'
26
26
  require 'selenium/webdriver/remote/http/common'
27
27
  require 'selenium/webdriver/remote/http/default'
28
28
 
29
+ require 'selenium/webdriver/remote/bridge'
29
30
  require 'selenium/webdriver/remote/capabilities'
30
- require 'selenium/webdriver/remote/oss/bridge'
31
- require 'selenium/webdriver/remote/oss/commands'
32
-
33
- require 'selenium/webdriver/remote/w3c/bridge'
34
- require 'selenium/webdriver/remote/w3c/capabilities'
35
- require 'selenium/webdriver/remote/w3c/commands'
31
+ require 'selenium/webdriver/remote/commands'
@@ -22,48 +22,11 @@ module Selenium
22
22
  module Remote
23
23
  class Bridge
24
24
  include Atoms
25
- include BridgeHelper
26
25
 
27
26
  PORT = 4444
28
- COMMANDS = {
29
- new_session: [:post, 'session']
30
- }.freeze
31
27
 
32
28
  attr_accessor :context, :http, :file_detector
33
- attr_reader :capabilities, :dialect
34
-
35
- #
36
- # Implements protocol handshake which:
37
- #
38
- # 1. Creates session with driver.
39
- # 2. Sniffs response.
40
- # 3. Based on the response, understands which dialect we should use.
41
- #
42
- # @return [OSS:Bridge, W3C::Bridge]
43
- #
44
- def self.handshake(**opts)
45
- desired_capabilities = opts.delete(:desired_capabilities) { Capabilities.new }
46
-
47
- if desired_capabilities.is_a?(Symbol)
48
- unless Capabilities.respond_to?(desired_capabilities)
49
- raise Error::WebDriverError, "invalid desired capability: #{desired_capabilities.inspect}"
50
- end
51
-
52
- desired_capabilities = Capabilities.__send__(desired_capabilities)
53
- end
54
-
55
- bridge = new(opts)
56
- capabilities = bridge.create_session(desired_capabilities, opts.delete(:options))
57
-
58
- case bridge.dialect
59
- when :oss
60
- Remote::OSS::Bridge.new(capabilities, bridge.session_id, **opts)
61
- when :w3c
62
- Remote::W3C::Bridge.new(capabilities, bridge.session_id, **opts)
63
- else
64
- raise WebDriverError, 'cannot understand dialect'
65
- end
66
- end
29
+ attr_reader :capabilities
67
30
 
68
31
  #
69
32
  # Initializes the bridge with the given server URL
@@ -86,7 +49,7 @@ module Selenium
86
49
  end
87
50
 
88
51
  uri = url.is_a?(URI) ? url : URI.parse(url)
89
- uri.path += '/' unless uri.path =~ %r{\/$}
52
+ uri.path += '/' unless %r{\/$}.match?(uri.path)
90
53
 
91
54
  http_client.server_url = uri
92
55
 
@@ -95,37 +58,18 @@ module Selenium
95
58
  end
96
59
 
97
60
  #
98
- # Creates session handling both OSS and W3C dialects.
61
+ # Creates session.
99
62
  #
100
63
 
101
64
  def create_session(desired_capabilities, options = nil)
102
65
  response = execute(:new_session, {}, merged_capabilities(desired_capabilities, options))
103
66
 
104
67
  @session_id = response['sessionId']
105
- oss_status = response['status']
106
- value = response['value']
107
-
108
- if value.is_a?(Hash)
109
- @session_id = value['sessionId'] if value.key?('sessionId')
110
-
111
- if value.key?('capabilities')
112
- value = value['capabilities']
113
- elsif value.key?('value')
114
- value = value['value']
115
- end
116
- end
68
+ capabilities = response['capabilities']
117
69
 
118
70
  raise Error::WebDriverError, 'no sessionId in returned payload' unless @session_id
119
71
 
120
- if oss_status
121
- WebDriver.logger.info 'Detected OSS dialect.'
122
- @dialect = :oss
123
- Capabilities.json_create(value)
124
- else
125
- WebDriver.logger.info 'Detected W3C dialect.'
126
- @dialect = :w3c
127
- W3C::Capabilities.json_create(value)
128
- end
72
+ @capabilities = Capabilities.json_create(capabilities)
129
73
  end
130
74
 
131
75
  #
@@ -143,6 +87,466 @@ module Selenium
143
87
  end
144
88
  end
145
89
 
90
+ def status
91
+ execute :status
92
+ end
93
+
94
+ def get(url)
95
+ execute :get, {}, {url: url}
96
+ end
97
+
98
+ def implicit_wait_timeout=(milliseconds)
99
+ timeout('implicit', milliseconds)
100
+ end
101
+
102
+ def script_timeout=(milliseconds)
103
+ timeout('script', milliseconds)
104
+ end
105
+
106
+ def timeout(type, milliseconds)
107
+ type = 'pageLoad' if type == 'page load'
108
+ execute :set_timeout, {}, {type => milliseconds}
109
+ end
110
+
111
+ #
112
+ # alerts
113
+ #
114
+
115
+ def accept_alert
116
+ execute :accept_alert
117
+ end
118
+
119
+ def dismiss_alert
120
+ execute :dismiss_alert
121
+ end
122
+
123
+ def alert=(keys)
124
+ execute :send_alert_text, {}, {value: keys.split(//), text: keys}
125
+ end
126
+
127
+ def alert_text
128
+ execute :get_alert_text
129
+ end
130
+
131
+ #
132
+ # navigation
133
+ #
134
+
135
+ def go_back
136
+ execute :back
137
+ end
138
+
139
+ def go_forward
140
+ execute :forward
141
+ end
142
+
143
+ def url
144
+ execute :get_current_url
145
+ end
146
+
147
+ def title
148
+ execute :get_title
149
+ end
150
+
151
+ def page_source
152
+ execute_script('var source = document.documentElement.outerHTML;' \
153
+ 'if (!source) { source = new XMLSerializer().serializeToString(document); }' \
154
+ 'return source;')
155
+ end
156
+
157
+ #
158
+ # Create a new top-level browsing context
159
+ # https://w3c.github.io/webdriver/#new-window
160
+ # @param type [String] Supports two values: 'tab' and 'window'.
161
+ # Use 'tab' if you'd like the new window to share an OS-level window
162
+ # with the current browsing context.
163
+ # Use 'window' otherwise
164
+ # @return [Hash] Containing 'handle' with the value of the window handle
165
+ # and 'type' with the value of the created window type
166
+ #
167
+ def new_window(type)
168
+ execute :new_window, {}, {type: type}
169
+ end
170
+
171
+ def switch_to_window(name)
172
+ execute :switch_to_window, {}, {handle: name}
173
+ end
174
+
175
+ def switch_to_frame(id)
176
+ id = find_element_by('id', id) if id.is_a? String
177
+ execute :switch_to_frame, {}, {id: id}
178
+ end
179
+
180
+ def switch_to_parent_frame
181
+ execute :switch_to_parent_frame
182
+ end
183
+
184
+ def switch_to_default_content
185
+ switch_to_frame nil
186
+ end
187
+
188
+ QUIT_ERRORS = [IOError].freeze
189
+
190
+ def quit
191
+ execute :delete_session
192
+ http.close
193
+ rescue *QUIT_ERRORS
194
+ end
195
+
196
+ def close
197
+ execute :close_window
198
+ end
199
+
200
+ def refresh
201
+ execute :refresh
202
+ end
203
+
204
+ #
205
+ # window handling
206
+ #
207
+
208
+ def window_handles
209
+ execute :get_window_handles
210
+ end
211
+
212
+ def window_handle
213
+ execute :get_window_handle
214
+ end
215
+
216
+ def resize_window(width, height, handle = :current)
217
+ raise Error::WebDriverError, 'Switch to desired window before changing its size' unless handle == :current
218
+
219
+ set_window_rect(width: width, height: height)
220
+ end
221
+
222
+ def window_size(handle = :current)
223
+ raise Error::UnsupportedOperationError, 'Switch to desired window before getting its size' unless handle == :current
224
+
225
+ data = execute :get_window_rect
226
+ Dimension.new data['width'], data['height']
227
+ end
228
+
229
+ def minimize_window
230
+ execute :minimize_window
231
+ end
232
+
233
+ def maximize_window(handle = :current)
234
+ raise Error::UnsupportedOperationError, 'Switch to desired window before changing its size' unless handle == :current
235
+
236
+ execute :maximize_window
237
+ end
238
+
239
+ def full_screen_window
240
+ execute :fullscreen_window
241
+ end
242
+
243
+ def reposition_window(x, y)
244
+ set_window_rect(x: x, y: y)
245
+ end
246
+
247
+ def window_position
248
+ data = execute :get_window_rect
249
+ Point.new data['x'], data['y']
250
+ end
251
+
252
+ def set_window_rect(x: nil, y: nil, width: nil, height: nil)
253
+ params = {x: x, y: y, width: width, height: height}
254
+ params.update(params) { |_k, v| Integer(v) unless v.nil? }
255
+ execute :set_window_rect, {}, params
256
+ end
257
+
258
+ def window_rect
259
+ data = execute :get_window_rect
260
+ Rectangle.new data['x'], data['y'], data['width'], data['height']
261
+ end
262
+
263
+ def screenshot
264
+ execute :take_screenshot
265
+ end
266
+
267
+ #
268
+ # HTML 5
269
+ #
270
+
271
+ def local_storage_item(key, value = nil)
272
+ if value
273
+ execute_script("localStorage.setItem('#{key}', '#{value}')")
274
+ else
275
+ execute_script("return localStorage.getItem('#{key}')")
276
+ end
277
+ end
278
+
279
+ def remove_local_storage_item(key)
280
+ execute_script("localStorage.removeItem('#{key}')")
281
+ end
282
+
283
+ def local_storage_keys
284
+ execute_script('return Object.keys(localStorage)')
285
+ end
286
+
287
+ def clear_local_storage
288
+ execute_script('localStorage.clear()')
289
+ end
290
+
291
+ def local_storage_size
292
+ execute_script('return localStorage.length')
293
+ end
294
+
295
+ def session_storage_item(key, value = nil)
296
+ if value
297
+ execute_script("sessionStorage.setItem('#{key}', '#{value}')")
298
+ else
299
+ execute_script("return sessionStorage.getItem('#{key}')")
300
+ end
301
+ end
302
+
303
+ def remove_session_storage_item(key)
304
+ execute_script("sessionStorage.removeItem('#{key}')")
305
+ end
306
+
307
+ def session_storage_keys
308
+ execute_script('return Object.keys(sessionStorage)')
309
+ end
310
+
311
+ def clear_session_storage
312
+ execute_script('sessionStorage.clear()')
313
+ end
314
+
315
+ def session_storage_size
316
+ execute_script('return sessionStorage.length')
317
+ end
318
+
319
+ def location
320
+ raise Error::UnsupportedOperationError, 'The W3C standard does not currently support getting location'
321
+ end
322
+
323
+ def set_location(_lat, _lon, _alt)
324
+ raise Error::UnsupportedOperationError, 'The W3C standard does not currently support setting location'
325
+ end
326
+
327
+ def network_connection
328
+ raise Error::UnsupportedOperationError, 'The W3C standard does not currently support getting network connection'
329
+ end
330
+
331
+ def network_connection=(_type)
332
+ raise Error::UnsupportedOperationError, 'The W3C standard does not currently support setting network connection'
333
+ end
334
+
335
+ #
336
+ # javascript execution
337
+ #
338
+
339
+ def execute_script(script, *args)
340
+ result = execute :execute_script, {}, {script: script, args: args}
341
+ unwrap_script_result result
342
+ end
343
+
344
+ def execute_async_script(script, *args)
345
+ result = execute :execute_async_script, {}, {script: script, args: args}
346
+ unwrap_script_result result
347
+ end
348
+
349
+ #
350
+ # cookies
351
+ #
352
+
353
+ def manage
354
+ @manage ||= WebDriver::Manager.new(self)
355
+ end
356
+
357
+ def add_cookie(cookie)
358
+ execute :add_cookie, {}, {cookie: cookie}
359
+ end
360
+
361
+ def delete_cookie(name)
362
+ execute :delete_cookie, name: name
363
+ end
364
+
365
+ def cookie(name)
366
+ execute :get_cookie, name: name
367
+ end
368
+
369
+ def cookies
370
+ execute :get_all_cookies
371
+ end
372
+
373
+ def delete_all_cookies
374
+ execute :delete_all_cookies
375
+ end
376
+
377
+ #
378
+ # actions
379
+ #
380
+
381
+ def action(async = false)
382
+ ActionBuilder.new self,
383
+ Interactions.pointer(:mouse, name: 'mouse'),
384
+ Interactions.key('keyboard'),
385
+ async
386
+ end
387
+ alias_method :actions, :action
388
+
389
+ def mouse
390
+ raise Error::UnsupportedOperationError, '#mouse is no longer supported, use #action instead'
391
+ end
392
+
393
+ def keyboard
394
+ raise Error::UnsupportedOperationError, '#keyboard is no longer supported, use #action instead'
395
+ end
396
+
397
+ def send_actions(data)
398
+ execute :actions, {}, {actions: data}
399
+ end
400
+
401
+ def release_actions
402
+ execute :release_actions
403
+ end
404
+
405
+ def click_element(element)
406
+ execute :element_click, id: element
407
+ end
408
+
409
+ def send_keys_to_element(element, keys)
410
+ # TODO: rework file detectors before Selenium 4.0
411
+ if @file_detector
412
+ local_files = keys.first.split("\n").map { |key| @file_detector.call(Array(key)) }.compact
413
+ if local_files.any?
414
+ keys = local_files.map { |local_file| upload(local_file) }
415
+ keys = Array(keys.join("\n"))
416
+ end
417
+ end
418
+
419
+ # Keep .split(//) for backward compatibility for now
420
+ text = keys.join('')
421
+ execute :element_send_keys, {id: element}, {value: text.split(//), text: text}
422
+ end
423
+
424
+ def upload(local_file)
425
+ unless File.file?(local_file)
426
+ WebDriver.logger.debug("File detector only works with files. #{local_file.inspect} isn`t a file!")
427
+ raise Error::WebDriverError, "You are trying to work with something that isn't a file."
428
+ end
429
+
430
+ execute :upload_file, {}, {file: Zipper.zip_file(local_file)}
431
+ end
432
+
433
+ def clear_element(element)
434
+ execute :element_clear, id: element
435
+ end
436
+
437
+ def submit_element(element)
438
+ form = find_element_by('xpath', "./ancestor-or-self::form", element)
439
+ execute_script("var e = arguments[0].ownerDocument.createEvent('Event');" \
440
+ "e.initEvent('submit', true, true);" \
441
+ 'if (arguments[0].dispatchEvent(e)) { arguments[0].submit() }', form.as_json)
442
+ end
443
+
444
+ def screen_orientation=(orientation)
445
+ execute :set_screen_orientation, {}, {orientation: orientation}
446
+ end
447
+
448
+ def screen_orientation
449
+ execute :get_screen_orientation
450
+ end
451
+
452
+ #
453
+ # element properties
454
+ #
455
+
456
+ def element_tag_name(element)
457
+ execute :get_element_tag_name, id: element
458
+ end
459
+
460
+ def element_attribute(element, name)
461
+ WebDriver.logger.info "Using script for :getAttribute of #{name}"
462
+ execute_atom :getAttribute, element, name
463
+ end
464
+
465
+ def element_property(element, name)
466
+ execute :get_element_property, id: element.ref, name: name
467
+ end
468
+
469
+ def element_value(element)
470
+ element_property element, 'value'
471
+ end
472
+
473
+ def element_text(element)
474
+ execute :get_element_text, id: element
475
+ end
476
+
477
+ def element_location(element)
478
+ data = execute :get_element_rect, id: element
479
+
480
+ Point.new data['x'], data['y']
481
+ end
482
+
483
+ def element_rect(element)
484
+ data = execute :get_element_rect, id: element
485
+
486
+ Rectangle.new data['x'], data['y'], data['width'], data['height']
487
+ end
488
+
489
+ def element_location_once_scrolled_into_view(element)
490
+ send_keys_to_element(element, [''])
491
+ element_location(element)
492
+ end
493
+
494
+ def element_size(element)
495
+ data = execute :get_element_rect, id: element
496
+
497
+ Dimension.new data['width'], data['height']
498
+ end
499
+
500
+ def element_enabled?(element)
501
+ execute :is_element_enabled, id: element
502
+ end
503
+
504
+ def element_selected?(element)
505
+ execute :is_element_selected, id: element
506
+ end
507
+
508
+ def element_displayed?(element)
509
+ WebDriver.logger.info 'Using script for :isDisplayed'
510
+ execute_atom :isDisplayed, element
511
+ end
512
+
513
+ def element_value_of_css_property(element, prop)
514
+ execute :get_element_css_value, id: element, property_name: prop
515
+ end
516
+
517
+ #
518
+ # finding elements
519
+ #
520
+
521
+ def active_element
522
+ Element.new self, element_id_from(execute(:get_active_element))
523
+ end
524
+
525
+ alias_method :switch_to_active_element, :active_element
526
+
527
+ def find_element_by(how, what, parent = nil)
528
+ how, what = convert_locators(how, what)
529
+
530
+ id = if parent
531
+ execute :find_child_element, {id: parent}, {using: how, value: what}
532
+ else
533
+ execute :find_element, {}, {using: how, value: what}
534
+ end
535
+ Element.new self, element_id_from(id)
536
+ end
537
+
538
+ def find_elements_by(how, what, parent = nil)
539
+ how, what = convert_locators(how, what)
540
+
541
+ ids = if parent
542
+ execute :find_child_elements, {id: parent}, {using: how, value: what}
543
+ else
544
+ execute :find_elements, {}, {using: how, value: what}
545
+ end
546
+
547
+ ids.map { |id| Element.new self, element_id_from(id) }
548
+ end
549
+
146
550
  private
147
551
 
148
552
  #
@@ -164,7 +568,7 @@ module Selenium
164
568
  end
165
569
 
166
570
  WebDriver.logger.info("-> #{verb.to_s.upcase} #{path}")
167
- http.call(verb, path, command_hash)
571
+ http.call(verb, path, command_hash)['value']
168
572
  end
169
573
 
170
574
  def escaper
@@ -172,23 +576,65 @@ module Selenium
172
576
  end
173
577
 
174
578
  def commands(command)
175
- raise NotImplementedError unless command == :new_session
176
-
177
579
  COMMANDS[command]
178
580
  end
179
581
 
180
- def merged_capabilities(oss_capabilities, options = nil)
181
- w3c_capabilities = W3C::Capabilities.from_oss(oss_capabilities)
182
- w3c_capabilities.merge!(options.as_json) if options
582
+ def merged_capabilities(capabilities, options = nil)
583
+ capabilities.merge!(options.as_json) if options
183
584
 
184
585
  {
185
- desiredCapabilities: oss_capabilities,
186
586
  capabilities: {
187
- firstMatch: [w3c_capabilities]
587
+ firstMatch: [capabilities]
188
588
  }
189
589
  }
190
590
  end
191
591
 
592
+ def unwrap_script_result(arg)
593
+ case arg
594
+ when Array
595
+ arg.map { |e| unwrap_script_result(e) }
596
+ when Hash
597
+ element_id = element_id_from(arg)
598
+ return Element.new(self, element_id) if element_id
599
+
600
+ arg.each { |k, v| arg[k] = unwrap_script_result(v) }
601
+ else
602
+ arg
603
+ end
604
+ end
605
+
606
+ def element_id_from(id)
607
+ id['ELEMENT'] || id['element-6066-11e4-a52e-4f735466cecf']
608
+ end
609
+
610
+ def convert_locators(how, what)
611
+ case how
612
+ when 'class name'
613
+ how = 'css selector'
614
+ what = ".#{escape_css(what)}"
615
+ when 'id'
616
+ how = 'css selector'
617
+ what = "##{escape_css(what)}"
618
+ when 'name'
619
+ how = 'css selector'
620
+ what = "*[name='#{escape_css(what)}']"
621
+ when 'tag name'
622
+ how = 'css selector'
623
+ end
624
+ [how, what]
625
+ end
626
+
627
+ ESCAPE_CSS_REGEXP = /(['"\\#.:;,!?+<>=~*^$|%&@`{}\-\[\]\(\)])/.freeze
628
+ UNICODE_CODE_POINT = 30
629
+
630
+ # Escapes invalid characters in CSS selector.
631
+ # @see https://mathiasbynens.be/notes/css-escapes
632
+ def escape_css(string)
633
+ string = string.gsub(ESCAPE_CSS_REGEXP) { |match| "\\#{match}" }
634
+ string = "\\#{UNICODE_CODE_POINT + Integer(string[0])} #{string[1..-1]}" if !string.empty? && string[0].match?(/[[:digit:]]/)
635
+
636
+ string
637
+ end
192
638
  end # Bridge
193
639
  end # Remote
194
640
  end # WebDriver