capybara 1.1.4 → 2.0.0.beta2

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 (84) hide show
  1. data/History.txt +100 -0
  2. data/License.txt +22 -0
  3. data/README.md +829 -0
  4. data/lib/capybara.rb +124 -6
  5. data/lib/capybara/cucumber.rb +2 -5
  6. data/lib/capybara/driver/base.rb +5 -5
  7. data/lib/capybara/driver/node.rb +2 -2
  8. data/lib/capybara/dsl.rb +3 -121
  9. data/lib/capybara/node/actions.rb +12 -28
  10. data/lib/capybara/node/base.rb +5 -13
  11. data/lib/capybara/node/element.rb +21 -21
  12. data/lib/capybara/node/finders.rb +27 -89
  13. data/lib/capybara/node/matchers.rb +107 -69
  14. data/lib/capybara/node/simple.rb +11 -13
  15. data/lib/capybara/query.rb +78 -0
  16. data/lib/capybara/rack_test/browser.rb +16 -27
  17. data/lib/capybara/rack_test/driver.rb +11 -1
  18. data/lib/capybara/rack_test/node.rb +17 -1
  19. data/lib/capybara/result.rb +84 -0
  20. data/lib/capybara/rspec/matchers.rb +28 -63
  21. data/lib/capybara/selector.rb +97 -33
  22. data/lib/capybara/selenium/driver.rb +14 -61
  23. data/lib/capybara/selenium/node.rb +6 -15
  24. data/lib/capybara/server.rb +32 -27
  25. data/lib/capybara/session.rb +54 -30
  26. data/lib/capybara/spec/public/jquery-ui.js +791 -0
  27. data/lib/capybara/spec/public/jquery.js +9046 -0
  28. data/lib/capybara/spec/public/test.js +4 -1
  29. data/lib/capybara/spec/session.rb +56 -27
  30. data/lib/capybara/spec/session/all_spec.rb +8 -4
  31. data/lib/capybara/spec/session/attach_file_spec.rb +12 -9
  32. data/lib/capybara/spec/session/check_spec.rb +6 -3
  33. data/lib/capybara/spec/session/choose_spec.rb +4 -1
  34. data/lib/capybara/spec/session/click_button_spec.rb +5 -14
  35. data/lib/capybara/spec/session/click_link_or_button_spec.rb +2 -1
  36. data/lib/capybara/spec/session/click_link_spec.rb +3 -17
  37. data/lib/capybara/spec/session/current_url_spec.rb +77 -9
  38. data/lib/capybara/spec/session/fill_in_spec.rb +8 -18
  39. data/lib/capybara/spec/session/find_spec.rb +19 -46
  40. data/lib/capybara/spec/session/first_spec.rb +2 -34
  41. data/lib/capybara/spec/session/has_css_spec.rb +1 -1
  42. data/lib/capybara/spec/session/has_field_spec.rb +28 -0
  43. data/lib/capybara/spec/session/has_select_spec.rb +84 -31
  44. data/lib/capybara/spec/session/has_table_spec.rb +7 -69
  45. data/lib/capybara/spec/session/has_text_spec.rb +168 -0
  46. data/lib/capybara/spec/session/javascript.rb +65 -81
  47. data/lib/capybara/spec/session/node_spec.rb +115 -0
  48. data/lib/capybara/spec/session/screenshot.rb +29 -0
  49. data/lib/capybara/spec/session/select_spec.rb +12 -12
  50. data/lib/capybara/spec/session/text_spec.rb +9 -4
  51. data/lib/capybara/spec/session/unselect_spec.rb +12 -6
  52. data/lib/capybara/spec/session/visit_spec.rb +76 -0
  53. data/lib/capybara/spec/session/within_frame_spec.rb +33 -0
  54. data/lib/capybara/spec/session/within_spec.rb +47 -58
  55. data/lib/capybara/spec/session/within_window_spec.rb +40 -0
  56. data/lib/capybara/spec/test_app.rb +27 -3
  57. data/lib/capybara/spec/views/form.erb +11 -10
  58. data/lib/capybara/spec/views/host_links.erb +2 -2
  59. data/lib/capybara/spec/views/tables.erb +6 -66
  60. data/lib/capybara/spec/views/with_html.erb +3 -3
  61. data/lib/capybara/spec/views/with_js.erb +11 -8
  62. data/lib/capybara/util/save_and_open_page.rb +4 -3
  63. data/lib/capybara/version.rb +1 -1
  64. data/spec/basic_node_spec.rb +15 -3
  65. data/spec/dsl_spec.rb +12 -10
  66. data/spec/rack_test_spec.rb +152 -0
  67. data/spec/rspec/features_spec.rb +0 -2
  68. data/spec/rspec/matchers_spec.rb +164 -89
  69. data/spec/rspec_spec.rb +0 -2
  70. data/spec/selenium_spec.rb +67 -0
  71. data/spec/server_spec.rb +35 -23
  72. data/spec/spec_helper.rb +18 -2
  73. metadata +30 -30
  74. data/README.rdoc +0 -722
  75. data/lib/capybara/spec/driver.rb +0 -301
  76. data/lib/capybara/spec/session/current_host_spec.rb +0 -68
  77. data/lib/capybara/spec/session/has_content_spec.rb +0 -106
  78. data/lib/capybara/util/timeout.rb +0 -27
  79. data/spec/driver/rack_test_driver_spec.rb +0 -89
  80. data/spec/driver/selenium_driver_spec.rb +0 -37
  81. data/spec/session/rack_test_session_spec.rb +0 -55
  82. data/spec/session/selenium_session_spec.rb +0 -26
  83. data/spec/string_spec.rb +0 -77
  84. data/spec/timeout_spec.rb +0 -28
