capybara 2.8.1 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) 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 +13 -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 +9 -0
  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 +7 -16
  14. data/lib/capybara/queries/text_query.rb +11 -10
  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.rb +277 -175
  20. data/lib/capybara/selector/filter_set.rb +3 -1
  21. data/lib/capybara/selector/selector.rb +227 -0
  22. data/lib/capybara/session.rb +2 -2
  23. data/lib/capybara/spec/session/element/matches_selector_spec.rb +29 -1
  24. data/lib/capybara/spec/session/selectors_spec.rb +12 -0
  25. data/lib/capybara/spec/views/form.erb +11 -0
  26. data/lib/capybara/version.rb +1 -1
  27. data/spec/fixtures/capybara.csv +1 -0
  28. data/spec/fixtures/selenium_driver_rspec_failure.rb +1 -1
  29. data/spec/fixtures/selenium_driver_rspec_success.rb +1 -1
  30. data/spec/rack_test_spec.rb +8 -0
  31. data/spec/result_spec.rb +3 -0
  32. data/spec/selenium_firefox_spec.rb +44 -0
  33. data/spec/selenium_spec_chrome.rb +5 -2
  34. data/spec/{selenium_spec.rb → shared_selenium_session.rb} +9 -43
  35. metadata +10 -3
@@ -8,15 +8,71 @@ module Capybara
8
8
  attr_reader :options
9
9
 
10
10
  def wait
11
- if @options.has_key?(:wait)
12
- @options[:wait] || 0
11
+ self.class.wait(options)
12
+ end
13
+
14
+ def self.wait(options)
15
+ options.fetch(:wait, Capybara.default_max_wait_time) || 0
16
+ end
17
+
18
+ ##
19
+ #
20
+ # Checks if a count of 0 is valid for the query
21
+ # Returns false if query does not have any count options specified.
22
+ #
23
+ def expects_none?
24
+ if COUNT_KEYS.any? { |k| options.has_key? k }
25
+ matches_count?(0)
13
26
  else
14
- Capybara.default_max_wait_time
27
+ false
15
28
  end
16
29
  end
17
30
 
31
+ ##
32
+ #
33
+ # Checks if the given count matches the query count options.
34
+ # Defaults to true if no count options are specified. If multiple
35
+ # count options exist, it tests that all conditions are met;
36
+ # however, if :count is specified, all other options are ignored.
37
+ #
38
+ # @param [Integer] count The actual number. Should be coercible via Integer()
39
+ #
40
+ def matches_count?(count)
41
+ return (Integer(options[:count]) == count) if options[:count]
42
+ return false if options[:maximum] && (Integer(options[:maximum]) < count)
43
+ return false if options[:minimum] && (Integer(options[:minimum]) > count)
44
+ return false if options[:between] && !(options[:between] === count)
45
+ return true
46
+ end
47
+
48
+ ##
49
+ #
50
+ # Generates a failure message from the query description and count options.
51
+ #
52
+ def failure_message
53
+ String.new("expected to find #{description}") << count_message
54
+ end
55
+
56
+ def negative_failure_message
57
+ String.new("expected not to find #{description}") << count_message
58
+ end
59
+
18
60
  private
19
61
 
62
+ def count_message
63
+ message = String.new()
64
+ if options[:count]
65
+ message << " #{options[:count]} #{Capybara::Helpers.declension('time', 'times', options[:count])}"
66
+ elsif options[:between]
67
+ message << " between #{options[:between].first} and #{options[:between].last} times"
68
+ elsif options[:maximum]
69
+ message << " at most #{options[:maximum]} #{Capybara::Helpers.declension('time', 'times', options[:maximum])}"
70
+ elsif options[:minimum]
71
+ message << " at least #{options[:minimum]} #{Capybara::Helpers.declension('time', 'times', options[:minimum])}"
72
+ end
73
+ message
74
+ end
75
+
20
76
  def assert_valid_keys
21
77
  invalid_keys = @options.keys - valid_keys
22
78
  unless invalid_keys.empty?
