selenium-webdriver 4.27.0 → 4.29.1

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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +21 -0
  3. data/Gemfile +3 -1
  4. data/LICENSE +1 -1
  5. data/NOTICE +1 -1
  6. data/bin/linux/selenium-manager +0 -0
  7. data/bin/macos/selenium-manager +0 -0
  8. data/bin/windows/selenium-manager.exe +0 -0
  9. data/lib/selenium/server.rb +3 -3
  10. data/lib/selenium/webdriver/atoms/findElements.js +41 -39
  11. data/lib/selenium/webdriver/bidi/log_inspector.rb +4 -4
  12. data/lib/selenium/webdriver/bidi/network/cookies.rb +38 -0
  13. data/lib/selenium/webdriver/bidi/network/credentials.rb +43 -0
  14. data/lib/selenium/webdriver/bidi/network/headers.rb +38 -0
  15. data/lib/selenium/webdriver/bidi/network/intercepted_auth.rb +38 -0
  16. data/lib/selenium/webdriver/bidi/network/intercepted_item.rb +37 -0
  17. data/lib/selenium/webdriver/bidi/network/intercepted_request.rb +69 -0
  18. data/lib/selenium/webdriver/bidi/network/intercepted_response.rb +81 -0
  19. data/lib/selenium/webdriver/bidi/network/url_pattern.rb +65 -0
  20. data/lib/selenium/webdriver/bidi/network.rb +80 -11
  21. data/lib/selenium/webdriver/bidi/struct.rb +1 -1
  22. data/lib/selenium/webdriver/bidi.rb +6 -2
  23. data/lib/selenium/webdriver/common/driver_extensions/has_network_interception.rb +2 -2
  24. data/lib/selenium/webdriver/common/logger.rb +1 -1
  25. data/lib/selenium/webdriver/common/manager.rb +1 -1
  26. data/lib/selenium/webdriver/common/network.rb +66 -16
  27. data/lib/selenium/webdriver/common/script.rb +4 -4
  28. data/lib/selenium/webdriver/firefox/driver.rb +0 -18
  29. data/lib/selenium/webdriver/remote/bridge.rb +2 -2
  30. data/lib/selenium/webdriver/support/guards.rb +2 -2
  31. data/lib/selenium/webdriver/version.rb +1 -1
  32. data/selenium-webdriver.gemspec +1 -0
  33. metadata +11 -2
