capybara 2.8.0 → 2.9.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.yard/templates_custom/default/class/html/selectors.erb +38 -0
  3. data/.yard/templates_custom/default/class/html/setup.rb +17 -0
  4. data/.yard/yard_extensions.rb +78 -0
  5. data/.yardopts +1 -0
  6. data/History.md +20 -1
  7. data/README.md +1 -1
  8. data/lib/capybara/helpers.rb +0 -59
  9. data/lib/capybara/node/actions.rb +17 -11
  10. data/lib/capybara/node/finders.rb +10 -1
  11. data/lib/capybara/node/matchers.rb +54 -15
  12. data/lib/capybara/queries/base_query.rb +59 -3
  13. data/lib/capybara/queries/selector_query.rb +22 -16
  14. data/lib/capybara/queries/text_query.rb +12 -11
  15. data/lib/capybara/rack_test/form.rb +2 -1
  16. data/lib/capybara/result.rb +25 -10
  17. data/lib/capybara/rspec/features.rb +3 -2
  18. data/lib/capybara/rspec/matchers.rb +1 -1
  19. data/lib/capybara/selector/filter_set.rb +3 -1
  20. data/lib/capybara/selector/selector.rb +227 -0
  21. data/lib/capybara/selector.rb +277 -175
  22. data/lib/capybara/session.rb +2 -2
  23. data/lib/capybara/spec/session/assert_text.rb +7 -0
  24. data/lib/capybara/spec/session/element/matches_selector_spec.rb +29 -1
  25. data/lib/capybara/spec/session/find_spec.rb +10 -4
  26. data/lib/capybara/spec/session/selectors_spec.rb +12 -0
  27. data/lib/capybara/spec/views/form.erb +11 -0
  28. data/lib/capybara/version.rb +1 -1
  29. data/spec/fixtures/capybara.csv +1 -0
  30. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
  31. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  32. data/spec/rack_test_spec.rb +8 -0
  33. data/spec/result_spec.rb +3 -0
  34. data/spec/selenium_firefox_spec.rb +44 -0
  35. data/spec/selenium_spec_chrome.rb +5 -2
  36. data/spec/{selenium_spec.rb → shared_selenium_session.rb} +9 -43
  37. metadata +10 -3
@@ -4,7 +4,7 @@ module Capybara
4
4
  class SelectorQuery < Queries::BaseQuery
5
5
  attr_accessor :selector, :locator, :options, :expression, :find, :negative
6
6
 
7
- VALID_KEYS = [:text, :visible, :between, :count, :maximum, :minimum, :exact, :match, :wait, :filter_set]
7
+ VALID_KEYS = COUNT_KEYS + [:text, :visible, :exact, :match, :wait, :filter_set]
8
8
  VALID_MATCH = [:first, :smart, :prefer_exact, :one]
9
9
 
10
10
  def initialize(*args)
@@ -26,7 +26,10 @@ module Capybara
26
26
  @options[:exact] = true
27
27
  end
28
28
 
29
- @expression = @selector.call(@locator)
29
+ @expression = @selector.call(@locator, @options)
30
+
31
+ warn_exact_usage
32
+
30
33
  assert_valid_keys
31
34
  end
32
35
 
@@ -79,23 +82,16 @@ module Capybara
79
82
  end
80
83
 
81
84
  def exact?
82
- if options.has_key?(:exact)
83
- @options[:exact]
84
- else
85
- Capybara.exact
86
- end
85
+ return false if !supports_exact?
86
+ options.fetch(:exact, Capybara.exact)
87
87
  end
88
88
 
89
89
  def match
90
- if options.has_key?(:match)
91
- @options[:match]
92
- else
93
- Capybara.match
94
- end
90
+ options.fetch(:match, Capybara.match)
95
91
  end
96
92
 
97
93
  def xpath(exact=nil)
98
- exact = self.exact? if exact == nil
94
+ exact = self.exact? if exact.nil?
99
95
  if @expression.respond_to?(:to_xpath) and exact
