capybara 3.18.0 → 3.19.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +16 -0
  3. data/README.md +14 -44
  4. data/lib/capybara/node/actions.rb +2 -2
  5. data/lib/capybara/node/element.rb +3 -5
  6. data/lib/capybara/queries/selector_query.rb +30 -11
  7. data/lib/capybara/rack_test/node.rb +1 -1
  8. data/lib/capybara/result.rb +2 -0
  9. data/lib/capybara/rspec/matcher_proxies.rb +2 -0
  10. data/lib/capybara/rspec/matchers/base.rb +2 -2
  11. data/lib/capybara/rspec/matchers/count_sugar.rb +36 -0
  12. data/lib/capybara/rspec/matchers/have_selector.rb +3 -0
  13. data/lib/capybara/rspec/matchers/have_text.rb +3 -0
  14. data/lib/capybara/selector.rb +196 -599
  15. data/lib/capybara/selector/css.rb +2 -0
  16. data/lib/capybara/selector/definition.rb +276 -0
  17. data/lib/capybara/selector/definition/button.rb +46 -0
  18. data/lib/capybara/selector/definition/checkbox.rb +23 -0
  19. data/lib/capybara/selector/definition/css.rb +5 -0
  20. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  21. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  22. data/lib/capybara/selector/definition/element.rb +27 -0
  23. data/lib/capybara/selector/definition/field.rb +40 -0
  24. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  25. data/lib/capybara/selector/definition/file_field.rb +13 -0
  26. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  27. data/lib/capybara/selector/definition/frame.rb +17 -0
  28. data/lib/capybara/selector/definition/id.rb +6 -0
  29. data/lib/capybara/selector/definition/label.rb +43 -0
  30. data/lib/capybara/selector/definition/link.rb +45 -0
  31. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  32. data/lib/capybara/selector/definition/option.rb +27 -0
  33. data/lib/capybara/selector/definition/radio_button.rb +24 -0
  34. data/lib/capybara/selector/definition/select.rb +62 -0
  35. data/lib/capybara/selector/definition/table.rb +106 -0
  36. data/lib/capybara/selector/definition/table_row.rb +21 -0
  37. data/lib/capybara/selector/definition/xpath.rb +5 -0
  38. data/lib/capybara/selector/filters/base.rb +4 -0
  39. data/lib/capybara/selector/filters/locator_filter.rb +12 -2
  40. data/lib/capybara/selector/selector.rb +40 -452
  41. data/lib/capybara/selenium/driver.rb +4 -10
  42. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +3 -9
  43. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +8 -0
  44. data/lib/capybara/selenium/extensions/find.rb +1 -1
  45. data/lib/capybara/selenium/logger_suppressor.rb +5 -0
  46. data/lib/capybara/selenium/node.rb +19 -13
  47. data/lib/capybara/selenium/nodes/chrome_node.rb +30 -0
  48. data/lib/capybara/selenium/nodes/firefox_node.rb +14 -12
  49. data/lib/capybara/selenium/nodes/ie_node.rb +11 -0
  50. data/lib/capybara/selenium/nodes/safari_node.rb +7 -12
  51. data/lib/capybara/server/checker.rb +7 -3
  52. data/lib/capybara/session.rb +2 -2
  53. data/lib/capybara/spec/session/all_spec.rb +1 -1
  54. data/lib/capybara/spec/session/find_spec.rb +1 -1
  55. data/lib/capybara/spec/session/first_spec.rb +1 -1
  56. data/lib/capybara/spec/session/has_css_spec.rb +7 -0
  57. data/lib/capybara/spec/session/has_text_spec.rb +6 -0
  58. data/lib/capybara/spec/session/save_screenshot_spec.rb +11 -0
  59. data/lib/capybara/spec/session/select_spec.rb +0 -5
  60. data/lib/capybara/spec/test_app.rb +8 -3
  61. data/lib/capybara/version.rb +1 -1
  62. data/lib/capybara/window.rb +1 -1
  63. data/spec/minitest_spec_spec.rb +1 -0
  64. data/spec/selector_spec.rb +12 -6
  65. data/spec/selenium_spec_firefox.rb +0 -3
  66. data/spec/selenium_spec_firefox_remote.rb +0 -3
  67. data/spec/selenium_spec_ie.rb +3 -1
  68. data/spec/server_spec.rb +1 -1
  69. data/spec/shared_selenium_session.rb +1 -1
  70. data/spec/spec_helper.rb +9 -2
  71. metadata +54 -2