@@ -33,29 +33,19 @@ module Capybara
33
33
  @base = base
34
34
  end
35
35
 
36
+ # overridden in subclasses, e.g. Capybara::Node::Element
36
37
  def reload
37
38
  self
38
39
  end
39
40
 
40
- def without_wait
41
- orig = @wait_disabled
42
- @wait_disabled = true
43
- yield
44
- ensure
45
- @wait_disabled = orig
46
- end
47
-
48
- protected
49
-
50
- def wait_until(seconds=Capybara.default_wait_time)
41
+ def synchronize(seconds=Capybara.default_wait_time)
51
42
  start_time = Time.now
52
43
 
53
44
  begin
54
45
  yield
55
46
  rescue => e
56
- raise e if @wait_disabled
57
47
  raise e unless driver.wait?
58
- raise e unless (driver.respond_to?(:invalid_element_errors) and driver.invalid_element_errors.include?(e.class)) or e.is_a?(Capybara::ElementNotFound)
48
+ raise e unless driver.invalid_element_errors.include?(e.class) or e.is_a?(Capybara::ElementNotFound)
59
49
  raise e if (Time.now - start_time) >= seconds
60
50
  sleep(0.05)
61
51
  raise Capybara::FrozenInTime, "time appears to be frozen, Capybara does not work with libraries which freeze time, consider using time travelling instead" if Time.now == start_time
@@ -64,6 +54,8 @@ module Capybara
64
54
  end
65
55
  end
66
56
 
57
+ protected
58
+
67
59
  def driver
68
60
  session.driver
69
61
  end
@@ -22,10 +22,10 @@ module Capybara
22
22
  #
23
23
  class Element < Base
24
24
 
25
- def initialize(session, base, parent, selector)
25
+ def initialize(session, base, parent, query)
26
26
  super(session, base)
27
27
  @parent = parent
28
- @selector = selector
28
+ @query = query
29
29
  end
30
30
 
31
31
  def allow_reload!
@@ -37,7 +37,7 @@ module Capybara
37
37
  # @return [Object] The native element from the driver, this allows access to driver specific methods
38
38
  #
39
39
  def native
40
- wait_until { base.native }
40
+ synchronize { base.native }
41
41
  end
42
42
 
43
43
  ##
@@ -45,7 +45,7 @@ module Capybara
45
45
  # @return [String] The text of the element
46
46
  #
47
47
  def text
