capybara 3.18.0 → 3.19.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![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.
|
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'
|