@@ -226,10 +226,10 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
226
226
  @browser&.quit
227
227
  rescue Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED # rubocop:disable Lint/HandleExceptions
228
228
  # Browser must have already gone
229
- rescue Selenium::WebDriver::Error::UnknownError => err
230
- unless silenced_unknown_error_message?(err.message) # Most likely already gone
229
+ rescue Selenium::WebDriver::Error::UnknownError => e
230
+ unless silenced_unknown_error_message?(e.message) # Most likely already gone
231
231
  # probably already gone but not sure - so warn
232
- warn "Ignoring Selenium UnknownError during driver quit: #{err.message}"
232
+ warn "Ignoring Selenium UnknownError during driver quit: #{e.message}"
233
233
  end
234
234
  ensure
235
235
  @browser = nil
@@ -283,13 +283,7 @@ private
283
283
  end
284
284
 
285
285
  def clear_browser_state_errors
286
- @clear_browser_state_errors ||= [Selenium::WebDriver::Error::UnknownError].tap do |errors|
287
- unless selenium_4?
288
- ::Selenium::WebDriver.logger.suppress_deprecations do
289
- errors << Selenium::WebDriver::Error::UnhandledError
290
- end
291
- end
292
- end
286
+ @clear_browser_state_errors ||= [Selenium::WebDriver::Error::UnknownError]
293
287
  end
294
288
 
295
289
  def unhandled_alert_errors
@@ -18,8 +18,8 @@ module Capybara::Selenium::Driver::ChromeDriver
18
18
 
19
19
  def resize_window_to(handle, width, height)
20
20
  super
21
- rescue Selenium::WebDriver::Error::UnknownError => err
22
- raise unless err.message.match?(/failed to change window state/)
21
+ rescue Selenium::WebDriver::Error::UnknownError => e
22
+ raise unless e.message.match?(/failed to change window state/)
23
23
 
24
24
  # Chromedriver doesn't wait long enough for state to change when coming out of fullscreen
25
25
  # and raises unnecessary error. Wait a bit and try again.
@@ -53,13 +53,7 @@ private
53
53
  end
54
54
 
55
55
  def cdp_unsupported_errors
56
- @cdp_unsupported_errors ||= [Selenium::WebDriver::Error::WebDriverError].tap do |errors|
57
- unless selenium_4?
58
- ::Selenium::WebDriver.logger.suppress_deprecations do
59
- errors << Selenium::WebDriver::Error::UnhandledError
60
- end
61
- end
62
- end
56
+ @cdp_unsupported_errors ||= [Selenium::WebDriver::Error::WebDriverError]
63
57
  end
64
58
 
65
59
  def execute_cdp(cmd, params = {})
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'capybara/selenium/nodes/ie_node'
4
+
3
5
  module Capybara::Selenium::Driver::InternetExplorerDriver
4
6
  def switch_to_frame(frame)
5
7
  return super unless frame == :parent
@@ -10,6 +12,12 @@ module Capybara::Selenium::Driver::InternetExplorerDriver
10
12
  browser.switch_to.default_content
11
13
  handles.tap(&:pop).each { |fh| browser.switch_to.frame(fh) }
12
14
  end
15
+
16
+ private
17
+
18
+ def build_node(native_node, initial_cache = {})
19
+ ::Capybara::Selenium::IENode.new(self, native_node, initial_cache)
20
+ end
13
21
  end
14
22
 
15
23
  module Capybara::Selenium
@@ -87,7 +87,7 @@ module Capybara
87
87
  end
88
88
 
89
89
  def is_displayed_atom # rubocop:disable Naming/PredicateName
90
- @@is_displayed_atom ||= begin
90
+ @@is_displayed_atom ||= begin # rubocop:disable Style/ClassVars
91
91
  browser.send(:bridge).send(:read_atom, 'isDisplayed')
92
92
  rescue StandardError
93
93
  # If the atom doesn't exist or other error
@@ -3,6 +3,11 @@
3
3
  module Capybara
4
4
  module Selenium
5
5
  module DeprecationSuppressor
6
+ def initialize(*)
7
+ @suppress_for_capybara = false
8
+ super
9
+ end
10
+
6
11
  def deprecate(*)