@@ -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,7 @@ module Capybara
26
26
  @options[:exact] = true
27
27
  end
28
28
 
29
- @expression = @selector.call(@locator)
29
+ @expression = @selector.call(@locator, @options)
30
30
 
31
31
  warn_exact_usage
32
32
 
@@ -83,23 +83,15 @@ module Capybara
83
83
 
84
84
  def exact?
85
85
  return false if !supports_exact?
86
- if options.has_key?(:exact)
87
- @options[:exact]
88
- else
89
- Capybara.exact
90
- end
86
+ options.fetch(:exact, Capybara.exact)
91
87
  end
92
88
 
93
89
  def match
94
- if options.has_key?(:match)
95
- @options[:match]
96
- else
97
- Capybara.match
98
- end
90
+ options.fetch(:match, Capybara.match)
99
91
  end
100
92
 
101
93
  def xpath(exact=nil)
102
- exact = self.exact? if exact == nil
94
+ exact = self.exact? if exact.nil?
103
95
  if @expression.respond_to?(:to_xpath) and exact
104
96
  @expression.to_xpath(:exact)
105
97
  else
@@ -137,8 +129,7 @@ module Capybara
137
129
  private
138
130
 
139
131
  def valid_keys
140
- vk = COUNT_KEYS + [:text, :visible, :exact, :match, :wait, :filter_set]
141
- vk + custom_keys
132
+ VALID_KEYS + custom_keys
142
133
  end
143
134
 
144
135
  def query_filters
@@ -150,7 +141,7 @@ module Capybara
150
141
  end
151
142
 
152
143
  def custom_keys
153
- query_filters.keys
144
+ query_filters.keys + @selector.expression_filters
154
145
  end
155
146
 
156
147
  def assert_valid_keys
@@ -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
@@ -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)
@@ -1,188 +1,134 @@
1
1
  # frozen_string_literal: true
2
- require 'capybara/selector/filter_set'
3
-
4
- module Capybara
5
- class Selector
6
-
7
- attr_reader :name, :format
8
-
9
- class << self
10
- def all
11
- @selectors ||= {}
12
- end
13
-
14
- def add(name, &block)
15
- all[name.to_sym] = Capybara::Selector.new(name.to_sym, &block)
16
- end
17
-
18
- def update(name, &block)
19
- all[name.to_sym].instance_eval(&block)
20
- end
21
-
22
- def remove(name)
23
- all.delete(name.to_sym)
24
- end
25
- end
26
-
27
- def initialize(name, &block)
28
- @name = name
29
- @filter_set = FilterSet.add(name){}
30
- @match = nil
31
- @label = nil
32
- @failure_message = nil
33
- @description = nil
34
- @format = nil
35
- @expression = nil
36
- instance_eval(&block)
37
- end
38
-
39
- def custom_filters
40
- @filter_set.filters
41
- end
42
-
43
- def xpath(&block)
44
- @format, @expression = :xpath, block if block
45
- format == :xpath ? @expression : nil
46
- end
47
-
48
- def css(&block)
49
- @format, @expression = :css, block if block
50
- format == :css ? @expression : nil
51
- end
52
-
53
- def match(&block)
54
- @match = block if block
55
- @match
56
- end
57
-
58
- def label(label=nil)
59
- @label = label if label
60
- @label
61
- end
62
-
63
- def description(options={})
64
- @filter_set.description(options)
65
- end
66
-
67
- def call(locator)
68
- if format
69
- @expression.call(locator)
70
- else
71
- warn "Selector has no format"
72
- end
73
- end
74
-
75
- def match?(locator)
76
- @match and @match.call(locator)
77
- end
78
-
79
- def filter(name, options={}, &block)
80
- custom_filters[name] = Filter.new(name, block, options)
81
- end
82
-
83
- def filter_set(name, filters_to_use = nil)
84
- f_set = FilterSet.all[name]
85
- f_set.filters.each do | name, filter |
86
- custom_filters[name] = filter if filters_to_use.nil? || filters_to_use.include?(name)
87
- end
88
- f_set.descriptions.each { |desc| @filter_set.describe &desc }
89
- end
90
-
91
- def describe &block
92
- @filter_set.describe &block
93
- end
94
-
95
- private
96
-
97
- def locate_field(xpath, locator)
98
- attr_matchers = XPath.attr(:id).equals(locator) |
99
- XPath.attr(:name).equals(locator) |
100
- XPath.attr(:placeholder).equals(locator) |
101
- XPath.attr(:id).equals(XPath.anywhere(:label)[XPath.string.n.is(locator)].attr(:for))
102
- attr_matchers |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
2
+ require 'capybara/selector/selector'
3
+ Capybara::Selector::FilterSet.add(:_field) do
4
+ filter(:checked, :boolean) { |node, value| not(value ^ node.checked?) }
5
+ filter(:unchecked, :boolean) { |node, value| (value ^ node.checked?) }
6
+ filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
7
+ filter(:multiple, :boolean) { |node, value| !(value ^ node.multiple?) }
103
8
 