@@ -0,0 +1,37 @@
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 BiDi
23
+ class InterceptedItem
24
+ attr_reader :network, :request
25
+
26
+ def initialize(network, request)
27
+ @network = network
28
+ @request = request
29
+ end
30
+
31
+ def id
32
+ @id ||= @request['request']
33
+ end
34
+ end
35
+ end # BiDi
36
+ end # WebDriver
37
+ end # Selenium
@@ -0,0 +1,69 @@
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_relative 'cookies'
21
+ require_relative 'headers'
22
+
23
+ module Selenium
24
+ module WebDriver
25
+ class BiDi
26
+ class InterceptedRequest < InterceptedItem
27
+ attr_accessor :method, :url
28
+ attr_reader :body
29
+
30
+ def initialize(network, request)
31
+ super
32
+ @method = nil
33
+ @url = nil
34
+ @body = nil
35
+ end
36
+
37
+ def continue
38
+ network.continue_request(
39
+ id: id,
40
+ body: body,
41
+ cookies: cookies.as_json,
42
+ headers: headers.as_json,
43
+ method: method,
44
+ url: url
45
+ )
46
+ end
47
+
48
+ def fail
49
+ network.fail_request(id)
50
+ end
51
+
52
+ def body=(value)
53
+ @body = {
54
+ type: 'string',
55
+ value: value.to_json
56
+ }
57
+ end
58
+
59
+ def headers
60
+ @headers ||= Headers.new
61
+ end
62
+
63
+ def cookies(cookies = {})
64
+ @cookies ||= Cookies.new(cookies)
65
+ end
66
+ end
67
+ end # BiDi
68
+ end # WebDriver
69
+ end # Selenium
@@ -0,0 +1,81 @@
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_relative 'credentials'
21
+ require_relative 'headers'
22
+ require_relative 'cookies'
23
+
24
+ module Selenium
25
+ module WebDriver
26
+ class BiDi
27
+ class InterceptedResponse < InterceptedItem
28
+ attr_accessor :reason, :status
29
+ attr_reader :body
30
+
31
+ def initialize(network, request)
32
+ super
33
+ @reason = nil
34
+ @status = nil
35
+ @body = nil
36
+ end
37
+
38
+ def continue
39
+ network.continue_response(
40
+ id: id,
41
+ cookies: cookies.as_json,
42
+ headers: headers.as_json,
43
+ credentials: credentials.as_json,
44
+ reason: reason,
45
+ status: status
46
+ )
47
+ end
48
+
49
+ def provide_response
50
+ network.provide_response(
51
+ id: id,
52
+ cookies: cookies.as_json,
53
+ headers: headers.as_json,
54
+ body: body,
55
+ reason: reason,
56
+ status: status
57
+ )
58
+ end
59
+
60
+ def credentials(username: nil, password: nil)
61
+ @credentials ||= Credentials.new(username: username, password: password)
62
+ end
63
+
64
+ def headers
65
+ @headers ||= Headers.new
66
+ end
67
+
68
+ def cookies(cookies = {})
69
+ @cookies ||= Cookies.new(cookies)
70
+ end
71
+
72
+ def body=(value)
73
+ @body = {
74
+ type: 'string',
75
+ value: value.to_json
76
+ }
77
+ end
78
+ end
79
+ end # BiDi
80
+ end # WebDriver
81
+ end # Selenium
@@ -0,0 +1,65 @@
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 'uri'
21
+
22
+ module Selenium
23
+ module WebDriver
24
+ class BiDi
25
+ module UrlPattern
26
+ module_function
27
+
28
+ def format_pattern(url_patterns, pattern_type)
29
+ case pattern_type
30
+ when :string
31
+ to_url_string_pattern(url_patterns)
32
+ when :url
33
+ to_url_pattern(url_patterns)
34
+ else
35
+ raise ArgumentError, "Unknown pattern type: #{pattern_type}"
36
+ end
37
+ end
38
+
39
+ def to_url_pattern(*url_patterns)
40
+ url_patterns.flatten.map do |url_pattern|
41
+ uri = URI.parse(url_pattern)
42
+
43
+ {
44
+ type: 'pattern',
45
+ protocol: uri.scheme || '',
46
+ hostname: uri.host || '',
47
+ port: uri.port.to_s || '',
48
+ pathname: uri.path || '',
49
+ search: uri.query || ''
50
+ }
51
+ end
52
+ end
53
+
54
+ def to_url_string_pattern(*url_patterns)
55
+ url_patterns.flatten.map do |url_pattern|
56
+ {
57
+ type: 'string',
58
+ pattern: url_pattern
59
+ }
60
+ end
61
+ end
62
+ end
63
+ end # BiDi
64
+ end # WebDriver
65
+ end # Selenium
@@ -16,6 +16,7 @@
16
16
  # KIND, either express or implied. See the License for the
17
17
  # specific language governing permissions and limitations
18
18
  # under the License.
19
+ require_relative 'network/url_pattern'
19
20
 
20
21
  module Selenium
21
22
  module WebDriver
@@ -26,7 +27,7 @@ module Selenium
26
27
  response_started: 'network.responseStarted',
27
28
  response_completed: 'network.responseCompleted',
28
29
  auth_required: 'network.authRequired',
29
- FETCH_ERROR: 'network.fetchError'
30
+ fetch_error: 'network.fetchError'
30
31
  }.freeze
31
32
 