100
96
  @expression.to_xpath(:exact)
101
97
  else
@@ -125,11 +121,15 @@ module Capybara
125
121
  end
126
122
  end
127
123
 
124
+ # @api private
125
+ def supports_exact?
126
+ @expression.respond_to? :to_xpath
127
+ end
128
+
128
129
  private
129
130
 
130
131
  def valid_keys
131
- vk = COUNT_KEYS + [:text, :visible, :exact, :match, :wait, :filter_set]
132
- vk + custom_keys
132
+ VALID_KEYS + custom_keys
133
133
  end
134
134
 
135
135
  def query_filters
@@ -141,7 +141,7 @@ module Capybara
141
141
  end
142
142
 
143
143
  def custom_keys
144
- query_filters.keys
144
+ query_filters.keys + @selector.expression_filters
145
145
  end
146
146
 
147
147
  def assert_valid_keys
@@ -150,6 +150,12 @@ module Capybara
150
150
  raise ArgumentError, "invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(", ")}"
151
151
  end
152
152
  end
153
+
154
+ def warn_exact_usage
155
+ if options.has_key?(:exact) && !supports_exact?
156
+ warn "The :exact option only has an effect on queries using the XPath#is method. Using it with the query \"#{expression.to_s}\" has no effect."
157
+ end
158
+ end
153
159
  end
154
160
  end
155
161
  end
@@ -22,24 +22,25 @@ module Capybara
22
22
  end
23
23
 
24
24
  def failure_message
25
- build_message(true)
25
+ super << build_message(true)
26
26
  end
27
27
 
28
28
  def negative_failure_message
29
- build_message(false).sub(/(to find)/, 'not \1')
29
+ super << build_message(false)
30
+ end
31
+
32
+ def description
33
+ if @expected_text.is_a?(Regexp)
34
+ "text matching #{@expected_text.inspect}"
35
+ else
36
+ "text #{@expected_text.inspect}"
37
+ end
30
38
  end
31
39
 
32
40
  private
33
41
 
34
42
  def build_message(report_on_invisible)
35
- description =
36
- if @expected_text.is_a?(Regexp)
37
- "text matching #{@expected_text.inspect}"
38
- else
39
- "text #{@expected_text.inspect}"
40
- end
41
-
42
- message = Capybara::Helpers.failure_message(description, @options)
43
+ message = String.new()
43
44
  unless (COUNT_KEYS & @options.keys).empty?
44
45
  message << " but found #{@count} #{Capybara::Helpers.declension('time', 'times', @count)}"
45
46
  end
@@ -48,7 +49,7 @@ module Capybara
48
49
  details_message = []
49
50
 
50
51
  if @node and !@expected_text.is_a? Regexp
51
- insensitive_regexp = Regexp.new(@expected_text, Regexp::IGNORECASE)
52
+ insensitive_regexp = Capybara::Helpers.to_regexp(@expected_text, Regexp::IGNORECASE)
52
53
  insensitive_count = @actual_text.scan(insensitive_regexp).size
53
54
  if insensitive_count != @count
54
55
  details_message << "it was found #{insensitive_count} #{Capybara::Helpers.declension("time", "times", insensitive_count)} using a case insensitive search"
@@ -42,7 +42,8 @@ class Capybara::RackTest::Form < Capybara::RackTest::Node
42
42
  if (value = field['value']).to_s.empty?
43
43
  NilUploadedFile.new
44
44
  else
45
- content_type = MIME::Types.type_for(value).first.to_s
45
+ types = MIME::Types.type_for(value)
46
+ content_type = types.sort_by.with_index { |type, idx| [type.obsolete? ? 1 : 0, idx] }.first.to_s
46
47
  Rack::Test::UploadedFile.new(value, content_type)
47
48
  end
48
49
  merge_param!(params, field['name'].to_s, file)
@@ -45,7 +45,7 @@ module Capybara
45
45
  end
46
46
 
47
47
  def [](*args)
