selenium-webdriver 4.0.0.rc1 → 4.0.0.rc2

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES +33 -0
  3. data/lib/selenium/webdriver/atoms/getAttribute.js +25 -25
  4. data/lib/selenium/webdriver/chrome/driver.rb +3 -0
  5. data/lib/selenium/webdriver/chrome/features.rb +44 -4
  6. data/lib/selenium/webdriver/chrome/options.rb +24 -1
  7. data/lib/selenium/webdriver/common/driver.rb +2 -0
  8. data/lib/selenium/webdriver/common/driver_extensions/has_apple_permissions.rb +51 -0
  9. data/lib/selenium/webdriver/common/driver_extensions/has_casting.rb +77 -0
  10. data/lib/selenium/webdriver/common/driver_extensions/has_context.rb +45 -0
  11. data/lib/selenium/webdriver/common/driver_extensions/has_launching.rb +38 -0
  12. data/lib/selenium/webdriver/common/driver_extensions/has_network_conditions.rb +8 -0
  13. data/lib/selenium/webdriver/common/driver_extensions/has_network_interception.rb +87 -18
  14. data/lib/selenium/webdriver/common/driver_extensions/has_permissions.rb +11 -11
  15. data/lib/selenium/webdriver/common/driver_extensions/prints_page.rb +1 -1
  16. data/lib/selenium/webdriver/common/log_entry.rb +2 -2
  17. data/lib/selenium/webdriver/common/manager.rb +3 -13
  18. data/lib/selenium/webdriver/common/options.rb +13 -5
  19. data/lib/selenium/webdriver/common/target_locator.rb +28 -0
  20. data/lib/selenium/webdriver/common/window.rb +0 -4
  21. data/lib/selenium/webdriver/common.rb +4 -0
  22. data/lib/selenium/webdriver/devtools/request.rb +27 -17
  23. data/lib/selenium/webdriver/devtools/response.rb +66 -0
  24. data/lib/selenium/webdriver/devtools.rb +49 -12
  25. data/lib/selenium/webdriver/edge/features.rb +5 -0
  26. data/lib/selenium/webdriver/firefox/driver.rb +5 -0
  27. data/lib/selenium/webdriver/firefox/features.rb +14 -0
  28. data/lib/selenium/webdriver/firefox/options.rb +24 -1
  29. data/lib/selenium/webdriver/firefox.rb +0 -1
  30. data/lib/selenium/webdriver/remote/bridge.rb +1 -1
  31. data/lib/selenium/webdriver/remote/capabilities.rb +1 -0
  32. data/lib/selenium/webdriver/remote/driver.rb +2 -1
  33. data/lib/selenium/webdriver/safari/driver.rb +1 -1
  34. data/lib/selenium/webdriver/safari/options.rb +7 -0
  35. data/lib/selenium/webdriver/version.rb +1 -1
  36. data/lib/selenium/webdriver.rb +1 -0
  37. metadata +7 -2
@@ -28,39 +28,108 @@ module Selenium
28
28
  # a stubbed response instead.
29
29
  #
30
30
  # @example Log requests and pass through
31
- # driver.intercept do |request|
31
+ # driver.intercept do |request, &continue|
32
32
  # puts "#{request.method} #{request.url}"
33
- # request.continue
33
+ # continue.call(request)
34
34
  # end
35
35
  #
36
- # @example Stub response for image requests
37
- # driver.intercept do |request|
36
+ # @example Stub requests for images
37
+ # driver.intercept do |request, &continue|
38
38
  # if request.url.match?(/\.png$/)
39
- # request.respond(body: File.read('myfile.png'))
40
- # else
41
- # request.continue
39
+ # request.url = 'https://upload.wikimedia.org/wikipedia/commons/d/d5/Selenium_Logo.png'
42
40
  # end
41
+ # continue.call(request)
43
42
  # end
44
43
  #
