selenium-webdriver 3.142.7 → 4.0.0.alpha1
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.
- checksums.yaml +4 -4
- data/CHANGES +21 -43
- data/lib/selenium/webdriver.rb +2 -4
- data/lib/selenium/webdriver/chrome/bridge.rb +3 -21
- data/lib/selenium/webdriver/chrome/driver.rb +12 -39
- data/lib/selenium/webdriver/chrome/options.rb +3 -7
- data/lib/selenium/webdriver/chrome/profile.rb +2 -2
- data/lib/selenium/webdriver/chrome/service.rb +4 -9
- data/lib/selenium/webdriver/common.rb +7 -16
- data/lib/selenium/webdriver/common/action_builder.rb +97 -249
- data/lib/selenium/webdriver/common/driver.rb +2 -4
- data/lib/selenium/webdriver/common/driver_extensions/takes_screenshot.rb +1 -1
- data/lib/selenium/webdriver/common/element.rb +3 -6
- data/lib/selenium/webdriver/common/error.rb +27 -203
- data/lib/selenium/webdriver/common/interactions/key_actions.rb +5 -5
- data/lib/selenium/webdriver/common/interactions/pointer_actions.rb +13 -13
- data/lib/selenium/webdriver/common/manager.rb +1 -1
- data/lib/selenium/webdriver/common/options.rb +148 -24
- data/lib/selenium/webdriver/common/service.rb +16 -34
- data/lib/selenium/webdriver/common/socket_poller.rb +2 -2
- data/lib/selenium/webdriver/common/w3c_options.rb +45 -0
- data/lib/selenium/webdriver/edge.rb +0 -1
- data/lib/selenium/webdriver/edge/driver.rb +14 -10
- data/lib/selenium/webdriver/edge/service.rb +6 -7
- data/lib/selenium/webdriver/firefox.rb +2 -6
- data/lib/selenium/webdriver/firefox/binary.rb +3 -80
- data/lib/selenium/webdriver/firefox/bridge.rb +47 -0
- data/lib/selenium/webdriver/firefox/driver.rb +44 -22
- data/lib/selenium/webdriver/firefox/marionette/driver.rb +1 -1
- data/lib/selenium/webdriver/firefox/options.rb +2 -2
- data/lib/selenium/webdriver/firefox/profile.rb +25 -14
- data/lib/selenium/webdriver/firefox/service.rb +4 -4
- data/lib/selenium/webdriver/ie/driver.rb +5 -18
- data/lib/selenium/webdriver/ie/options.rb +2 -2
- data/lib/selenium/webdriver/ie/service.rb +4 -4
- data/lib/selenium/webdriver/remote.rb +2 -6
- data/lib/selenium/webdriver/remote/bridge.rb +515 -69
- data/lib/selenium/webdriver/remote/capabilities.rb +77 -99
- data/lib/selenium/webdriver/remote/commands.rb +156 -0
- data/lib/selenium/webdriver/remote/driver.rb +12 -5
- data/lib/selenium/webdriver/remote/http/default.rb +0 -9
- data/lib/selenium/webdriver/remote/response.rb +16 -47
- data/lib/selenium/webdriver/safari.rb +1 -1
- data/lib/selenium/webdriver/safari/bridge.rb +3 -3
- data/lib/selenium/webdriver/safari/driver.rb +4 -1
- data/lib/selenium/webdriver/safari/service.rb +4 -4
- data/lib/selenium/webdriver/support/select.rb +1 -1
- data/lib/selenium/webdriver/version.rb +1 -1
- data/selenium-webdriver.gemspec +3 -3
- 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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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,
|
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
|
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 =>
|
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
|
-
|
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
|
-
|
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) &&
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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
|
-
|
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
|
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 =>
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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/
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
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(
|
181
|
-
|
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: [
|
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
|