104
- locate_field = xpath[attr_matchers]
105
- locate_field += XPath.descendant(:label)[XPath.string.n.is(locator)].descendant(xpath)
106
- locate_field
107
- end
9
+ describe do |options|
10
+ desc, states = String.new, []
11
+ states << 'checked' if options[:checked] || (options[:unchecked] === false)
12
+ states << 'not checked' if options[:unchecked] || (options[:checked] === false)
13
+ states << 'disabled' if options[:disabled] == true
14
+ desc << " that is #{states.join(' and ')}" unless states.empty?
15
+ desc << " with the multiple attribute" if options[:multiple] == true
16
+ desc << " without the multiple attribute" if options[:multiple] === false
17
+ desc
108
18
  end
109
19
  end
110
20
 
21
+ ##
22
+ #
23
+ # Select elements by XPath expression
24
+ #
25
+ # @locator An XPath expression
26
+ #
111
27
  Capybara.add_selector(:xpath) do
112
28
  xpath { |xpath| xpath }
113
29
  end
114
30
 
31
+ ##
32
+ #
33
+ # Select elements by CSS selector
34
+ #
35
+ # @locator A CSS selector
36
+ #
115
37
  Capybara.add_selector(:css) do
116
38
  css { |css| css }
117
39
  end
118
40
 
41
+ ##
42
+ #
43
+ # Select element by id
44
+ #
45
+ # @locator The id of the element to match
46
+ #
119
47
  Capybara.add_selector(:id) do
120
48
  xpath { |id| XPath.descendant[XPath.attr(:id) == id.to_s] }
121
49
  end
122
50
 
123
- Capybara::Selector::FilterSet.add(:_field) do
124
- filter(:id) { |node, id| node['id'] == id }
125
- filter(:name) { |node, name| node['name'] == name }
126
- filter(:placeholder) { |node, placeholder| node['placeholder'] == placeholder }
127
- filter(:checked, boolean: true) { |node, value| not(value ^ node.checked?) }
128
- filter(:unchecked, boolean: true) { |node, value| (value ^ node.checked?) }
129
- filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
130
- filter(:multiple, boolean: true) { |node, value| !(value ^ node.multiple?) }
131
-
132
- describe do |options|
133
- desc, states = String.new, []
134
- [:id, :name, :placeholder].each do |opt|
135
- desc << " with #{opt.to_s} #{options[opt]}" if options.has_key?(opt)
136
- end
137
- states << 'checked' if options[:checked] || (options[:unchecked] === false)
138
- states << 'not checked' if options[:unchecked] || (options[:checked] === false)
139
- states << 'disabled' if options[:disabled] == true
140
- desc << " that is #{states.join(' and ')}" unless states.empty?
141
- desc << " with the multiple attribute" if options[:multiple] == true
142
- desc << " without the multiple attribute" if options[:multiple] === false
143
- desc
144
- end
145
- end
146
-
51
+ ##
52
+ #
53
+ # Select field elements (input [not of type submit, image, or hidden], textarea, select)
54
+ #
55
+ # @locator Matches against the id, name, or placeholder
56
+ # @filter [String] :id Matches the id attribute
57
+ # @filter [String] :name Matches the name attribute
58
+ # @filter [String] :placeholder Matches the placeholder attribute
59
+ # @filter [String] :type Matches the type attribute of the field or element type for 'textarea' and 'select'
60
+ # @filter [Boolean] :readonly
61
+ # @filter [String] :with Matches the current value of the field
62
+ # @filter [String, Array<String>] :class Matches the class(es) provided
63
+ # @filter [Boolean] :checked Match checked fields?
64
+ # @filter [Boolean] :unchecked Match unchecked fields?
65
+ # @filter [Boolean] :disabled Match disabled field?
66
+ # @filter [Boolean] :multiple Match fields that accept multiple values
147
67
  Capybara.add_selector(:field) do