7
12
  super unless @suppress_for_capybara
8
13
  end
@@ -56,10 +56,12 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
56
56
  def set(value, **options)
57
57
  raise ArgumentError, "Value cannot be an Array when 'multiple' attribute is not present. Not a #{value.class}" if value.is_a?(Array) && !multiple?
58
58
 
59
- tag_name, type = attrs(:tagName, :type)
60
- case tag_name.downcase
59
+ tag_name, type = attrs(:tagName, :type).map { |val| val&.downcase }
60
+ @tag_name ||= tag_name
61
+
62
+ case tag_name
61
63
  when 'input'
62
- case type.downcase
64
+ case type
63
65
  when 'radio'
64
66
  click
65
67
  when 'checkbox'
@@ -78,7 +80,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
78
80
  when 'textarea'
79
81
  set_text(value, options)
80
82
  else
81
- set_content_editable(value) if content_editable?
83
+ set_content_editable(value)
82
84
  end
83
85
  end
84
86
 
@@ -136,7 +138,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
136
138
  end
137
139
 
138
140
  def tag_name
139
- native.tag_name.downcase
141
+ @tag_name ||= native.tag_name.downcase
140
142
  end
141
143
 
142
144
  def visible?; boolean_attr(native.displayed?); end
@@ -291,20 +293,24 @@ private
291
293
  # Ensure we are focused on the element
292
294
  click
293
295
 
294
- driver.execute_script <<-JS, self
295
- var range = document.createRange();
296
- var sel = window.getSelection();
297
- arguments[0].focus();
298
- range.selectNodeContents(arguments[0]);
299
- sel.removeAllRanges();
300
- sel.addRange(range);
296
+ editable = driver.execute_script <<-JS, self
297
+ if (arguments[0].isContentEditable) {
298
+ var range = document.createRange();
299
+ var sel = window.getSelection();
300
+ arguments[0].focus();
301
+ range.selectNodeContents(arguments[0]);
302
+ sel.removeAllRanges();
303
+ sel.addRange(range);
304
+ return true;
305
+ }
306
+ return false;
301
307
  JS
302
308
 
303
309
  # The action api has a speed problem but both chrome and firefox 58 raise errors
304
310
  # if we use the faster direct send_keys. For now just send_keys to the element
305
311
  # we've already focused.
306
312
  # native.send_keys(value.to_s)
307
- browser_action.send_keys(value.to_s).perform
313
+ browser_action.send_keys(value.to_s).perform if editable
308
314
  end
309
315
 
310
316
  def action_with_modifiers(click_options)
@@ -13,6 +13,14 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
13
13
  end
14
14
 
15
15
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
16
+ # In Chrome 75+ files are appended (due to WebDriver spec - why?) so we have to clear here if its multiple and already set
17
+ if browser_version >= 75.0
18
+ driver.execute_script(<<~JS, self)
19
+ if (arguments[0].multiple && (arguments[0].files.length > 0)){
20
+ arguments[0].value = null;
21
+ }
22
+ JS
23
+ end
16
24
  super(value)
17
25
  rescue *file_errors => e
18
26
  raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload" if e.message.match?(/File not found : .+\n.+/m)
@@ -35,6 +43,18 @@ class Capybara::Selenium::ChromeNode < Capybara::Selenium::Node
35
43
  raise
36
44
  end
37
45
 
46
+ def disabled?
47
+ driver.evaluate_script("arguments[0].matches(':disabled, select:disabled *')", self)
48
+ end
49
+
50
+ def select_option
51
+ # To optimize to only one check and then click
52
+ selected_or_disabled = driver.evaluate_script(<<~JS, self)
53
+ arguments[0].matches(':disabled, select:disabled *, :checked')
54
+ JS
55
+ click unless selected_or_disabled
56
+ end
57
+
38
58
  private
39
59
 
40
60
  def file_errors
@@ -46,4 +66,14 @@ private
46
66
  def bridge
47
67
  driver.browser.send(:bridge)
48
68
  end
69
+
70
+ def w3c?
71
+ (defined?(Selenium::WebDriver::VERSION) && (Selenium::WebDriver::VERSION.to_f >= 4)) ||
72
+ driver.browser.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
73
+ end
74
+
75
+ def browser_version
76
+ caps = driver.browser.capabilities
77
+ (caps[:browser_version] || caps[:version]).to_f
78
+ end
49
79
  end