48
- if (args.size == 1) && ((idx = args[0]).is_a? Integer) && (idx > 0)
48
+ if (args.size == 1) && ((idx = args[0]).is_a? Integer) && (idx >= 0)
49
49
  @result_cache << @results_enum.next while @result_cache.size <= idx
50
50
  @result_cache[idx]
51
51
  else
@@ -61,31 +61,48 @@ module Capybara
61
61
  end
62
62
 
63
63
  def matches_count?
64
- return Integer(@query.options[:count]) == count if @query.options[:count]
65
-
66
- return false if @query.options[:between] && !(@query.options[:between] === count)
64
+ # Only check filters for as many elements as necessary to determine result
65
+ if @query.options[:count]
66
+ count_opt = Integer(@query.options[:count])
67
+ loop do
68
+ break if @result_cache.size > count_opt
69
+ @result_cache << @results_enum.next
70
+ end
71
+ return @result_cache.size == count_opt
72
+ end
67
73
 
68
74
  if @query.options[:minimum]
75
+ min_opt = Integer(@query.options[:minimum])
69
76
  begin
70
- @result_cache << @results_enum.next while @result_cache.size < Integer(@query.options[:minimum])
77
+ @result_cache << @results_enum.next while @result_cache.size < min_opt
71
78
  rescue StopIteration
72
79
  return false
73
80
  end
74
81
  end
75
82
 
76
83
  if @query.options[:maximum]
84
+ max_opt = Integer(@query.options[:maximum])
77
85
  begin
78
- @result_cache << @results_enum.next while @result_cache.size <= Integer(@query.options[:maximum])
86
+ @result_cache << @results_enum.next while @result_cache.size <= max_opt
79
87
  return false
80
88
  rescue StopIteration
81
89
  end
82
90
  end
83
91
 
92
+ if @query.options[:between]
93
+ max = Integer(@query.options[:between].max)
94
+ loop do
95
+ break if @result_cache.size > max
96
+ @result_cache << @results_enum.next
97
+ end
98
+ return false unless (@query.options[:between] === @result_cache.size)
99
+ end
100
+
84
101
  return true
85
102
  end
86
103
 
87
104
  def failure_message
88
- message = Capybara::Helpers.failure_message(@query.description, @query.options)
105
+ message = @query.failure_message
89
106
  if count > 0
90
107
  message << ", found #{count} #{Capybara::Helpers.declension("match", "matches", count)}: " << full_results.map(&:text).map(&:inspect).join(", ")
91
108
  else
@@ -105,9 +122,7 @@ module Capybara
105
122
  private
106
123
 
107
124
  def full_results
108
- loop do
109
- @result_cache << @results_enum.next
110
- end
125
+ loop { @result_cache << @results_enum.next }
111
126
  @result_cache
112
127
  end
113
128
 
@@ -39,7 +39,6 @@ else
39
39
  end
40
40
  end
41
41
 
42
-
43
42
  def self.feature(*args, &block)
44
43
  options = if args.last.is_a?(Hash) then args.pop else {} end
45
44
  options[:capybara_feature] = true
@@ -51,5 +50,7 @@ else
51
50
  RSpec.describe(*args, &block)
52
51
  end
53
52
 
54
- RSpec.configuration.include Capybara::Features, :capybara_feature => true
53
+ RSpec.configure do |config|
54
+ config.include(Capybara::Features, :capybara_feature => true)
55
+ end
55
56
  end
@@ -161,7 +161,7 @@ module Capybara
161
161
 
162
162
  class BecomeClosed
163
163
  def initialize(options)
164
- @wait_time = Capybara::Queries::SelectorQuery.new(options).wait
164
+ @wait_time = Capybara::Queries::BaseQuery.wait(options)
165
165
  end
166
166
 
167
167
  def matches?(window)
@@ -12,7 +12,9 @@ module Capybara
12
12
  instance_eval(&block)
13
13
  end
14
14
 
15
- def filter(name, options={}, &block)
15
+ def filter(name, *types_and_options, &block)
16
+ options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
17
+ types_and_options.each { |k| options[k] = true}
16
18
  filters[name] = Filter.new(name, block, options)
