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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89c68b6ae8db07e10f1e0ead164a3d36f5294c62166d361b1ec1fa42647e3672
4
- data.tar.gz: c1d0c0c2322acaaa95b98d5187315c975289282b3ee2085c14b86426b819de45
3
+ metadata.gz: 2be9fb14d69ce82170e3ddbe0cbcf2f2428e1978544d8a7c11191ebd37cf0241
4
+ data.tar.gz: 2e40d6ad67ccfb269ee1ae9d3f47d024e90081da9881f757f22170751875fa69
5
5
  SHA512:
6
- metadata.gz: 120b0f57601862015b62dc9ae23b510ca2433342cc500ae4fe38a70550c07e91ff20e26fd2b5a03066830b8e1f06246d1e23151070207c1c8e5e4cfc37c20889
7
- data.tar.gz: 3664cea6889b141e7de551ed518471fc2617ae6321c56981e898249fa688564aed92c5a05dba66333bf97457ed551e7051b009340a5e9abb91249e608633b416
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
 
@@ -92,7 +92,7 @@ module Selenium
92
92
  download_server(redirected, destination)
93
93
  end
94
94
  rescue StandardError
95
- FileUtils.rm download_file_name if File.exist? download_file_name
95
+ FileUtils.rm_rf download_file_name
96
96
  raise
97
97
  end
98
98
 
@@ -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
- devtools.network.set_cache_disabled(cache_disabled: true)
65
- devtools.fetch.on(:request_paused) do |params|
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 middle of the given element.
72
- # its location is calculated using getBoundingClientRect.
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 top-left corner. A negative value means
92
- # coordinates to the left of the element.
93
- # @param [Integer] down_by Optional offset from the top-left corner. A negative value means
94
- # coordinates above the element.
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 { @messages.find { |m| m['id'] == id } }
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
- @messages << message
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
- ' to respond to #width and #height'
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
- ' to respond to #x and #y'
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
- ' to respond to #x, #y, #width, and #height'
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,
@@ -41,7 +41,7 @@ module Selenium
41
41
  to = File.join(destination, entry.name)
42
42
  dirname = File.dirname(to)
43
43
 
44
- FileUtils.mkdir_p dirname unless File.exist? dirname
44
+ FileUtils.mkdir_p dirname
45
45
  zip.extract(entry, to)
46
46
  end
47
47
  end
@@ -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
- " form = form.parentNode;\n" \
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
@@ -19,6 +19,6 @@
19
19
 
20
20
  module Selenium
21
21
  module WebDriver
22
- VERSION = '4.3.0'
22
+ VERSION = '4.4.0'
23
23
  end # WebDriver
24
24
  end # Selenium
@@ -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.22']
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.3.0
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-06-23 00:00:00.000000000 Z
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.22'
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.22'
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: '0'
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: '0'
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: '0'
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: '0'
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