@@ -18,22 +18,16 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
18
18
  end
19
19
 
20
20
  def disabled?
21
- # Not sure exactly what version of FF fixed the below issue, but it is definitely fixed in 61+
22
- return super unless browser_version < 61.0
23
-
24
- return true if super
25
-
26
- # workaround for selenium-webdriver/geckodriver reporting elements as enabled when they are nested in disabling elements
27
- if %w[option optgroup].include? tag_name
28
- find_xpath('parent::*[self::optgroup or self::select]')[0].disabled?
29
- else
30
- !find_xpath(DISABLED_BY_FIELDSET_XPATH).empty?
31
- end
21
+ driver.evaluate_script("arguments[0].matches(':disabled, select:disabled *')", self)
32
22
  end
33
23
 
34
24
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
35
25
  # By default files are appended so we have to clear here if its multiple and already set
36
- native.clear if multiple? && driver.evaluate_script('arguments[0].files', self).any?
26
+ driver.execute_script(<<~JS, self)
27
+ if (arguments[0].multiple && (arguments[0].files.length > 0)){
28
+ arguments[0].value = null;
29
+ }
30
+ JS
37
31
  return super if browser_version >= 62.0
38
32
 
39
33
  # Workaround lack of support for multiple upload by uploading one at a time
@@ -65,6 +59,14 @@ class Capybara::Selenium::FirefoxNode < Capybara::Selenium::Node
65
59
  scroll_if_needed { browser_action.move_to(native, 0, 0).move_to(native).perform }
66
60
  end
67
61
 
62
+ def select_option
63
+ # To optimize to only one check and then click
64
+ selected_or_disabled = driver.evaluate_script(<<~JS, self)
65
+ arguments[0].matches(':disabled, select:disabled *, :checked')
66
+ JS
67
+ click unless selected_or_disabled
68
+ end
69
+
68
70
  private
69
71
 
70
72
  def click_with_options(click_options)
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/selenium/extensions/html5_drag'
4
+
5
+ class Capybara::Selenium::IENode < Capybara::Selenium::Node
6
+ def disabled?
7
+ # TODO: Doesn't work for a bunch of cases - need to get IE running to see if it can be done like this
8
+ # driver.evaluate_script("arguments[0].msMatchesSelector(':disabled, select:disabled *')", self)
9
+ super
10
+ end
11
+ end
@@ -19,8 +19,12 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
19
19
  end
20
20
 
21
21
  def select_option
22
- driver.execute_script("arguments[0].closest('select').scrollIntoView()", self)
23
- super
22
+ # To optimize to only one check and then click
23
+ selected_or_disabled = driver.execute_script(<<~JS, self)
24
+ arguments[0].closest('select').scrollIntoView();
25
+ return arguments[0].matches(':disabled, select:disabled *, :checked');
26
+ JS
27
+ click unless selected_or_disabled
24
28
  end
25
29
 
26
30
  def unselect_option
@@ -40,16 +44,7 @@ class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
40
44
  end
41
45
 
42
46
  def disabled?
43
- return true if super
44
-
45
- # workaround for safaridriver reporting elements as enabled when they are nested in disabling elements
46
- if %w[option optgroup].include? tag_name
47
- return true if self[:disabled] == 'true'
48
-
49
- find_xpath('parent::*[self::optgroup or self::select]')[0].disabled?
50
- else
51
- !find_xpath(DISABLED_BY_FIELDSET_XPATH).empty?
52
- end
47
+ driver.evaluate_script("arguments[0].matches(':disabled, select:disabled *')", self)
53
48
  end
54
49
 
55
50
  def set_file(value) # rubocop:disable Naming/AccessorMethodName
@@ -12,7 +12,7 @@ module Capybara
12
12
 
13
13
  def request(&block)
14
14
  ssl? ? https_request(&block) : http_request(&block)
15
- rescue *TRY_HTTPS_ERRORS # rubocop:disable Naming/RescuedExceptionsVariableName
15
+ rescue *TRY_HTTPS_ERRORS # _rubocop:disable Naming/RescuedExceptionsVariableName
16
16
  res = https_request(&block)
17
17
  @ssl = true
18
18
  res
@@ -25,11 +25,15 @@ module Capybara
25
25
  private