48
- wait_until { base.text }
48
+ synchronize { base.text }
49
49
  end
50
50
 
51
51
  ##
@@ -58,7 +58,7 @@ module Capybara
58
58
  # @return [String] The value of the attribute
59
59
  #
60
60
  def [](attribute)
61
- wait_until { base[attribute] }
61
+ synchronize { base[attribute] }
62
62
  end
63
63
 
64
64
  ##
@@ -66,7 +66,7 @@ module Capybara
66
66
  # @return [String] The value of the form element
67
67
  #
68
68
  def value
69
- wait_until { base.value }
69
+ synchronize { base.value }
70
70
  end
71
71
 
72
72
  ##
@@ -76,7 +76,7 @@ module Capybara
76
76
  # @param [String] value The new value
77
77
  #
78
78
  def set(value)
79
- wait_until { base.set(value) }
79
+ synchronize { base.set(value) }
80
80
  end
81
81
 
82
82
  ##
@@ -84,7 +84,7 @@ module Capybara
84
84
  # Select this node if is an option element inside a select tag
85
85
  #
86
86
  def select_option
87
- wait_until { base.select_option }
87
+ synchronize { base.select_option }
88
88
  end
89
89
 
90
90
  ##
@@ -92,7 +92,7 @@ module Capybara
92
92
  # Unselect this node if is an option element inside a multiple select tag
93
93
  #
94
94
  def unselect_option
95
- wait_until { base.unselect_option }
95
+ synchronize { base.unselect_option }
96
96
  end
97
97
 
98
98
  ##
@@ -100,7 +100,7 @@ module Capybara
100
100
  # Click the Element
101
101
  #
102
102
  def click
103
- wait_until { base.click }
103
+ synchronize { base.click }
104
104
  end
105
105
 
106
106
  ##
@@ -108,7 +108,7 @@ module Capybara
108
108
  # @return [String] The tag name of the element
109
109
  #
110
110
  def tag_name
111
- wait_until { base.tag_name }
111
+ synchronize { base.tag_name }
112
112
  end
113
113
 
114
114
  ##
@@ -119,7 +119,7 @@ module Capybara
119
119
  # @return [Boolean] Whether the element is visible
120
120
  #
121
121
  def visible?
122
- wait_until { base.visible? }
122
+ synchronize { base.visible? }
123
123
  end
124
124
 
125
125
  ##
@@ -129,7 +129,7 @@ module Capybara
129
129
  # @return [Boolean] Whether the element is checked
130
130
  #
131
131
  def checked?
132
- wait_until { base.checked? }
132
+ synchronize { base.checked? }
133
133
  end
134
134
 
135
135
  ##
@@ -139,7 +139,7 @@ module Capybara
139
139
  # @return [Boolean] Whether the element is selected
140
140
  #
141
141
  def selected?
142
- wait_until { base.selected? }
142
+ synchronize { base.selected? }
143
143
  end
144
144
 
145
145
  ##
@@ -149,7 +149,7 @@ module Capybara
149
149
  # @return [String] An XPath expression
150
150
  #
151
151
  def path
152
- wait_until { base.path }
152
+ synchronize { base.path }
153
153
  end
154
154
 
155
155
  ##
@@ -160,7 +160,7 @@ module Capybara
160
160
  # @param [String] event The name of the event to trigger
161
161
  #
162
162
  def trigger(event)
163
- wait_until { base.trigger(event) }
163
+ synchronize { base.trigger(event) }
164
164
  end
165
165
 
166
166
  ##
@@ -174,24 +174,24 @@ module Capybara
174
174
  # @param [Capybara::Element] node The element to drag to
175
175
  #
176
176
  def drag_to(node)
177
- wait_until { base.drag_to(node.base) }
177
+ synchronize { base.drag_to(node.base) }
178
178
  end
179
179
 
180
180
  def find(*args)
181
- wait_until { super }
181
+ synchronize { super }
182
182
  end
183
183
 
184
184
  def first(*args)
185
- wait_until { super }
185
+ synchronize { super }
186
186
  end
187
187
 
188
188
  def all(*args)