45
- # @param [#call] block which is called when request is interecepted
46
- # @yieldparam [DevTools::Request]
44
+ # @example Log responses and pass through
45
+ # driver.intercept do |request, &continue|
46
+ # continue.call(request) do |response|
47
+ # puts "#{response.code} #{response.body}"
48
+ # end
49
+ # end
50
+ #
51
+ # @example Mutate specific response
52
+ # driver.intercept do |request, &continue|
53
+ # continue.call(request) do |response|
54
+ # response.body << 'Added by Selenium!' if request.url.include?('/myurl')
55
+ # end
56
+ # end
57
+ #
58
+ # @param [Proc] block which is called when request is intercepted
59
+ # @yieldparam [DevTools::Request] request
60
+ # @yieldparam [Proc] continue block which proceeds with the request and optionally yields response
47
61
  #
48
62
 
49
- def intercept
63
+ def intercept(&block)
50
64
  devtools.network.set_cache_disabled(cache_disabled: true)
51
65
  devtools.fetch.on(:request_paused) do |params|
52
- request = DevTools::Request.new(
53
- devtools: devtools,
54
- id: params['requestId'],
55
- url: params.dig('request', 'url'),
56
- method: params.dig('request', 'method'),
57
- headers: params.dig('request', 'headers')
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'}])
74
+ end
75
+
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
58
123
  )
59
- yield request
60
124
  end
61
- devtools.fetch.enable
62
125
  end
63
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
64
133
  end # HasNetworkInterception
65
134
  end # DriverExtensions
66
135
  end # WebDriver
@@ -23,26 +23,26 @@ module Selenium
23
23
  module HasPermissions
24
24
 
25
25
  #
26
- # Returns permissions.
26
+ # Set one permission.
27
27
  #
28
- # @return [Hash]
28
+ # @param [String] name which permission to set
29
+ # @param [String] value what to set the permission to
29
30
  #
30
31
 
31
- def permissions
32
- @bridge.permissions
32
+ def add_permission(name, value)
33
+ @bridge.set_permission(name, value)
33
34
  end
34
35
 
35
36
  #
36
- # Sets permissions.
37
+ # Set multiple permissions.
37
38
  #
38
- # @example
39
- # driver.permissions = {'getUserMedia' => true}
40
- #
41
- # @param [Hash<Symbol, Boolean>] permissions
39
+ # @param [Hash] opt key/value pairs to set permissions
42
40
  #
43
41
 
44
- def permissions=(permissions)
45
- @bridge.permissions = permissions
42
+ def add_permissions(opt)
43
+ opt.each do |key, value|
44
+ @bridge.set_permission(key, value)
45
+ end
46
46
  end
47
47
 
48
48
  end # HasPermissions
@@ -34,7 +34,7 @@ module Selenium
34
34
 
35
35
  def save_print_page(path, **options)
36
36
  File.open(path, 'wb') do |file|
37
- content = Base64.decode64 print_page(options)
37
+ content = Base64.decode64 print_page(**options)
38
38
  file << content
39
39
  end
40
40
  end
@@ -30,14 +30,14 @@ module Selenium
30
30
 
31
31
  def as_json(*)
32
32
  {
33
- 'level' => level,
34
33
  'timestamp' => timestamp,
34
+ 'level' => level,
35
35
  'message' => message
36
36
  }
37
37
  end
38
38
 
39
39
  def to_s
40
- "#{level} #{time}: #{message}"
40
+ "#{time} #{level}: #{message}"
41
41
  end
42
42
 
43
43
  def time
@@ -104,25 +104,19 @@ module Selenium
104
104
  @timeouts ||= Timeouts.new(@bridge)
105
105
  end
106
106
 
107
- #
108
- # @api beta This API may be changed or removed in a future release.
109
- #
110
-
111
107
  def logs
112
108
  WebDriver.logger.deprecate('Manager#logs', 'Chrome::Driver#logs')
113
109
  @logs ||= Logs.new(@bridge)
