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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3cb32760328428839877de78f879a68af52098b0d1540c1d0de102acce58bc4b
4
- data.tar.gz: b2c40ed521b21b43b4a44e1fe91f38b704e6c85e04829673fcc4493ff8fd217a
3
+ metadata.gz: cdee23fb17ec93c0e1c5e8e9b61607f11211e065ff94a99084dc28e3ff0cc24c
4
+ data.tar.gz: 428519d0593d850d7c355e7300ed0805637dbbd498805b70452dcb9521a78a04
5
5
  SHA512:
6
- metadata.gz: 69163470d9db2ce8831cad207bbc14aa286181357ac650b909f266fde4e50e2400a851f1451fb0091d54f859f8e270cfe267afbfc8d76b74b6e46f4a2cca21b9
7
- data.tar.gz: 48b79d23c6c3c87e9bdcbfd2128a15b475b001699b3b15b79f208772d2dfbc8444f442267ece4d331be25b7489f6c39c869c4edcc017f4d03629d79b09dbbe57
6
+ metadata.gz: dd137a4698f4c9e797ecce6da32a1fb62e2011daf412b9898ecd85f1e8146447278c0fd60350b9b9baeff37ac35d1f3da62794ec7c17dd87509f767d5847cfa7
7
+ data.tar.gz: 85625218afed65274c180acb2e9cf2f8e809e52855150966f07c6db8e181dd0cd28bfd908ce39a1a53b52cdbcb09af631e7a4d5d694afd9af4330cd0588122fd
data/History.md CHANGED
@@ -1,3 +1,19 @@
1
+ # Version 3.19.0
2
+ Release date: 2019-05-09
3
+
4
+ ### Added
5
+
6
+
7
+ * Syntactic sugar `#once`, `#twice`, `#thrice`, `#excatly`, `#at_least`, `#at_most` to
8
+ `have_selector`, `have_css`, `have_xpath`, and `have_text` RSpec matchers
9
+ * Support for multiple expression types in Selector definitions
10
+ * Reduced wirecalls for common actions in Selenium driver
11
+
12
+ ### Fixed
13
+
14
+ * Workaround Chrome 75 appending files to multiple file inputs
15
+ * Suppressed retry when detecting http vs https server connection
16
+
1
17
  # Version 3.18.0
2
18
  Release date: 2019-04-22
3
19
 