148
- xpath do |locator|
68
+ xpath(:id, :name, :placeholder, :type, :class) do |locator, options|
149
69
  xpath = XPath.descendant(:input, :textarea, :select)[~XPath.attr(:type).one_of('submit', 'image', 'hidden')]
150
- xpath = locate_field(xpath, locator.to_s) unless locator.nil?
70
+ if options[:type]
71
+ type=options[:type].to_s
72
+ if ['textarea', 'select'].include?(type)
73
+ xpath = XPath.descendant(type.to_sym)
74
+ else
75
+ xpath = xpath[XPath.attr(:type).equals(type)]
76
+ end
77
+ end
78
+ xpath=locate_field(xpath, locator, options)
151
79
  xpath
152
80
  end
153
81
 
154
- filter_set(:_field)
82
+ filter_set(:_field) # checked/unchecked/disabled/multiple
155
83
 
156
- filter(:readonly, boolean: true) { |node, value| not(value ^ node.readonly?) }
84
+ filter(:readonly, :boolean) { |node, value| not(value ^ node.readonly?) }
157
85
  filter(:with) do |node, with|
158
86
  with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s
159
87
  end
160
- filter(:type) do |node, type|
161
- type = type.to_s
162
- if ['textarea', 'select'].include?(type)
163
- node.tag_name == type
164
- else
165
- node[:type] == type
166
- end
167
- end
168
88
  describe do |options|
169
- desc, states = String.new, []
89
+ desc = String.new
90
+ (expression_filters - [:type]).each { |ef| desc << " with #{ef.to_s} #{options[ef]}" if options.has_key?(ef) }
170
91
  desc << " of type #{options[:type].inspect}" if options[:type]
171
92
  desc << " with value #{options[:with].to_s.inspect}" if options.has_key?(:with)
172
93
  desc
173
94
  end
174
95
  end
175
96
 
97
+ ##
98
+ #
99
+ # Select fieldset elements
100
+ #
101
+ # @locator Matches id or contents of wrapped legend
102
+ #
103
+ # @filter [String] :id Matches id attribute
104
+ # @filter [String] :legend Matches contents of wrapped legend
105
+ # @filter [String, Array<String>] :class Matches the class(es) provided
106
+ #
176
107
  Capybara.add_selector(:fieldset) do
177
- xpath do |locator|
108
+ xpath(:id, :legend, :class) do |locator, options|
178
109
  xpath = XPath.descendant(:fieldset)
179
110
  xpath = xpath[XPath.attr(:id).equals(locator.to_s) | XPath.child(:legend)[XPath.string.n.is(locator.to_s)]] unless locator.nil?
111
+ xpath = xpath[XPath.attr(:id).equals(options[:id])] if options[:id]
112
+ xpath = xpath[XPath.child(:legend)[XPath.string.n.is(options[:legend])]] if options[:legend]
113
+ xpath = xpath[find_by_attr(:class, options[:class])]
180
114
  xpath
181
115
  end
182
116
  end
183
117
 