26
26
 
27
27
  def http_request(&block)
28
- Net::HTTP.start(@host, @port, read_timeout: 2, &block)
28
+ make_request(read_timeout: 2, &block)
29
29
  end
30
30
 
31
31
  def https_request(&block)
32
- Net::HTTP.start(@host, @port, ssl_options, &block)
32
+ make_request(ssl_options, &block)
33
+ end
34
+
35
+ def make_request(**options, &block)
36
+ Net::HTTP.start(@host, @port, options.merge(max_retries: 0), &block)
33
37
  end
34
38
 
35
39
  def ssl_options
@@ -77,7 +77,7 @@ module Capybara
77
77
  def initialize(mode, app = nil)
78
78
  raise TypeError, 'The second parameter to Session::new should be a rack app if passed.' if app && !app.respond_to?(:call)
79
79
 
80
- @@instance_created = true
80
+ @@instance_created = true # rubocop:disable Style/ClassVars
81
81
  @mode = mode
82
82
  @app = app
83
83
  if block_given?
@@ -807,7 +807,7 @@ module Capybara
807
807
 
808
808
  private
809
809
 
810
- @@instance_created = false
810
+ @@instance_created = false # rubocop:disable Style/ClassVars
811
811
 
812
812
  def driver_args(args)
813
813
  args.map { |arg| arg.is_a?(Capybara::Node::Element) ? arg.base : arg }
@@ -30,7 +30,7 @@ Capybara::SpecHelper.spec '#all' do
30
30
 
31
31
  it 'should accept an XPath instance', :exact_false do
32
32
  @session.visit('/form')
33
- @xpath = Capybara::Selector[:fillable_field].call('Name')
33
+ @xpath = Capybara::Selector.new(:fillable_field, config: {}, format: :xpath).call('Name')
34
34
  expect(@xpath).to be_a(::XPath::Union)
35
35
  @result = @session.all(@xpath).map(&:value)
36
36
  expect(@result).to include('Smith', 'John', 'John Smith')
@@ -235,7 +235,7 @@ Capybara::SpecHelper.spec '#find' do
235
235
 
236
236
  it 'should accept an XPath instance' do
237
237
  @session.visit('/form')
238
- @xpath = Capybara::Selector[:fillable_field].call('First Name')
238
+ @xpath = Capybara::Selector.new(:fillable_field, config: {}, format: :xpath).call('First Name')
239
239
  expect(@xpath).to be_a(::XPath::Union)
240
240
  expect(@session.find(@xpath).value).to eq('John')
241
241
  end
@@ -24,7 +24,7 @@ Capybara::SpecHelper.spec '#first' do
24
24
 
25
25
  it 'should accept an XPath instance' do
26
26
  @session.visit('/form')
27
- @xpath = Capybara::Selector[:fillable_field].call('First Name')
27
+ @xpath = Capybara::Selector.new(:fillable_field, config: {}, format: :xpath).call('First Name')
28
28
  expect(@xpath).to be_a(::XPath::Union)
29
29
  expect(@session.first(@xpath).value).to eq('John')
30
30
  end
@@ -146,7 +146,9 @@ Capybara::SpecHelper.spec '#has_css?' do
146
146
  context 'with count' do
147
147
  it 'should be true if the content occurs the given number of times' do
148
148
  expect(@session).to have_css('p', count: 3)
149
+ expect(@session).to have_css('p').exactly(3).times
149
150
  expect(@session).to have_css('p a#foo', count: 1)
151
+ expect(@session).to have_css('p a#foo').once
150
152
  expect(@session).to have_css('p a.doesnotexist', count: 0)
151
153
  expect(@session).to have_css('li', class: /guitar|drummer/, count: 4)
152
154
  expect(@session).to have_css('li', id: /john|paul/, class: /guitar|drummer/, count: 2)
@@ -161,6 +163,7 @@ Capybara::SpecHelper.spec '#has_css?' do
161
163
 
162
164
  it 'should be false if the content occurs a different number of times than the given' do
163
165
  expect(@session).not_to have_css('p', count: 6)
166
+ expect(@session).not_to have_css('p').exactly(5).times
164
167
  expect(@session).not_to have_css('p a#foo', count: 2)
165
168
  expect(@session).not_to have_css('p a.doesnotexist', count: 1)
166
169
  end
