watir 6.17.0 → 6.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/actions/enable-safari/action.yml +11 -0
  3. data/.github/actions/install-chrome/action.yml +11 -0
  4. data/.github/workflows/linux.yml +61 -0
  5. data/.github/workflows/mac.yml +55 -0
  6. data/.github/workflows/unit.yml +31 -0
  7. data/.github/workflows/windows.yml +39 -0
  8. data/.rubocop_todo.yml +36 -0
  9. data/CHANGES.md +14 -0
  10. data/LICENSE +2 -2
  11. data/README.md +9 -10
  12. data/Rakefile +1 -1
  13. data/lib/watir.rb +1 -0
  14. data/lib/watir/adjacent.rb +1 -1
  15. data/lib/watir/alert.rb +1 -0
  16. data/lib/watir/attribute_helper.rb +2 -0
  17. data/lib/watir/browser.rb +2 -2
  18. data/lib/watir/cookies.rb +2 -0
  19. data/lib/watir/element_collection.rb +21 -6
  20. data/lib/watir/elements/element.rb +10 -10
  21. data/lib/watir/elements/html_elements.rb +0 -1
  22. data/lib/watir/elements/iframe.rb +2 -1
  23. data/lib/watir/elements/select.rb +20 -5
  24. data/lib/watir/generator/html/generator.rb +1 -1
  25. data/lib/watir/has_window.rb +17 -15
  26. data/lib/watir/js_execution.rb +2 -2
  27. data/lib/watir/js_snippets.rb +2 -2
  28. data/lib/watir/locators.rb +1 -3
  29. data/lib/watir/locators/element/selector_builder.rb +1 -1
  30. data/lib/watir/logger.rb +2 -18
  31. data/lib/watir/radio_set.rb +2 -2
  32. data/lib/watir/user_editable.rb +6 -2
  33. data/lib/watir/version.rb +1 -1
  34. data/lib/watir/wait.rb +2 -0
  35. data/lib/watir/wait/timer.rb +1 -1
  36. data/lib/watir/window.rb +7 -3
  37. data/lib/watir/window_collection.rb +105 -0
  38. data/lib/watirspec.rb +1 -0
  39. data/lib/watirspec/implementation.rb +3 -5
  40. data/lib/watirspec/runner.rb +1 -1
  41. data/lib/watirspec/server.rb +1 -1
  42. data/spec/spec_helper.rb +2 -7
  43. data/spec/unit/match_elements/element_spec.rb +17 -15
  44. data/spec/unit/unit_helper.rb +2 -4
  45. data/spec/watirspec/after_hooks_spec.rb +15 -11
  46. data/spec/watirspec/browser_spec.rb +3 -2
  47. data/spec/watirspec/elements/element_spec.rb +14 -11
  48. data/spec/watirspec/elements/filefield_spec.rb +2 -2
  49. data/spec/watirspec/elements/iframe_spec.rb +5 -7
  50. data/spec/watirspec/elements/link_spec.rb +5 -3
  51. data/spec/watirspec/elements/select_list_spec.rb +156 -37
  52. data/spec/watirspec/html/wait.html +5 -5
  53. data/spec/watirspec/html/window_switching.html +10 -0
  54. data/spec/watirspec/legacy_wait_spec.rb +216 -0
  55. data/spec/watirspec/support/rspec_matchers.rb +10 -7
  56. data/spec/watirspec/wait_spec.rb +257 -301
  57. data/spec/watirspec/window_switching_spec.rb +282 -160
  58. data/spec/watirspec_helper.rb +10 -15
  59. data/support/doctest_helper.rb +0 -2
  60. data/watir.gemspec +2 -2
  61. metadata +25 -13
  62. data/.travis.yml +0 -87
  63. data/appveyor.yml +0 -13
  64. data/spec/watirspec/relaxed_locate_spec.rb +0 -109
@@ -149,7 +149,6 @@ module Watir
149
149
  #
150
150
 
151
151
  def click(*modifiers)
152
- # TODO: Should wait_for_enabled be default, or `Button` specific behavior?
153
152
  element_call(:wait_for_enabled) do
154
153
  if modifiers.any?