114
110
  end
115
111
 
116
112
  #
117
- # Create a new top-level browsing context
118
- # https://w3c.github.io/webdriver/#new-window
119
113
  # @param type [Symbol] Supports two values: :tab and :window.
120
- # Use :tab if you'd like the new window to share an OS-level window
121
- # with the current browsing context.
122
- # Use :window otherwise
123
114
  # @return [String] The value of the window handle
124
115
  #
125
116
  def new_window(type = :tab)
117
+ WebDriver.logger.deprecate('Manager#new_window', 'TargetLocator#new_window', id: :new_window) do
118
+ 'e.g., `driver.switch_to.new_window(:tab)`'
119
+ end
126
120
  case type
127
121
  when :tab, :window
128
122
  result = @bridge.new_window(type)
@@ -137,10 +131,6 @@ module Selenium
137
131
  end
138
132
  end
139
133
 
140
- #
141
- # @api beta This API may be changed or removed in a future release.
142
- #
143
-
144
134
  def window
145
135
  @window ||= Window.new(@bridge)
146
136
  end
@@ -21,7 +21,8 @@ module Selenium
21
21
  module WebDriver
22
22
  class Options
23
23
  W3C_OPTIONS = %i[browser_name browser_version platform_name accept_insecure_certs page_load_strategy proxy
24
- set_window_rect timeouts unhandled_prompt_behavior strict_file_interactability].freeze
24
+ set_window_rect timeouts unhandled_prompt_behavior strict_file_interactability
25
+ web_socket_url].freeze
25
26
 
26
27
  class << self
27
28
  attr_reader :driver_path
@@ -90,7 +91,8 @@ module Selenium
90
91
  # @param [Boolean, String, Integer] value Value of the option
91
92
  #
92
93
 
93
- def add_option(name, value)
94
+ def add_option(name, value = nil)
95
+ @options[name.keys.first] = name.values.first if value.nil? && name.is_a?(Hash)
94
96
  @options[name] = value
95
97
  end
96
98
 
@@ -123,10 +125,14 @@ module Selenium
123
125
 
124
126
  private
125
127
 
128
+ def w3c?(key)
129
+ W3C_OPTIONS.include?(key) || key.to_s.include?(':')
130
+ end
131
+
126
132
  def process_w3c_options(options)
127
- w3c_options = options.select { |key, _val| W3C_OPTIONS.include?(key) }
133
+ w3c_options = options.select { |key, _val| w3c?(key) }
128
134
  w3c_options[:unhandled_prompt_behavior] &&= w3c_options[:unhandled_prompt_behavior]&.to_s&.tr('_', ' ')
129
- options.delete_if { |key, _val| W3C_OPTIONS.include?(key) }
135
+ options.delete_if { |key, _val| w3c?(key) }
130
136
  w3c_options
131
137
  end
132
138
 
@@ -173,6 +179,8 @@ module Selenium
173
179
  def camel_case(str)
174
180
  str.gsub(/_([a-z])/) { Regexp.last_match(1).upcase }
175
181
  end
176
- end # Options
182
+ end
183
+
184
+ # Options
177
185
  end # WebDriver
178
186
  end # Selenium
@@ -44,6 +44,34 @@ module Selenium
44
44
  @bridge.switch_to_parent_frame
45
45
  end
46
46
 
47
+ #
48
+ # Switch to a new top-level browsing context
49
+ #
50
+ # @param type either :tab or :window
51
+ #
52
+
53
+ def new_window(type = :window)
54
+ unless %i[window tab].include?(type)
55
+ raise ArgumentError, "Valid types are :tab and :window, received: #{type.inspect}"
56
+ end
57
+
58
+ handle = @bridge.new_window(type)['handle']
59
+
60
+ if block_given?
61
+ execute_and_close = proc do
62
+ yield(self)
63
+ begin
64
+ @bridge.close
65
+ rescue Error::NoSuchWindowError
66
+ # window already closed
67
+ end
68
+ end
69
+ window(handle, &execute_and_close)
70
+ else
71
+ window(handle)
72
+ end
73
+ end
74
+
47
75
  #