@@ -175,6 +178,7 @@ Capybara::SpecHelper.spec '#has_css?' do
175
178
  it 'should be true when content occurs same or fewer times than given' do
176
179
  expect(@session).to have_css('h2.head', maximum: 5) # edge case
177
180
  expect(@session).to have_css('h2', maximum: 10)
181
+ expect(@session).to have_css('h2').at_most(10).times
178
182
  expect(@session).to have_css('p a.doesnotexist', maximum: 1)
179
183
  expect(@session).to have_css('p a.doesnotexist', maximum: 0)
180
184
  end
@@ -182,6 +186,7 @@ Capybara::SpecHelper.spec '#has_css?' do
182
186
  it 'should be false when content occurs more times than given' do
183
187
  expect(@session).not_to have_css('h2.head', maximum: 4) # edge case
184
188
  expect(@session).not_to have_css('h2', maximum: 3)
189
+ expect(@session).not_to have_css('h2').at_most(3).times
185
190
  expect(@session).not_to have_css('p', maximum: 1)
186
191
  end
187
192
 
@@ -195,12 +200,14 @@ Capybara::SpecHelper.spec '#has_css?' do
195
200
  it 'should be true when content occurs same or more times than given' do
196
201
  expect(@session).to have_css('h2.head', minimum: 5) # edge case
197
202
  expect(@session).to have_css('h2', minimum: 3)
203
+ expect(@session).to have_css('h2').at_least(2).times
198
204
  expect(@session).to have_css('p a.doesnotexist', minimum: 0)
199
205
  end
200
206
 
201
207
  it 'should be false when content occurs fewer times than given' do
202
208
  expect(@session).not_to have_css('h2.head', minimum: 6) # edge case
203
209
  expect(@session).not_to have_css('h2', minimum: 8)
210
+ expect(@session).not_to have_css('h2').at_least(8).times
204
211
  expect(@session).not_to have_css('p', minimum: 10)
205
212
  expect(@session).not_to have_css('p a.doesnotexist', minimum: 1)
206
213
  end
@@ -166,12 +166,14 @@ Capybara::SpecHelper.spec '#has_text?' do
166
166
  it 'should be true if the text occurs the given number of times' do
167
167
  @session.visit('/with_count')
168
168
  expect(@session).to have_text('count', count: 2)
169
+ expect(@session).to have_text('count').exactly(2).times
169
170
  end
170
171
 
171
172
  it 'should be false if the text occurs a different number of times than the given' do
172
173
  @session.visit('/with_count')
173
174
  expect(@session).not_to have_text('count', count: 0)
174
175
  expect(@session).not_to have_text('count', count: 1)
176
+ expect(@session).not_to have_text('count').once
175
177
  expect(@session).not_to have_text(/count/, count: 3)
176
178
  end
177
179
 
@@ -186,12 +188,14 @@ Capybara::SpecHelper.spec '#has_text?' do
186
188
  it 'should be true when text occurs same or fewer times than given' do
187
189
  @session.visit('/with_count')
188
190
  expect(@session).to have_text('count', maximum: 2)
191
+ expect(@session).to have_text('count').at_most(2).times
189
192
  expect(@session).to have_text(/count/, maximum: 3)
190
193
  end
191
194
 
192
195
  it 'should be false when text occurs more times than given' do
193
196
  @session.visit('/with_count')
194
197
  expect(@session).not_to have_text('count', maximum: 1)
198
+ expect(@session).not_to have_text('count').at_most(1).times
195
199
  expect(@session).not_to have_text('count', maximum: 0)
196
200
  end
197
201
 
@@ -206,12 +210,14 @@ Capybara::SpecHelper.spec '#has_text?' do
206
210
  it 'should be true when text occurs same or more times than given' do
207
211
  @session.visit('/with_count')
208
212
  expect(@session).to have_text('count', minimum: 2)
213
+ expect(@session).to have_text('count').at_least(2).times
209
214
  expect(@session).to have_text(/count/, minimum: 0)
210
215
  end
211
216
 
212
217
  it 'should be false when text occurs fewer times than given' do
213
218
  @session.visit('/with_count')
214
219
  expect(@session).not_to have_text('count', minimum: 3)
220
+ expect(@session).not_to have_text('count').at_least(3).times
215
221
  end
216
222
 
217
223
  it 'should coerce minimum to an integer' do