189
- wait_until { super }
189
+ synchronize { super }
190
190
  end
191
191
 
192
192
  def reload
193
193
  if @allow_reload
194
- reloaded = parent.reload.first(@selector.name, @selector.locator, @selector.options)
194
+ reloaded = parent.reload.first(@query.name, @query.locator, @query.options)
195
195
  @base = reloaded.base if reloaded
196
196
  end
197
197
  self
@@ -24,7 +24,7 @@ module Capybara
24
24
  # @raise [Capybara::ElementNotFound] If the element can't be found before time expires
25
25
  #
26
26
  def find(*args)
27
- wait_until { first(*args) or raise_find_error(*args) }.tap(&:allow_reload!)
27
+ synchronize { all(*args).find! }.tap(&:allow_reload!)
28
28
  end
29
29
 
30
30
  ##
@@ -35,7 +35,7 @@ module Capybara
35
35
  # @return [Capybara::Element] The found element
36
36
  #
37
37
  def find_field(locator)
38
- find(:xpath, XPath::HTML.field(locator))
38
+ find(:field, locator)
39
39
  end
40
40
  alias_method :field_labeled, :find_field
41
41
 
@@ -47,7 +47,7 @@ module Capybara
47
47
  # @return [Capybara::Element] The found element
48
48
  #
49
49
  def find_link(locator)
50
- find(:xpath, XPath::HTML.link(locator))
50
+ find(:link, locator)
51
51
  end
52
52
 
53
53
  ##
@@ -58,18 +58,18 @@ module Capybara
58
58
  # @return [Capybara::Element] The found element
59
59
  #
60
60
  def find_button(locator)
61
- find(:xpath, XPath::HTML.button(locator))
61
+ find(:button, locator)
62
62
  end
63
63
 
64
64
  ##
65
65
  #
66
66
  # Find a element on the page, given its id.
67
67
  #
68
- # @param [String] locator Which element to find
68
+ # @param [String] id Which element to find
69
69
  # @return [Capybara::Element] The found element
70
70
  #
71
71
  def find_by_id(id)
72
- find(:css, "##{id}")
72
+ find(:id, id)
73
73
  end
74
74
 
75
75
  ##
@@ -99,20 +99,23 @@ module Capybara
99
99
  # page.all('a', :text => 'Home')
100
100
  # page.all('#menu li', :visible => true)
101
101
  #
102
- # @param [:css, :xpath, String] kind_or_locator Either the kind of selector or the selector itself
103
- # @param [String] locator The selector
104
- # @param [Hash{Symbol => Object}] options Additional options
105
- # @option options [String, Regexp] text Only find elements which contain this text or match this regexp
106
- # @option options [Boolean] visible Only find elements that are visible on the page
107
- # @return [Capybara::Element] The found elements
102
+ # @overload all([kind], locator, options)
103
+ # @param [:css, :xpath] kind The type of selector
104
+ # @param [String] locator The selector
105
+ # @option options [String, Regexp] text Only find elements which contain this text or match this regexp
106
+ # @option options [Boolean] visible Only find elements that are visible on the page. Setting this to false
107
+ # (the default, unless Capybara.ignore_hidden_elements = true), finds
108
+ # invisible _and_ visible elements.
109
+ # @return [Array[Capybara::Element]] The found elements
108
110
  #
109
111
  def all(*args)
110
- selector = Capybara::Selector.normalize(*args)
111
- options = extract_normalized_options(args)
112
-
113
- selector.xpaths.
114
- map { |path| find_in_base(selector, path) }.flatten.
115
- select { |node| node.without_wait { matches_options(node, options) } }
112
+ query = Capybara::Query.new(*args)
113
+ elements = synchronize do
114
+ base.find(query.xpath).map do |node|
115
+ Capybara::Node::Element.new(session, node, self, query)
116
+ end
117
+ end
118
+ Capybara::Result.new(elements, query)
116
119
  end
117
120
 
118
121
  ##
@@ -120,79 +123,14 @@ module Capybara
120
123
  # Find the first element on the page matching the given selector