32
33
  PHASES = {
@@ -39,8 +40,12 @@ module Selenium
39
40
  @bidi = bidi
40
41
  end
41
42
 
42
- def add_intercept(phases: [], contexts: nil, url_patterns: nil)
43
- @bidi.send_cmd('network.addIntercept', phases: phases, contexts: contexts, urlPatterns: url_patterns)
43
+ def add_intercept(phases: [], contexts: nil, url_patterns: nil, pattern_type: :string)
44
+ url_patterns = url_patterns && pattern_type ? UrlPattern.format_pattern(url_patterns, pattern_type) : nil
45
+ @bidi.send_cmd('network.addIntercept',
46
+ phases: phases,
47
+ contexts: contexts,
48
+ urlPatterns: url_patterns)
44
49
  end
45
50
 
46
51
  def remove_intercept(intercept)
@@ -50,19 +55,83 @@ module Selenium
50
55
  def continue_with_auth(request_id, username, password)
51
56
  @bidi.send_cmd(
52
57
  'network.continueWithAuth',
53
- 'request' => request_id,
54
- 'action' => 'provideCredentials',
55
- 'credentials' => {
56
- 'type' => 'password',
57
- 'username' => username,
58
- 'password' => password
58
+ request: request_id,
59
+ action: 'provideCredentials',
60
+ credentials: {
61
+ type: 'password',
62
+ username: username,
63
+ password: password
59
64
  }
60
65
  )
61
66
  end
62
67
 
63
- def on(event, &)
68
+ def continue_without_auth(request_id)
69
+ @bidi.send_cmd(
70
+ 'network.continueWithAuth',
71
+ request: request_id,
72
+ action: 'default'
73
+ )
74
+ end
75
+
76
+ def cancel_auth(request_id)
77
+ @bidi.send_cmd(
78
+ 'network.continueWithAuth',
79
+ request: request_id,
80
+ action: 'cancel'
81
+ )
82
+ end
83
+
84
+ def continue_request(**args)
85
+ @bidi.send_cmd(
86
+ 'network.continueRequest',
87
+ request: args[:id],
88
+ body: args[:body],
89
+ cookies: args[:cookies],
90
+ headers: args[:headers],
91
+ method: args[:method],
92
+ url: args[:url]
93
+ )
94
+ end
95
+
96
+ def fail_request(request_id)
97
+ @bidi.send_cmd(
98
+ 'network.failRequest',
99
+ request: request_id
100
+ )
101
+ end
102
+
103
+ def continue_response(**args)
104
+ @bidi.send_cmd(
105
+ 'network.continueResponse',
106
+ request: args[:id],
107
+ cookies: args[:cookies],
108
+ credentials: args[:credentials],
109
+ headers: args[:headers],
110
+ reasonPhrase: args[:reason],
111
+ statusCode: args[:status]
112
+ )
113
+ end
114
+
115
+ def provide_response(**args)
116
+ @bidi.send_cmd(
117
+ 'network.provideResponse',
118
+ request: args[:id],
119
+ body: args[:body],
120
+ cookies: args[:cookies],
121
+ headers: args[:headers],
122
+ reasonPhrase: args[:reason],
123
+ statusCode: args[:status]
124
+ )
125
+ end
126
+
127
+ def set_cache_behavior(behavior, *contexts)
128
+ @bidi.send_cmd('network.setCacheBehavior', cacheBehavior: behavior, contexts: contexts)
129
+ end
130
+
131
+ def on(event, &block)
64
132
  event = EVENTS[event] if event.is_a?(Symbol)
65
- @bidi.add_callback(event, &)
133
+ @bidi.add_callback(event, &block)
134
+ @bidi.session.subscribe(event)
66
135
  end
67
136
  end # Network
68
137
  end # BiDi
@@ -23,7 +23,7 @@ module Selenium
23
23
  class Struct < ::Struct
24
24
  class << self
25
25
  def new(*args, &block)
26
- super(*args) do
26
+ super do
27
27
  define_method(:initialize) do |**kwargs|
28
28
  converted_kwargs = kwargs.transform_keys { |key| self.class.camel_to_snake(key.to_s).to_sym }
29
29
  super(*converted_kwargs.values_at(*self.class.members))
@@ -26,6 +26,10 @@ module Selenium
26
26
  autoload :BrowsingContext, 'selenium/webdriver/bidi/browsing_context'
27
27
  autoload :Struct, 'selenium/webdriver/bidi/struct'
28
28
  autoload :Network, 'selenium/webdriver/bidi/network'
29
+ autoload :InterceptedRequest, 'selenium/webdriver/bidi/network/intercepted_request'
30
+ autoload :InterceptedResponse, 'selenium/webdriver/bidi/network/intercepted_response'
31
+ autoload :InterceptedAuth, 'selenium/webdriver/bidi/network/intercepted_auth'
32
+ autoload :InterceptedItem, 'selenium/webdriver/bidi/network/intercepted_item'
29
33
 
30
34
  def initialize(url:)
31
35
  @ws = WebSocketConnection.new(url: url)
@@ -39,8 +43,8 @@ module Selenium
39
43
  @ws.callbacks
40
44
  end
41
45
 
42
- def add_callback(event, &)
43
- @ws.add_callback(event, &)
46
+ def add_callback(event, &block)
47
+ @ws.add_callback(event, &block)
44
48
  end
45
49
 
46
50
  def remove_callback(event, id)
@@ -59,7 +59,7 @@ module Selenium
59
59
  # @yieldparam [Proc] continue block which proceeds with the request and optionally yields response
60
60
  #
61
61
 
62
- def intercept(&)
62
+ def intercept(&block)
63
63
  if browser == :firefox
64
64
  WebDriver.logger.deprecate(
65
65
  'Driver#intercept on Firefox',
@@ -68,7 +68,7 @@ module Selenium
68
68
  )
69
69
  end
70
70
  @interceptor ||= DevTools::NetworkInterceptor.new(devtools)
71
- @interceptor.intercept(&)
71
+ @interceptor.intercept(&block)
72
72
  end
73
73
  end # HasNetworkInterception
74
74
  end # DriverExtensions
@@ -194,7 +194,7 @@ module Selenium
194
194
  def discard_or_log(level, message, id)
195
195
  id = Array(id)
196
196
  return if @ignored.intersect?(id)
197
- return if @allowed.any? && (@allowed & id).none?
197
+ return if @allowed.any? && !@allowed.intersect?(id)
198
198
 
199
199
  return if ::Logger::Severity.const_get(level.upcase) < @logger.level
200
200
 
@@ -65,7 +65,7 @@ module Selenium
65
65
  # Get the cookie with the given name
66
66
  #
67
67
  # @param [String] name the name of the cookie
68
- # @return [Hash, nil] the cookie, or nil if it wasn't found.
68
+ # @return [Hash] the cookie, or throws a NoSuchCookieError if it wasn't found.
69
69
  #
70
70
 
71
71
  def cookie_named(name)
@@ -17,35 +17,85 @@
17
17
  # specific language governing permissions and limitations
18
18
  # under the License.
19
19
 
20
+ require 'forwardable'
21
+
20
22
  module Selenium
21
23
  module WebDriver
22
24
  class Network
23
- attr_reader :auth_callbacks
25
+ extend Forwardable
26
+
27
+ attr_reader :callbacks, :network
28
+ alias bidi network
29
+
30
+ def_delegators :network, :continue_with_auth, :continue_with_request, :continue_with_response
24
31
 
25
32
  def initialize(bridge)
26
33
  @network = BiDi::Network.new(bridge.bidi)
27
- @auth_callbacks = {}
34
+ @callbacks = {}
28
35
  end
29
36
 
30
- def add_authentication_handler(username, password)
31
- intercept = @network.add_intercept(phases: [BiDi::Network::PHASES[:auth_required]])
32
- auth_id = @network.on(:auth_required) do |event|
33
- request_id = event['requestId']
34
- @network.continue_with_auth(request_id, username, password)
35
- end
36
- @auth_callbacks[auth_id] = intercept
37
+ def remove_handler(id)
38
+ intercept = callbacks[id]
39
+ network.remove_intercept(intercept['intercept'])
40
+ callbacks.delete(id)
41
+ end
37
42
 
38
- auth_id
43
+ def clear_handlers
44
+ callbacks.each_key { |id| remove_handler(id) }
39
45
  end
40
46
 
41
- def remove_authentication_handler(id)
42
- intercept = @auth_callbacks[id]
43
- @network.remove_intercept(intercept['intercept'])
44
- @auth_callbacks.delete(id)
47
+ def add_authentication_handler(username = nil, password = nil, *filter, pattern_type: nil, &block)
48
+ selected_block =
49
+ if username && password
50
+ proc { |auth| auth.authenticate(username, password) }
51
+ else
52
+ block
53
+ end
54
+
55
+ add_handler(
56
+ :auth_required,
57
+ BiDi::Network::PHASES[:auth_required],
58
+ BiDi::InterceptedAuth,
59
+ filter,
60
+ pattern_type: pattern_type,
61
+ &selected_block
62
+ )
45
63
  end
46
64
 
47
- def clear_authentication_handlers
48
- @auth_callbacks.each_key { |id| remove_authentication_handler(id) }
65
+ def add_request_handler(*filter, pattern_type: nil, &block)
66
+ add_handler(
67
+ :before_request,
68
+ BiDi::Network::PHASES[:before_request],
69
+ BiDi::InterceptedRequest,
70
+ filter,
71
+ pattern_type: pattern_type,
72
+ &block
73
+ )
74
+ end
75
+
76
+ def add_response_handler(*filter, pattern_type: nil, &block)
77
+ add_handler(
78
+ :response_started,
79
+ BiDi::Network::PHASES[:response_started],
80
+ BiDi::InterceptedResponse,
81
+ filter,
82
+ pattern_type: pattern_type,
83
+ &block
84
+ )
85
+ end
86
+
87
+ private
88
+
89
+ def add_handler(event_type, phase, intercept_type, filter, pattern_type: nil)
90
+ intercept = network.add_intercept(phases: [phase], url_patterns: [filter].flatten, pattern_type: pattern_type)
91
+ callback_id = network.on(event_type) do |event|
92
+ request = event['request']
93
+ intercepted_item = intercept_type.new(network, request)
94
+ yield(intercepted_item)
95
+ end
96
+
97
+ callbacks[callback_id] = intercept
98
+ callback_id
49
99
  end
50
100
  end # Network
51
101
  end # WebDriver
@@ -25,13 +25,13 @@ module Selenium
25
25
  end
26
26
 
27
27
  # @return [int] id of the handler
28
- def add_console_message_handler(&)
29
- @log_handler.add_message_handler('console', &)
28
+ def add_console_message_handler(&block)
29
+ @log_handler.add_message_handler('console', &block)
30
30
  end
31
31
 
32
32
  # @return [int] id of the handler
33
- def add_javascript_error_handler(&)
34
- @log_handler.add_message_handler('javascript', &)
33
+ def add_javascript_error_handler(&block)
34
+ @log_handler.add_message_handler('javascript', &block)
35
35
  end
36
36
 
37
37
  # @param [int] id of the handler previously added
@@ -30,7 +30,6 @@ module Selenium
30
30
  DriverExtensions::FullPageScreenshot,
31
31
  DriverExtensions::HasContext,
32
32
  DriverExtensions::HasBiDi,
33
- DriverExtensions::HasDevTools,
34
33
  DriverExtensions::HasLogEvents,
35
34
  DriverExtensions::HasNetworkInterception,
36
35
  DriverExtensions::HasWebStorage,
@@ -46,23 +45,6 @@ module Selenium
46
45
  def browser
47
46
  :firefox
48
47
  end
49
-
50
- private
51
-
52
- def devtools_url
53
- if capabilities['moz:debuggerAddress'].nil?
54
- raise(Error::WebDriverError, 'DevTools is not supported by this version of Firefox; use v85 or higher')
55
- end
56
-
57
- uri = URI("http://#{capabilities['moz:debuggerAddress']}")
58
- response = Net::HTTP.get(uri.hostname, '/json/version', uri.port)
59
-
60
- JSON.parse(response)['webSocketDebuggerUrl']
61
- end
62
-
63
- def devtools_version
64
- Firefox::DEVTOOLS_VERSION
65
- end
66
48
  end # Driver
67
49
  end # Firefox
68
50
  end # WebDriver
@@ -35,10 +35,10 @@ module Selenium
35
35
  attr_reader :extra_commands
36
36
  attr_writer :element_class, :locator_converter
37
37
 
38
- def add_command(name, verb, url, &)
38
+ def add_command(name, verb, url, &block)
39
39
  @extra_commands ||= {}
40
40
  @extra_commands[name] = [verb, url]
41
- define_method(name, &)
41
+ define_method(name, &block)
42
42
  end
43
43
 
44
44
  def locator_converter
@@ -37,8 +37,8 @@ module Selenium
37
37
  @messages = {}
38
38
  end
39
39
 
40
- def add_condition(name, condition = nil, &)
41
- @guard_conditions << GuardCondition.new(name, condition, &)
40
+ def add_condition(name, condition = nil, &block)
41
+ @guard_conditions << GuardCondition.new(name, condition, &block)
42
42
  end
43
43
 
44
44
  def add_message(name, message)
@@ -19,6 +19,6 @@
19
19
 
20
20
  module Selenium
21
21
  module WebDriver
22
- VERSION = '4.27.0'
22
+ VERSION = '4.29.1'
23
23
  end # WebDriver
24
24
  end # Selenium
@@ -22,6 +22,7 @@ Gem::Specification.new do |s|
22
22
  s.homepage = 'https://selenium.dev'
23
23
  s.metadata = {
24
24
  'changelog_uri' => 'https://github.com/SeleniumHQ/selenium/blob/trunk/rb/CHANGES',
25
+ 'documentation_uri' => 'https://www.selenium.dev/documentation/?tab=ruby',
25
26
  'github_repo' => 'ssh://github.com/SeleniumHQ/selenium',
26
27
  'source_code_uri' => 'https://github.com/SeleniumHQ/selenium/tree/trunk/rb',
27
28
  'rubygems_mfa_required' => 'true',