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.
- checksums.yaml +4 -4
- data/History.md +16 -0
- data/README.md +14 -44
- data/lib/capybara/node/actions.rb +2 -2
- data/lib/capybara/node/element.rb +3 -5
- data/lib/capybara/queries/selector_query.rb +30 -11
- data/lib/capybara/rack_test/node.rb +1 -1
- data/lib/capybara/result.rb +2 -0
- data/lib/capybara/rspec/matcher_proxies.rb +2 -0
- data/lib/capybara/rspec/matchers/base.rb +2 -2
- data/lib/capybara/rspec/matchers/count_sugar.rb +36 -0
- data/lib/capybara/rspec/matchers/have_selector.rb +3 -0
- data/lib/capybara/rspec/matchers/have_text.rb +3 -0
- data/lib/capybara/selector.rb +196 -599
- data/lib/capybara/selector/css.rb +2 -0
- data/lib/capybara/selector/definition.rb +276 -0
- data/lib/capybara/selector/definition/button.rb +46 -0
- data/lib/capybara/selector/definition/checkbox.rb +23 -0
- data/lib/capybara/selector/definition/css.rb +5 -0
- data/lib/capybara/selector/definition/datalist_input.rb +35 -0
- data/lib/capybara/selector/definition/datalist_option.rb +25 -0
- data/lib/capybara/selector/definition/element.rb +27 -0
- data/lib/capybara/selector/definition/field.rb +40 -0
- data/lib/capybara/selector/definition/fieldset.rb +14 -0
- data/lib/capybara/selector/definition/file_field.rb +13 -0
- data/lib/capybara/selector/definition/fillable_field.rb +33 -0
- data/lib/capybara/selector/definition/frame.rb +17 -0
- data/lib/capybara/selector/definition/id.rb +6 -0
- data/lib/capybara/selector/definition/label.rb +43 -0
- data/lib/capybara/selector/definition/link.rb +45 -0
- data/lib/capybara/selector/definition/link_or_button.rb +16 -0
- data/lib/capybara/selector/definition/option.rb +27 -0
- data/lib/capybara/selector/definition/radio_button.rb +24 -0
- data/lib/capybara/selector/definition/select.rb +62 -0
- data/lib/capybara/selector/definition/table.rb +106 -0
- data/lib/capybara/selector/definition/table_row.rb +21 -0
- data/lib/capybara/selector/definition/xpath.rb +5 -0
- data/lib/capybara/selector/filters/base.rb +4 -0
- data/lib/capybara/selector/filters/locator_filter.rb +12 -2
- data/lib/capybara/selector/selector.rb +40 -452
- data/lib/capybara/selenium/driver.rb +4 -10
- data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +3 -9
- data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +8 -0
- data/lib/capybara/selenium/extensions/find.rb +1 -1
- data/lib/capybara/selenium/logger_suppressor.rb +5 -0
- data/lib/capybara/selenium/node.rb +19 -13
- data/lib/capybara/selenium/nodes/chrome_node.rb +30 -0
- data/lib/capybara/selenium/nodes/firefox_node.rb +14 -12
- data/lib/capybara/selenium/nodes/ie_node.rb +11 -0
- data/lib/capybara/selenium/nodes/safari_node.rb +7 -12
- data/lib/capybara/server/checker.rb +7 -3
- data/lib/capybara/session.rb +2 -2
- data/lib/capybara/spec/session/all_spec.rb +1 -1
- data/lib/capybara/spec/session/find_spec.rb +1 -1
- data/lib/capybara/spec/session/first_spec.rb +1 -1
- data/lib/capybara/spec/session/has_css_spec.rb +7 -0
- data/lib/capybara/spec/session/has_text_spec.rb +6 -0
- data/lib/capybara/spec/session/save_screenshot_spec.rb +11 -0
- data/lib/capybara/spec/session/select_spec.rb +0 -5
- data/lib/capybara/spec/test_app.rb +8 -3
- data/lib/capybara/version.rb +1 -1
- data/lib/capybara/window.rb +1 -1
- data/spec/minitest_spec_spec.rb +1 -0
- data/spec/selector_spec.rb +12 -6
- data/spec/selenium_spec_firefox.rb +0 -3
- data/spec/selenium_spec_firefox_remote.rb +0 -3
- data/spec/selenium_spec_ie.rb +3 -1
- data/spec/server_spec.rb +1 -1
- data/spec/shared_selenium_session.rb +1 -1
- data/spec/spec_helper.rb +9 -2
- metadata +54 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cdee23fb17ec93c0e1c5e8e9b61607f11211e065ff94a99084dc28e3ff0cc24c
|
4
|
+
data.tar.gz: 428519d0593d850d7c355e7300ed0805637dbbd498805b70452dcb9521a78a04
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
[](https://travis-ci.org/teamcapybara/capybara)
|
4
4
|
[](https://ci.appveyor.com/api/projects/github/teamcapybara/capybara)
|
5
5
|
[](https://codeclimate.com/github/teamcapybara/capybara)
|
6
|
+
[](https://coveralls.io/github/teamcapybara/capybara?branch=master)
|
6
7
|
[](https://gitter.im/jnicklas/capybara?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
7
8
|
[](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.
|
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: :
|
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
|
-
|
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
|
345
|
-
leaving the faster `:rack_test` as the __default_driver__, and
|
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 = :
|
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)
|
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
|
416
|
-
|
417
|
-
|
418
|
-
teamcapybara repo once
|
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 #
|
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
|
-
|
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
|
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 =
|
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
|
-
|
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 : []) #
|
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
|
206
|
+
hints[:texts] = text_fragments unless selector_format == :xpath
|
196
207
|
hints[:styles] = options[:style] if use_default_style_filter?
|
197
208
|
|
198
|
-
if
|
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
|
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: #{
|
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
|
121
|
+
private "unchecked_#{meth_name}" # rubocop:disable Style/AccessModifierDeclarations
|
122
122
|
|
123
123
|
define_method meth_name do |*args|
|
124
124
|
stale_check
|
data/lib/capybara/result.rb
CHANGED
@@ -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 =>
|
52
|
-
@failure_message_when_negated =
|
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
|
data/lib/capybara/selector.rb
CHANGED
@@ -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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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'
|