121
124
  # and options, or nil if no element matches.
122
125
  #
123
- # When only the first matching element is needed, this method can
124
- # be faster than all(*args).first.
125
- #
126
- # @param [:css, :xpath, String] kind_or_locator Either the kind of selector or the selector itself
127
- # @param [String] locator The selector
128
- # @param [Hash{Symbol => Object}] options Additional options; see {all}
129
- # @return Capybara::Element The found element
126
+ # @overload first([kind], locator, options)
127
+ # @param [:css, :xpath] kind The type of selector
128
+ # @param [String] locator The selector
129
+ # @param [Hash] options Additional options; see {all}
130
+ # @return [Capybara::Element] The found element or nil
130
131
  #
131
132
  def first(*args)
132
- selector = Capybara::Selector.normalize(*args)
133
- options = extract_normalized_options(args)
134
- found_elements = []
135
-
136
- selector.xpaths.each do |path|
137
- find_in_base(selector, path).each do |node|
138
- node.without_wait do
139
- if matches_options(node, options)
140
- found_elements << node
141
- return found_elements.last if not Capybara.prefer_visible_elements or node.visible?
142
- end
143
- end
144
- end
145
- end
146
- found_elements.first
147
- end
148
-
149
- protected
150
-
151
- def raise_find_error(*args)
152
- options = extract_normalized_options(args)
153
- normalized = Capybara::Selector.normalize(*args)
154
- message = options[:message] || "Unable to find #{normalized.name} #{normalized.locator.inspect}"
155
- message = normalized.failure_message.call(self, normalized) if normalized.failure_message
156
- raise Capybara::ElementNotFound, message
157
- end
158
-
159
- def find_in_base(selector, xpath)
160
- base.find(xpath).map do |node|
161
- Capybara::Node::Element.new(session, node, self, selector)
162
- end
163
- end
164
-
165
- def extract_normalized_options(args)
166
- options = if args.last.is_a?(Hash) then args.pop.dup else {} end
167
-
168
- if text = options[:text]
169
- options[:text] = Regexp.escape(text) unless text.kind_of?(Regexp)
170
- end
171
-
172
- if !options.has_key?(:visible)
173
- options[:visible] = Capybara.ignore_hidden_elements
174
- end
175
-
176
- if selected = options[:selected]
177
- options[:selected] = [selected].flatten
178
- end
179
-
180
- options
181
- end
182
-
183
- def matches_options(node, options)
184
- return false if options[:text] and not node.text.match(options[:text])
185
- return false if options[:visible] and not node.visible?
186
- return false if options[:with] and not node.value == options[:with]
187
- return false if options[:checked] and not node.checked?
188
- return false if options[:unchecked] and node.checked?
189
- return false if options[:selected] and not has_selected_options?(node, options[:selected])
190
- true
191
- end
192
-
193
- def has_selected_options?(node, expected)
194
- actual = node.all(:xpath, './/option').select { |option| option.selected? }.map { |option| option.text }
195
- (expected - actual).empty?
133
+ all(*args).first
196
134
  end
197
135
  end
198
136
  end
@@ -33,29 +33,19 @@ module Capybara
33
33
  # @return [Boolean] If the expression exists
34
34
  #
35
35
  def has_selector?(*args)
36
- options = if args.last.is_a?(Hash) then args.last else {} end
37
- wait_until do
38
- results = all(*args)
39
-
40
- case
41
- when results.empty?
42
- false
43
- when options[:between]
44
- options[:between] === results.size
45
- when options[:count]
46
- options[:count].to_i == results.size
47
- when options[:maximum]
48
- options[:maximum].to_i >= results.size
49
- when options[:minimum]
50
- options[:minimum].to_i <= results.size
51
- else
52
- results.size > 0
53
- end or raise ExpectationNotMet
54
- end
36
+ assert_selector(*args)
55
37
  rescue Capybara::ExpectationNotMet
56
38
  return false
57
39
  end
58
40
 
41
+ def assert_selector(*args)
42
+ synchronize do
43
+ result = all(*args)
44
+ result.matches_count? or raise Capybara::ExpectationNotMet, result.failure_message
45
+ end
46
+ return true
47
+ end
48
+
59
49
  ##