155
154
  action = driver.action
@@ -700,7 +699,7 @@ module Watir
700
699
  return assert_enabled unless Watir.relaxed_locate?
701
700
 
702
701
  wait_for_exists
703
- return unless [Input, Button, Select, Option].any? { |c| is_a? c } || @content_editable
702
+ return unless [Input, Button, Select, Option].any? { |c| is_a? c } || content_editable
704
703
  return if enabled?
705
704
 
706
705
  begin
@@ -712,9 +711,7 @@ module Watir
712
711
 
713
712
  def wait_for_writable
714
713
  wait_for_enabled
715
- unless Watir.relaxed_locate?
716
- raise_writable unless !respond_to?(:readonly?) || !readonly?
717
- end
714
+ raise_writable unless Watir.relaxed_locate? || (!respond_to?(:readonly?) || !readonly?)
718
715
 
719
716
  return if !respond_to?(:readonly?) || !readonly?
720
717
 
@@ -734,7 +731,9 @@ module Watir
734
731
  end
735
732
 
736
733
  def ensure_context
737
- if @query_scope.is_a?(Browser) || !@query_scope.located? || @query_scope.located? && @query_scope.stale?
734
+ if @query_scope.is_a?(Browser) || !@query_scope.located? && @query_scope.is_a?(IFrame)
735
+ @query_scope.browser.locate
736
+ elsif @query_scope.located? && @query_scope.stale?
738
737
  @query_scope.locate
739
738
  end
740
739
  @query_scope.switch_to! if @query_scope.is_a?(IFrame)
@@ -793,6 +792,7 @@ module Watir
793
792
  # rubocop:disable Metrics/AbcSize
794
793
  # rubocop:disable Metrics/MethodLength
795
794
  # rubocop:disable Metrics/CyclomaticComplexity:
795
+ # rubocop:disable Metrics/PerceivedComplexity::
796
796
  def element_call(precondition = nil, &block)
797
797
  caller = caller_locations(1, 1)[0].label
798
798
  already_locked = browser.timer.locked?
@@ -806,12 +806,12 @@ module Watir
806
806
  element_call(:wait_for_exists, &block) if precondition.nil?
807
807
  msg = e.message
808
808
  msg += '; Maybe look in an iframe?' if @query_scope.iframe.exists?
809
- custom_attributes = @locator.nil? ? [] : selector_builder.custom_attributes
809
+ custom_attributes = !defined?(@locator) || @locator.nil? ? [] : selector_builder.custom_attributes
810
810
  unless custom_attributes.empty?
811
811
  msg += "; Watir treated #{custom_attributes} as a non-HTML compliant attribute, ensure that was intended"
812
812
  end
813
813
  raise unknown_exception, msg
814
- rescue Selenium::WebDriver::Error::StaleElementReferenceError
814
+ rescue Selenium::WebDriver::Error::StaleElementReferenceError, Selenium::WebDriver::Error::NoSuchElementError
815
815
  reset!
816
816
  retry
817
817
  # TODO: - InvalidElementStateError is deprecated, so no longer calling `raise_disabled`
@@ -829,8 +829,8 @@ module Watir
829
829
  end
830
830
  # rubocop:enable Metrics/AbcSize
831
831
  # rubocop:enable Metrics/MethodLength
832
-
833
- # rubocop:enable Metrics/CyclomaticComplexity:
832
+ # rubocop:enable Metrics/CyclomaticComplexity
833
+ # rubocop:enable Metrics/PerceivedComplexity
834
834
 
835
835
  def check_condition(condition, caller)
836
836
  Watir.logger.debug "<- `Verifying precondition #{inspect}##{condition} for #{caller}`"
@@ -272,7 +272,6 @@ module Watir
272
272
  attribute(String, :form, :form)
273
273
  attribute(String, :label, :label)
274
274
  attribute("Boolean", :defaultselected?, :defaultSelected)
275
- attribute("Boolean", :selected?, :selected)
276
275
  attribute(String, :value, :value)
277
276
  attribute(Integer, :index, :index)
278
277
  end
@@ -44,8 +44,9 @@ module Watir
44
44
  # @see Watir::Browser#execute_script