118
+ ##
119
+ #
120
+ # Find links ( <a> elements with an href attribute )
121
+ #
122
+ # @locator Matches the id or title attributes, or the string content of the link, or the alt attribute of a contained img element
123
+ #
124
+ # @filter [String] :id Matches the id attribute
125
+ # @filter [String] :title Matches the title attribute
126
+ # @filter [String] :alt Matches the alt attribute of a contained img element
127
+ # @filter [String] :class Matches the class(es) provided
128
+ # @filter [String, Regexp] :href Matches the normalized href of the link
129
+ #
184
130
  Capybara.add_selector(:link) do
185
- xpath do |locator|
131
+ xpath(:id, :title, :alt, :class) do |locator, options={}|
186
132
  xpath = XPath.descendant(:a)[XPath.attr(:href)]
187
133
  unless locator.nil?
188
134
  locator = locator.to_s
@@ -193,6 +139,8 @@ Capybara.add_selector(:link) do
193
139
  matchers |= XPath.attr(:'aria-label').is(locator) if Capybara.enable_aria_label
194
140
  xpath = xpath[matchers]
195
141
  end
142
+ xpath = [:id, :title, :class].inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
143
+ xpath = xpath[XPath.descendant(:img)[XPath.attr(:alt).equals(options[:alt])]] if options[:alt]
196
144
  xpath
197
145
  end
198
146
 
@@ -207,8 +155,19 @@ Capybara.add_selector(:link) do
207
155
  describe { |options| " with href #{options[:href].inspect}" if options[:href] }
208
156
  end
209
157
 
158
+ ##
159
+ #
160
+ # Find buttons ( input [of type submit, reset, image, button] or button elements )
161
+ #
162
+ # @locator Matches the id, value, or title attributes, string content of a button, or the alt attribute of an image type button
163
+ #
164
+ # @filter [String] :id Matches the id attribute
165
+ # @filter [String] :title Matches the title attribute
166
+ # @filter [String] :class Matches the class(es) provided
167
+ # @filter [String] :value Matches the value of an input button
168
+ #
210
169
  Capybara.add_selector(:button) do
211
- xpath do |locator|
170
+ xpath(:id, :value, :title, :class) do |locator, options={}|
212
171
  input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
213
172
  btn_xpath = XPath.descendant(:button)
214
173
  image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).equals('image')]
@@ -227,82 +186,158 @@ Capybara.add_selector(:button) do
227
186
  image_btn_xpath = image_btn_xpath[alt_matches]
228
187
  end
229
188
 
230
- input_btn_xpath + btn_xpath + image_btn_xpath
189
+ res_xpath = input_btn_xpath + btn_xpath + image_btn_xpath
190
+
191
+ res_xpath = expression_filters.inject(res_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
192
+
193
+ res_xpath
231
194
  end
232
195
 
233
- filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
196
+ filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| not(value ^ node.disabled?) }
234
197
 
235
- describe { |options| " that is disabled" if options[:disabled] == true }
198
+ describe do |options|
199
+ desc = String.new
200
+ desc << " that is disabled" if options[:disabled] == true
201
+ expression_filters.each { |ef| desc << " with #{ef.to_s} #{options[ef]}" if options.has_key?(ef) }
202
+ desc
203
+ end
236
204
  end
237
205
 
206
+ ##
207
+ #
208
+ # Find links or buttons
209
+ #
238
210
  Capybara.add_selector(:link_or_button) do
239
211
  label "link or button"
240
- xpath do |locator|
241
- self.class.all.values_at(:link, :button).map {|selector| selector.xpath.call(locator)}.reduce(:+)
212
+ xpath do |locator, options|
213
+ self.class.all.values_at(:link, :button).map {|selector| selector.xpath.call(locator, options)}.reduce(:+)
242
214
  end
243
215
 
244
- filter(:disabled, default: false, boolean: true, skip_if: :all) { |node, value| node.tag_name == "a" or not(value ^ node.disabled?) }
216
+ filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| node.tag_name == "a" or not(value ^ node.disabled?) }
245
217
 
246
218
  describe { |options| " that is disabled" if options[:disabled] }