data/README.md CHANGED
@@ -3,10 +3,11 @@
3
3
  [![Build Status](https://secure.travis-ci.org/teamcapybara/capybara.svg)](https://travis-ci.org/teamcapybara/capybara)
4
4
  [![Build Status](https://ci.appveyor.com/api/projects/status/github/teamcapybara/capybara?svg=true)](https://ci.appveyor.com/api/projects/github/teamcapybara/capybara)
5
5
  [![Code Climate](https://codeclimate.com/github/teamcapybara/capybara.svg)](https://codeclimate.com/github/teamcapybara/capybara)
6
+ [![Coverage Status](https://coveralls.io/repos/github/teamcapybara/capybara/badge.svg?branch=master)](https://coveralls.io/github/teamcapybara/capybara?branch=master)
6
7
  [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jnicklas/capybara?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
7
8
  [![SemVer](https://api.dependabot.com/badges/compatibility_score?dependency-name=capybara&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=capybara&package-manager=bundler&version-scheme=semver)
8
9
 
9
- **Note** You are viewing the README for the 3.18.x version of Capybara.
10
+ **Note** You are viewing the README for the 3.19.x version of Capybara.
10
11
 
11
12
 
12
13
  Capybara helps you test web applications by simulating how a real user would
@@ -183,7 +184,7 @@ to one specific driver. For example:
183
184
  ```ruby
184
185
  describe 'some stuff which requires js', js: true do
185
186
  it 'will use the default js driver'
186
- it 'will switch to one specific driver', driver: :webkit
187
+ it 'will switch to one specific driver', driver: :apparition
187
188
  end
188
189
  ```
189
190
 
@@ -260,7 +261,7 @@ end
260
261
 
261
262
  ## <a name="using-capybara-with-minitest"></a>Using Capybara with Minitest
262
263
 
263
- * If you are using Rails, add the following code in your `test_helper.rb`
264
+ * If you are using Rails, but not using Rails system tests, add the following code in your `test_helper.rb`
264
265
  file to make Capybara available in all test cases deriving from
265
266
  `ActionDispatch::IntegrationTest`:
266
267
 
@@ -275,8 +276,7 @@ end
275
276
  include Capybara::Minitest::Assertions
276
277
 
277
278
  # Reset sessions and driver between tests
278
- # Use super wherever this method is redefined in your individual test classes
279
- def teardown
279
+ teardown do
280
280
  Capybara.reset_sessions!
281
281
  Capybara.use_default_driver
282
282
  end
@@ -341,9 +341,9 @@ For example if you'd prefer to run everything in Selenium, you could do:
341
341
  Capybara.default_driver = :selenium # :selenium_chrome and :selenium_chrome_headless are also registered
342
342
  ```
343
343
 
344
- However, if you are using RSpec or Cucumber, you may instead want to consider
345
- leaving the faster `:rack_test` as the __default_driver__, and marking only those
346
- tests that require a JavaScript-capable driver using `js: true` or
344
+ However, if you are using RSpec or Cucumber (and your app runs correctly without JS),
345
+ you may instead want to consider leaving the faster `:rack_test` as the __default_driver__, and
346
+ marking only those tests that require a JavaScript-capable driver using `js: true` or
347
347
  `@javascript`, respectively. By default, JavaScript tests are run using the
348
348
  `:selenium` driver. You can change this by setting
349
349
  `Capybara.javascript_driver`.
@@ -352,7 +352,7 @@ You can also change the driver temporarily (typically in the Before/setup and
352
352
  After/teardown blocks):
353
353
 
354
354
  ```ruby
355
- Capybara.current_driver = :webkit # temporarily select different driver
355
+ Capybara.current_driver = :apparition # temporarily select different driver
356
356
  # tests here
357
357
  Capybara.use_default_driver # switch back to default driver
358
358
  ```
@@ -410,42 +410,12 @@ and test server, see [Transactions and database setup](#transactions-and-databas
410
410
 
411
411
  ### <a name="apparition"></a>Apparition
412
412
 
413
- The [apparition driver](https://github.com/twalpole/apparition) in a new driver that allows you to run tests using Chrome in a headless
413
+ The [apparition driver](https://github.com/twalpole/apparition) is a new driver that allows you to run tests using Chrome in a headless
414
414
  or headed configuration. It attempts to provide backwards compatibility with the [Poltergeist driver API](https://github.com/teampoltergeist/poltergeist)
415
- while allowing for the use of modern JS/CSS. It uses CDP to communicate with Chrome, thereby obviating the need for chromedriver.
416
- A compatibility layer for capybara-webkit is planned, although has not yet been started. This driver is being developed by the
417
- maintainer of Capybara and will attempt to keep up to date with new Capybara releases. It will probably be moved into the
418
- teamcapybara repo once completely stable.
419
-
420
- ### <a name="capybara-webkit"></a>Capybara-webkit
421
-
422
- Note: `capybara-webkit` depends on QtWebkit which went end-of-life quite some time ago. There has been an attempt to revive the project but `capybara-webkit` is not yet (as far as I know) compatible with the revived version of QtWebKit (could be a good open source project for someone) and as such is still limited to an old version of QtWebKit. This means its support for modern JS and CSS is severely limited.
423
-
424
- The [capybara-webkit driver](https://github.com/thoughtbot/capybara-webkit) is for true headless
425
- testing. It uses QtWebKit to start a rendering engine process. It can execute JavaScript as well.
426
- It is significantly faster than drivers like Selenium since it does not load an entire browser.
427
-
428
- You can install it with:
429
-
430
- ```bash
431
- gem install capybara-webkit
432
- ```
433
-
434
- And you can use it by:
435
-
436
- ```ruby
437
- Capybara.javascript_driver = :webkit
438
- ```
439
-
440
- ### <a name="poltergeist"></a>Poltergeist
441
-
442
- Note: `poltergeist` depends on PhantomJS for which active development ended quite some time ago (2.1.1). As such it is roughly equivalent to a 6-7 year old version of Safari, meaning lack of support for modern JS and CSS. If any effort to update PhantomJS succeeds in the future this situation could change.
443
-
444
- [Poltergeist](https://github.com/teampoltergeist/poltergeist) is another
445
- headless driver which integrates Capybara with
446
- [PhantomJS](http://phantomjs.org/). It is truly headless, so doesn't
447
- require Xvfb to run on your CI server. It will also detect and report
448
- any Javascript errors that happen within the page.
415
+ and [capybara-webkit API](https://github.com/thoughtbot/capybara-webkit) while allowing for the use of modern JS/CSS. It
416
+ uses CDP to communicate with Chrome, thereby obviating the need for chromedriver. This driver is being developed by the
417
+ current developer of Capybara and will attempt to keep up to date with new Capybara releases. It will probably be moved into the
418
+ teamcapybara repo once it reaches v1.0.
449
419
 
450
420
  ## <a name="the-dsl"></a>The DSL
451
421
 
@@ -300,12 +300,12 @@ module Capybara
300
300
  synchronize(Capybara::Queries::BaseQuery.wait(options, session_options.default_max_wait_time)) do
301
301
  begin
302
302
  find(:select, from, options)
303
- rescue Capybara::ElementNotFound => select_error # rubocop:disable Naming/RescuedExceptionsVariableName
303
+ rescue Capybara::ElementNotFound => select_error # _rubocop:disable Naming/RescuedExceptionsVariableName
304
304
  raise if %i[selected with_selected multiple].any? { |option| options.key?(option) }
305
305
 
306
306
  begin
307
307
  find(:datalist_input, from, options)
308
- rescue Capybara::ElementNotFound => dlinput_error
308
+ rescue Capybara::ElementNotFound => dlinput_error # _rubocop:disable Naming/RescuedExceptionsVariableName
309
309
  raise Capybara::ElementNotFound, "#{select_error.message} and #{dlinput_error.message}"
310
310
  end
311
311
  end
@@ -126,7 +126,6 @@ module Capybara
126
126
  #
127
127
  # @return [Capybara::Node::Element] The element
128
128
  def select_option(wait: nil)
129
- warn "Attempt to select disabled option: #{value || text}" if disabled?
130
129
  synchronize(wait) { base.select_option }
131
130
  self
132
131
  end
@@ -276,7 +275,8 @@ module Capybara
276
275
  # @return [String] The tag name of the element
277
276
  #
278
277
  def tag_name
279
- synchronize { base.tag_name }
278
+ # Element type is immutable so cache it
279
+ @tag_name ||= synchronize { base.tag_name }
280
280
  end
281
281
 
282
282
  ##
@@ -480,9 +480,7 @@ module Capybara
480
480
  %(#<Capybara::Node::Element tag="#{base.tag_name}" path="#{base.path}">)
481
481
  rescue NotSupportedByDriverError
482
482
  %(#<Capybara::Node::Element tag="#{base.tag_name}">)
483
- rescue StandardError => err
484
- raise unless session.driver.invalid_element_errors.any? { |et| err.is_a?(et) }
485
-
483
+ rescue *session.driver.invalid_element_errors
486
484
  %(Obsolete #<Capybara::Node::Element>)
487
485
  end
488
486
 
@@ -11,6 +11,7 @@ module Capybara
11
11
  session_options:,
12
12
  enable_aria_label: session_options.enable_aria_label,
13
13
  test_id: session_options.test_id,
14
+ selector_format: nil,
14
15
  **options,
15
16
  &filter_block)
16
17
  @resolved_node = nil
@@ -19,14 +20,18 @@ module Capybara
19
20
  super(@options)
20
21
  self.session_options = session_options
21
22
 
22
- @selector = find_selector(args[0].is_a?(Symbol) ? args.shift : args[0])
23
+ @selector = Selector.new(
24
+ find_selector(args[0].is_a?(Symbol) ? args.shift : args[0]),
25
+ config: { enable_aria_label: enable_aria_label, test_id: test_id },
26
+ format: selector_format
27
+ )
28
+
23
29
  @locator = args.shift
24
30
  @filter_block = filter_block
25
31
 
26
32
  raise ArgumentError, "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?
27
33
 
28
- selector_config = { enable_aria_label: enable_aria_label, test_id: test_id }
29
- @expression = selector.call(@locator, @options.merge(selector_config: selector_config))
34
+ @expression = selector.call(@locator, @options)
30
35
 
31
36
  warn_exact_usage
32
37
 
@@ -83,7 +88,7 @@ module Capybara
83
88
  matches_system_filters?(node) &&
84
89
  matches_node_filters?(node, node_filter_errors) &&
85
90
  matches_filter_block?(node)
86
- rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : []) # rubocop:disable Naming/RescuedExceptionsVariableName
91
+ rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : []) # _rubocop:disable Naming/RescuedExceptionsVariableName
87
92
  false
88
93
  end
89
94
 
@@ -129,7 +134,9 @@ module Capybara
129
134
 
130
135
  # @api private
131
136
  def supports_exact?
132
- @expression.respond_to? :to_xpath
137
+ return @expression.respond_to? :to_xpath if @selector.supports_exact?.nil?
138
+
139
+ @selector.supports_exact?
133
140
  end
134
141
 
135
142
  def failure_message
@@ -142,6 +149,10 @@ module Capybara
142
149
 
143
150
  private
144
151
 
152
+ def selector_format
153
+ @selector.format
154
+ end
155
+
145
156
  def text_fragments
146
157
  text = (options[:text] || options[:exact_text])
147
158
  text.is_a?(String) ? text.split : []
@@ -192,23 +203,23 @@ module Capybara
192
203
  def find_nodes_by_selector_format(node, exact)
193
204
  hints = {}
194
205
  hints[:uses_visibility] = true unless visible == :all
195
- hints[:texts] = text_fragments unless selector.format == :xpath
206
+ hints[:texts] = text_fragments unless selector_format == :xpath
196
207
  hints[:styles] = options[:style] if use_default_style_filter?
197
208
 
198
- if selector.format == :css
209
+ if selector_format == :css
199
210
  if node.method(:find_css).arity != 1
200
211
  node.find_css(css, **hints)
201
212
  else
202
213
  node.find_css(css)
203
214
  end
204
- elsif selector.format == :xpath
215
+ elsif selector_format == :xpath
205
216
  if node.method(:find_xpath).arity != 1
206
217
  node.find_xpath(xpath(exact), **hints)
207
218
  else
208
219
  node.find_xpath(xpath(exact))
209
220
  end
210
221
  else
211
- raise ArgumentError, "Unknown format: #{selector.format}"
222
+ raise ArgumentError, "Unknown format: #{selector_format}"
212
223
  end
213
224
  end
214
225
 
@@ -230,6 +241,8 @@ module Capybara
230
241
  unapplied_options = options.keys - valid_keys
231
242
  @selector.with_filter_errors(errors) do
232
243
  node_filters.all? do |filter_name, filter|
244
+ next true unless apply_filter?(filter)
245
+
233
246
  if filter.matcher?
234
247
  unapplied_options.select { |option_name| filter.handles_option?(option_name) }.all? do |option_name|
235
248
  unapplied_options.delete(option_name)
@@ -320,6 +333,8 @@ module Capybara
320
333
  def apply_expression_filters(expression)
321
334
  unapplied_options = options.keys - valid_keys
322
335
  expression_filters.inject(expression) do |expr, (name, ef)|
336
+ next expr unless apply_filter?(ef)
337
+
323
338
  if ef.matcher?
324
339
  unapplied_options.select(&ef.method(:handles_option?)).inject(expr) do |memo, option_name|
325
340
  unapplied_options.delete(option_name)
@@ -358,10 +373,14 @@ module Capybara
358
373
  node.is_a?(::Capybara::Node::Simple) && node.path == '/'
359
374
  end
360
375
 
376
+ def apply_filter?(filter)
377
+ filter.format.nil? || (filter.format == selector_format)
378
+ end
379
+
361
380
  def matches_locator_filter?(node)
362
- return true unless @selector.locator_filter
381
+ return true unless @selector.locator_filter && apply_filter?(@selector.locator_filter)
363
382
 
364
- @selector.locator_filter.matches?(node, @locator, @selector)
383
+ @selector.locator_filter.matches?(node, @locator, @selector, exact: exact?)
365
384
  end
366
385
 
367
386
  def matches_system_filters?(node)
@@ -118,7 +118,7 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
118
118
 
119
119
  public_instance_methods(false).each do |meth_name|
120
120
  alias_method "unchecked_#{meth_name}", meth_name
121
- private "unchecked_#{meth_name}" # rubocop:disable Layout/AccessModifierIndentation,Style/AccessModifierDeclarations
121
+ private "unchecked_#{meth_name}" # rubocop:disable Style/AccessModifierDeclarations
122
122
 
123
123
  define_method meth_name do |*args|
124
124
  stale_check
@@ -153,7 +153,9 @@ module Capybara
153
153
  # causes a concurrency issue with network requests here
154
154
  # https://github.com/jruby/jruby/issues/4212
155
155
  if (RUBY_PLATFORM == 'java') && (Gem::Version.new(JRUBY_VERSION) < Gem::Version.new('9.2.8.0'))
156
+ # :nocov:
156
157
  @elements.select(&block).to_enum # non-lazy evaluation
158
+ # :nocov:
157
159
  else
158
160
  @elements.lazy.select(&block)
159
161
  end
@@ -21,6 +21,7 @@ module Capybara
21
21
  end
22
22
 
23
23
  if RUBY_ENGINE == 'jruby'
24
+ # :nocov:
24
25
  module Capybara::DSL
25
26
  class <<self
26
27
  remove_method :included
@@ -43,6 +44,7 @@ if RUBY_ENGINE == 'jruby'
43
44
  end
44
45
  end
45
46
  end
47
+ # :nocov:
46
48
  else
47
49
  module Capybara::DSLRSpecProxyInstaller
48
50
  module ClassMethods
@@ -48,8 +48,8 @@ module Capybara
48
48
 
49
49
  def does_not_match?(actual)
50
50
  element_does_not_match?(wrap(actual))
51
- rescue Capybara::ExpectationNotMet => err
52
- @failure_message_when_negated = err.message
51
+ rescue Capybara::ExpectationNotMet => e
52
+ @failure_message_when_negated = e.message
53
53
  false
54
54
  end
55
55
 
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module RSpecMatchers
5
+ module CountSugar
6
+ def once; exactly(1); end
7
+ def twice; exactly(2); end
8
+ def thrice; exactly(3); end
9
+
10
+ def exactly(number)
11
+ options[:count] = number
12
+ self
13
+ end
14
+
15
+ def at_most(number)
16
+ options[:maximum] = number
17
+ self
18
+ end
19
+
20
+ def at_least(number)
21
+ options[:minimum] = number
22
+ self
23
+ end
24
+
25
+ def times
26
+ self
27
+ end
28
+
29
+ private
30
+
31
+ def options
32
+ (@args.last.is_a?(Hash) ? @args : @args.push({})).last
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/rspec/matchers/base'
4
+ require 'capybara/rspec/matchers/count_sugar'
4
5
 
5
6
  module Capybara
6
7
  module RSpecMatchers
7
8
  module Matchers
8
9
  class HaveSelector < WrappedElementMatcher
10
+ include CountSugar
11
+
9
12
  def element_matches?(el)
10
13
  el.assert_selector(*@args, &@filter_block)
11
14
  end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'capybara/rspec/matchers/base'
4
+ require 'capybara/rspec/matchers/count_sugar'
4
5
 
5
6
  module Capybara
6
7
  module RSpecMatchers
7
8
  module Matchers
8
9
  class HaveText < WrappedElementMatcher
10
+ include CountSugar
11
+
9
12
  def element_matches?(el)
10
13
  el.assert_text(*@args)
11
14
  end
@@ -2,6 +2,181 @@
2
2
 
3
3
  require 'capybara/selector/xpath_extensions'
4
4
  require 'capybara/selector/selector'
5
+ require 'capybara/selector/definition'
6
+
7
+ # rubocop:disable Style/AsciiComments
8
+ #
9
+ # ## Built-in Selectors
10
+ #
11
+ # * **:xpath** - Select elements by XPath expression
12
+ # * Locator: An XPath expression
13
+ #
14
+ # * **:css** - Select elements by CSS selector
15
+ # * Locator: A CSS selector
16
+ #
17
+ # * **:id** - Select element by id
18
+ # * Locator: (String, Regexp, XPath::Expression) The id of the element to match
19
+ #
20
+ # * **:field** - Select field elements (input [not of type submit, image, or hidden], textarea, select)
21
+ # * Locator: Matches against the id, Capybara.test_id attribute, name, or placeholder
22
+ # * Filters:
23
+ # * :id (String, Regexp, XPath::Expression) — Matches the id attribute
24
+ # * :name (String) — Matches the name attribute
25
+ # * :placeholder (String) — Matches the placeholder attribute
26
+ # * :type (String) — Matches the type attribute of the field or element type for 'textarea' and 'select'
27
+ # * :readonly (Boolean)
28
+ # * :with (String) — Matches the current value of the field
29
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
30
+ # * :checked (Boolean) — Match checked fields?
31
+ # * :unchecked (Boolean) — Match unchecked fields?
32
+ # * :disabled (Boolean) — Match disabled field?
33
+ # * :multiple (Boolean) — Match fields that accept multiple values
34
+ # * :style (String, Regexp, Hash)
35
+ #
36
+ # * **:fieldset** - Select fieldset elements
37
+ # * Locator: Matches id or contents of wrapped legend
38
+ # * Filters:
39
+ # * :id (String, Regexp, XPath::Expression) — Matches id attribute
40
+ # * :legend (String) — Matches contents of wrapped legend
41
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
42
+ # * :style (String, Regexp, Hash)
43
+ #
44
+ # * **:link** - Find links ( <a> elements with an href attribute )
45
+ # * Locator: Matches the id or title attributes, or the string content of the link, or the alt attribute of a contained img element
46
+ # * Filters:
47
+ # * :id (String, Regexp, XPath::Expression) — Matches the id attribute
48
+ # * :title (String) — Matches the title attribute
49
+ # * :alt (String) — Matches the alt attribute of a contained img element
50
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
51
+ # * :href (String, Regexp, nil) — Matches the normalized href of the link, if nil will find <a> elements with no href attribute
52
+ # * :style (String, Regexp, Hash)
53
+ #
54
+ # * **:button** - Find buttons ( input [of type submit, reset, image, button] or button elements )
55
+ # * Locator: Matches the id, Capybara.test_id attribute, name, value, or title attributes, string content of a button, or the alt attribute of an image type button or of a descendant image of a button
56
+ # * Filters:
57
+ # * :id (String, Regexp, XPath::Expression) — Matches the id attribute
58
+ # * :name (String) - Matches the name attribute
59
+ # * :title (String) — Matches the title attribute
60
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
61
+ # * :value (String) — Matches the value of an input button
62
+ # * :type
63
+ # * :style (String, Regexp, Hash)
64
+ #
65
+ # * **:link_or_button** - Find links or buttons
66
+ # * Locator: See :link and :button selectors
67
+ #
68
+ # * **:fillable_field** - Find text fillable fields ( textarea, input [not of type submit, image, radio, checkbox, hidden, file] )
69
+ # * Locator: Matches against the id, Capybara.test_id attribute, name, or placeholder
70
+ # * Filters:
71
+ # * :id (String, Regexp, XPath::Expression) — Matches the id attribute
72
+ # * :name (String) — Matches the name attribute
73
+ # * :placeholder (String) — Matches the placeholder attribute
74
+ # * :with (String) — Matches the current value of the field
75
+ # * :type (String) — Matches the type attribute of the field or element type for 'textarea'
76
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
77
+ # * :disabled (Boolean) — Match disabled field?
78
+ # * :multiple (Boolean) — Match fields that accept multiple values
79
+ # * :style (String, Regexp, Hash)
80
+ #
81
+ # * **:radio_button** - Find radio buttons
82
+ # * Locator: Match id, Capybara.test_id attribute, name, or associated label text
83
+ # * Filters:
84
+ # * :id (String, Regexp, XPath::Expression) — Matches the id attribute
85
+ # * :name (String) — Matches the name attribute
86
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
87
+ # * :checked (Boolean) — Match checked fields?
88
+ # * :unchecked (Boolean) — Match unchecked fields?
89
+ # * :disabled (Boolean) — Match disabled field?
90
+ # * :option (String) — Match the value
91
+ # * :style (String, Regexp, Hash)
92
+ #
93
+ # * **:checkbox** - Find checkboxes
94
+ # * Locator: Match id, Capybara.test_id attribute, name, or associated label text
95
+ # * Filters:
96
+ # * *:id (String, Regexp, XPath::Expression) — Matches the id attribute
97
+ # * *:name (String) — Matches the name attribute
98
+ # * *:class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
99
+ # * *:checked (Boolean) — Match checked fields?
100
+ # * *:unchecked (Boolean) — Match unchecked fields?
101
+ # * *:disabled (Boolean) — Match disabled field?
102
+ # * *:option (String) — Match the value
103
+ # * :style (String, Regexp, Hash)
104
+ #
105
+ # * **:select** - Find select elements
106
+ # * Locator: Match id, Capybara.test_id attribute, name, placeholder, or associated label text
107
+ # * Filters:
108
+ # * :id (String, Regexp, XPath::Expression) — Matches the id attribute
109
+ # * :name (String) — Matches the name attribute
110
+ # * :placeholder (String) — Matches the placeholder attribute
111
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
112
+ # * :disabled (Boolean) — Match disabled field?
113
+ # * :multiple (Boolean) — Match fields that accept multiple values
114
+ # * :options (Array<String>) — Exact match options
115
+ # * :with_options (Array<String>) — Partial match options
116
+ # * :selected (String, Array<String>) — Match the selection(s)
117
+ # * :with_selected (String, Array<String>) — Partial match the selection(s)
118
+ # * :style (String, Regexp, Hash)
119
+ #
120
+ # * **:option** - Find option elements
121
+ # * Locator: Match text of option
122
+ # * Filters:
123
+ # * :disabled (Boolean) — Match disabled option
124
+ # * :selected (Boolean) — Match selected option
125
+ #
126
+ # * **:datalist_input**
127
+ # * Locator:
128
+ # * Filters:
129
+ # * :disabled
130
+ # * :name
131
+ # * :placeholder
132
+ #
133
+ # * **:datalist_option**
134
+ # * Locator:
135
+ #
136
+ # * **:file_field** - Find file input elements
137
+ # * Locator: Match id, Capybara.test_id attribute, name, or associated label text
138
+ # * Filters:
139
+ # * :id (String, Regexp, XPath::Expression) — Matches the id attribute
140
+ # * :name (String) — Matches the name attribute
141
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
142
+ # * :disabled (Boolean) — Match disabled field?
143
+ # * :multiple (Boolean) — Match field that accepts multiple values
144
+ # * :style (String, Regexp, Hash)
145
+ #
146
+ # * **:label** - Find label elements
147
+ # * Locator: Match id or text contents
148
+ # * Filters:
149
+ # * :for (Element, String) — The element or id of the element associated with the label
150
+ #
151
+ # * **:table** - Find table elements
152
+ # * Locator: id or caption text of table
153
+ # * Filters:
154
+ # * :id (String, Regexp, XPath::Expression) — Match id attribute of table
155
+ # * :caption (String) — Match text of associated caption
156
+ # * :class ((String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
157
+ # * :style (String, Regexp, Hash)
158
+ # * :with_rows (Array<Array<String>>, Array<Hash<String, String>>) - Partial match <td> data - visibility of <td> elements is not considered
159
+ # * :rows (Array<Array<String>>) — Match all <td>s - visibility of <td> elements is not considered
160
+ # * :with_cols (Array<Array<String>>, Array<Hash<String, String>>) - Partial match <td> data - visibility of <td> elements is not considered
161
+ # * :cols (Array<Array<String>>) — Match all <td>s - visibility of <td> elements is not considered
162
+ #
163
+ # * **:table_row** - Find table row
164
+ # * Locator: Array<String>, Hash<String,String> table row <td> contents - visibility of <td> elements is not considered
165
+ #
166
+ # * **:frame** - Find frame/iframe elements
167
+ # * Locator: Match id or name
168
+ # * Filters:
169
+ # * :id (String, Regexp, XPath::Expression) — Match id attribute
170
+ # * :name (String) — Match name attribute
171
+ # * :class (String, Array<String>, Regexp, XPath::Expression) — Matches the class(es) provided
172
+ # * :style (String, Regexp, Hash)
173
+ #
174
+ # * **:element**
175
+ # * Locator: Type of element ('div', 'a', etc) - if not specified defaults to '*'
176
+ # * Filters: Matches on any element attribute
177
+ class Capybara::Selector; end
178
+ #
179
+ # rubocop:enable Style/AsciiComments
5
180
 
6
181
  Capybara::Selector::FilterSet.add(:_field) do
7
182
  node_filter(:checked, :boolean) { |node, value| !(value ^ node.checked?) }
@@ -33,602 +208,24 @@ Capybara::Selector::FilterSet.add(:_field) do
33
208
  end
34
209
  end
35
210
 
36
- # rubocop:disable Metrics/BlockLength
37
-
38
- Capybara.add_selector(:xpath, locator_type: [:to_xpath, String], raw_locator: true) do
39
- xpath { |xpath| xpath }
40
- end
41
-
42
- Capybara.add_selector(:css, locator_type: [String, Symbol], raw_locator: true) do
43
- css { |css| css }
44
- end
45
-
46
- Capybara.add_selector(:id, locator_type: [String, Symbol, Regexp]) do
47
- xpath { |id| builder(XPath.descendant).add_attribute_conditions(id: id) }
48
- locator_filter { |node, id| id.is_a?(Regexp) ? id.match?(node[:id]) : true }
49
- end
50
-
51
- Capybara.add_selector(:field, locator_type: [String, Symbol]) do
52
- visible { |options| :hidden if options[:type].to_s == 'hidden' }
53
- xpath do |locator, **options|
54
- invalid_types = %w[submit image]
55
- invalid_types << 'hidden' unless options[:type].to_s == 'hidden'
56
- xpath = XPath.descendant(:input, :textarea, :select)[!XPath.attr(:type).one_of(*invalid_types)]
57
- locate_field(xpath, locator, options)
58
- end
59
-
60
- expression_filter(:type) do |expr, type|
61
- type = type.to_s
62
- if %w[textarea select].include?(type)
63
- expr.self(type.to_sym)
64
- else
65
- expr[XPath.attr(:type) == type]
66
- end
67
- end
68
-
69
- filter_set(:_field) # checked/unchecked/disabled/multiple/name/placeholder
70
-
71
- node_filter(:readonly, :boolean) { |node, value| !(value ^ node.readonly?) }
72
- node_filter(:with) do |node, with|
73
- val = node.value
74
- (with.is_a?(Regexp) ? with.match?(val) : val == with.to_s).tap do |res|
75
- add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
76
- end
77
- end
78
-
79
- describe_expression_filters do |type: nil, **|
80
- " of type #{type.inspect}" if type
81
- end
82
-
83
- describe_node_filters do |**options|
84
- " with value #{options[:with].to_s.inspect}" if options.key?(:with)
85
- end
86
- end
87
-
88
- Capybara.add_selector(:fieldset, locator_type: [String, Symbol]) do
89
- xpath do |locator, legend: nil, **|
90
- locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.child(:legend)[XPath.string.n.is(locator.to_s)]
91
- locator_matchers |= XPath.attr(test_id) == locator.to_s if test_id
92
- xpath = XPath.descendant(:fieldset)[locator && locator_matchers]
93
- xpath = xpath[XPath.child(:legend)[XPath.string.n.is(legend)]] if legend
94
- xpath
95
- end
96
-
97
- node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
98
- expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
99
- end
100
-
101
- Capybara.add_selector(:link, locator_type: [String, Symbol]) do
102
- xpath do |locator, href: true, alt: nil, title: nil, **|
103
- xpath = builder(XPath.descendant(:a)).add_attribute_conditions(href: href)
104
-
105
- unless locator.nil?
106
- locator = locator.to_s
107
- matchers = [XPath.attr(:id) == locator,
108
- XPath.string.n.is(locator),
109
- XPath.attr(:title).is(locator),
110
- XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
111
- matchers << XPath.attr(:'aria-label').is(locator) if enable_aria_label
112
- matchers << XPath.attr(test_id).equals(locator) if test_id
113
- xpath = xpath[matchers.reduce(:|)]
114
- end
115
-
116
- xpath = xpath[find_by_attr(:title, title)]
117
- xpath = xpath[XPath.descendant(:img)[XPath.attr(:alt) == alt]] if alt
118
- xpath
119
- end
120
-
121
- node_filter(:href) do |node, href|
122
- # If not a Regexp it's been handled in the main XPath
123
- (href.is_a?(Regexp) ? node[:href].match?(href) : true).tap do |res|
124
- add_error "Expected href to match #{href.inspect} but it was #{node[:href].inspect}" unless res
125
- end
126
- end
127
-
128
- expression_filter(:download, valid_values: [true, false, String]) do |expr, download|
129
- builder(expr).add_attribute_conditions(download: download)
130
- end
131
-
132
- describe_expression_filters do |download: nil, **options|
133
- desc = +''
134
- if (href = options[:href])
135
- desc << " with href #{'matching ' if href.is_a? Regexp}#{href.inspect}"
136
- elsif options.key?(:href) # is nil/false specified?
137
- desc << ' with no href attribute'
138
- end
139
- desc << " with download attribute#{" #{download}" if download.is_a? String}" if download
140
- desc << ' without download attribute' if download == false
141
- desc
142
- end
143
- end
144
-
145
- Capybara.add_selector(:button, locator_type: [String, Symbol]) do
146
- xpath(:value, :title, :type, :name) do |locator, **options|
147
- input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
148
- btn_xpath = XPath.descendant(:button)
149
- image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type) == 'image']
150
-
151
- unless locator.nil?
152
- locator = locator.to_s
153
- locator_matchers = XPath.attr(:id).equals(locator) | XPath.attr(:name).equals(locator) | XPath.attr(:value).is(locator) | XPath.attr(:title).is(locator)
154
- locator_matchers |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
155
- locator_matchers |= XPath.attr(test_id) == locator if test_id
156
-
157
- input_btn_xpath = input_btn_xpath[locator_matchers]
158
-
159
- btn_xpath = btn_xpath[locator_matchers | XPath.string.n.is(locator) | XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
160
-
161
- alt_matches = XPath.attr(:alt).is(locator)
162
- alt_matches |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
163
- image_btn_xpath = image_btn_xpath[alt_matches]
164
- end
165
-
166
- %i[value title type name].inject(input_btn_xpath.union(btn_xpath).union(image_btn_xpath)) do |memo, ef|
167
- memo[find_by_attr(ef, options[ef])]
168
- end
169
- end
170
-
171
- node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
172
- expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
173
-
174
- describe_expression_filters do |disabled: nil, **options|
175
- desc = +''
176
- desc << ' that is not disabled' if disabled == false
177
- desc << describe_all_expression_filters(options)
178
- end
179
-
180
- describe_node_filters do |disabled: nil, **|
181
- ' that is disabled' if disabled == true
182
- end
183
- end
184
-
185
- Capybara.add_selector(:link_or_button, locator_type: [String, Symbol]) do
186
- label 'link or button'
187
- xpath do |locator, **options|
188
- self.class.all.values_at(:link, :button).map do |selector|
189
- instance_exec(locator, options, &selector.xpath)
190
- end.reduce(:union)
191
- end
192
-
193
- node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
194
-
195
- describe_node_filters do |disabled: nil, **|
196
- ' that is disabled' if disabled == true
197
- end
198
- end
199
-
200
- Capybara.add_selector(:fillable_field, locator_type: [String, Symbol]) do
201
- label 'field'
202
- xpath do |locator, allow_self: nil, **options|
203
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input, :textarea)[
204
- !XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')
205
- ]
206
- locate_field(xpath, locator, options)
207
- end
208
-
209
- expression_filter(:type) do |expr, type|
210
- type = type.to_s
211
- if type == 'textarea'
212
- expr.self(type.to_sym)
213
- else
214
- expr[XPath.attr(:type) == type]
215
- end
216
- end
217
-
218
- filter_set(:_field, %i[disabled multiple name placeholder])
219
-
220
- node_filter(:with) do |node, with|
221
- val = node.value
222
- (with.is_a?(Regexp) ? with.match?(val) : val == with.to_s).tap do |res|
223
- add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
224
- end
225
- end
226
-
227
- describe_node_filters do |**options|
228
- " with value #{options[:with].to_s.inspect}" if options.key?(:with)
229
- end
230
- end
231
-
232
- Capybara.add_selector(:radio_button, locator_type: [String, Symbol]) do
233
- label 'radio button'
234
- xpath do |locator, allow_self: nil, **options|
235
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
236
- XPath.attr(:type) == 'radio'
237
- ]
238
- locate_field(xpath, locator, options)
239
- end
240
-
241
- filter_set(:_field, %i[checked unchecked disabled name])
242
-
243
- node_filter(:option) do |node, value|
244
- val = node.value
245
- (val == value.to_s).tap do |res|
246
- add_error("Expected option value to be #{value.inspect} but it was #{val.inspect}") unless res
247
- end
248
- end
249
-
250
- describe_node_filters do |option: nil, **|
251
- " with value #{option.inspect}" if option
252
- end
253
- end
254
-
255
- Capybara.add_selector(:checkbox, locator_type: [String, Symbol]) do
256
- xpath do |locator, allow_self: nil, **options|
257
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
258
- XPath.attr(:type) == 'checkbox'
259
- ]
260
- locate_field(xpath, locator, options)
261
- end
262
-
263
- filter_set(:_field, %i[checked unchecked disabled name])
264
-
265
- node_filter(:option) do |node, value|
266
- val = node.value
267
- (val == value.to_s).tap do |res|
268
- add_error("Expected option value to be #{value.inspect} but it was #{val.inspect}") unless res
269
- end
270
- end
271
-
272
- describe_node_filters do |option: nil, **|
273
- " with value #{option.inspect}" if option
274
- end
275
- end
276
-
277
- Capybara.add_selector(:select, locator_type: [String, Symbol]) do
278
- label 'select box'
279
-
280
- xpath do |locator, **options|
281
- xpath = XPath.descendant(:select)
282
- locate_field(xpath, locator, options)
283
- end
284
-
285
- filter_set(:_field, %i[disabled multiple name placeholder])
286
-
287
- node_filter(:options) do |node, options|
288
- actual = if node.visible?
289
- node.all(:xpath, './/option', wait: false).map(&:text)
290
- else
291
- node.all(:xpath, './/option', visible: false, wait: false).map { |option| option.text(:all) }
292
- end
293
- (options.sort == actual.sort).tap do |res|
294
- add_error("Expected options #{options.inspect} found #{actual.inspect}") unless res
295
- end
296
- end
297
-
298
- expression_filter(:with_options) do |expr, options|
299
- options.inject(expr) do |xpath, option|
300
- xpath[self.class.all[:option].call(option)]
301
- end
302
- end
303
-
304
- node_filter(:selected) do |node, selected|
305
- actual = node.all(:xpath, './/option', visible: false, wait: false).select(&:selected?).map { |option| option.text(:all) }
306
- (Array(selected).sort == actual.sort).tap do |res|
307
- add_error("Expected #{selected.inspect} to be selected found #{actual.inspect}") unless res
308
- end
309
- end
310
-
311
- node_filter(:with_selected) do |node, selected|
312
- actual = node.all(:xpath, './/option', visible: false, wait: false).select(&:selected?).map { |option| option.text(:all) }
313
- (Array(selected) - actual).empty?.tap do |res|
314
- add_error("Expected at least #{selected.inspect} to be selected found #{actual.inspect}") unless res
315
- end
316
- end
317
-
318
- describe_expression_filters do |with_options: nil, **|
319
- desc = +''
320
- desc << " with at least options #{with_options.inspect}" if with_options
321
- desc
322
- end
323
-
324
- describe_node_filters do |options: nil, selected: nil, with_selected: nil, disabled: nil, **|
325
- desc = +''
326
- desc << " with options #{options.inspect}" if options
327
- desc << " with #{selected.inspect} selected" if selected
328
- desc << " with at least #{with_selected.inspect} selected" if with_selected
329
- desc << ' which is disabled' if disabled
330
- desc
331
- end
332
- end
333
-
334
- Capybara.add_selector(:datalist_input, locator_type: [String, Symbol]) do
335
- label 'input box with datalist completion'
336
-
337
- xpath do |locator, **options|
338
- xpath = XPath.descendant(:input)[XPath.attr(:list)]
339
- locate_field(xpath, locator, options)
340
- end
341
-
342
- filter_set(:_field, %i[disabled name placeholder])
343
-
344
- node_filter(:options) do |node, options|
345
- actual = node.find("//datalist[@id=#{node[:list]}]", visible: :all).all(:datalist_option, wait: false).map(&:value)
346
- (options.sort == actual.sort).tap do |res|
347
- add_error("Expected #{options.inspect} options found #{actual.inspect}") unless res
348
- end
349
- end
350
-
351
- expression_filter(:with_options) do |expr, options|
352
- options.inject(expr) do |xpath, option|
353
- xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[self.class.all[:datalist_option].call(option)].attr(:id)]
354
- end
355
- end
356
-
357
- describe_expression_filters do |with_options: nil, **|
358
- desc = +''
359
- desc << " with at least options #{with_options.inspect}" if with_options
360
- desc
361
- end
362
-
363
- describe_node_filters do |options: nil, **|
364
- " with options #{options.inspect}" if options
365
- end
366
- end
367
-
368
- Capybara.add_selector(:option, locator_type: [String, Symbol]) do
369
- xpath do |locator|
370
- xpath = XPath.descendant(:option)
371
- xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil?
372
- xpath
373
- end
374
-
375
- node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
376
- expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
377
-
378
- node_filter(:selected, :boolean) { |node, value| !(value ^ node.selected?) }
379
-
380
- describe_expression_filters do |disabled: nil, **options|
381
- desc = +''
382
- desc << ' that is not disabled' if disabled == false
383
- (expression_filters.keys & options.keys).inject(desc) { |memo, ef| memo << " with #{ef} #{options[ef]}" }
384
- end
385
-
386
- describe_node_filters do |**options|
387
- desc = +''
388
- desc << ' that is disabled' if options[:disabled]
389
- desc << " that is#{' not' unless options[:selected]} selected" if options.key?(:selected)
390
- desc
391
- end
392
- end
393
-
394
- Capybara.add_selector(:datalist_option, locator_type: [String, Symbol]) do
395
- label 'datalist option'
396
- visible(:all)
397
-
398
- xpath do |locator|
399
- xpath = XPath.descendant(:option)
400
- xpath = xpath[XPath.string.n.is(locator.to_s) | (XPath.attr(:value) == locator.to_s)] unless locator.nil?
401
- xpath
402
- end
403
-
404
- node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
405
- expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
406
-
407
- describe_expression_filters do |disabled: nil, **options|
408
- desc = +''
409
- desc << ' that is not disabled' if disabled == false
410
- desc << describe_all_expression_filters(options)
411
- end
412
-
413
- describe_node_filters do |**options|
414
- ' that is disabled' if options[:disabled]
415
- end
416
- end
417
-
418
- Capybara.add_selector(:file_field, locator_type: [String, Symbol]) do
419
- label 'file field'
420
- xpath do |locator, allow_self: nil, **options|
421
- xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
422
- XPath.attr(:type) == 'file'
423
- ]
424
- locate_field(xpath, locator, options)
425
- end
426
-
427
- filter_set(:_field, %i[disabled multiple name])
428
- end
429
-
430
- Capybara.add_selector(:label, locator_type: [String, Symbol]) do
431
- label 'label'
432
- xpath(:for) do |locator, options|
433
- xpath = XPath.descendant(:label)
434
- unless locator.nil?
435
- locator_matchers = XPath.string.n.is(locator.to_s) | (XPath.attr(:id) == locator.to_s)
436
- locator_matchers |= XPath.attr(test_id) == locator if test_id
437
- xpath = xpath[locator_matchers]
438
- end
439
- if options.key?(:for)
440
- if (for_option = options[:for].is_a?(Capybara::Node::Element) ? options[:for][:id] : options[:for])
441
- with_attr = XPath.attr(:for) == for_option.to_s
442
- labelable_elements = %i[button input keygen meter output progress select textarea]
443
- wrapped = !XPath.attr(:for) &
444
- XPath.descendant(*labelable_elements)[XPath.attr(:id) == for_option.to_s]
445
- xpath = xpath[with_attr | wrapped]
446
- end
447
- end
448
- xpath
449
- end
450
-
451
- node_filter(:for) do |node, field_or_value|
452
- # Non element values were handled through the expression filter
453
- next true unless field_or_value.is_a? Capybara::Node::Element
454
-
455
- if (for_val = node[:for])
456
- field_or_value[:id] == for_val
457
- else
458
- field_or_value.find_xpath('./ancestor::label[1]').include? node.base
459
- end
460
- end
461
-
462
- describe_expression_filters do |**options|
463
- " for element with id of \"#{options[:for]}\"" if options.key?(:for) && !options[:for].is_a?(Capybara::Node::Element)
464
- end
465
- describe_node_filters do |**options|
466
- " for element #{options[:for]}" if options[:for]&.is_a?(Capybara::Node::Element)
467
- end
468
- end
469
-
470
- Capybara.add_selector(:table, locator_type: [String, Symbol]) do
471
- xpath do |locator, caption: nil, **|
472
- xpath = XPath.descendant(:table)
473
- unless locator.nil?
474
- locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.descendant(:caption).is(locator.to_s)
475
- locator_matchers |= XPath.attr(test_id) == locator if test_id
476
- xpath = xpath[locator_matchers]
477
- end
478
- xpath = xpath[XPath.descendant(:caption) == caption] if caption
479
- xpath
480
- end
481
-
482
- expression_filter(:with_cols, valid_values: [Array]) do |xpath, cols|
483
- col_conditions = cols.map do |col|
484
- if col.is_a? Hash
485
- col.reduce(nil) do |xp, (header, cell_str)|
486
- header = XPath.descendant(:th)[XPath.string.n.is(header)]
487
- td = XPath.descendant(:tr)[header].descendant(:td)
488
- cell_condition = XPath.string.n.is(cell_str)
489
- cell_condition &= prev_col_position?(XPath.ancestor(:table)[1].join(xp)) if xp
490
- td[cell_condition]
491
- end
492
- else
493
- cells_xp = col.reduce(nil) do |prev_cell, cell_str|
494
- cell_condition = XPath.string.n.is(cell_str)
495
-
496
- if prev_cell
497
- prev_cell = XPath.ancestor(:tr)[1].preceding_sibling(:tr).join(prev_cell)
498
- cell_condition &= prev_col_position?(prev_cell)
499
- end
500
-
501
- XPath.descendant(:td)[cell_condition]
502
- end
503
- XPath.descendant(:tr).join(cells_xp)
504
- end
505
- end.reduce(:&)
506
- xpath[col_conditions]
507
- end
508
-
509
- expression_filter(:cols, valid_values: [Array]) do |xpath, cols|
510
- raise ArgumentError, ':cols must be an Array of Arrays' unless cols.all? { |col| col.is_a? Array }
511
-
512
- rows = cols.transpose
513
- col_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&)
514
- xpath[match_row_count(rows.size)][col_conditions]
515
- end
516
-
517
- expression_filter(:with_rows, valid_values: [Array]) do |xpath, rows|
518
- rows_conditions = rows.map { |row| match_row(row) }.reduce(:&)
519
- xpath[rows_conditions]
520
- end
521
-
522
- expression_filter(:rows, valid_values: [Array]) do |xpath, rows|
523
- rows_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&)
524
- xpath[match_row_count(rows.size)][rows_conditions]
525
- end
526
-
527
- describe_expression_filters do |caption: nil, **|
528
- " with caption \"#{caption}\"" if caption
529
- end
530
-
531
- def prev_col_position?(cell)
532
- XPath.position.equals(cell_position(cell))
533
- end
534
-
535
- def cell_position(cell)
536
- cell.preceding_sibling(:td).count.plus(1)
537
- end
538
-
539
- def match_row(row, match_size: false)
540
- xp = XPath.descendant(:tr)[
541
- if row.is_a? Hash
542
- row_match_cells_to_headers(row)
543
- else
544
- XPath.descendant(:td)[row_match_ordered_cells(row)]
545
- end
546
- ]
547
- xp = xp[XPath.descendant(:td).count.equals(row.size)] if match_size
548
- xp
549
- end
550
-
551
- def match_row_count(size)
552
- XPath.descendant(:tbody).descendant(:tr).count.equals(size) | (XPath.descendant(:tr).count.equals(size) & ~XPath.descendant(:tbody))
553
- end
554
-
555
- def row_match_cells_to_headers(row)
556
- row.map do |header, cell|
557
- header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
558
- XPath.descendant(:td)[
559
- XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
560
- ]
561
- end.reduce(:&)
562
- end
563
-
564
- def row_match_ordered_cells(row)
565
- row_conditions = row.map do |cell|
566
- XPath.self(:td)[XPath.string.n.is(cell)]
567
- end
568
- row_conditions.reverse.reduce do |cond, cell|
569
- cell[XPath.following_sibling[cond]]
570
- end
571
- end
572
- end
573
-
574
- Capybara.add_selector(:table_row, locator_type: [Array, Hash]) do
575
- xpath do |locator|
576
- xpath = XPath.descendant(:tr)
577
- if locator.is_a? Hash
578
- locator.reduce(xpath) do |xp, (header, cell)|
579
- header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
580
- cell_xp = XPath.descendant(:td)[
581
- XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
582
- ]
583
- xp[cell_xp]
584
- end
585
- else
586
- initial_td = XPath.descendant(:td)[XPath.string.n.is(locator.shift)]
587
- tds = locator.reverse.map { |cell| XPath.following_sibling(:td)[XPath.string.n.is(cell)] }.reduce { |xp, cell| xp[cell] }
588
- xpath[initial_td[tds]]
589
- end
590
- end
591
- end
592
-
593
- Capybara.add_selector(:frame, locator_type: [String, Symbol]) do
594
- xpath do |locator, name: nil, **|
595
- xpath = XPath.descendant(:iframe).union(XPath.descendant(:frame))
596
- unless locator.nil?
597
- locator_matchers = (XPath.attr(:id) == locator.to_s) | (XPath.attr(:name) == locator.to_s)
598
- locator_matchers |= XPath.attr(test_id) == locator if test_id
599
- xpath = xpath[locator_matchers]
600
- end
601
- xpath[find_by_attr(:name, name)]
602
- end
603
-
604
- describe_expression_filters do |name: nil, **|
605
- " with name #{name}" if name
606
- end
607
- end
608
-
609
- Capybara.add_selector(:element, locator_type: [String, Symbol]) do
610
- xpath do |locator, **|
611
- XPath.descendant.where(locator ? XPath.local_name == locator.to_s : nil)
612
- end
613
-
614
- expression_filter(:attributes, matcher: /.+/) do |xpath, name, val|
615
- builder(xpath).add_attribute_conditions(name => val)
616
- end
617
-
618
- node_filter(:attributes, matcher: /.+/) do |node, name, val|
619
- next true unless val.is_a?(Regexp)
620
-
621
- (val.match? node[name]).tap do |res|
622
- add_error("Expected #{name} to match #{val.inspect} but it was #{node[name]}") unless res
623
- end
624
- end
625
-
626
- describe_expression_filters do |**options|
627
- booleans, values = options.partition { |_k, v| [true, false].include? v }.map(&:to_h)
628
- desc = describe_all_expression_filters(values)
629
- desc + booleans.map do |k, v|
630
- v ? " with #{k} attribute" : "without #{k} attribute"
631
- end.join
632
- end
633
- end
634
- # rubocop:enable Metrics/BlockLength
211
+ require 'capybara/selector/definition/xpath'
212
+ require 'capybara/selector/definition/css'
213
+ require 'capybara/selector/definition/id'
214
+ require 'capybara/selector/definition/field'
215
+ require 'capybara/selector/definition/fieldset'
216
+ require 'capybara/selector/definition/link'
217
+ require 'capybara/selector/definition/button'
218
+ require 'capybara/selector/definition/link_or_button'
219
+ require 'capybara/selector/definition/fillable_field'
220
+ require 'capybara/selector/definition/radio_button'
221
+ require 'capybara/selector/definition/checkbox'
222
+ require 'capybara/selector/definition/select'
223
+ require 'capybara/selector/definition/datalist_input'
224
+ require 'capybara/selector/definition/option'
225
+ require 'capybara/selector/definition/datalist_option'
226
+ require 'capybara/selector/definition/file_field'
227
+ require 'capybara/selector/definition/label'
228
+ require 'capybara/selector/definition/table'
229
+ require 'capybara/selector/definition/table_row'
230
+ require 'capybara/selector/definition/frame'
231
+ require 'capybara/selector/definition/element'