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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +85 -1
  3. data/LICENSE +1 -1
  4. data/NOTICE +1 -1
  5. data/lib/selenium/server.rb +15 -10
  6. data/lib/selenium/webdriver/bidi/session.rb +38 -0
  7. data/lib/selenium/webdriver/bidi.rb +55 -0
  8. data/lib/selenium/webdriver/chrome/features.rb +5 -0
  9. data/lib/selenium/webdriver/chrome/options.rb +33 -19
  10. data/lib/selenium/webdriver/chrome.rb +0 -14
  11. data/lib/selenium/webdriver/common/action_builder.rb +108 -21
  12. data/lib/selenium/webdriver/common/driver.rb +22 -55
  13. data/lib/selenium/webdriver/common/driver_extensions/{has_remote_status.rb → has_bidi.rb} +12 -5
  14. data/lib/selenium/webdriver/common/driver_extensions/has_casting.rb +10 -0
  15. data/lib/selenium/webdriver/common/driver_extensions/has_context.rb +1 -2
  16. data/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb +1 -1
  17. data/lib/selenium/webdriver/common/driver_extensions/has_network_interception.rb +2 -67
  18. data/lib/selenium/webdriver/common/driver_extensions/has_pinned_scripts.rb +1 -1
  19. data/lib/selenium/webdriver/common/element.rb +1 -1
  20. data/lib/selenium/webdriver/common/error.rb +1 -1
  21. data/lib/selenium/webdriver/common/interactions/input_device.rb +10 -4
  22. data/lib/selenium/webdriver/common/interactions/interaction.rb +12 -25
  23. data/lib/selenium/webdriver/common/interactions/interactions.rb +24 -4
  24. data/lib/selenium/webdriver/common/interactions/key_actions.rb +5 -1
  25. data/lib/selenium/webdriver/common/interactions/key_input.rb +11 -27
  26. data/lib/selenium/webdriver/common/interactions/none_input.rb +10 -8
  27. data/lib/selenium/webdriver/common/interactions/pause.rb +49 -0
  28. data/lib/selenium/webdriver/common/interactions/pointer_actions.rb +59 -70
  29. data/lib/selenium/webdriver/common/interactions/pointer_cancel.rb +45 -0
  30. data/lib/selenium/webdriver/common/interactions/pointer_event_properties.rb +63 -0
  31. data/lib/selenium/webdriver/common/interactions/pointer_input.rb +15 -84
  32. data/lib/selenium/webdriver/common/interactions/pointer_move.rb +60 -0
  33. data/lib/selenium/webdriver/common/interactions/pointer_press.rb +85 -0
  34. data/lib/selenium/webdriver/common/interactions/scroll.rb +57 -0
  35. data/lib/selenium/webdriver/common/interactions/scroll_origin.rb +48 -0
  36. data/lib/selenium/webdriver/common/interactions/typing_interaction.rb +54 -0
  37. data/lib/selenium/webdriver/common/interactions/wheel_actions.rb +113 -0
  38. data/lib/selenium/webdriver/common/interactions/wheel_input.rb +42 -0
  39. data/lib/selenium/webdriver/common/keys.rb +1 -0
  40. data/lib/selenium/webdriver/common/manager.rb +0 -27
  41. data/lib/selenium/webdriver/common/options.rb +2 -9
  42. data/lib/selenium/webdriver/common/platform.rb +4 -4
  43. data/lib/selenium/webdriver/common/search_context.rb +0 -6
  44. data/lib/selenium/webdriver/common/service_manager.rb +2 -3
  45. data/lib/selenium/webdriver/common/shadow_root.rb +1 -1
  46. data/lib/selenium/webdriver/common/socket_poller.rb +1 -1
  47. data/lib/selenium/webdriver/common/takes_screenshot.rb +1 -1
  48. data/lib/selenium/webdriver/common/virtual_authenticator/credential.rb +83 -0
  49. data/lib/selenium/webdriver/common/virtual_authenticator/virtual_authenticator.rb +73 -0
  50. data/lib/selenium/webdriver/common/virtual_authenticator/virtual_authenticator_options.rb +62 -0
  51. data/lib/selenium/webdriver/common/websocket_connection.rb +156 -0
  52. data/lib/selenium/webdriver/common/window.rb +6 -6
  53. data/lib/selenium/webdriver/common/zipper.rb +1 -1
  54. data/lib/selenium/webdriver/common.rb +17 -3
  55. data/lib/selenium/webdriver/devtools/network_interceptor.rb +176 -0
  56. data/lib/selenium/webdriver/devtools/request.rb +1 -1
  57. data/lib/selenium/webdriver/devtools/response.rb +1 -1
  58. data/lib/selenium/webdriver/devtools.rb +6 -112
  59. data/lib/selenium/webdriver/edge/features.rb +1 -0
  60. data/lib/selenium/webdriver/firefox/driver.rb +1 -0
  61. data/lib/selenium/webdriver/firefox/features.rb +2 -5
  62. data/lib/selenium/webdriver/firefox/options.rb +3 -1
  63. data/lib/selenium/webdriver/firefox/profile.rb +1 -5
  64. data/lib/selenium/webdriver/firefox/util.rb +46 -0
  65. data/lib/selenium/webdriver/firefox.rb +1 -14
  66. data/lib/selenium/webdriver/ie.rb +0 -14
  67. data/lib/selenium/webdriver/remote/bridge.rb +54 -19
  68. data/lib/selenium/webdriver/remote/commands.rb +15 -6
  69. data/lib/selenium/webdriver/remote/driver.rb +0 -1
  70. data/lib/selenium/webdriver/remote/http/default.rb +6 -12
  71. data/lib/selenium/webdriver/remote/response.rb +2 -2
  72. data/lib/selenium/webdriver/safari.rb +0 -14
  73. data/lib/selenium/webdriver/support/cdp_client_generator.rb +4 -4
  74. data/lib/selenium/webdriver/support/color.rb +7 -7
  75. data/lib/selenium/webdriver/support/guards/guard_condition.rb +1 -1
  76. data/lib/selenium/webdriver/support/guards.rb +1 -1
  77. data/lib/selenium/webdriver/version.rb +1 -1
  78. data/lib/selenium/webdriver.rb +1 -0
  79. data/selenium-webdriver.gemspec +9 -6
  80. metadata +64 -12
  81. 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
- ' 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
@@ -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['responseHeaders'].each_with_object({}) do |header, hash|
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
- @callback_threads = ThreadGroup.new
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
- @callback_threads.list.each(&:exit)
47
- @socket_thread.exit
48
- socket.close
38
+ @ws.close
49
39
  end
50
40
 
51
41
  def callbacks
52
- @callbacks ||= Hash.new { |callbacks, event| callbacks[event] = [] }
42
+ @ws.callbacks
53
43
  end
54
44
 
55
45
  def send_cmd(method, **params)
56
- id = next_id
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
- data = JSON.generate(data)
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
- if @file_detector
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 = {path: path}
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://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html
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