capybara 3.18.0 → 3.19.0

Sign up to get free protection for your applications and to get access to all the features.
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