selenium-webdriver 4.1.0 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -123,8 +123,8 @@ module Selenium
123
123
  # @see ActionBuilder
124
124
  #
125
125
 
126
- def action
127
- bridge.action
126
+ def action(**opts)
127
+ bridge.action(**opts)
128
128
  end
129
129
 
130
130
  def mouse
@@ -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
  #
@@ -307,71 +316,29 @@ module Selenium
307
316
 
308
317
  attr_reader :bridge
309
318
 
310
- def create_bridge(**opts)
311
- opts[:url] ||= service_url(opts)
312
- caps = opts.delete(:capabilities)
313
- # NOTE: This is deprecated
314
- cap_array = caps.is_a?(Hash) ? [caps] : Array(caps)
315
-
316
- desired_capabilities = opts.delete(:desired_capabilities)
317
- if desired_capabilities
318
- WebDriver.logger.deprecate(':desired_capabilities as a parameter for driver initialization',
319
- ':capabilities with an Array value of capabilities/options if necessary',
320
- id: :desired_capabilities)
321
- desired_capabilities = Remote::Capabilities.new(desired_capabilities) if desired_capabilities.is_a?(Hash)
322
- cap_array << desired_capabilities
323
- end
324
-
325
- options = opts.delete(:options)
326
- if options
327
- WebDriver.logger.deprecate(':options as a parameter for driver initialization',
328
- ':capabilities with an Array of value capabilities/options if necessary',
329
- id: :browser_options)
330
- cap_array << options
319
+ def create_bridge(capabilities: nil, options: nil, url: nil, service: nil, http_client: nil)
320
+ Remote::Bridge.new(http_client: http_client,
321
+ url: url || service_url(service)).tap do |bridge|
322
+ generated_caps = options ? options.as_json : generate_capabilities(capabilities)
323
+ bridge.create_session(generated_caps)
331
324
  end
332
-
333
- capabilities = generate_capabilities(cap_array)
334
-
335
- bridge_opts = {http_client: opts.delete(:http_client), url: opts.delete(:url)}
336
- raise ArgumentError, "Unable to create a driver with parameters: #{opts}" unless opts.empty?
337
-
338
- bridge = Remote::Bridge.new(**bridge_opts)
339
-
340
- bridge.create_session(capabilities)
341
- bridge
342
325
  end
343
326
 