45
45
  #
46
46
 
47
- def execute_script(script, *args)
47
+ def execute_script(script, *args, function_name: nil)
48
48
  args.map! do |e|
49
+ Watir.logger.info "Executing Script on Frame: #{function_name}" if function_name
49
50
  e.is_a?(Element) ? e.wait_until(&:exists?).wd : e
50
51
  end
51
52
  returned = driver.execute_script(script, *args)
@@ -30,8 +30,11 @@ module Watir
30
30
  #
31
31
 
32
32
  def select(*str_or_rx)
33
- results = str_or_rx.flatten.map { |v| select_by v }
34
- results.first
33
+ if str_or_rx.size > 1 || str_or_rx.first.is_a?(Array)
34
+ str_or_rx.flatten.map { |v| select_all_by v }.first
35
+ else
36
+ str_or_rx.flatten.map { |v| select_by v }.first
37
+ end
35
38
  end
36
39
 
37
40
  #
@@ -43,6 +46,10 @@ module Watir
43
46
  #
44
47
 
45
48
  def select_all(*str_or_rx)
49
+ Watir.logger.deprecate('#select_all',
50
+ '#select with an Array instance',
51
+ ids: [:select_all])
52
+
46
53
  results = str_or_rx.flatten.map { |v| select_all_by v }
47
54
  results.first
48
55
  end
@@ -55,8 +62,11 @@ module Watir
55
62
  #
56
63
 
57
64
  def select!(*str_or_rx)
58
- results = str_or_rx.flatten.map { |v| select_by!(v, :single) }
59
- results.first
65
+ if str_or_rx.size > 1 || str_or_rx.first.is_a?(Array)
66
+ str_or_rx.flatten.map { |v| select_by! v, :multiple }.first
67
+ else
68
+ str_or_rx.flatten.map { |v| select_by! v, :single }.first
69
+ end
60
70
  end
61
71
 
62
72
  #
@@ -67,6 +77,10 @@ module Watir
67
77
  #
68
78
 
69
79
  def select_all!(*str_or_rx)
80
+ Watir.logger.deprecate('#select_all!',
81
+ '#select! with an Array instance',
82
+ ids: [:select_all])
83
+
70
84
  results = str_or_rx.flatten.map { |v| select_by!(v, :multiple) }
71
85
  results.first
72
86
  end
@@ -145,7 +159,8 @@ module Watir
145
159
  found = find_options(:value, str_or_rx)
146
160
 
147
161
  if found.size > 1
148
- Watir.logger.deprecate 'Selecting Multiple Options with #select', '#select_all',
162
+ Watir.logger.deprecate 'Selecting multiple options with #select using a String or Regexp value',
163
+ '#select with the desired values in an Array instance',
149
164
  ids: [:select_by]
150
165
  end
151
166
  select_matching(found)
@@ -14,7 +14,7 @@ module Watir
14
14
  end
15
15
 
16
16
  def ignored_attributes
17
- %w[cells elements hash rows span text size selected? style width height tHead tFoot link options]
17
+ %w[cells elements hash rows span text size selected? style width height tHead tFoot link options selected]
18
18
  end
19
19
 
20
20
  def generator_implementation
@@ -10,13 +10,7 @@ module Watir
10
10
  #
11
11
 
12
12
  def windows(*args)
13
- all = @driver.window_handles.map { |handle| Window.new(self, handle: handle) }
14
-
15
- if args.empty?
16
- all
17
- else
18
- filter_windows extract_selector(args), all
19
- end
13
+ WindowCollection.new self, extract_selector(args)
20
14
  end
21
15
 
22
16
  #
@@ -51,16 +45,24 @@ module Watir
51
45
  @original_window ||= window
52
46
  end
53
47
 
54
- private
48
+ #
49
+ # Waits for and returns second window if present
50
+ # See Window#use
51
+ #
52
+ # @example
53
+ # browser.switch_window
54
+ #
55
+ # @return [Window]
56
+ #
55
57
 
