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.
- checksums.yaml +4 -4
- data/CHANGES +46 -1
- data/LICENSE +1 -1
- data/NOTICE +1 -1
- data/lib/selenium/server.rb +14 -9
- data/lib/selenium/webdriver/bidi/session.rb +38 -0
- data/lib/selenium/webdriver/bidi.rb +55 -0
- data/lib/selenium/webdriver/chrome/features.rb +5 -0
- data/lib/selenium/webdriver/chrome/options.rb +19 -19
- data/lib/selenium/webdriver/common/action_builder.rb +108 -21
- data/lib/selenium/webdriver/common/driver.rb +2 -2
- data/lib/selenium/webdriver/common/driver_extensions/has_bidi.rb +38 -0
- data/lib/selenium/webdriver/common/driver_extensions/has_casting.rb +10 -0
- data/lib/selenium/webdriver/common/driver_extensions/has_context.rb +1 -2
- data/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb +1 -1
- data/lib/selenium/webdriver/common/driver_extensions/has_pinned_scripts.rb +1 -1
- data/lib/selenium/webdriver/common/element.rb +1 -1
- data/lib/selenium/webdriver/common/error.rb +1 -1
- data/lib/selenium/webdriver/common/interactions/input_device.rb +10 -4
- data/lib/selenium/webdriver/common/interactions/interaction.rb +12 -25
- data/lib/selenium/webdriver/common/interactions/interactions.rb +24 -4
- data/lib/selenium/webdriver/common/interactions/key_actions.rb +5 -1
- data/lib/selenium/webdriver/common/interactions/key_input.rb +11 -27
- data/lib/selenium/webdriver/common/interactions/none_input.rb +10 -8
- data/lib/selenium/webdriver/common/interactions/pause.rb +49 -0
- data/lib/selenium/webdriver/common/interactions/pointer_actions.rb +49 -43
- data/lib/selenium/webdriver/common/interactions/pointer_cancel.rb +45 -0
- data/lib/selenium/webdriver/common/interactions/pointer_event_properties.rb +63 -0
- data/lib/selenium/webdriver/common/interactions/pointer_input.rb +15 -84
- data/lib/selenium/webdriver/common/interactions/pointer_move.rb +60 -0
- data/lib/selenium/webdriver/common/interactions/pointer_press.rb +85 -0
- data/lib/selenium/webdriver/common/interactions/scroll.rb +57 -0
- data/lib/selenium/webdriver/common/interactions/scroll_origin.rb +48 -0
- data/lib/selenium/webdriver/common/interactions/typing_interaction.rb +54 -0
- data/lib/selenium/webdriver/common/interactions/wheel_actions.rb +113 -0
- data/lib/selenium/webdriver/common/interactions/wheel_input.rb +42 -0
- data/lib/selenium/webdriver/common/keys.rb +1 -0
- data/lib/selenium/webdriver/common/platform.rb +4 -4
- data/lib/selenium/webdriver/common/search_context.rb +0 -6
- data/lib/selenium/webdriver/common/service_manager.rb +2 -3
- data/lib/selenium/webdriver/common/shadow_root.rb +1 -1
- data/lib/selenium/webdriver/common/socket_poller.rb +1 -1
- data/lib/selenium/webdriver/common/websocket_connection.rb +149 -0
- data/lib/selenium/webdriver/common.rb +14 -2
- data/lib/selenium/webdriver/devtools/request.rb +1 -1
- data/lib/selenium/webdriver/devtools/response.rb +1 -1
- data/lib/selenium/webdriver/devtools.rb +5 -112
- data/lib/selenium/webdriver/edge/features.rb +1 -0
- data/lib/selenium/webdriver/firefox/driver.rb +1 -0
- data/lib/selenium/webdriver/firefox/features.rb +2 -5
- data/lib/selenium/webdriver/firefox/options.rb +3 -1
- data/lib/selenium/webdriver/firefox/profile.rb +1 -5
- data/lib/selenium/webdriver/firefox/util.rb +46 -0
- data/lib/selenium/webdriver/firefox.rb +1 -0
- data/lib/selenium/webdriver/remote/bridge.rb +21 -19
- data/lib/selenium/webdriver/remote/commands.rb +0 -5
- data/lib/selenium/webdriver/remote/http/default.rb +6 -12
- data/lib/selenium/webdriver/remote/response.rb +2 -2
- data/lib/selenium/webdriver/support/cdp_client_generator.rb +4 -4
- data/lib/selenium/webdriver/support/color.rb +7 -7
- data/lib/selenium/webdriver/support/guards/guard_condition.rb +1 -1
- data/lib/selenium/webdriver/support/guards.rb +1 -1
- data/lib/selenium/webdriver/version.rb +1 -1
- data/lib/selenium/webdriver.rb +1 -0
- data/selenium-webdriver.gemspec +7 -4
- metadata +55 -5
@@ -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
|
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
|
-
@
|
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
|
-
@
|
47
|
-
@socket_thread.exit
|
48
|
-
socket.close
|
37
|
+
@ws.close
|
49
38
|
end
|
50
39
|
|
51
40
|
def callbacks
|
52
|
-
@
|
41
|
+
@ws.callbacks
|
53
42
|
end
|
54
43
|
|
55
44
|
def send_cmd(method, **params)
|
56
|
-
|
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
|
-
|
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
|
-
|
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 = {
|
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://
|
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]
|
34
|
-
# @param [Object]
|
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.
|
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
|
-
|
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(
|
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.
|
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
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
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
|
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
|
146
|
-
no_proxy = ENV
|
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.
|
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
|
-
|
90
|
+
end
|
91
91
|
end
|
92
92
|
|
93
93
|
def process_error
|