247
219
  end
248
220
 
221
+ ##
222
+ #
223
+ # Find text fillable fields ( textarea, input [not of type submit, image, radio, checkbox, hidden, file] )
224
+ #
225
+ # @locator Matches against the id, name, or placeholder
226
+ # @filter [String] :id Matches the id attribute
227
+ # @filter [String] :name Matches the name attribute
228
+ # @filter [String] :placeholder Matches the placeholder attribute
229
+ # @filter [String] :with Matches the current value of the field
230
+ # @filter [String, Array<String>] :class Matches the class(es) provided
231
+ # @filter [Boolean] :disabled Match disabled field?
232
+ # @filter [Boolean] :multiple Match fields that accept multiple values
233
+ #
249
234
  Capybara.add_selector(:fillable_field) do
250
235
  label "field"
251
- xpath do |locator|
236
+ xpath(:id, :name, :placeholder, :class) do |locator, options|
252
237
  xpath = XPath.descendant(:input, :textarea)[~XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')]
253
- xpath = locate_field(xpath, locator.to_s) unless locator.nil?
254
- xpath
238
+ locate_field(xpath, locator, options)
239
+ end
240
+
241
+ filter_set(:_field, [:disabled, :multiple])
242
+
243
+ filter(:with) do |node, with|
244
+ with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s
255
245
  end
256
246
 
257
- filter_set(:_field, [:id, :name, :placeholder, :disabled, :multiple])
247
+ describe do |options|
248
+ desc = String.new
249
+ expression_filters.each { |ef| desc << " with #{ef.to_s} #{options[ef]}" if options.has_key?(ef) }
250
+ desc << " with value #{options[:with].to_s.inspect}" if options.has_key?(:with)
251
+ desc
252
+ end
258
253
  end
259
254
 
255
+ ##
256
+ #
257
+ # Find radio buttons
258
+ #
259
+ # @locator Match id, name, or associated label text
260
+ # @filter [String] :id Matches the id attribute
261
+ # @filter [String] :name Matches the name attribute
262
+ # @filter [String, Array<String>] :class Matches the class(es) provided
263
+ # @filter [Boolean] :checked Match checked fields?
264
+ # @filter [Boolean] :unchecked Match unchecked fields?
265
+ # @filter [Boolean] :disabled Match disabled field?
266
+ # @filter [String] :option Match the value
267
+ #
260
268
  Capybara.add_selector(:radio_button) do
261
269
  label "radio button"
262
- xpath do |locator|
270
+ xpath(:id, :name, :class) do |locator, options|
263
271
  xpath = XPath.descendant(:input)[XPath.attr(:type).equals('radio')]
264
- xpath = locate_field(xpath, locator.to_s) unless locator.nil?
265
- xpath
272
+ locate_field(xpath, locator, options)
266
273
  end
267
274
 
268
- filter_set(:_field, [:id, :name, :checked, :unchecked, :disabled])
275
+ filter_set(:_field, [:checked, :unchecked, :disabled])
269
276
 
270
277
  filter(:option) { |node, value| node.value == value.to_s }
271
278
 
272
279
  describe do |options|
273
280
  desc = String.new
274
281
  desc << " with value #{options[:option].inspect}" if options[:option]
282
+ expression_filters.each { |ef| desc << " with #{ef.to_s} #{options[ef]}" if options.has_key?(ef) }
275
283
  desc
276
284
  end
277
285
  end
278
286
 
287
+ ##
288
+ #
289
+ # Find checkboxes
290
+ #
291
+ # @locator Match id, name, or associated label text
292
+ # @filter [String] :id Matches the id attribute
293
+ # @filter [String] :name Matches the name attribute
294
+ # @filter [String, Array<String>] :class Matches the class(es) provided
295
+ # @filter [Boolean] :checked Match checked fields?
296
+ # @filter [Boolean] :unchecked Match unchecked fields?
297
+ # @filter [Boolean] :disabled Match disabled field?
298
+ # @filter [String] :option Match the value
299
+ #
279
300
  Capybara.add_selector(:checkbox) do
280
- xpath do |locator|
301
+ xpath(:id, :name, :class) do |locator, options|
281
302
  xpath = XPath.descendant(:input)[XPath.attr(:type).equals('checkbox')]
282
- xpath = locate_field(xpath, locator.to_s) unless locator.nil?
283
- xpath
303
+ locate_field(xpath, locator, options)
284
304
  end
285
305
 
286
- filter_set(:_field, [:id, :name, :checked, :unchecked, :disabled])
306
+ filter_set(:_field, [:checked, :unchecked, :disabled])
287
307
 
288
308
  filter(:option) { |node, value| node.value == value.to_s }
289
309
 
290
310
  describe do |options|
291
311
  desc = String.new
292
312
  desc << " with value #{options[:option].inspect}" if options[:option]
313
+ expression_filters.each { |ef| desc << " with #{ef.to_s} #{options[ef]}" if options.has_key?(ef) }
293
314
  desc
294
315
  end
295
316
  end
296
317
 
318
+ ##
319
+ #
320
+ # Find select elements
321
+ #
322
+ # @locator Match id, name, placeholder, or associated label text
323
+ # @filter [String] :id Matches the id attribute
324
+ # @filter [String] :name Matches the name attribute
325
+ # @filter [String] :placeholder Matches the placeholder attribute
326
+ # @filter [String, Array<String>] :class Matches the class(es) provided
327
+ # @filter [Boolean] :disabled Match disabled field?
328
+ # @filter [Boolean] :multiple Match fields that accept multiple values
329
+ # @filter [Array<String>] :options Exact match options
330
+ # @filter [Array<String>] :with_options Partial match options
331
+ # @filter [String, Array<String>] :selected Match the selection(s)
332
+ #
297
333
  Capybara.add_selector(:select) do
298
334
  label "select box"
299
- xpath do |locator|
335
+ xpath(:id, :name, :placeholder, :class) do |locator, options|
300
336
  xpath = XPath.descendant(:select)
301
- xpath = locate_field(xpath, locator.to_s) unless locator.nil?
302
- xpath
337
+ locate_field(xpath, locator, options)
303
338
  end
304
339
 
305
- filter_set(:_field, [:id, :name, :placeholder, :disabled, :multiple])
340
+ filter_set(:_field, [:disabled, :multiple])
306
341
 
307
342
  filter(:options) do |node, options|
308
343
  if node.visible?
@@ -329,10 +364,19 @@ Capybara.add_selector(:select) do
329
364
  desc << " with options #{options[:options].inspect}" if options[:options]
330
365
  desc << " with at least options #{options[:with_options].inspect}" if options[:with_options]
331
366
  desc << " with #{options[:selected].inspect} selected" if options[:selected]
367
+ expression_filters.each { |ef| desc << " with #{ef.to_s} #{options[ef]}" if options.has_key?(ef) }
332
368
  desc
333
369
  end
334
370
  end
335
371
 
372
+ ##
373
+ #
374
+ # Find option elements
375
+ #
376
+ # @locator Match text of option
377
+ # @filter [Boolean] :disabled Match disabled option
378
+ # @filter [Boolean] :selected Match selected option
379
+ #
336
380
  Capybara.add_selector(:option) do
337
381
  xpath do |locator|
338
382
  xpath = XPath.descendant(:option)
@@ -340,8 +384,8 @@ Capybara.add_selector(:option) do
340
384
  xpath
341
385
  end
342
386
 
343
- filter(:disabled, boolean: true) { |node, value| not(value ^ node.disabled?) }
344
- filter(:selected, boolean: true) { |node, value| not(value ^ node.selected?) }
387
+ filter(:disabled, :boolean) { |node, value| not(value ^ node.disabled?) }
388
+ filter(:selected, :boolean) { |node, value| not(value ^ node.selected?) }
345
389
 
346
390
  describe do |options|
347
391
  desc = String.new
@@ -351,17 +395,40 @@ Capybara.add_selector(:option) do
351
395
  end
352
396
  end
353
397
 
398
+ ##
399
+ #
400
+ # Find file input elements
401
+ #
402
+ # @locator Match id, name, or associated label text
403
+ # @filter [String] :id Matches the id attribute
404
+ # @filter [String] :name Matches the name attribute
405
+ # @filter [String, Array<String>] :class Matches the class(es) provided
406
+ # @filter [Boolean] :disabled Match disabled field?
407
+ # @filter [Boolean] :multiple Match field that accepts multiple values
408
+ #
354
409
  Capybara.add_selector(:file_field) do
355
410
  label "file field"
356
- xpath do |locator|
411
+ xpath(:id, :name, :class) do |locator, options|
357
412
  xpath = XPath.descendant(:input)[XPath.attr(:type).equals('file')]
358
- xpath = locate_field(xpath, locator.to_s) unless locator.nil?
359
- xpath
413
+ locate_field(xpath, locator, options)
360
414
  end
361
415
 
362
- filter_set(:_field, [:id, :name, :disabled, :multiple])
416
+ filter_set(:_field, [:disabled, :multiple])
417
+
418
+ describe do |options|
419
+ desc = String.new
420
+ expression_filters.each { |ef| desc << " with #{ef.to_s} #{options[ef]}" if options.has_key?(ef) }
421
+ desc
422
+ end
363
423
  end
364
424
 
425
+ ##
426
+ #
427
+ # Find label elements
428
+ #
429
+ # @locator Match id or text contents
430
+ # @filter [Element, String] :for The element or id of the element associated with the label
431
+ #
365
432
  Capybara.add_selector(:label) do
366
433
  label "label"
367
434
  xpath do |locator|
@@ -389,18 +456,53 @@ Capybara.add_selector(:label) do
389
456
  end
390
457
  end
391
458
 
459
+ ##
460
+ #
461
+ # Find table elements
462
+ #
463
+ # @locator id or caption text of table
464
+ # @filter [String] :id Match id attribute of table
465
+ # @filter [String] :caption Match text of associated caption
466
+ # @filter [String, Array<String>] :class Matches the class(es) provided
467
+ #
392
468
  Capybara.add_selector(:table) do
393
- xpath do |locator|
469
+ xpath(:id, :caption, :class) do |locator, options|
394
470
  xpath = XPath.descendant(:table)
395
471
  xpath = xpath[XPath.attr(:id).equals(locator.to_s) | XPath.descendant(:caption).is(locator.to_s)] unless locator.nil?
472
+ xpath = xpath[XPath.descendant(:caption).equals(options[:caption])] if options[:caption]
473
+ xpath = [:id, :class].inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
396
474
  xpath
397
475
  end
476
+
477
+ describe do |options|
478
+ desc = String.new
479
+ desc << " with id #{options[:id]}" if options[:id]
480
+ desc << " with caption #{options[:caption]}" if options[:caption]
481
+ desc
482
+ end
398
483
  end
399
484
 
485
+ ##
486
+ #
487
+ # Find frame/iframe elements
488
+ #
489
+ # @locator Match id or name
490
+ # @filter [String] :id Match id attribute
491
+ # @filter [String] :name Match name attribute
492
+ # @filter [String, Array<String>] :class Matches the class(es) provided
493
+ #
400
494
  Capybara.add_selector(:frame) do
401
- xpath do |locator|
495
+ xpath(:id, :name, :class) do |locator, options|
402
496
  xpath = XPath.descendant(:iframe) + XPath.descendant(:frame)
403
497
  xpath = xpath[XPath.attr(:id).equals(locator.to_s) | XPath.attr(:name).equals(locator)] unless locator.nil?
498
+ xpath = expression_filters.inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
404
499
  xpath
405
500
  end
501
+
502
+ describe do |options|
503
+ desc = String.new
504
+ desc << " with id #{options[:id]}" if options[:id]
505
+ desc << " with name #{options[:name]}" if options[:name]
506
+ desc
507
+ end
406
508
  end