56
- def filter_windows(selector, windows)
57
- unless selector.keys.all? { |k| %i[title url].include? k }
58
- raise ArgumentError, "invalid window selector: #{selector.inspect}"
59
- end
58
+ def switch_window
59
+ current_window = window
60
+ wins = windows
61
+ wait_until { (wins = windows) && wins.size > 1 } if wins.size == 1
62
+ raise StandardError, 'Unable to determine which window to switch to' if wins.size > 2
60
63
 
61
- windows.select do |win|
62
- selector.all? { |key, value| win.send(key) =~ /#{value}/ }
63
- end
64
+ wins.find { |w| w != current_window }.use
65
+ window
64
66
  end
65
67
  end # HasWindow
66
68
  end # Watir
@@ -3,8 +3,8 @@ module Watir
3
3
  #
4
4
  # Delegates script execution to Browser or IFrame.
5
5
  #
6
- def execute_script(script, *args)
7
- @query_scope.execute_script(script, *args)
6
+ def execute_script(script, *args, function_name: nil)
7
+ @query_scope.execute_script(script, *args, function_name: function_name)
8
8
  end
9
9
 
10
10
  #
@@ -6,11 +6,11 @@ module Watir
6
6
 
7
7
  def execute_js(function_name, *arguments)
8
8
  file = File.expand_path("../js_snippets/#{function_name}.js", __FILE__)
9
- raise Exception::Error, "Can not excute script as #{function_name}.js does not exist" unless File.exist?(file)
9
+ raise Exception::Error, "Can not execute script as #{function_name}.js does not exist" unless File.exist?(file)
10
10
 
11
11
  js = File.read(file)
12
12
  script = "return (#{js}).apply(null, arguments)"
13
- @query_scope.execute_script(script, *arguments)
13
+ @query_scope.execute_script(script, *arguments, function_name: function_name)
14
14
  end
15
15
  end # JSSnippets
16
16
  end # Watir
@@ -55,9 +55,7 @@ module Watir
55
55
  end
56
56
 
57
57
  def class_from_string(string)
58
- Kernel.const_get(string)
59
- rescue NameError
60
- nil
58
+ Watir.const_get(string) if Watir.const_defined?(string)
61
59
  end
62
60
 
63
61
  def element_class_name
@@ -142,7 +142,7 @@ module Watir
142
142
 
143
143
  # Extensions implement this method when creating a different selector builder
144
144
  def implementation_class
145
- Kernel.const_get("#{self.class.name}::XPath")
145
+ Watir.const_get("#{self.class.name}::XPath")
146
146
  end
147
147
 
148
148
  def build_wd_selector(selector)
data/lib/watir/logger.rb CHANGED
@@ -2,7 +2,7 @@ require 'forwardable'
2
2
  require 'logger'
3
3
 
4
4
  # Code adapted from Selenium Implementation
5
- # https://github.com/SeleniumHQ/selenium/blob/master/rb/lib/selenium/webdriver/common/logger.rb
5
+ # https://github.com/SeleniumHQ/selenium/blob/trunk/rb/lib/selenium/webdriver/common/logger.rb
6
6
 
7
7
  module Watir
8
8
  #
@@ -25,7 +25,7 @@ module Watir
25
25
  :warn?,
26
26
  :error, :error?,
27
27
  :fatal, :fatal?,
28
- :level
28
+ :level, :level=
29
29
 
30
30
  def initialize(progname = 'Watir')
31
31
  @logger = create_logger($stdout)
@@ -50,22 +50,6 @@ module Watir
50
50
  @logger.warn(msg, &block) unless (@ignored & ids).any?
51
51
  end
52
52
 
53
- #
54
- # For Ruby < 2.4 compatibility
55
- # Based on https://github.com/ruby/ruby/blob/ruby_2_3/lib/logger.rb#L250
56
- #
57
-
58
- def level=(severity)
59
- if severity.is_a?(Integer)
60
- @logger.level = severity
61
- else
62
- levels = %w[debug info warn error fatal unknown]
63
- raise ArgumentError, "invalid log level: #{severity}" unless levels.include? severity.to_s.downcase
64
-
65
- @logger.level = severity.to_s.upcase
66
- end
67
- end
68
-
69
53
  #
70
54
  # Returns IO object used by logger internally.
71
55
  #
@@ -4,7 +4,7 @@ module Watir
4
4
  include Exception
5
5
  include Enumerable
6
6
 
7
- delegate %i[exists? present? visible? browser] => :source
7
+ delegate %i[exist? exists? present? visible? browser] => :source
8
8
 
9
9
  attr_reader :source, :frame
10
10
 
@@ -201,7 +201,7 @@ module Watir
201
201
  end
202
202
  alias eql? ==
203
203
 
204
- # Ruby 2.4+ complains about using #delegate to do this
204
+ # Delegating to Private Methods
205
205
  %i[assert_exists element_call].each do |method|
206
206
  define_method(method) do |*args, &blk|
207
207
  source.send(method, *args, &blk)
@@ -21,13 +21,17 @@ module Watir
21
21
  # @param [String, Symbol] args
22
22
  #
23
23
 
24
+ def content_editable
25
+ defined?(@content_editable) && content_editable?
26
+ end
27
+
24
28
  def set!(*args)
25
29
  msg = '#set! does not support special keys, use #set instead'
26
30
  raise ArgumentError, msg if args.any? { |v| v.is_a?(::Symbol) }
27
31
 
28
32
  input_value = args.join
29
33
  set input_value[0]
30
- return content_editable_set!(*args) if @content_editable
34
+ return content_editable_set!(*args) if content_editable
31
35
 
32
36
  element_call { execute_js(:setValue, @element, input_value[0..-2]) }
33
37
  append(input_value[-1])
@@ -43,7 +47,7 @@ module Watir
43
47
  #
44
48
 
45
49
  def append(*args)
46
- raise NotImplementedError, '#append method is not supported with contenteditable element' if @content_editable
50
+ raise NotImplementedError, '#append method is not supported with contenteditable element' if content_editable
47
51
 
48
52
  send_keys(*args)
49
53
  end
data/lib/watir/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Watir
2
- VERSION = '6.17.0'.freeze
2
+ VERSION = '6.18.0'.freeze
3
3
  end
data/lib/watir/wait.rb CHANGED
@@ -229,6 +229,8 @@ module Watir
229
229
  case expected
230
230
  when Regexp
231
231
  expected =~ actual
232
+ when Numeric
233
+ expected == actual
232
234
  else
233
235
  expected.to_s == actual
234
236
  end
@@ -2,7 +2,7 @@ module Watir
2
2
  module Wait
3
3
  class Timer
4
4
  def initialize(timeout: nil)
5
- @end_time = current_time + timeout if timeout
5
+ @end_time = timeout ? current_time + timeout : nil
6
6
  @remaining_time = @end_time - current_time if @end_time
7
7
  end
8
8
 
data/lib/watir/window.rb CHANGED
@@ -6,7 +6,7 @@ module Watir
6
6
 
7
7
  attr_reader :browser
8
8
 
9
- def initialize(browser, selector)
9
+ def initialize(browser, selector = {})
10
10
  @browser = browser
11
11
  @driver = browser.driver
12
12
  @selector = selector
@@ -16,7 +16,7 @@ module Watir
16
16
  elsif selector.key? :handle
17
17
  @handle = selector.delete :handle
18
18
  else
19
- return if selector.keys.all? { |k| %i[title url index].include? k }
19
+ return if selector.keys.all? { |k| %i[title url index element].include? k }
20
20
 
21
21
  raise ArgumentError, "invalid window selector: #{selector_string}"
22
22
  end
@@ -207,6 +207,9 @@ module Watir
207
207
  if @selector.empty?
208
208
  nil
209
209
  elsif @selector.key?(:index)
210
+ Watir.logger.deprecate 'Using :index as a selector for Window', ':title or :url or :element',
211
+ reference: 'http://watir.com/guides/windows/#locating-by-index-is-no-longer-supported',
212
+ ids: [:window_index]
210
213
  @driver.window_handles[Integer(@selector[:index])]
211
214
  else
212
215
  @driver.window_handles.find { |wh| matches?(wh) }
@@ -230,8 +233,9 @@ module Watir
230
233
  @driver.switch_to.window(handle) do
231
234
  matches_title = @selector[:title].nil? || @browser.title =~ /#{@selector[:title]}/
232
235
  matches_url = @selector[:url].nil? || @browser.url =~ /#{@selector[:url]}/
236
+ matches_element = @selector[:element].nil? || @selector[:element].exists?
233
237
 
234
- matches_title && matches_url
238
+ matches_title && matches_url && matches_element
235
239
  end
236
240
  rescue Selenium::WebDriver::Error::NoSuchWindowError
237
241
  # the window may disappear while we're iterating.
@@ -0,0 +1,105 @@
1
+ module Watir
2
+ class WindowCollection
3
+ include Enumerable
4
+ include Waitable
5
+
6
+ def initialize(browser, selector = {})
7
+ unless selector.keys.all? { |k| %i[title url element].include? k }
8
+ raise ArgumentError, "invalid window selector: #{selector.inspect}"
9
+ end
10
+
11
+ @browser = browser
12
+ @selector = selector
13
+ end
14
+
15
+ #
16
+ # Yields each window in collection.
17
+ #
18
+ # @yieldparam [Watir::Window]
19
+ #
20
+
21
+ def each(&blk)
22
+ reset!
23
+ to_a.each(&blk)
24
+ end
25
+
26
+ alias length count
27
+ alias size count
28
+ alias empty? none?
29
+
30
+ #
31
+ # First window of the collection
32
+ #
33
+ # @note windows in a collection are not ordered so this is not reliably
34
+ # @deprecated use Browser#switch_window or a better Window locator
35
+ # @return [Watir::Window] Returns an instance of a Watir::Window
36
+ #
37
+
38
+ def first
39
+ self[0]
40
+ end
41
+
42
+ #
43
+ # Last window of the collection
44
+ #
45
+ # @note windows in a collection are not ordered so this is not reliably
46
+ # @deprecated use Browser#switch_window or a better Window locator
47
+ # @return [Watir::Window] Returns an instance of a Watir::Window
48
+ #
49
+
50
+ def last
51
+ self[-1]
52
+ end
53
+
54
+ #
55
+ # Get the window at the given index or range.
56
+ #
57
+ # @note windows in a collection are not ordered so this is not reliably
58
+ # @deprecated use Browser#switch_window or a better Window locator
59
+ # @param [Integer, Range] value Index (0-based) or Range of desired window(s)
60
+ # @return [Watir::Window] Returns an instance of a Watir::Window
61
+ #
62
+
63
+ def [](value)
64
+ old = 'using indexing with windows'
65
+ new = 'Browser#switch_window or Browser#window with :title, :url or :element selectors'
66
+ reference = 'http://watir.com/window_indexes'
67
+ Watir.logger.deprecate old, new, reference: reference, ids: [:window_index]
68
+
69
+ to_a[value]
70
+ end
71
+
72
+ def ==(other)
73
+ to_a == other.to_a
74
+ end
75
+ alias eql? ==
76
+
77
+ def to_a
78
+ @to_a ||= begin
79
+ handles = @browser.driver.window_handles.select { |wh| matches?(wh) }
80
+ handles.map { |wh| Window.new(@browser, handle: wh) }
81
+ end
82
+ end
83
+
84
+ def reset!
85
+ @to_a = nil
86
+ end
87
+
88
+ private
89
+
90
+ # NOTE: This is the exact same code from `Window#matches?`
91
+ # TODO: Move this code into a separate WindowLocator class
92
+ def matches?(handle)
93
+ @selector.empty? || @browser.driver.switch_to.window(handle) do
94
+ matches_title = @selector[:title].nil? || @browser.title =~ /#{@selector[:title]}/
95
+ matches_url = @selector[:url].nil? || @browser.url =~ /#{@selector[:url]}/
96
+ matches_element = @selector[:element].nil? || @selector[:element].exists?
97
+
98
+ matches_title && matches_url && matches_element
99
+ end
100
+ rescue Selenium::WebDriver::Error::NoSuchWindowError
101
+ # the window may disappear while we're iterating.
102
+ false
103
+ end
104
+ end # Window
105
+ end # Watir