60
50
  #
61
51
  # Checks if a given selector is not on the page or current node.
@@ -65,29 +55,19 @@ module Capybara
65
55
  # @return [Boolean]
66
56
  #
67
57
  def has_no_selector?(*args)
68
- options = if args.last.is_a?(Hash) then args.last else {} end
69
- wait_until do
70
- results = all(*args)
71
-
72
- case
73
- when results.empty?
74
- true
75
- when options[:between]
76
- not(options[:between] === results.size)
77
- when options[:count]
78
- not(options[:count].to_i == results.size)
79
- when options[:maximum]
80
- not(options[:maximum].to_i >= results.size)
81
- when options[:minimum]
82
- not(options[:minimum].to_i <= results.size)
83
- else
84
- results.empty?
85
- end or raise ExpectationNotMet
86
- end
58
+ assert_no_selector(*args)
87
59
  rescue Capybara::ExpectationNotMet
88
60
  return false
89
61
  end
90
62
 
63
+ def assert_no_selector(*args)
64
+ synchronize do
65
+ result = all(*args)
66
+ result.matches_count? and raise Capybara::ExpectationNotMet, result.negative_failure_message
67
+ end
68
+ return true
69
+ end
70
+
91
71
  ##
92
72
  #
93
73
  # Checks if a given XPath expression is on the page or current node.
@@ -157,7 +137,7 @@ module Capybara
157
137
  # @return [Boolean] If the selector exists
158
138
  #
159
139
  def has_css?(path, options={})
160
- has_xpath?(XPath.css(path), options)
140
+ has_selector?(:css, path, options)
161
141
  end
162
142
 
163
143
  ##
@@ -169,7 +149,7 @@ module Capybara
169
149
  # @return [Boolean]
170
150
  #
171
151
  def has_no_css?(path, options={})
172
- has_no_xpath?(XPath.css(path), options)
152
+ has_no_selector?(:css, path, options)
173
153
  end
174
154
 
175
155
  ##
@@ -177,24 +157,48 @@ module Capybara
177
157
  # Checks if the page or current node has the given text content,
178
158
  # ignoring any HTML tags and normalizing whitespace.
179
159
  #
160
+ # Unlike has_content this only matches displayable text and specifically
161
+ # excludes text contained within non-display nodes such as script or head tags.
162
+ #
180
163
  # @param [String] content The text to check for
181
164
  # @return [Boolean] Whether it exists
182
165
  #
183
- def has_content?(content)
184
- has_xpath?(XPath::HTML.content(content))
166
+ def has_text?(content)
167
+ normalized_content = normalize_whitespace(content)
168
+
169
+ synchronize do
170
+ normalize_whitespace(text).match(escape_regexp(normalized_content)) or
171
+ raise ExpectationNotMet
172
+ end
173
+ return true
174
+ rescue Capybara::ExpectationNotMet
175
+ return false
185
176
  end
177
+ alias_method :has_content?, :has_text?
186
178
 
187
179
  ##
188
180
  #
189
181
  # Checks if the page or current node does not have the given text
190
182
  # content, ignoring any HTML tags and normalizing whitespace.
191
183
  #
184
+ # Unlike has_content this only matches displayable text and specifically
185
+ # excludes text contained within non-display nodes such as script or head tags.
186
+ #
192
187
  # @param [String] content The text to check for
193
188
  # @return [Boolean] Whether it exists
194
189
  #
195
- def has_no_content?(content)
196
- has_no_xpath?(XPath::HTML.content(content))
190
+ def has_no_text?(content)
191
+ normalized_content = normalize_whitespace(content)
192
+
193
+ synchronize do
194
+ !normalize_whitespace(text).match(escape_regexp(normalized_content)) or
195
+ raise ExpectationNotMet
196
+ end
197
+ return true
198
+ rescue Capybara::ExpectationNotMet
199
+ return false
197
200
  end
201
+ alias_method :has_no_content?, :has_no_text?
198
202
 
199
203
  ##