344
- def generate_capabilities(cap_array)
345
- cap_array.map { |cap|
327
+ def generate_capabilities(capabilities)
328
+ Array(capabilities).map { |cap|
346
329
  if cap.is_a? Symbol
347
330
  cap = Remote::Capabilities.send(cap)
348
- elsif cap.is_a? Hash
349
- new_message = 'Capabilities instance initialized with the Hash, or build values with Options class'
350
- WebDriver.logger.deprecate("passing a Hash value to :capabilities",
351
- new_message,
352
- id: :capabilities_hash)
353
- cap = Remote::Capabilities.new(cap)
354
331
  elsif !cap.respond_to? :as_json
355
332
  msg = ":capabilities parameter only accepts objects responding to #as_json which #{cap.class} does not"
356
333
  raise ArgumentError, msg
357
334
  end
358
- cap&.as_json
335
+ cap.as_json
359
336
  }.inject(:merge) || Remote::Capabilities.send(browser || :new)
360
337
  end
361
338
 
362
- def service_url(opts)
363
- service_config = opts.delete(:service)
364
- %i[driver_opts driver_path port].each do |key|
365
- next unless opts.key? key
366
-
367
- WebDriver.logger.deprecate(":#{key}", ':service with an instance of Selenium::WebDriver::Service',
368
- id: "service_#{key}".to_sym)
369
- end
370
- service_config ||= Service.send(browser,
371
- args: opts.delete(:driver_opts),
372
- path: opts.delete(:driver_path),
373
- port: opts.delete(:port))
374
- @service = service_config.launch
339
+ def service_url(service)
340
+ service ||= Service.send(browser)
341
+ @service = service.launch
375
342
  @service.uri
376
343
  end
377
344
 
@@ -20,12 +20,19 @@
20
20
  module Selenium
21
21
  module WebDriver
22
22
  module DriverExtensions
23
- module HasRemoteStatus
24
- def remote_status
25
- WebDriver.logger.deprecate('#remote_status', '#status')
26
- @bridge.status
23
+ module HasBiDi
24
+
25
+ #
26
+ # Retrieves WebDriver BiDi connection.
27
+ #
28
+ # @return [BiDi]
29
+ #
30
+
31
+ def bidi
32
+ @bidi ||= Selenium::WebDriver::BiDi.new(url: capabilities[:web_socket_url])
27
33
  end
28
- end # HasRemoteStatus
34
+
35
+ end # HasBiDi
29
36
  end # DriverExtensions
30
37
  end # WebDriver
31
38
  end # Selenium
@@ -52,6 +52,16 @@ module Selenium
52
52
  @bridge.start_cast_tab_mirroring(name)
53
53
  end
54
54
 
55
+ #
56
+ # Starts a tab mirroring session on a specific receiver target.
57
+ #
58
+ # @param [String] name the sink to use as the target
59
+ #
60
+
61
+ def start_cast_desktop_mirroring(name)
62
+ @bridge.start_cast_desktop_mirroring(name)
63
+ end
64
+
55
65
  #
56
66
  # Gets error messages when there is any issue in a Cast session.
57
67
  #
@@ -27,8 +27,7 @@ module Selenium
27
27
  # a `with` statement. The state of the context on the server is
28
28
  # saved before entering the block, and restored upon exiting it.
29
29
  #
30
- # @param [String] name which permission to set
31
- # @param [String] value what to set the permission to
30
+ # @param [String] value which context gets set (either 'chrome' or 'content')
32
31
  #
33
32
 
34
33
  def context=(value)
@@ -114,7 +114,7 @@ module Selenium
114
114
  execute_script(mutation_listener)
115
115
  devtools.page.add_script_to_evaluate_on_new_document(source: mutation_listener)
116
116
 
117
- devtools.runtime.on(:binding_called, &method(:log_mutation_event))
117
+ devtools.runtime.on(:binding_called) { |event| log_mutation_event(event) }
118
118
  end
119
119
 
120
120
  def log_mutation_event(params)
@@ -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
@@ -62,7 +62,7 @@ module Selenium
62
62
  #
63
63
  # Unpins script making it undefined for the subsequent calls.
64
64
  #
65
- # @param [DevTools::PinnedScript]
65
+ # @param [DevTools::PinnedScript] script
66
66
  #
67
67
 
68
68
  def unpin_script(script)
@@ -46,7 +46,7 @@ module Selenium
46
46
  alias_method :eql?, :==
47
47
 
48
48
  def hash
49
- @id.hash ^ @bridge.hash
49
+ [@id, @bridge].hash
50
50
  end
51
51
 
52
52
  #
@@ -29,7 +29,7 @@ module Selenium
29
29
  def self.for_error(error)
30
30
  return if error.nil?
31
31
 
32
- klass_name = error.split(' ').map(&:capitalize).join.sub(/Error$/, '')
32
+ klass_name = error.split.map(&:capitalize).join.sub(/Error$/, '')
33
33
  const_get("#{klass_name}Error", false)
34
34
  rescue NameError
35
35
  WebDriverError
@@ -22,8 +22,15 @@ require 'securerandom'
22
22
  module Selenium
23
23
  module WebDriver
24
24
  module Interactions
25
+ #
26
+ # Superclass for the input device sources
27
+ # Manages Array of Interaction instances for the device
28
+ #
29
+ # @api private
30
+ #
31
+
25
32
  class InputDevice
26
- attr_reader :name, :actions
33
+ attr_reader :name, :actions, :type
27
34
 
28
35
  def initialize(name = nil)
29
36
  @name = name || SecureRandom.uuid
@@ -44,9 +51,8 @@ module Selenium
44
51
  add_action(Pause.new(self, duration))
45
52
  end
46
53
 
47
- def no_actions? # Determine if only pauses are present
48
- actions = @actions.reject { |action| action.type == Interaction::PAUSE }
49
- actions.empty?
54
+ def encode
55
+ {type: type, id: name, actions: @actions.map(&:encode)} unless @actions.empty?
50
56
  end
51
57
  end # InputDevice
52
58
  end # Interactions
@@ -20,37 +20,24 @@
20
20
  module Selenium
21
21
  module WebDriver
22
22
  module Interactions
23
- class Interaction
24
- PAUSE = :pause
23
+ #
24
+ # Superclass for classes defining actions
25
+ # Do not initialize directly, only use subclass
26
+ #
27
+ # @api private
28
+ #
25
29
 
26
- attr_reader :source
30
+ class Interaction
31
+ attr_reader :type
27
32
 
28
33
  def initialize(source)
29
- unless Interactions::SOURCE_TYPES.include? source.type
30
- raise TypeError,
31
- "#{source.type} is not a valid input type"
32
- end
33
-
34
- @source = source
34
+ assert_source(source)
35
35
  end
36
- end
37
36
 
38
- class Pause < Interaction
39
- def initialize(source, duration = nil)
40
- super(source)
41
- @duration = duration
37
+ def assert_source(_source)
38
+ raise NotImplementedError, 'subclass responsibility'
42
39
  end
43
-
44
- def type
45
- PAUSE
46
- end
47
-
48
- def encode
49
- output = {type: type}
50
- output[:duration] = (@duration * 1000).to_i if @duration
51
- output
52
- end
53
- end # Interaction
40
+ end
54
41
  end # Interactions
55
42
  end # WebDriver
56
43
  end # Selenium
@@ -23,20 +23,40 @@ module Selenium
23
23
  KEY = :key
24
24
  POINTER = :pointer
25
25
  NONE = :none
26
- SOURCE_TYPES = [KEY, POINTER, NONE].freeze
26
+ WHEEL = :wheel
27
+
28
+ #
29
+ # Class methods for initializing known Input devices
30
+ #
27
31
 
28
32
  class << self
29
- def key(name)
33
+ def key(name = nil)
30
34
  KeyInput.new(name)
31
35
  end
32
36
 
33
- def pointer(kind, **kwargs)
34
- PointerInput.new(kind, **kwargs)
37
+ def pointer(kind = :mouse, name: nil)
38
+ PointerInput.new(kind, name: name)
39
+ end
40
+
41
+ def mouse(name: nil)
42
+ pointer(name: name)
43
+ end
44
+
45
+ def pen(name: nil)
46
+ pointer(:pen, name: name)
47
+ end
48
+
49
+ def touch(name: nil)
50
+ pointer(:touch, name: name)
35
51
  end
36
52
 
37
53
  def none(name = nil)
38
54
  NoneInput.new(name)
39
55
  end
56
+
57
+ def wheel(name = nil)
58
+ WheelInput.new(name)
59
+ end
40
60
  end
41
61
  end # Interactions
42
62
  end # WebDriver
@@ -134,12 +134,16 @@ module Selenium
134
134
  #
135
135
 
136
136
  def key_action(*args, action: nil, device: nil)
137
- key_input = get_device(device) || key_inputs.first
137
+ key_input = key_input(device)
138
138
  click(args.shift) if args.first.is_a? Element
139
139
  key_input.send(action, args.last)
140
140
  tick(key_input)
141
141
  self
142
142
  end
143
+
144
+ def key_input(name = nil)
145
+ device(name: name, type: Interactions::KEY) || add_key_input('keyboard')
146
+ end
143
147
  end # KeyActions
144
148
  end # WebDriver
145
149
  end # Selenium
@@ -20,17 +20,18 @@
20
20
  module Selenium
21
21
  module WebDriver
22
22
  module Interactions
23
+ #
24
+ # Creates actions specific to Key Input devices
25
+ #
26
+ # @api private
27
+ #
28
+
23
29
  class KeyInput < InputDevice
24
30
  SUBTYPES = {down: :keyDown, up: :keyUp, pause: :pause}.freeze
25
31
 
26
- def type
27
- Interactions::KEY
28
- end
29
-
30
- def encode
31
- return nil if no_actions?
32
-
33
- {type: type, id: name, actions: @actions.map(&:encode)}
32
+ def initialize(name = nil)
33
+ super
34
+ @type = Interactions::KEY
34
35
  end
35
36
 
36
37
  def create_key_down(key)
@@ -41,25 +42,8 @@ module Selenium
41
42
  add_action(TypingInteraction.new(self, :up, key))
42
43
  end
43
44
 
44
- class TypingInteraction < Interaction
45
- attr_reader :type
46
-
47
- def initialize(source, type, key)
48
- super(source)
49
- @type = assert_type(type)
50
- @key = Keys.encode_key(key)
51
- end
52
-
53
- def assert_type(type)
54
- raise TypeError, "#{type.inspect} is not a valid key subtype" unless KeyInput::SUBTYPES.key? type
55
-
56
- KeyInput::SUBTYPES[type]
57
- end
58
-
59
- def encode
60
- {type: @type, value: @key}
61
- end
62
- end # TypingInteraction
45
+ # Backward compatibility in case anyone called this directly
46
+ class TypingInteraction < Interactions::TypingInteraction; end
63
47
  end # KeyInput
64
48
  end # Interactions
65
49
  end # WebDriver
@@ -20,15 +20,17 @@
20
20
  module Selenium
21
21
  module WebDriver
22
22
  module Interactions
23
- class NoneInput < InputDevice
24
- def type
25
- Interactions::NONE
26
- end
23
+ #
24
+ # Creates actions specific to null input source
25
+ # This is primarily used for adding pauses
26
+ #
27
+ # @api private
28
+ #
27
29
 
28
- def encode
29
- return nil if no_actions?
30
-
31
- {type: type, id: name, actions: @actions.map(&:encode)}
30
+ class NoneInput < InputDevice
31
+ def initialize(name = nil)
32
+ super
33
+ @type = Interactions::NONE
32
34
  end
33
35
  end # NoneInput
34
36
  end # Interactions
@@ -0,0 +1,49 @@
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
+ module Interactions
23
+ #
24
+ # Action to create a waiting period between actions
25
+ # Also used for synchronizing actions across devices
26
+ #
27
+ # @api private
28
+ #
29
+
30
+ class Pause < Interaction
31
+ def initialize(source, duration = nil)
32
+ super(source)
33
+ @duration = duration
34
+ @type = :pause
35
+ end
36
+
37
+ def assert_source(source)
38
+ raise TypeError, "#{source.type} is not a valid input type" unless source.is_a? InputDevice
39
+ end
40
+
41
+ def encode
42
+ output = {type: type}
43
+ output[:duration] = (@duration * 1000).to_i if @duration
44
+ output
45
+ end
46
+ end # Pause
47
+ end # Interactions
48
+ end # WebDriver
49
+ end # Selenium