selenium-webdriver 4.3.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 +15 -0
- data/lib/selenium/server.rb +1 -1
- data/lib/selenium/webdriver/chrome/options.rb +14 -0
- data/lib/selenium/webdriver/common/driver.rb +9 -0
- data/lib/selenium/webdriver/common/driver_extensions/has_network_interception.rb +2 -67
- data/lib/selenium/webdriver/common/interactions/pointer_actions.rb +6 -7
- 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 +13 -6
- data/lib/selenium/webdriver/common/window.rb +6 -6
- data/lib/selenium/webdriver/common/zipper.rb +1 -1
- data/lib/selenium/webdriver/common.rb +3 -0
- data/lib/selenium/webdriver/devtools/network_interceptor.rb +176 -0
- data/lib/selenium/webdriver/devtools.rb +1 -0
- data/lib/selenium/webdriver/remote/bridge.rb +35 -2
- data/lib/selenium/webdriver/remote/commands.rb +15 -1
- data/lib/selenium/webdriver/version.rb +1 -1
- data/selenium-webdriver.gemspec +3 -3
- metadata +16 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2be9fb14d69ce82170e3ddbe0cbcf2f2428e1978544d8a7c11191ebd37cf0241
|
4
|
+
data.tar.gz: 2e40d6ad67ccfb269ee1ae9d3f47d024e90081da9881f757f22170751875fa69
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b7f419d9b1349a1afbf68999eecaa53391503cc793026228577fbcb40ac4958a35c539286f7c4c3c913992336dc797481f7c57f6aec9d58813671e24d1cd09ad
|
7
|
+
data.tar.gz: '0093bd2d0fbb0c1c8e7abee9d1a632f80d458b1dc0d1b22635f403230ed7c40a2be498915d8abed40bd4992295a1444a69c3e653ebf1d4cf106886c8a9d5a499'
|
data/CHANGES
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
4.4.0 (Unreleased)
|
2
|
+
=========================
|
3
|
+
|
4
|
+
BiDi:
|
5
|
+
* Released selenium-devtools 0.103.1 to fix websocket dependency requirement
|
6
|
+
* Released selenium-devtools 0.104.0 (supports CDP v85, v102, v103, v104)
|
7
|
+
* Have network interceptor ignore cancelled requests (#10856)
|
8
|
+
* Improve websocket message handling
|
9
|
+
|
10
|
+
Chromium:
|
11
|
+
* Prevent users from setting w3c: false and warn for true (thanks Tamsil Amani!)
|
12
|
+
|
13
|
+
Ruby:
|
14
|
+
* Implement Virtual Authenticator (#10903, #10541) (thanks Tamsil Amani!)
|
15
|
+
|
1
16
|
4.3.0 (2022-06-23)
|
2
17
|
=========================
|
3
18
|
|
data/lib/selenium/server.rb
CHANGED
@@ -227,6 +227,9 @@ module Selenium
|
|
227
227
|
|
228
228
|
options = browser_options[self.class::KEY]
|
229
229
|
options['binary'] ||= binary_path if binary_path
|
230
|
+
|
231
|
+
check_w3c(options[:w3c]) if options.key?(:w3c)
|
232
|
+
|
230
233
|
if @profile
|
231
234
|
options['args'] ||= []
|
232
235
|
options['args'] << "--user-data-dir=#{@profile.directory}"
|
@@ -237,6 +240,17 @@ module Selenium
|
|
237
240
|
options['extensions'] = @encoded_extensions + @extensions.map { |ext| encode_extension(ext) }
|
238
241
|
end
|
239
242
|
|
243
|
+
def check_w3c(w3c)
|
244
|
+
if w3c
|
245
|
+
WebDriver.logger.warn("Setting 'w3c: true' is redundant and will no longer be allowed", id: :w3c)
|
246
|
+
return
|
247
|
+
end
|
248
|
+
|
249
|
+
raise Error::InvalidArgumentError,
|
250
|
+
"Setting 'w3c: false' is not allowed.\n" \
|
251
|
+
"Please update to W3C Syntax: https://www.selenium.dev/blog/2022/legacy-protocol-support/"
|
252
|
+
end
|
253
|
+
|
240
254
|
def binary_path
|
241
255
|
Chrome.path
|
242
256
|
end
|
@@ -248,6 +248,15 @@ module Selenium
|
|
248
248
|
bridge.execute_async_script(script, *args)
|
249
249
|
end
|
250
250
|
|
251
|
+
#
|
252
|
+
# @return [VirtualAuthenticator]
|
253
|
+
# @see VirtualAuthenticator
|
254
|
+
#
|
255
|
+
|
256
|
+
def add_virtual_authenticator(options)
|
257
|
+
bridge.add_virtual_authenticator(options)
|
258
|
+
end
|
259
|
+
|
251
260
|
#-------------------------------- sugar --------------------------------
|
252
261
|
|
253
262
|
#
|
@@ -61,75 +61,10 @@ module Selenium
|
|
61
61
|
#
|
62
62
|
|
63
63
|
def intercept(&block)
|
64
|
-
|
65
|
-
|
66
|
-
id = params['requestId']
|
67
|
-
if params.key?('responseStatusCode') || params.key?('responseErrorReason')
|
68
|
-
intercept_response(id, params, &pending_response_requests.delete(id))
|
69
|
-
else
|
70
|
-
intercept_request(id, params, &block)
|
71
|
-
end
|
72
|
-
end
|
73
|
-
devtools.fetch.enable(patterns: [{requestStage: 'Request'}, {requestStage: 'Response'}])
|
64
|
+
@interceptor ||= DevTools::NetworkInterceptor.new(devtools)
|
65
|
+
@interceptor.intercept(&block)
|
74
66
|
end
|
75
67
|
|
76
|
-
private
|
77
|
-
|
78
|
-
def pending_response_requests
|
79
|
-
@pending_response_requests ||= {}
|
80
|
-
end
|
81
|
-
|
82
|
-
def intercept_request(id, params, &block)
|
83
|
-
original = DevTools::Request.from(id, params)
|
84
|
-
mutable = DevTools::Request.from(id, params)
|
85
|
-
|
86
|
-
block.call(mutable) do |&continue| # rubocop:disable Performance/RedundantBlockCall
|
87
|
-
pending_response_requests[id] = continue
|
88
|
-
|
89
|
-
if original == mutable
|
90
|
-
devtools.fetch.continue_request(request_id: id)
|
91
|
-
else
|
92
|
-
devtools.fetch.continue_request(
|
93
|
-
request_id: id,
|
94
|
-
url: mutable.url,
|
95
|
-
method: mutable.method,
|
96
|
-
post_data: mutable.post_data,
|
97
|
-
headers: mutable.headers.map do |k, v|
|
98
|
-
{name: k, value: v}
|
99
|
-
end
|
100
|
-
)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
def intercept_response(id, params)
|
106
|
-
return devtools.fetch.continue_request(request_id: id) unless block_given?
|
107
|
-
|
108
|
-
body = fetch_response_body(id)
|
109
|
-
original = DevTools::Response.from(id, body, params)
|
110
|
-
mutable = DevTools::Response.from(id, body, params)
|
111
|
-
yield mutable
|
112
|
-
|
113
|
-
if original == mutable
|
114
|
-
devtools.fetch.continue_request(request_id: id)
|
115
|
-
else
|
116
|
-
devtools.fetch.fulfill_request(
|
117
|
-
request_id: id,
|
118
|
-
body: (Base64.strict_encode64(mutable.body) if mutable.body),
|
119
|
-
response_code: mutable.code,
|
120
|
-
response_headers: mutable.headers.map do |k, v|
|
121
|
-
{name: k, value: v}
|
122
|
-
end
|
123
|
-
)
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
def fetch_response_body(id)
|
128
|
-
devtools.fetch.get_response_body(request_id: id).dig('result', 'body')
|
129
|
-
rescue Error::WebDriverError
|
130
|
-
# CDP fails to get body on certain responses (301) and raises:
|
131
|
-
# Can only get response body on requests captured after headers received.
|
132
|
-
end
|
133
68
|
end # HasNetworkInterception
|
134
69
|
end # DriverExtensions
|
135
70
|
end # WebDriver
|
@@ -68,9 +68,8 @@ module Selenium
|
|
68
68
|
end
|
69
69
|
|
70
70
|
#
|
71
|
-
# Moves the pointer to the
|
72
|
-
#
|
73
|
-
# Then the pointer is moved to optional offset coordinates from the element.
|
71
|
+
# Moves the pointer to the in-view center point of the given element.
|
72
|
+
# Then the pointer is moved to optional offset coordinates.
|
74
73
|
#
|
75
74
|
# The element is not scrolled into view.
|
76
75
|
# MoveTargetOutOfBoundsError will be raised if element with offset is outside the viewport
|
@@ -88,10 +87,10 @@ module Selenium
|
|
88
87
|
# driver.action.move_to(el, 100, 100).perform
|
89
88
|
#
|
90
89
|
# @param [Selenium::WebDriver::Element] element to move to.
|
91
|
-
# @param [Integer] right_by Optional offset from the
|
92
|
-
# coordinates to the left of the
|
93
|
-
# @param [Integer] down_by Optional offset from the
|
94
|
-
# coordinates
|
90
|
+
# @param [Integer] right_by Optional offset from the in-view center of the
|
91
|
+
# element. A negative value means coordinates to the left of the center.
|
92
|
+
# @param [Integer] down_by Optional offset from the in-view center of the
|
93
|
+
# element. A negative value means coordinates to the top of the center.
|
95
94
|
# @param [Symbol || String] device optional name of the PointerInput device to move.
|
96
95
|
# @return [ActionBuilder] A self reference.
|
97
96
|
#
|
@@ -32,7 +32,7 @@ module Selenium
|
|
32
32
|
def save_screenshot(png_path, full_page: false)
|
33
33
|
extension = File.extname(png_path).downcase
|
34
34
|
if extension != '.png'
|
35
|
-
WebDriver.logger.warn "name used for saved screenshot does not match file type. "\
|
35
|
+
WebDriver.logger.warn "name used for saved screenshot does not match file type. " \
|
36
36
|
"It should end with .png extension",
|
37
37
|
id: :screenshot
|
38
38
|
end
|
@@ -0,0 +1,83 @@
|
|
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
|
+
#
|
21
|
+
# A credential stored in a virtual authenticator.
|
22
|
+
# @see https://w3c.github.io/webauthn/#credential-parameters
|
23
|
+
#
|
24
|
+
|
25
|
+
module Selenium
|
26
|
+
module WebDriver
|
27
|
+
class Credential
|
28
|
+
class << self
|
29
|
+
def resident(**opts)
|
30
|
+
Credential.new(resident_credential: true, **opts)
|
31
|
+
end
|
32
|
+
|
33
|
+
def non_resident(**opts)
|
34
|
+
Credential.new(resident_credential: false, **opts)
|
35
|
+
end
|
36
|
+
|
37
|
+
def encode(byte_array)
|
38
|
+
Base64.urlsafe_encode64(byte_array&.pack('C*'))
|
39
|
+
end
|
40
|
+
|
41
|
+
def decode(base64)
|
42
|
+
Base64.urlsafe_decode64(base64).unpack('C*')
|
43
|
+
end
|
44
|
+
|
45
|
+
def from_json(opts)
|
46
|
+
user_handle = opts['userHandle'] ? decode(opts['userHandle']) : nil
|
47
|
+
new(id: decode(opts["credentialId"]),
|
48
|
+
resident_credential: opts["isResidentCredential"],
|
49
|
+
rp_id: opts['rpId'],
|
50
|
+
private_key: opts['privateKey'],
|
51
|
+
sign_count: opts['signCount'],
|
52
|
+
user_handle: user_handle)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
attr_reader :id, :resident_credential, :rp_id, :user_handle, :private_key, :sign_count
|
57
|
+
alias_method :resident_credential?, :resident_credential
|
58
|
+
|
59
|
+
def initialize(id:, resident_credential:, rp_id:, private_key:, user_handle: nil, sign_count: 0)
|
60
|
+
@id = id
|
61
|
+
@resident_credential = resident_credential
|
62
|
+
@rp_id = rp_id
|
63
|
+
@user_handle = user_handle
|
64
|
+
@private_key = private_key
|
65
|
+
@sign_count = sign_count
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# @api private
|
70
|
+
#
|
71
|
+
|
72
|
+
def as_json(*)
|
73
|
+
credential_data = {'credentialId' => Credential.encode(id),
|
74
|
+
'isResidentCredential' => resident_credential?,
|
75
|
+
'rpId' => rp_id,
|
76
|
+
'privateKey' => Credential.encode(private_key),
|
77
|
+
'signCount' => sign_count}
|
78
|
+
credential_data['userHandle'] = Credential.encode(user_handle) if user_handle
|
79
|
+
credential_data
|
80
|
+
end
|
81
|
+
end # Credential
|
82
|
+
end # WebDriver
|
83
|
+
end # Selenium
|
@@ -0,0 +1,73 @@
|
|
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 VirtualAuthenticator
|
23
|
+
|
24
|
+
attr_reader :options
|
25
|
+
|
26
|
+
#
|
27
|
+
# api private
|
28
|
+
# Use `Driver#add_virtual_authenticator`
|
29
|
+
#
|
30
|
+
|
31
|
+
def initialize(bridge, authenticator_id, options)
|
32
|
+
@id = authenticator_id
|
33
|
+
@bridge = bridge
|
34
|
+
@options = options
|
35
|
+
@valid = true
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_credential(credential)
|
39
|
+
credential = credential.as_json
|
40
|
+
@bridge.add_credential credential, @id
|
41
|
+
end
|
42
|
+
|
43
|
+
def credentials
|
44
|
+
credential_data = @bridge.credentials @id
|
45
|
+
credential_data.map do |cred|
|
46
|
+
Credential.from_json(cred)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def remove_credential(credential_id)
|
51
|
+
credential_id = Credential.encode(credential_id) if credential_id.instance_of?(Array)
|
52
|
+
@bridge.remove_credential credential_id, @id
|
53
|
+
end
|
54
|
+
|
55
|
+
def remove_all_credentials
|
56
|
+
@bridge.remove_all_credentials @id
|
57
|
+
end
|
58
|
+
|
59
|
+
def user_verified=(verified)
|
60
|
+
@bridge.user_verified verified, @id
|
61
|
+
end
|
62
|
+
|
63
|
+
def remove!
|
64
|
+
@bridge.remove_virtual_authenticator(@id)
|
65
|
+
@valid = false
|
66
|
+
end
|
67
|
+
|
68
|
+
def valid?
|
69
|
+
@valid
|
70
|
+
end
|
71
|
+
end # VirtualAuthenticator
|
72
|
+
end # WebDriver
|
73
|
+
end # Selenium
|
@@ -0,0 +1,62 @@
|
|
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
|
+
#
|
21
|
+
# Options for the creation of virtual authenticators.
|
22
|
+
# @see http://w3c.github.io/webauthn/#sctn-automation
|
23
|
+
#
|
24
|
+
|
25
|
+
module Selenium
|
26
|
+
module WebDriver
|
27
|
+
class VirtualAuthenticatorOptions
|
28
|
+
|
29
|
+
PROTOCOL = {ctap2: "ctap2", u2f: "ctap1/u2f"}.freeze
|
30
|
+
TRANSPORT = {ble: "ble", usb: "usb", nfc: "nfc", internal: "internal"}.freeze
|
31
|
+
|
32
|
+
attr_accessor :protocol, :transport, :resident_key, :user_verification, :user_consenting, :user_verified
|
33
|
+
alias_method :resident_key?, :resident_key
|
34
|
+
alias_method :user_verification?, :user_verification
|
35
|
+
alias_method :user_consenting?, :user_consenting
|
36
|
+
alias_method :user_verified?, :user_verified
|
37
|
+
|
38
|
+
def initialize(protocol: :ctap2, transport: :usb, resident_key: false,
|
39
|
+
user_verification: false, user_consenting: true, user_verified: false)
|
40
|
+
@protocol = protocol
|
41
|
+
@transport = transport
|
42
|
+
@resident_key = resident_key
|
43
|
+
@user_verification = user_verification
|
44
|
+
@user_consenting = user_consenting
|
45
|
+
@user_verified = user_verified
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# @api private
|
50
|
+
#
|
51
|
+
|
52
|
+
def as_json(*)
|
53
|
+
{'protocol' => PROTOCOL[protocol],
|
54
|
+
'transport' => TRANSPORT[transport],
|
55
|
+
'hasResidentKey' => resident_key?,
|
56
|
+
'hasUserVerification' => user_verification?,
|
57
|
+
'isUserConsenting' => user_consenting?,
|
58
|
+
'isUserVerified' => user_verified?}
|
59
|
+
end
|
60
|
+
end # VirtualAuthenticatorOptions
|
61
|
+
end # WebDriver
|
62
|
+
end # Selenium
|
@@ -25,10 +25,11 @@ module Selenium
|
|
25
25
|
RESPONSE_WAIT_TIMEOUT = 30
|
26
26
|
RESPONSE_WAIT_INTERVAL = 0.1
|
27
27
|
|
28
|
+
MAX_LOG_MESSAGE_SIZE = 9999
|
29
|
+
|
28
30
|
def initialize(url:)
|
29
31
|
@callback_threads = ThreadGroup.new
|
30
32
|
|
31
|
-
@messages = []
|
32
33
|
@session_id = nil
|
33
34
|
@url = url
|
34
35
|
|
@@ -49,17 +50,23 @@ module Selenium
|
|
49
50
|
def send_cmd(**payload)
|
50
51
|
id = next_id
|
51
52
|
data = payload.merge(id: id)
|
53
|
+
WebDriver.logger.debug "WebSocket -> #{data}"[...MAX_LOG_MESSAGE_SIZE]
|
52
54
|
data = JSON.generate(data)
|
53
|
-
WebDriver.logger.debug "WebSocket -> #{data}"
|
54
|
-
|
55
55
|
out_frame = WebSocket::Frame::Outgoing::Client.new(version: ws.version, data: data, type: 'text')
|
56
56
|
socket.write(out_frame.to_s)
|
57
57
|
|
58
|
-
wait.until {
|
58
|
+
wait.until { messages.delete(id) }
|
59
59
|
end
|
60
60
|
|
61
61
|
private
|
62
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
|
+
|
63
70
|
def process_handshake
|
64
71
|
socket.print(ws.to_s)
|
65
72
|
ws << socket.readpartial(1024)
|
@@ -97,8 +104,8 @@ module Selenium
|
|
97
104
|
return {} if message.empty?
|
98
105
|
|
99
106
|
message = JSON.parse(message)
|
100
|
-
|
101
|
-
WebDriver.logger.debug "WebSocket <- #{message}"
|
107
|
+
messages[message["id"]] = message
|
108
|
+
WebDriver.logger.debug "WebSocket <- #{message}"[...MAX_LOG_MESSAGE_SIZE]
|
102
109
|
|
103
110
|
message
|
104
111
|
end
|
@@ -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,
|
@@ -57,6 +57,9 @@ require 'selenium/webdriver/common/interactions/wheel_input'
|
|
57
57
|
require 'selenium/webdriver/common/interactions/scroll_origin'
|
58
58
|
require 'selenium/webdriver/common/interactions/wheel_actions'
|
59
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'
|
60
63
|
require 'selenium/webdriver/common/html5/shared_web_storage'
|
61
64
|
require 'selenium/webdriver/common/html5/local_storage'
|
62
65
|
require 'selenium/webdriver/common/html5/session_storage'
|
@@ -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
|
@@ -23,6 +23,7 @@ module Selenium
|
|
23
23
|
autoload :ConsoleEvent, 'selenium/webdriver/devtools/console_event'
|
24
24
|
autoload :ExceptionEvent, 'selenium/webdriver/devtools/exception_event'
|
25
25
|
autoload :MutationEvent, 'selenium/webdriver/devtools/mutation_event'
|
26
|
+
autoload :NetworkInterceptor, 'selenium/webdriver/devtools/network_interceptor'
|
26
27
|
autoload :PinnedScript, 'selenium/webdriver/devtools/pinned_script'
|
27
28
|
autoload :Request, 'selenium/webdriver/devtools/request'
|
28
29
|
autoload :Response, 'selenium/webdriver/devtools/response'
|
@@ -426,8 +426,8 @@ module Selenium
|
|
426
426
|
|
427
427
|
def submit_element(element)
|
428
428
|
script = "var form = arguments[0];\n" \
|
429
|
-
"while (form.nodeName != \"FORM\" && form.parentNode) {\n" \
|
430
|
-
"
|
429
|
+
"while (form.nodeName != \"FORM\" && form.parentNode) {\n " \
|
430
|
+
"form = form.parentNode;\n" \
|
431
431
|
"}\n" \
|
432
432
|
"if (!form) { throw Error('Unable to find containing form element'); }\n" \
|
433
433
|
"if (!form.ownerDocument) { throw Error('Unable to find owning document'); }\n" \
|
@@ -568,6 +568,39 @@ module Selenium
|
|
568
568
|
ShadowRoot.new self, shadow_root_id_from(id)
|
569
569
|
end
|
570
570
|
|
571
|
+
#
|
572
|
+
# virtual-authenticator
|
573
|
+
#
|
574
|
+
|
575
|
+
def add_virtual_authenticator(options)
|
576
|
+
authenticator_id = execute :add_virtual_authenticator, {}, options.as_json
|
577
|
+
VirtualAuthenticator.new(self, authenticator_id, options)
|
578
|
+
end
|
579
|
+
|
580
|
+
def remove_virtual_authenticator(id)
|
581
|
+
execute :remove_virtual_authenticator, {authenticatorId: id}
|
582
|
+
end
|
583
|
+
|
584
|
+
def add_credential(credential, id)
|
585
|
+
execute :add_credential, {authenticatorId: id}, credential
|
586
|
+
end
|
587
|
+
|
588
|
+
def credentials(authenticator_id)
|
589
|
+
execute :get_credentials, {authenticatorId: authenticator_id}
|
590
|
+
end
|
591
|
+
|
592
|
+
def remove_credential(credential_id, authenticator_id)
|
593
|
+
execute :remove_credential, {credentialId: credential_id, authenticatorId: authenticator_id}
|
594
|
+
end
|
595
|
+
|
596
|
+
def remove_all_credentials(authenticator_id)
|
597
|
+
execute :remove_all_credentials, {authenticatorId: authenticator_id}
|
598
|
+
end
|
599
|
+
|
600
|
+
def user_verified(verified, authenticator_id)
|
601
|
+
execute :set_user_verified, {authenticatorId: authenticator_id}, {isUserVerified: verified}
|
602
|
+
end
|
603
|
+
|
571
604
|
private
|
572
605
|
|
573
606
|
#
|
@@ -149,7 +149,21 @@ module Selenium
|
|
149
149
|
# server extensions
|
150
150
|
#
|
151
151
|
|
152
|
-
upload_file: [:post, 'session/:session_id/se/file']
|
152
|
+
upload_file: [:post, 'session/:session_id/se/file'],
|
153
|
+
|
154
|
+
#
|
155
|
+
# virtual-authenticator
|
156
|
+
#
|
157
|
+
|
158
|
+
add_virtual_authenticator: [:post, 'session/:session_id/webauthn/authenticator'],
|
159
|
+
remove_virtual_authenticator: [:delete, 'session/:session_id/webauthn/authenticator/:authenticatorId'],
|
160
|
+
add_credential: [:post, 'session/:session_id/webauthn/authenticator/:authenticatorId/credential'],
|
161
|
+
get_credentials: [:get, 'session/:session_id/webauthn/authenticator/:authenticatorId/credentials'],
|
162
|
+
remove_credential: [:delete,
|
163
|
+
'session/:session_id/webauthn/authenticator/:authenticatorId/credentials/:credentialId'],
|
164
|
+
remove_all_credentials: [:delete, 'session/:session_id/webauthn/authenticator/:authenticatorId/credentials'],
|
165
|
+
set_user_verified: [:post, 'session/:session_id/webauthn/authenticator/:authenticatorId/uv']
|
166
|
+
|
153
167
|
}.freeze
|
154
168
|
|
155
169
|
end # Bridge
|
data/selenium-webdriver.gemspec
CHANGED
@@ -56,10 +56,10 @@ Gem::Specification.new do |s|
|
|
56
56
|
s.add_development_dependency 'rack', ['~> 2.0']
|
57
57
|
s.add_development_dependency 'rake'
|
58
58
|
s.add_development_dependency 'rspec', ['~> 3.0']
|
59
|
-
s.add_development_dependency 'rubocop', ['~> 1.
|
60
|
-
s.add_development_dependency 'rubocop-performance'
|
59
|
+
s.add_development_dependency 'rubocop', ['~> 1.31']
|
60
|
+
s.add_development_dependency 'rubocop-performance', ['~> 1.13']
|
61
61
|
s.add_development_dependency 'rubocop-rake'
|
62
|
-
s.add_development_dependency 'rubocop-rspec'
|
62
|
+
s.add_development_dependency 'rubocop-rspec', ['~> 2.12']
|
63
63
|
s.add_development_dependency 'webmock', ['~> 3.5']
|
64
64
|
s.add_development_dependency 'webrick', ['~> 1.7']
|
65
65
|
s.add_development_dependency 'yard', ['~> 0.9.11']
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: selenium-webdriver
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alex Rodionov
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2022-
|
13
|
+
date: 2022-08-09 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: childprocess
|
@@ -162,28 +162,28 @@ dependencies:
|
|
162
162
|
requirements:
|
163
163
|
- - "~>"
|
164
164
|
- !ruby/object:Gem::Version
|
165
|
-
version: '1.
|
165
|
+
version: '1.31'
|
166
166
|
type: :development
|
167
167
|
prerelease: false
|
168
168
|
version_requirements: !ruby/object:Gem::Requirement
|
169
169
|
requirements:
|
170
170
|
- - "~>"
|
171
171
|
- !ruby/object:Gem::Version
|
172
|
-
version: '1.
|
172
|
+
version: '1.31'
|
173
173
|
- !ruby/object:Gem::Dependency
|
174
174
|
name: rubocop-performance
|
175
175
|
requirement: !ruby/object:Gem::Requirement
|
176
176
|
requirements:
|
177
|
-
- - "
|
177
|
+
- - "~>"
|
178
178
|
- !ruby/object:Gem::Version
|
179
|
-
version: '
|
179
|
+
version: '1.13'
|
180
180
|
type: :development
|
181
181
|
prerelease: false
|
182
182
|
version_requirements: !ruby/object:Gem::Requirement
|
183
183
|
requirements:
|
184
|
-
- - "
|
184
|
+
- - "~>"
|
185
185
|
- !ruby/object:Gem::Version
|
186
|
-
version: '
|
186
|
+
version: '1.13'
|
187
187
|
- !ruby/object:Gem::Dependency
|
188
188
|
name: rubocop-rake
|
189
189
|
requirement: !ruby/object:Gem::Requirement
|
@@ -202,16 +202,16 @@ dependencies:
|
|
202
202
|
name: rubocop-rspec
|
203
203
|
requirement: !ruby/object:Gem::Requirement
|
204
204
|
requirements:
|
205
|
-
- - "
|
205
|
+
- - "~>"
|
206
206
|
- !ruby/object:Gem::Version
|
207
|
-
version: '
|
207
|
+
version: '2.12'
|
208
208
|
type: :development
|
209
209
|
prerelease: false
|
210
210
|
version_requirements: !ruby/object:Gem::Requirement
|
211
211
|
requirements:
|
212
|
-
- - "
|
212
|
+
- - "~>"
|
213
213
|
- !ruby/object:Gem::Version
|
214
|
-
version: '
|
214
|
+
version: '2.12'
|
215
215
|
- !ruby/object:Gem::Dependency
|
216
216
|
name: webmock
|
217
217
|
requirement: !ruby/object:Gem::Requirement
|
@@ -359,6 +359,9 @@ files:
|
|
359
359
|
- lib/selenium/webdriver/common/takes_screenshot.rb
|
360
360
|
- lib/selenium/webdriver/common/target_locator.rb
|
361
361
|
- lib/selenium/webdriver/common/timeouts.rb
|
362
|
+
- lib/selenium/webdriver/common/virtual_authenticator/credential.rb
|
363
|
+
- lib/selenium/webdriver/common/virtual_authenticator/virtual_authenticator.rb
|
364
|
+
- lib/selenium/webdriver/common/virtual_authenticator/virtual_authenticator_options.rb
|
362
365
|
- lib/selenium/webdriver/common/wait.rb
|
363
366
|
- lib/selenium/webdriver/common/websocket_connection.rb
|
364
367
|
- lib/selenium/webdriver/common/window.rb
|
@@ -367,6 +370,7 @@ files:
|
|
367
370
|
- lib/selenium/webdriver/devtools/console_event.rb
|
368
371
|
- lib/selenium/webdriver/devtools/exception_event.rb
|
369
372
|
- lib/selenium/webdriver/devtools/mutation_event.rb
|
373
|
+
- lib/selenium/webdriver/devtools/network_interceptor.rb
|
370
374
|
- lib/selenium/webdriver/devtools/pinned_script.rb
|
371
375
|
- lib/selenium/webdriver/devtools/request.rb
|
372
376
|
- lib/selenium/webdriver/devtools/response.rb
|