200
204
  #
@@ -207,7 +211,7 @@ module Capybara
207
211
  # @return [Boolean] Whether it exists
208
212
  #
209
213
  def has_link?(locator, options={})
210
- has_xpath?(XPath::HTML.link(locator, options))
214
+ has_selector?(:link, locator, options)
211
215
  end
212
216
 
213
217
  ##
@@ -219,7 +223,7 @@ module Capybara
219
223
  # @return [Boolean] Whether it doesn't exist
220
224
  #
221
225
  def has_no_link?(locator, options={})
222
- has_no_xpath?(XPath::HTML.link(locator, options))
226
+ has_no_selector?(:link, locator, options)
223
227
  end
224
228
 
225
229
  ##
@@ -231,7 +235,7 @@ module Capybara
231
235
  # @return [Boolean] Whether it exists
232
236
  #
233
237
  def has_button?(locator)
234
- has_xpath?(XPath::HTML.button(locator))
238
+ has_selector?(:button, locator)
235
239
  end
236
240
 
237
241
  ##
@@ -243,7 +247,7 @@ module Capybara
243
247
  # @return [Boolean] Whether it doesn't exist
244
248
  #
245
249
  def has_no_button?(locator)
246
- has_no_xpath?(XPath::HTML.button(locator))
250
+ has_no_selector?(:button, locator)
247
251
  end
248
252
 
249
253
  ##
@@ -257,13 +261,17 @@ module Capybara
257
261
  #
258
262
  # page.has_field?('Name', :with => 'Jonas')
259
263
  #
264
+ # It is also possible to filter by the field type attribute:
265
+ #
266
+ # page.has_field?('Email', :type => 'email')
267
+ #
260
268
  # @param [String] locator The label, name or id of a field to check for
261
269
  # @option options [String] :with The text content of the field
270
+ # @option options [String] :type The type attribute of the field
262
271
  # @return [Boolean] Whether it exists
263
272
  #
264
273
  def has_field?(locator, options={})
265
- options, with = split_options(options, :with)
266
- has_xpath?(XPath::HTML.field(locator, options), with)
274
+ has_selector?(:field, locator, options)
267
275
  end
268
276
 
269
277
  ##
@@ -273,11 +281,11 @@ module Capybara
273
281
  #
274
282
  # @param [String] locator The label, name or id of a field to check for
275
283
  # @option options [String] :with The text content of the field
284
+ # @option options [String] :type The type attribute of the field
276
285
  # @return [Boolean] Whether it doesn't exist
277
286
  #
278
287
  def has_no_field?(locator, options={})
279
- options, with = split_options(options, :with)
280
- has_no_xpath?(XPath::HTML.field(locator, options), with)
288
+ has_no_selector?(:field, locator, options)
281
289
  end
282
290
 
283
291
  ##
@@ -290,7 +298,7 @@ module Capybara
290
298
  # @return [Boolean] Whether it exists
291
299
  #
292
300
  def has_checked_field?(locator)
293
- has_xpath?(XPath::HTML.field(locator), :checked => true)
301
+ has_selector?(:field, locator, :checked => true)
294
302
  end
295
303
 
296
304
  ##
@@ -303,7 +311,7 @@ module Capybara
303
311
  # @return [Boolean] Whether it doesn't exists
304
312
  #
305
313
  def has_no_checked_field?(locator)
306
- has_no_xpath?(XPath::HTML.field(locator), :checked => true)
314
+ has_no_selector?(:field, locator, :checked => true)
307
315
  end
308
316
 
309
317
  ##
@@ -316,7 +324,7 @@ module Capybara
316
324
  # @return [Boolean] Whether it exists
317
325
  #
318
326
  def has_unchecked_field?(locator)
319
- has_xpath?(XPath::HTML.field(locator), :unchecked => true)
327
+ has_selector?(:field, locator, :unchecked => true)
320
328
  end
321
329
 
322
330
  ##
@@ -329,7 +337,7 @@ module Capybara
329
337
  # @return [Boolean] Whether it doesn't exists
330
338
  #
