selenium-webdriver 4.1.0 → 4.4.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 +85 -1
- data/LICENSE +1 -1
- data/NOTICE +1 -1
- data/lib/selenium/server.rb +15 -10
- 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 +33 -19
- data/lib/selenium/webdriver/chrome.rb +0 -14
- data/lib/selenium/webdriver/common/action_builder.rb +108 -21
- data/lib/selenium/webdriver/common/driver.rb +22 -55
- data/lib/selenium/webdriver/common/driver_extensions/{has_remote_status.rb → has_bidi.rb} +12 -5
- 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_network_interception.rb +2 -67
- 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 +59 -70
- 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/manager.rb +0 -27
- data/lib/selenium/webdriver/common/options.rb +2 -9
- 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/takes_screenshot.rb +1 -1
- data/lib/selenium/webdriver/common/virtual_authenticator/credential.rb +83 -0
- data/lib/selenium/webdriver/common/virtual_authenticator/virtual_authenticator.rb +73 -0
- data/lib/selenium/webdriver/common/virtual_authenticator/virtual_authenticator_options.rb +62 -0
- data/lib/selenium/webdriver/common/websocket_connection.rb +156 -0
- data/lib/selenium/webdriver/common/window.rb +6 -6
- data/lib/selenium/webdriver/common/zipper.rb +1 -1
- data/lib/selenium/webdriver/common.rb +17 -3
- data/lib/selenium/webdriver/devtools/network_interceptor.rb +176 -0
- data/lib/selenium/webdriver/devtools/request.rb +1 -1
- data/lib/selenium/webdriver/devtools/response.rb +1 -1
- data/lib/selenium/webdriver/devtools.rb +6 -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 -14
- data/lib/selenium/webdriver/ie.rb +0 -14
- data/lib/selenium/webdriver/remote/bridge.rb +54 -19
- data/lib/selenium/webdriver/remote/commands.rb +15 -6
- data/lib/selenium/webdriver/remote/driver.rb +0 -1
- data/lib/selenium/webdriver/remote/http/default.rb +6 -12
- data/lib/selenium/webdriver/remote/response.rb +2 -2
- data/lib/selenium/webdriver/safari.rb +0 -14
- 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 +9 -6
- metadata +64 -12
- data/lib/selenium/webdriver/remote/http/persistent.rb +0 -65
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
MAX_LOG_MESSAGE_SIZE = 9999
|
|
29
|
+
|
|
30
|
+
def initialize(url:)
|
|
31
|
+
@callback_threads = ThreadGroup.new
|
|
32
|
+
|
|
33
|
+
@session_id = nil
|
|
34
|
+
@url = url
|
|
35
|
+
|
|
36
|
+
process_handshake
|
|
37
|
+
@socket_thread = attach_socket_listener
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def close
|
|
41
|
+
@callback_threads.list.each(&:exit)
|
|
42
|
+
@socket_thread.exit
|
|
43
|
+
socket.close
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def callbacks
|
|
47
|
+
@callbacks ||= Hash.new { |callbacks, event| callbacks[event] = [] }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def send_cmd(**payload)
|
|
51
|
+
id = next_id
|
|
52
|
+
data = payload.merge(id: id)
|
|
53
|
+
WebDriver.logger.debug "WebSocket -> #{data}"[...MAX_LOG_MESSAGE_SIZE]
|
|
54
|
+
data = JSON.generate(data)
|
|
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.delete(id) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# We should be thread-safe to use the hash without synchronization
|
|
64
|
+
# because its keys are WebSocket message identifiers and they should be
|
|
65
|
+
# unique within a devtools session.
|
|
66
|
+
def messages
|
|
67
|
+
@messages ||= {}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def process_handshake
|
|
71
|
+
socket.print(ws.to_s)
|
|
72
|
+
ws << socket.readpartial(1024)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def attach_socket_listener
|
|
76
|
+
Thread.new do
|
|
77
|
+
Thread.current.abort_on_exception = true
|
|
78
|
+
Thread.current.report_on_exception = false
|
|
79
|
+
|
|
80
|
+
until socket.eof?
|
|
81
|
+
incoming_frame << socket.readpartial(1024)
|
|
82
|
+
|
|
83
|
+
while (frame = incoming_frame.next)
|
|
84
|
+
message = process_frame(frame)
|
|
85
|
+
next unless message['method']
|
|
86
|
+
|
|
87
|
+
params = message['params']
|
|
88
|
+
callbacks[message['method']].each do |callback|
|
|
89
|
+
@callback_threads.add(callback_thread(params, &callback))
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def incoming_frame
|
|
97
|
+
@incoming_frame ||= WebSocket::Frame::Incoming::Client.new(version: ws.version)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def process_frame(frame)
|
|
101
|
+
message = frame.to_s
|
|
102
|
+
|
|
103
|
+
# Firefox will periodically fail on unparsable empty frame
|
|
104
|
+
return {} if message.empty?
|
|
105
|
+
|
|
106
|
+
message = JSON.parse(message)
|
|
107
|
+
messages[message["id"]] = message
|
|
108
|
+
WebDriver.logger.debug "WebSocket <- #{message}"[...MAX_LOG_MESSAGE_SIZE]
|
|
109
|
+
|
|
110
|
+
message
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def callback_thread(params)
|
|
114
|
+
Thread.new do
|
|
115
|
+
Thread.current.abort_on_exception = true
|
|
116
|
+
|
|
117
|
+
# We might end up blocked forever when we have an error in event.
|
|
118
|
+
# For example, if network interception event raises error,
|
|
119
|
+
# the browser will keep waiting for the request to be proceeded
|
|
120
|
+
# before returning back to the original thread. In this case,
|
|
121
|
+
# we should at least print the error.
|
|
122
|
+
Thread.current.report_on_exception = true
|
|
123
|
+
|
|
124
|
+
yield params
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def wait
|
|
129
|
+
@wait ||= Wait.new(timeout: RESPONSE_WAIT_TIMEOUT, interval: RESPONSE_WAIT_INTERVAL)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def socket
|
|
133
|
+
@socket ||= if URI(@url).scheme == 'wss'
|
|
134
|
+
socket = TCPSocket.new(ws.host, ws.port)
|
|
135
|
+
socket = OpenSSL::SSL::SSLSocket.new(socket, OpenSSL::SSL::SSLContext.new)
|
|
136
|
+
socket.sync_close = true
|
|
137
|
+
socket.connect
|
|
138
|
+
|
|
139
|
+
socket
|
|
140
|
+
else
|
|
141
|
+
TCPSocket.new(ws.host, ws.port)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def ws
|
|
146
|
+
@ws ||= WebSocket::Handshake::Client.new(url: @url)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def next_id
|
|
150
|
+
@id ||= 0
|
|
151
|
+
@id += 1
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
end # BiDi
|
|
155
|
+
end # WebDriver
|
|
156
|
+
end # Selenium
|
|
@@ -36,8 +36,8 @@ module Selenium
|
|
|
36
36
|
|
|
37
37
|
def size=(dimension)
|
|
38
38
|
unless dimension.respond_to?(:width) && dimension.respond_to?(:height)
|
|
39
|
-
raise ArgumentError, "expected #{dimension.inspect}:#{dimension.class}" \
|
|
40
|
-
'
|
|
39
|
+
raise ArgumentError, "expected #{dimension.inspect}:#{dimension.class} " \
|
|
40
|
+
'to respond to #width and #height'
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
@bridge.resize_window dimension.width, dimension.height
|
|
@@ -61,8 +61,8 @@ module Selenium
|
|
|
61
61
|
|
|
62
62
|
def position=(point)
|
|
63
63
|
unless point.respond_to?(:x) && point.respond_to?(:y)
|
|
64
|
-
raise ArgumentError, "expected #{point.inspect}:#{point.class}" \
|
|
65
|
-
'
|
|
64
|
+
raise ArgumentError, "expected #{point.inspect}:#{point.class} " \
|
|
65
|
+
'to respond to #x and #y'
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
@bridge.reposition_window point.x, point.y
|
|
@@ -86,8 +86,8 @@ module Selenium
|
|
|
86
86
|
|
|
87
87
|
def rect=(rectangle)
|
|
88
88
|
unless %w[x y width height].all? { |val| rectangle.respond_to? val }
|
|
89
|
-
raise ArgumentError, "expected #{rectangle.inspect}:#{rectangle.class}" \
|
|
90
|
-
'
|
|
89
|
+
raise ArgumentError, "expected #{rectangle.inspect}:#{rectangle.class} " \
|
|
90
|
+
'to respond to #x, #y, #width, and #height'
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
@bridge.set_window_rect(x: rectangle.x,
|
|
@@ -38,15 +38,28 @@ 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'
|
|
60
|
+
require 'selenium/webdriver/common/virtual_authenticator/credential'
|
|
61
|
+
require 'selenium/webdriver/common/virtual_authenticator/virtual_authenticator_options'
|
|
62
|
+
require 'selenium/webdriver/common/virtual_authenticator/virtual_authenticator'
|
|
50
63
|
require 'selenium/webdriver/common/html5/shared_web_storage'
|
|
51
64
|
require 'selenium/webdriver/common/html5/local_storage'
|
|
52
65
|
require 'selenium/webdriver/common/html5/session_storage'
|
|
@@ -54,7 +67,6 @@ require 'selenium/webdriver/common/driver_extensions/has_web_storage'
|
|
|
54
67
|
require 'selenium/webdriver/common/driver_extensions/downloads_files'
|
|
55
68
|
require 'selenium/webdriver/common/driver_extensions/has_location'
|
|
56
69
|
require 'selenium/webdriver/common/driver_extensions/has_session_id'
|
|
57
|
-
require 'selenium/webdriver/common/driver_extensions/has_remote_status'
|
|
58
70
|
require 'selenium/webdriver/common/driver_extensions/has_network_conditions'
|
|
59
71
|
require 'selenium/webdriver/common/driver_extensions/has_network_connection'
|
|
60
72
|
require 'selenium/webdriver/common/driver_extensions/has_network_interception'
|
|
@@ -66,6 +78,7 @@ require 'selenium/webdriver/common/driver_extensions/prints_page'
|
|
|
66
78
|
require 'selenium/webdriver/common/driver_extensions/uploads_files'
|
|
67
79
|
require 'selenium/webdriver/common/driver_extensions/full_page_screenshot'
|
|
68
80
|
require 'selenium/webdriver/common/driver_extensions/has_addons'
|
|
81
|
+
require 'selenium/webdriver/common/driver_extensions/has_bidi'
|
|
69
82
|
require 'selenium/webdriver/common/driver_extensions/has_devtools'
|
|
70
83
|
require 'selenium/webdriver/common/driver_extensions/has_authentication'
|
|
71
84
|
require 'selenium/webdriver/common/driver_extensions/has_logs'
|
|
@@ -81,3 +94,4 @@ require 'selenium/webdriver/common/takes_screenshot'
|
|
|
81
94
|
require 'selenium/webdriver/common/driver'
|
|
82
95
|
require 'selenium/webdriver/common/element'
|
|
83
96
|
require 'selenium/webdriver/common/shadow_root'
|
|
97
|
+
require 'selenium/webdriver/common/websocket_connection'
|
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
class DevTools
|
|
23
|
+
|
|
24
|
+
#
|
|
25
|
+
# Wraps the network request/response interception, providing
|
|
26
|
+
# thread-safety guarantees and handling special cases such as browser
|
|
27
|
+
# canceling requests midst interception.
|
|
28
|
+
#
|
|
29
|
+
# You should not be using this class directly, use Driver#intercept instead.
|
|
30
|
+
# @api private
|
|
31
|
+
#
|
|
32
|
+
|
|
33
|
+
class NetworkInterceptor
|
|
34
|
+
|
|
35
|
+
# CDP fails to get body on certain responses (301) and raises:
|
|
36
|
+
# "Can only get response body on requests captured after headers received."
|
|
37
|
+
CANNOT_GET_BODY_ON_REDIRECT_ERROR_CODE = "-32000"
|
|
38
|
+
|
|
39
|
+
# CDP fails to operate with intercepted requests.
|
|
40
|
+
# Typical reason is browser cancelling intercepted requests/responses.
|
|
41
|
+
INVALID_INTERCEPTION_ID_ERROR_CODE = "-32602"
|
|
42
|
+
|
|
43
|
+
def initialize(devtools)
|
|
44
|
+
@devtools = devtools
|
|
45
|
+
@lock = Mutex.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def intercept(&block)
|
|
49
|
+
devtools.network.on(:loading_failed) { |params| track_cancelled_request(params) }
|
|
50
|
+
devtools.fetch.on(:request_paused) { |params| request_paused(params, &block) }
|
|
51
|
+
|
|
52
|
+
devtools.network.set_cache_disabled(cache_disabled: true)
|
|
53
|
+
devtools.network.enable
|
|
54
|
+
devtools.fetch.enable(patterns: [{requestStage: 'Request'}, {requestStage: 'Response'}])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
attr_accessor :devtools, :lock
|
|
60
|
+
|
|
61
|
+
# We should be thread-safe to use the hash without synchronization
|
|
62
|
+
# because its keys are interception job identifiers and they should be
|
|
63
|
+
# unique within a devtools session.
|
|
64
|
+
def pending_response_requests
|
|
65
|
+
@pending_response_requests ||= {}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Ensure usage of cancelled_requests is thread-safe via synchronization!
|
|
69
|
+
def cancelled_requests
|
|
70
|
+
@cancelled_requests ||= []
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def track_cancelled_request(data)
|
|
74
|
+
return unless data['canceled']
|
|
75
|
+
|
|
76
|
+
lock.synchronize { cancelled_requests << data['requestId'] }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def request_paused(data, &block)
|
|
80
|
+
id = data['requestId']
|
|
81
|
+
network_id = data['networkId']
|
|
82
|
+
|
|
83
|
+
with_cancellable_request(network_id) do
|
|
84
|
+
if response?(data)
|
|
85
|
+
block = pending_response_requests.delete(id)
|
|
86
|
+
intercept_response(id, data, &block)
|
|
87
|
+
else
|
|
88
|
+
intercept_request(id, data, &block)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The presence of any of these fields indicate we're at the response stage.
|
|
94
|
+
# @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#event-requestPaused
|
|
95
|
+
def response?(params)
|
|
96
|
+
params.key?('responseStatusCode') || params.key?('responseErrorReason')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def intercept_request(id, params, &block)
|
|
100
|
+
original = DevTools::Request.from(id, params)
|
|
101
|
+
mutable = DevTools::Request.from(id, params)
|
|
102
|
+
|
|
103
|
+
block.call(mutable) do |&continue| # rubocop:disable Performance/RedundantBlockCall
|
|
104
|
+
pending_response_requests[id] = continue
|
|
105
|
+
|
|
106
|
+
if original == mutable
|
|
107
|
+
continue_request(original.id)
|
|
108
|
+
else
|
|
109
|
+
mutate_request(mutable)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def intercept_response(id, params)
|
|
115
|
+
return continue_response(id) unless block_given?
|
|
116
|
+
|
|
117
|
+
body = fetch_response_body(id)
|
|
118
|
+
original = DevTools::Response.from(id, body, params)
|
|
119
|
+
mutable = DevTools::Response.from(id, body, params)
|
|
120
|
+
yield mutable
|
|
121
|
+
|
|
122
|
+
if original == mutable
|
|
123
|
+
continue_response(id)
|
|
124
|
+
else
|
|
125
|
+
mutate_response(mutable)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def continue_request(id)
|
|
130
|
+
devtools.fetch.continue_request(request_id: id)
|
|
131
|
+
end
|
|
132
|
+
alias_method :continue_response, :continue_request
|
|
133
|
+
|
|
134
|
+
def mutate_request(request)
|
|
135
|
+
devtools.fetch.continue_request(
|
|
136
|
+
request_id: request.id,
|
|
137
|
+
url: request.url,
|
|
138
|
+
method: request.method,
|
|
139
|
+
post_data: request.post_data,
|
|
140
|
+
headers: request.headers.map do |k, v|
|
|
141
|
+
{name: k, value: v}
|
|
142
|
+
end
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def mutate_response(response)
|
|
147
|
+
devtools.fetch.fulfill_request(
|
|
148
|
+
request_id: response.id,
|
|
149
|
+
body: (Base64.strict_encode64(response.body) if response.body),
|
|
150
|
+
response_code: response.code,
|
|
151
|
+
response_headers: response.headers.map do |k, v|
|
|
152
|
+
{name: k, value: v}
|
|
153
|
+
end
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def fetch_response_body(id)
|
|
158
|
+
devtools.fetch.get_response_body(request_id: id).dig('result', 'body')
|
|
159
|
+
rescue Error::WebDriverError => e
|
|
160
|
+
raise unless e.message.start_with?(CANNOT_GET_BODY_ON_REDIRECT_ERROR_CODE)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def with_cancellable_request(network_id)
|
|
164
|
+
yield
|
|
165
|
+
rescue Error::WebDriverError => e
|
|
166
|
+
raise if e.message.start_with?(INVALID_INTERCEPTION_ID_ERROR_CODE) && !cancelled?(network_id)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def cancelled?(network_id)
|
|
170
|
+
lock.synchronize { !!cancelled_requests.delete(network_id) }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
end # NetworkInterceptor
|
|
174
|
+
end # DevTools
|
|
175
|
+
end # WebDriver
|
|
176
|
+
end # Selenium
|
|
@@ -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,52 +20,32 @@
|
|
|
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'
|
|
26
|
+
autoload :NetworkInterceptor, 'selenium/webdriver/devtools/network_interceptor'
|
|
29
27
|
autoload :PinnedScript, 'selenium/webdriver/devtools/pinned_script'
|
|
30
28
|
autoload :Request, 'selenium/webdriver/devtools/request'
|
|
31
29
|
autoload :Response, 'selenium/webdriver/devtools/response'
|
|
32
30
|
|
|
33
31
|
def initialize(url:)
|
|
34
|
-
@
|
|
35
|
-
|
|
36
|
-
@messages = []
|
|
32
|
+
@ws = WebSocketConnection.new(url: url)
|
|
37
33
|
@session_id = nil
|
|
38
|
-
@url = url
|
|
39
|
-
|
|
40
|
-
process_handshake
|
|
41
|
-
@socket_thread = attach_socket_listener
|
|
42
34
|
start_session
|
|
43
35
|
end
|
|
44
36
|
|
|
45
37
|
def close
|
|
46
|
-
@
|
|
47
|
-
@socket_thread.exit
|
|
48
|
-
socket.close
|
|
38
|
+
@ws.close
|
|
49
39
|
end
|
|
50
40
|
|
|
51
41
|
def callbacks
|
|
52
|
-
@
|
|
42
|
+
@ws.callbacks
|
|
53
43
|
end
|
|
54
44
|
|
|
55
45
|
def send_cmd(method, **params)
|
|
56
|
-
|
|
57
|
-
data = {id: id, method: method, params: params.reject { |_, v| v.nil? }}
|
|
46
|
+
data = {method: method, params: params.compact}
|
|
58
47
|
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
|
-
|
|
48
|
+
message = @ws.send_cmd(**data)
|
|
69
49
|
raise Error::WebDriverError, error_message(message['error']) if message['error']
|
|
70
50
|
|
|
71
51
|
message
|
|
@@ -91,32 +71,6 @@ module Selenium
|
|
|
91
71
|
|
|
92
72
|
private
|
|
93
73
|
|
|
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
74
|
def start_session
|
|
121
75
|
targets = target.get_targets.dig('result', 'targetInfos')
|
|
122
76
|
page_target = targets.find { |target| target['type'] == 'page' }
|
|
@@ -124,66 +78,6 @@ module Selenium
|
|
|
124
78
|
@session_id = session.dig('result', 'sessionId')
|
|
125
79
|
end
|
|
126
80
|
|
|
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
81
|
def error_message(error)
|
|
188
82
|
[error['code'], error['message'], error['data']].join(': ')
|
|
189
83
|
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
|