48
76
  # switch to the given window handle
49
77
  #
@@ -19,10 +19,6 @@
19
19
 
20
20
  module Selenium
21
21
  module WebDriver
22
- #
23
- # @api beta This API may be changed or removed in a future release.
24
- #
25
-
26
22
  class Window
27
23
  #
28
24
  # @api private
@@ -58,8 +58,10 @@ require 'selenium/webdriver/common/driver_extensions/has_remote_status'
58
58
  require 'selenium/webdriver/common/driver_extensions/has_network_conditions'
59
59
  require 'selenium/webdriver/common/driver_extensions/has_network_connection'
60
60
  require 'selenium/webdriver/common/driver_extensions/has_network_interception'
61
+ require 'selenium/webdriver/common/driver_extensions/has_apple_permissions'
61
62
  require 'selenium/webdriver/common/driver_extensions/has_permissions'
62
63
  require 'selenium/webdriver/common/driver_extensions/has_debugger'
64
+ require 'selenium/webdriver/common/driver_extensions/has_context'
63
65
  require 'selenium/webdriver/common/driver_extensions/prints_page'
64
66
  require 'selenium/webdriver/common/driver_extensions/uploads_files'
65
67
  require 'selenium/webdriver/common/driver_extensions/full_page_screenshot'
@@ -70,6 +72,8 @@ require 'selenium/webdriver/common/driver_extensions/has_logs'
70
72
  require 'selenium/webdriver/common/driver_extensions/has_log_events'
71
73
  require 'selenium/webdriver/common/driver_extensions/has_pinned_scripts'
72
74
  require 'selenium/webdriver/common/driver_extensions/has_cdp'
75
+ require 'selenium/webdriver/common/driver_extensions/has_casting'
76
+ require 'selenium/webdriver/common/driver_extensions/has_launching'
73
77
  require 'selenium/webdriver/common/keys'
74
78
  require 'selenium/webdriver/common/profile_helper'
75
79
  require 'selenium/webdriver/common/options'
@@ -22,33 +22,43 @@ module Selenium
22
22
  class DevTools
23
23
  class Request
24
24
 
25
- attr_reader :url, :method, :headers
25
+ attr_accessor :url, :method, :headers, :post_data
26
+ attr_reader :id
26
27
 
27
- def initialize(devtools:, id:, url:, method:, headers:)
28
- @devtools = devtools
28
+ #
29
+ # Creates request from DevTools message.
30
+ # @api private
31
+ #
32
+
33
+ def self.from(id, params)
34
+ new(
35
+ id: id,
36
+ url: params.dig('request', 'url'),
37
+ method: params.dig('request', 'method'),
38
+ headers: params.dig('request', 'headers'),
39
+ post_data: params.dig('request', 'postData')
40
+ )
41
+ end
42
+
43
+ def initialize(id:, url:, method:, headers:, post_data:)
29
44
  @id = id
30
45
  @url = url
31
46
  @method = method
32
47
  @headers = headers
48
+ @post_data = post_data
33
49
  end
34
50
 
35
- def continue
36
- @devtools.fetch.continue_request(request_id: @id)
37
- end
38
-
39
- def respond(code: 200, headers: {}, body: '')
40
- @devtools.fetch.fulfill_request(
41
- request_id: @id,
42
- body: Base64.strict_encode64(body),
43
- response_code: code,
44
- response_headers: headers.map do |k, v|
45
- {name: k, value: v}
46
- end
47
- )
51
+ def ==(other)
52
+ self.class == other.class &&
53
+ id == other.id &&
54
+ url == other.url &&
55
+ method == other.method &&
56
+ headers == other.headers &&
57
+ post_data == other.post_data
48
58
  end