17
19
  end
18
20
 
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+ require 'capybara/selector/filter_set'
3
+ require 'xpath'
4
+
5
+ #Patch XPath to allow a nil condition in where
6
+ module XPath
7
+ class Renderer
8
+ def where(on, condition)
9
+ condition = condition.to_s
10
+ if !condition.empty?
11
+ "#{on}[#{condition}]"
12
+ else
13
+ "#{on}"
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ module Capybara
20
+ class Selector
21
+
22
+ attr_reader :name, :format, :expression_filters
23
+
24
+ class << self
25
+ def all
26
+ @selectors ||= {}
27
+ end
28
+
29
+ def add(name, &block)
30
+ all[name.to_sym] = Capybara::Selector.new(name.to_sym, &block)
31
+ end
32
+
33
+ def update(name, &block)
34
+ all[name.to_sym].instance_eval(&block)
35
+ end
36
+
37
+ def remove(name)
38
+ all.delete(name.to_sym)
39
+ end
40
+ end
41
+
42
+ def initialize(name, &block)
43
+ @name = name
44
+ @filter_set = FilterSet.add(name){}
45
+ @match = nil
46
+ @label = nil
47
+ @failure_message = nil
48
+ @description = nil
49
+ @format = nil
50
+ @expression = nil
51
+ @expression_filters = []
52
+ instance_eval(&block)
53
+ end
54
+
55
+ def custom_filters
56
+ @filter_set.filters
57
+ end
58
+
59
+ ##
60
+ #
61
+ # Define a selector by an xpath expression
62
+ #
63
+ # @overload xpath(*expression_filters, &block)
64
+ # @param [Array<Symbol>] expression_filters ([]) Names of filters that can be implemented via this expression
65
+ # @yield [locator, options] The block to use to generate the XPath expression
66
+ # @yieldparam [String] locator The locator string passed to the query
67
+ # @yieldparam [Hash] options The options hash passed to the query
68
+ # @yieldreturn [#to_xpath, #to_s] An object that can produce an xpath expression
69
+ #
70
+ # @overload xpath()
71
+ # @return [#call] The block that will be called to generate the XPath expression
72
+ #
73
+ def xpath(*expression_filters, &block)
74
+ @format, @expression_filters, @expression = :xpath, expression_filters.flatten, block if block
75
+ format == :xpath ? @expression : nil
76
+ end
77
+
78
+ ##
79
+ #
80
+ # Define a selector by a CSS selector
81
+ #
82
+ # @overload css(*expression_filters, &block)
83
+ # @param [Array<Symbol>] expression_filters ([]) Names of filters that can be implemented via this CSS selector
84
+ # @yield [locator, options] The block to use to generate the CSS selector
85
+ # @yieldparam [String] locator The locator string passed to the query
86
+ # @yieldparam [Hash] options The options hash passed to the query
87
+ # @yieldreturn [#to_s] An object that can produce a CSS selector
88
+ #
89
+ # @overload css()
90
+ # @return [#call] The block that will be called to generate the CSS selector
91
+ #
92
+ def css(*expression_filters, &block)
93
+ @format, @expression_filters, @expression = :css, expression_filters.flatten, block if block
94
+ format == :css ? @expression : nil
95
+ end
96
+
97
+ ##
98
+ #
99
+ # Automatic selector detection
100
+ #
101
+ # @yield [locator] This block takes the passed in locator string and returns whether or not it matches the selector
102
+ # @yieldparam [String], locator The locator string used to determin if it matches the selector
103
+ # @yieldreturn [Boolean] Whether this selector matches the locator string
104
+ # @return [#call] The block that will be used to detect selector match
105
+ #
106
+ def match(&block)
107
+ @match = block if block
108
+ @match
109
+ end
110
+
111
+ ##
112
+ #
113
+ # Set/get a descriptive label for the selector
114
+ #
115
+ # @overload label(label)
116
+ # @param [String] label A descriptive label for this selector - used in error messages
117
+ # @overload label()
118
+ # @return [String] The currently set label
119
+ #
120
+ def label(label=nil)
121
+ @label = label if label
122
+ @label
123
+ end
124
+
125
+ ##
126
+ #
127
+ # Description of the selector
128
+ #
129
+ # @param [Hash] options The options of the query used to generate the description
130
+ # @return [String] Description of the selector when used with the options passed
131
+ #
132
+ def description(options={})
133
+ @filter_set.description(options)
134
+ end
135
+
136
+ def call(locator, options={})
137
+ if format
138
+ # @expression.call(locator, options.select {|k,v| @expression_filters.include?(k)})
139
+ @expression.call(locator, options)
140
+ else
141
+ warn "Selector has no format"
142
+ end
143
+ end
144
+
145
+ ##
146
+ #
147
+ # Should this selector be used for the passed in locator
148
+ #
149
+ # This is used by the automatic selector selection mechanism when no selector type is passed to a selector query
150
+ #
151
+ # @param [String] locator The locator passed to the query
152
+ # @return [Boolean] Whether or not to use this selector
153
+ #
154
+ def match?(locator)
155
+ @match and @match.call(locator)
156
+ end
157
+
158
+ ##
159
+ #
160
+ # Define a non-expression filter for use with this selector
161
+ #
162
+ # @overload filter(name, *types, options={}, &block)
163
+ # @param [Symbol] name The filter name
164
+ # @param [Array<Symbol>] types The types of the filter - currently valid types are [:boolean]
165
+ # @param [Hash] options ({}) Options of the filter
166
+ # @option options [Array<>] :valid_values Valid values for this filter
167
+ # @option options :default The default value of the filter (if any)
168
+ # @option options :skip_if Value of the filter that will cause it to be skipped
169
+ #
170
+ def filter(name, *types_and_options, &block)
171
+ options = types_and_options.last.is_a?(Hash) ? types_and_options.pop.dup : {}
172
+ types_and_options.each { |k| options[k] = true}
173
+ custom_filters[name] = Filter.new(name, block, options)
174
+ end
175
+
176
+ def filter_set(name, filters_to_use = nil)
177
+ f_set = FilterSet.all[name]
178
+ f_set.filters.each do | name, filter |
179
+ custom_filters[name] = filter if filters_to_use.nil? || filters_to_use.include?(name)
180
+ end
181
+ f_set.descriptions.each { |desc| @filter_set.describe &desc }
182
+ end
183
+
184
+ def describe &block
185
+ @filter_set.describe &block
186
+ end
187
+
188
+ private
189
+
190
+ def locate_field(xpath, locator, options={})
191
+ locate_xpath = xpath #need to save original xpath for the label wrap
192
+ if locator
193
+ locator = locator.to_s
194
+ attr_matchers = XPath.attr(:id).equals(locator) |
195
+ XPath.attr(:name).equals(locator) |
196
+ XPath.attr(:placeholder).equals(locator) |
197
+ XPath.attr(:id).equals(XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for))
198
+ attr_matchers |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
199
+
200
+ locate_xpath = locate_xpath[attr_matchers]
201
+ locate_xpath += XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath)
202
+ end
203
+
204
+ locate_xpath = [:id, :name, :placeholder, :class].inject(locate_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
205
+ locate_xpath
206
+ end
207
+
208
+ def find_by_attr(attribute, value)
209
+ finder_name = "find_by_#{attribute.to_s}_attr"
210
+ if respond_to?(finder_name, true)
211
+ send(finder_name, value)
212
+ else
213
+ value ? XPath.attr(attribute).equals(value) : nil
214
+ end
215
+ end
216
+
217
+ def find_by_class_attr(classes)
218
+ if classes
219
+ Array(classes).map do |klass|
220
+ "contains(concat(' ',normalize-space(@class),' '),' #{klass} ')"
221
+ end.join(" and ").to_sym
222
+ else
223
+ nil
224
+ end
225
+ end
226
+ end
227
+ end