capybara 2.8.1 → 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 (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