49
59
 
50
60
  def inspect
51
- %(#<#{self.class.name} @method="#{method}" @url="#{url}")
61
+ %(#<#{self.class.name} @id="#{id}" @method="#{method}" @url="#{url}")
52
62
  end
53
63
 
54
64
  end # Request
@@ -0,0 +1,66 @@
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
+ class Response
24
+
25
+ attr_accessor :code, :body, :headers
26
+ attr_reader :id
27
+
28
+ #
29
+ # Creates response from DevTools message.
30
+ # @api private
31
+ #
32
+
33
+ def self.from(id, encoded_body, params)
34
+ new(
35
+ id: id,
36
+ code: params['responseStatusCode'],
37
+ body: (Base64.strict_decode64(encoded_body) if encoded_body),
38
+ headers: params['responseHeaders'].each_with_object({}) do |header, hash|
39
+ hash[header['name']] = header['value']
40
+ end
41
+ )
42
+ end
43
+
44
+ def initialize(id:, code:, body:, headers:)
45
+ @id = id
46
+ @code = code
47
+ @body = body
48
+ @headers = headers
49
+ end
50
+
51
+ def ==(other)
52
+ self.class == other.class &&
53
+ id == other.id &&
54
+ code == other.code &&
55
+ body == other.body &&
56
+ headers == other.headers
57
+ end
58
+
59
+ def inspect
60
+ %(#<#{self.class.name} @id="#{id}" @code="#{code}")
61
+ end
62
+
63
+ end # Response
64
+ end # DevTools
65
+ end # WebDriver
66
+ end # Selenium
@@ -20,22 +20,34 @@
20
20
  module Selenium
21
21
  module WebDriver
22
22
  class DevTools
23
+ RESPONSE_WAIT_TIMEOUT = 30
24
+ RESPONSE_WAIT_INTERVAL = 0.1
25
+
23
26
  autoload :ConsoleEvent, 'selenium/webdriver/devtools/console_event'
24
27
  autoload :ExceptionEvent, 'selenium/webdriver/devtools/exception_event'
25
28
  autoload :MutationEvent, 'selenium/webdriver/devtools/mutation_event'
26
29
  autoload :PinnedScript, 'selenium/webdriver/devtools/pinned_script'
27
30
  autoload :Request, 'selenium/webdriver/devtools/request'
31
+ autoload :Response, 'selenium/webdriver/devtools/response'
28
32
 
29
33
  def initialize(url:)
34
+ @callback_threads = ThreadGroup.new
35
+
30
36
  @messages = []
31
37
  @session_id = nil
32
38
  @url = url
33
39
 
34
40
  process_handshake
35
- attach_socket_listener
41
+ @socket_thread = attach_socket_listener
36
42
  start_session
37
43
  end
38
44
 
45
+ def close
46
+ @callback_threads.list.each(&:exit)
47
+ @socket_thread.exit
48
+ socket.close
49
+ end
50
+
39
51
  def callbacks
40
52
  @callbacks ||= Hash.new { |callbacks, event| callbacks[event] = [] }
41
53
  end
@@ -85,27 +97,24 @@ module Selenium
85
97
  end
86
98
 
87
99
  def attach_socket_listener
88
- socket_listener = Thread.new do
100
+ Thread.new do
101
+ Thread.current.abort_on_exception = true
102
+ Thread.current.report_on_exception = false
103
+
89
104
  until socket.eof?
90
105
  incoming_frame << socket.readpartial(1024)
91
106
 
92
107
  while (frame = incoming_frame.next)
93
- # Firefox will periodically fail on unparsable empty frame
94
- break if frame.to_s.empty?
95
-
96
- message = JSON.parse(frame.to_s)
97
- @messages << message
98
- WebDriver.logger.debug "DevTools <- #{message}"
108
+ message = process_frame(frame)
99
109
  next unless message['method']
100
110
 
111
+ params = message['params']
101
112
  callbacks[message['method']].each do |callback|
102
- params = message['params'] # take in current thread!
103
- Thread.new { callback.call(params) }
113
+ @callback_threads.add(callback_thread(params, &callback))
104
114
  end
105
115
  end
106
116
  end
107
117
  end
108
- socket_listener.abort_on_exception = true
109
118
  end
110
119
 
111
120
  def start_session
@@ -119,8 +128,36 @@ module Selenium
119
128
  @incoming_frame ||= WebSocket::Frame::Incoming::Client.new(version: ws.version)
120
129
  end
121
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
+
122
159
  def wait
123
- @wait ||= Wait.new(timeout: 10, interval: 0.1)
160
+ @wait ||= Wait.new(timeout: RESPONSE_WAIT_TIMEOUT, interval: RESPONSE_WAIT_INTERVAL)
124
161
  end
125
162
 
126
163
  def socket
@@ -27,6 +27,11 @@ module Selenium
27
27
  include WebDriver::Chrome::Features
28
28
 
29
29
  EDGE_COMMANDS = {
30
+ get_cast_sinks: [:get, 'session/:session_id/ms/cast/get_sinks'],
31
+ set_cast_sink_to_use: [:post, 'session/:session_id/ms/cast/set_sink_to_use'],
32
+ start_cast_tab_mirroring: [:post, 'session/:session_id/ms/cast/start_tab_mirroring'],
33
+ get_cast_issue_message: [:get, 'session/:session_id/ms/cast/get_issue_message'],
34
+ stop_casting: [:post, 'session/:session_id/ms/cast/stop_casting'],
30
35
  send_command: [:post, 'session/:session_id/ms/cdp/execute']
31
36
  }.freeze
32
37
 
@@ -29,6 +29,7 @@ module Selenium
29
29
  class Driver < WebDriver::Driver
30
30
  EXTENSIONS = [DriverExtensions::HasAddons,
31
31
  DriverExtensions::FullPageScreenshot,
32
+ DriverExtensions::HasContext,
32
33
  DriverExtensions::HasDevTools,
33
34
  DriverExtensions::HasLogEvents,
34
35
  DriverExtensions::HasNetworkInterception,
@@ -42,6 +43,10 @@ module Selenium
42
43
  private
43
44
 
44
45
  def devtools_url
46
+ if capabilities['moz:debuggerAddress'].nil?
47
+ raise(Error::WebDriverError, "DevTools is not supported by this version of Firefox; use v85 or higher")
48
+ end
49
+
45
50
  uri = URI("http://#{capabilities['moz:debuggerAddress']}")
46
51
  response = Net::HTTP.get(uri.hostname, '/json/version', uri.port)
47
52
 
@@ -23,6 +23,8 @@ module Selenium
23
23
  module Features
24
24
 
25
25
  FIREFOX_COMMANDS = {
26
+ get_context: [:get, 'session/:session_id/moz/context'],
27
+ set_context: [:post, 'session/:session_id/moz/context'],
26
28
  install_addon: [:post, 'session/:session_id/moz/addon/install'],
27
29
  uninstall_addon: [:post, 'session/:session_id/moz/addon/uninstall'],
28
30
  full_page_screenshot: [:get, 'session/:session_id/moz/screenshot/full']
@@ -33,6 +35,11 @@ module Selenium
33
35
  end
34
36
 
35
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
42
+
36
43
  payload = {path: path}
37
44
  payload[:temporary] = temporary unless temporary.nil?
38
45
  execute :install_addon, {}, payload
@@ -46,6 +53,13 @@ module Selenium
46
53
  execute :full_page_screenshot
47
54
  end
48
55
 
56
+ def context=(context)
57
+ execute :set_context, {}, {context: context}
58
+ end
59
+
60
+ def context
61
+ execute :get_context
62
+ end
49
63
  end # Bridge
50
64
  end # Firefox
51
65
  end # WebDriver