331
339
  def has_no_unchecked_field?(locator)
332
- has_no_xpath?(XPath::HTML.field(locator), :unchecked => true)
340
+ has_no_selector?(:field, locator, :unchecked => true)
333
341
  end
334
342
 
335
343
  ##
@@ -345,19 +353,23 @@ module Capybara
345
353
  #
346
354
  # page.has_select?('Language', :selected => ['English', 'German'])
347
355
  #
348
- # It's also possible to check if a given set of options exists for
356
+ # It's also possible to check if the exact set of options exists for
349
357
  # this select box:
350
358
  #
351
- # page.has_select?('Language', :options => ['English', 'German'])
359
+ # page.has_select?('Language', :options => ['English', 'German', 'Spanish'])
360
+ #
361
+ # You can also check for a partial set of options:
362
+ #
363
+ # page.has_select?('Language', :with_options => ['English', 'German'])
352
364
  #
353
365
  # @param [String] locator The label, name or id of a select box
354
366
  # @option options [Array] :options Options which should be contained in this select box
367
+ # @option options [Array] :with_options Partial set of options which should be contained in this select box
355
368
  # @option options [String, Array] :selected Options which should be selected
356
369
  # @return [Boolean] Whether it exists
357
370
  #
358
371
  def has_select?(locator, options={})
359
- options, selected = split_options(options, :selected)
360
- has_xpath?(XPath::HTML.select(locator, options), selected)
372
+ has_selector?(:select, locator, options)
361
373
  end
362
374
 
363
375
  ##
@@ -369,8 +381,7 @@ module Capybara
369
381
  # @return [Boolean] Whether it doesn't exist
370
382
  #
371
383
  def has_no_select?(locator, options={})
372
- options, selected = split_options(options, :selected)
373
- has_no_xpath?(XPath::HTML.select(locator, options), selected)
384
+ has_no_selector?(:select, locator, options)
374
385
  end
375
386
 
376
387
  ##
@@ -387,11 +398,10 @@ module Capybara
387
398
  # and the text needs to match exactly.
388
399
  #
389
400
  # @param [String] locator The id or caption of a table
390
- # @option options [Array[Array[String]]] :rows A set of rows the table should contain
391
401
  # @return [Boolean] Whether it exist
392
402
  #
393
403
  def has_table?(locator, options={})
394
- has_xpath?(XPath::HTML.table(locator, options))
404
+ has_selector?(:table, locator, options)
395
405
  end
396
406
 
397
407
  ##
@@ -403,14 +413,42 @@ module Capybara
403
413
  # @return [Boolean] Whether it doesn't exist
404
414
  #
405
415
  def has_no_table?(locator, options={})
406
- has_no_xpath?(XPath::HTML.table(locator, options))
416
+ has_no_selector?(:table, locator, options)
407
417
  end
408
418
 
409
- protected
419
+ def ==(other)
420
+ if other.respond_to?(:native)
421
+ self.eql?(other) or native == other.native
422
+ else
423
+ self.eql?(other)
424
+ end
425
+ end
426
+
427
+ private
428
+
429
+ ##
430
+ #
431
+ # Normalizes whitespace space by stripping leading and trailing
432
+ # whitespace and replacing sequences of whitespace characters
433
+ # with a single space.
434
+ #
435
+ # @param [String] text Text to normalize
436
+ # @return [String] Normalized text
437
+ #
438
+ def normalize_whitespace(text)
439
+ text.is_a?(Regexp) ? text : text.gsub(/\s+/, ' ').strip
440
+ end
410
441
 
411
- def split_options(options, key)
412
- options = options.dup
413
- [options, if options.has_key?(key) then {key => options.delete(key)} else {} end]
442
+ ##
443
+ #
444
+ # Escapes any characters that would have special meaning in a regexp
445
+ # if text is not a regexp
446
+ #
447
+ # @param [String] text Text to escape
448
+ # @return [String] Escaped text
449
+ #
450
+ def escape_regexp(text)
451
+ text.is_a?(Regexp) ? text : Regexp.escape(text)
414
452
  end
415
453
  end
416
454
  end