rubocop-capybara 2.17.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 +7 -0
- data/CHANGELOG.md +49 -0
- data/CODE_OF_CONDUCT.md +17 -0
- data/MIT-LICENSE.md +21 -0
- data/README.md +88 -0
- data/config/default.yml +56 -0
- data/lib/rubocop/capybara/config_formatter.rb +56 -0
- data/lib/rubocop/capybara/description_extractor.rb +70 -0
- data/lib/rubocop/capybara/version.rb +10 -0
- data/lib/rubocop/cop/capybara/current_path_expectation.rb +123 -0
- data/lib/rubocop/cop/capybara/match_style.rb +58 -0
- data/lib/rubocop/cop/capybara/mixin/capybara_help.rb +78 -0
- data/lib/rubocop/cop/capybara/mixin/css_selector.rb +144 -0
- data/lib/rubocop/cop/capybara/negation_matcher.rb +104 -0
- data/lib/rubocop/cop/capybara/specific_actions.rb +83 -0
- data/lib/rubocop/cop/capybara/specific_finders.rb +91 -0
- data/lib/rubocop/cop/capybara/specific_matcher.rb +77 -0
- data/lib/rubocop/cop/capybara/visibility_matcher.rb +71 -0
- data/lib/rubocop/cop/capybara_cops.rb +9 -0
- data/lib/rubocop-capybara.rb +24 -0
- metadata +82 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a9046dc7fd875441eeb556b848c7fc36099c2ab694b12a6cf5834ff191937a16
|
4
|
+
data.tar.gz: 354393ab09a0849d1c4c7398116827627c5b385e377f5337a135d3d5ff338b08
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7fab06dac5d6ba1989c2c0bf47cd0a674e067baf7265378b8d4493055a1597087a442b79d88b931eadaa0f4ebd5c78f5d1929a116b12afa0d9c20110506651cd
|
7
|
+
data.tar.gz: fd8f7cfebf049c450b25e20b76d76fba8ddbb1a50d626644e740e042589cdd2b1d406ea410df1d558439fc2d66a2b37733ce4ebd409f7f83568cd96dea27c61a
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## Edge (Unreleased)
|
4
|
+
|
5
|
+
## 2.17.0 (TBD)
|
6
|
+
|
7
|
+
- Extracted from `rubocop-rspec` into a separate repository for easier use with Minitest/Cucumber. ([@pirj])
|
8
|
+
|
9
|
+
## Previously (see rubocop-rspec's changelist for details)
|
10
|
+
|
11
|
+
- Fix a false positive for `Capybara/SpecificMatcher` when `have_css("a")` without attribute. ([@ydah])
|
12
|
+
- Add new `Capybara/NegationMatcher` cop. ([@ydah])
|
13
|
+
- Add new `Capybara/SpecificActions` cop. ([@ydah])
|
14
|
+
- Fix an error for `Capybara/SpecificFinders` with no parentheses. ([@ydah])
|
15
|
+
- Exclude `have_text` and `have_content` that raise `ArgumentError` with `Capybara/VisibilityMatcher` where `:visible` is an invalid option. ([@ydah])
|
16
|
+
- Fix a false negative for `Capybara/VisibilityMatcher` with negative matchers. ([@ydah])
|
17
|
+
- Fix a false positive for `Capybara/SpecificMatcher`. ([@ydah])
|
18
|
+
- Fix a false negative for `Capybara/SpecificMatcher` for `have_field`. ([@ydah])
|
19
|
+
- Fix a false positive for `Capybara/SpecificMatcher` when may not have a `href` by `have_link`. ([@ydah])
|
20
|
+
- Add new `Capybara/SpecificFinders` cop. ([@ydah])
|
21
|
+
- Fix a false positive for `Capybara/SpecificMatcher` when pseudo-classes. ([@ydah])
|
22
|
+
- Fix a false positive for `Capybara/SpecificMatcher`. ([@ydah])
|
23
|
+
- Add new `Capybara/SpecificMatcher` cop. ([@ydah])
|
24
|
+
- Fix `Capybara/CurrentPathExpectation` autocorrect incompatible with `Style/TrailingCommaInArguments` autocorrect. ([@ydah])
|
25
|
+
- Fix `FactoryBot/SyntaxMethods` and `Capybara/FeatureMethods` to inspect shared groups. ([@pirj])
|
26
|
+
- Change namespace of several cops (`Capybara/*` -> `RSpec/Capybara/*`, `FactoryBot/*` -> `RSpec/FactoryBot/*`, `Rails/*` -> `RSpec/Rails/*`). ([@pirj], [@bquorning])
|
27
|
+
- Expand `Capybara/VisibilityMatcher` to support more than just `have_selector`. ([@twalpole])
|
28
|
+
- Add new `Capybara/VisibilityMatcher` cop. ([@aried3r])
|
29
|
+
- Fix `Capybara/CurrentPathExpectation` auto-corrector, to include option `ignore_query: true`. ([@onumis])
|
30
|
+
- Add autocorrect support for `Capybara/CurrentPathExpectation` cop. ([@ypresto])
|
31
|
+
- Fix `Capybara/FeatureMethods` not working when there is require before the spec. ([@Darhazer])
|
32
|
+
- Fix false positives in `Capybara/FeatureMethods` when feature methods are used as property names in a factory. ([@Darhazer])
|
33
|
+
- Allow configuring enabled methods in `Capybara/FeatureMethods`. ([@Darhazer])
|
34
|
+
- Fix false positive in `Capybara/FeatureMethods`. ([@Darhazer])
|
35
|
+
- Add `Capybara/CurrentPathExpectation` cop for feature specs, disallowing setting expectations on `current_path`. ([@timrogers])
|
36
|
+
- Add `RSpec/Capybara` namespace including the first cop for feature specs: `Capybara/FeatureMethods`. ([@rspeicher])
|
37
|
+
|
38
|
+
<!-- Contributors (alphabetically) -->
|
39
|
+
|
40
|
+
[@aried3r]: https://github.com/aried3r
|
41
|
+
[@bquorning]: https://github.com/bquorning
|
42
|
+
[@darhazer]: https://github.com/Darhazer
|
43
|
+
[@onumis]: https://github.com/onumis
|
44
|
+
[@pirj]: https://github.com/pirj
|
45
|
+
[@rspeicher]: https://github.com/rspeicher
|
46
|
+
[@timrogers]: https://github.com/timrogers
|
47
|
+
[@twalpole]: https://github.com/twalpole
|
48
|
+
[@ydah]: https://github.com/ydah
|
49
|
+
[@ypresto]: https://github.com/ypresto
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# The RuboCop Community Code of Conduct
|
2
|
+
|
3
|
+
**Note:** We have picked the following code of conduct based on [Ruby's own
|
4
|
+
code of conduct](https://www.ruby-lang.org/en/conduct/).
|
5
|
+
|
6
|
+
This document provides a few simple community guidelines for a safe, respectful,
|
7
|
+
productive, and collaborative place for any person who is willing to contribute
|
8
|
+
to the RuboCop community. It applies to all "collaborative spaces", which are
|
9
|
+
defined as community communications channels (such as mailing lists, submitted
|
10
|
+
patches, commit comments, etc.).
|
11
|
+
|
12
|
+
- Participants will be tolerant of opposing views.
|
13
|
+
- Participants must ensure that their language and actions are free of personal
|
14
|
+
attacks and disparaging personal remarks.
|
15
|
+
- When interpreting the words and actions of others, participants should always
|
16
|
+
assume good intentions.
|
17
|
+
- Behaviour which can be reasonably considered harassment will not be tolerated.
|
data/MIT-LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 Ian MacLeod <ian@nevir.net>
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
9
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
10
|
+
so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# RuboCop Capybara
|
2
|
+
|
3
|
+
[](https://gitter.im/rubocop-rspec/Lobby)
|
4
|
+
[](https://rubygems.org/gems/rubocop-capybara)
|
5
|
+

|
6
|
+
|
7
|
+
Capybara-specific analysis for your projects, as an extension to
|
8
|
+
[RuboCop](https://github.com/rubocop/rubocop).
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Just install the `rubocop-capybara` gem
|
13
|
+
|
14
|
+
```bash
|
15
|
+
gem install rubocop-capybara
|
16
|
+
```
|
17
|
+
|
18
|
+
or if you use bundler put this in your `Gemfile`
|
19
|
+
|
20
|
+
```
|
21
|
+
gem 'rubocop-capybara', require: false
|
22
|
+
```
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
You need to tell RuboCop to load the Capybara extension. There are three
|
27
|
+
ways to do this:
|
28
|
+
|
29
|
+
### RuboCop configuration file
|
30
|
+
|
31
|
+
Put this into your `.rubocop.yml`.
|
32
|
+
|
33
|
+
```yaml
|
34
|
+
require: rubocop-capybara
|
35
|
+
```
|
36
|
+
|
37
|
+
Alternatively, use the following array notation when specifying multiple extensions.
|
38
|
+
|
39
|
+
```yaml
|
40
|
+
require:
|
41
|
+
- rubocop-other-extension
|
42
|
+
- rubocop-capybara
|
43
|
+
```
|
44
|
+
|
45
|
+
Now you can run `rubocop` and it will automatically load the RuboCop Capybara
|
46
|
+
cops together with the standard cops.
|
47
|
+
|
48
|
+
### Command line
|
49
|
+
|
50
|
+
```bash
|
51
|
+
rubocop --require rubocop-capybara
|
52
|
+
```
|
53
|
+
|
54
|
+
### Rake task
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
RuboCop::RakeTask.new do |task|
|
58
|
+
task.requires << 'rubocop-capybara'
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
## Documentation
|
63
|
+
|
64
|
+
You can read more about RuboCop Capybara in its [official manual](https://docs.rubocop.org/rubocop-capybara).
|
65
|
+
|
66
|
+
## The Cops
|
67
|
+
|
68
|
+
All cops are located under
|
69
|
+
[`lib/rubocop/cop/capybara`](lib/rubocop/cop/capybara), and contain
|
70
|
+
examples/documentation.
|
71
|
+
|
72
|
+
In your `.rubocop.yml`, you may treat the Capybara cops just like any other
|
73
|
+
cop. For example:
|
74
|
+
|
75
|
+
```yaml
|
76
|
+
Capybara/SpecificMatcher:
|
77
|
+
Exclude:
|
78
|
+
- spec/my_spec.rb
|
79
|
+
```
|
80
|
+
|
81
|
+
## Contributing
|
82
|
+
|
83
|
+
Checkout the [contribution guidelines](.github/CONTRIBUTING.md).
|
84
|
+
|
85
|
+
## License
|
86
|
+
|
87
|
+
`rubocop-capybara` is MIT licensed. [See the accompanying file](MIT-LICENSE.md) for
|
88
|
+
the full text.
|
data/config/default.yml
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
---
|
2
|
+
Capybara:
|
3
|
+
Enabled: true
|
4
|
+
DocumentationBaseURL: https://docs.rubocop.org/rubocop-capybara
|
5
|
+
Include:
|
6
|
+
- "**/*_spec.rb"
|
7
|
+
- "**/spec/**/*"
|
8
|
+
- "**/test/**/*"
|
9
|
+
|
10
|
+
Capybara/CurrentPathExpectation:
|
11
|
+
Description: Checks that no expectations are set on Capybara's `current_path`.
|
12
|
+
Enabled: true
|
13
|
+
VersionAdded: '1.18'
|
14
|
+
VersionChanged: '2.0'
|
15
|
+
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/CurrentPathExpectation
|
16
|
+
|
17
|
+
Capybara/MatchStyle:
|
18
|
+
Description: Checks for usage of deprecated style methods.
|
19
|
+
Enabled: pending
|
20
|
+
VersionAdded: "<<next>>"
|
21
|
+
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/MatchStyle
|
22
|
+
|
23
|
+
Capybara/NegationMatcher:
|
24
|
+
Description: Enforces use of `have_no_*` or `not_to` for negated expectations.
|
25
|
+
Enabled: pending
|
26
|
+
VersionAdded: '2.14'
|
27
|
+
EnforcedStyle: not_to
|
28
|
+
SupportedStyles:
|
29
|
+
- have_no
|
30
|
+
- not_to
|
31
|
+
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/NegationMatcher
|
32
|
+
|
33
|
+
Capybara/SpecificActions:
|
34
|
+
Description: Checks for there is a more specific actions offered by Capybara.
|
35
|
+
Enabled: pending
|
36
|
+
VersionAdded: '2.14'
|
37
|
+
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/SpecificActions
|
38
|
+
|
39
|
+
Capybara/SpecificFinders:
|
40
|
+
Description: Checks if there is a more specific finder offered by Capybara.
|
41
|
+
Enabled: pending
|
42
|
+
VersionAdded: '2.13'
|
43
|
+
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/SpecificFinders
|
44
|
+
|
45
|
+
Capybara/SpecificMatcher:
|
46
|
+
Description: Checks for there is a more specific matcher offered by Capybara.
|
47
|
+
Enabled: pending
|
48
|
+
VersionAdded: '2.12'
|
49
|
+
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/SpecificMatcher
|
50
|
+
|
51
|
+
Capybara/VisibilityMatcher:
|
52
|
+
Description: Checks for boolean visibility in Capybara finders.
|
53
|
+
Enabled: true
|
54
|
+
VersionAdded: '1.39'
|
55
|
+
VersionChanged: '2.0'
|
56
|
+
Reference: https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/Capybara/VisibilityMatcher
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module RuboCop
|
6
|
+
module Capybara
|
7
|
+
# Builds a YAML config file from two config hashes
|
8
|
+
class ConfigFormatter
|
9
|
+
EXTENSION_ROOT_DEPARTMENT = %r{^(Capybara/)}.freeze
|
10
|
+
SUBDEPARTMENTS = [].freeze
|
11
|
+
AMENDMENTS = [].freeze
|
12
|
+
COP_DOC_BASE_URL = 'https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/'
|
13
|
+
|
14
|
+
def initialize(config, descriptions)
|
15
|
+
@config = config
|
16
|
+
@descriptions = descriptions
|
17
|
+
end
|
18
|
+
|
19
|
+
def dump
|
20
|
+
YAML.dump(unified_config)
|
21
|
+
.gsub(EXTENSION_ROOT_DEPARTMENT, "\n\\1")
|
22
|
+
.gsub(/^(\s+)- /, '\1 - ')
|
23
|
+
.gsub('"~"', '~')
|
24
|
+
# .gsub(*AMENDMENTS, "\n\\0")
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def unified_config
|
30
|
+
cops.each_with_object(config.dup) do |cop, unified|
|
31
|
+
next if SUBDEPARTMENTS.include?(cop) || AMENDMENTS.include?(cop)
|
32
|
+
|
33
|
+
replace_nil(unified[cop])
|
34
|
+
unified[cop].merge!(descriptions.fetch(cop))
|
35
|
+
unified[cop]['Reference'] = reference(cop)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def cops
|
40
|
+
(descriptions.keys | config.keys).grep(EXTENSION_ROOT_DEPARTMENT)
|
41
|
+
end
|
42
|
+
|
43
|
+
def replace_nil(config)
|
44
|
+
config.each do |key, value|
|
45
|
+
config[key] = '~' if value.nil?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def reference(cop)
|
50
|
+
COP_DOC_BASE_URL + cop
|
51
|
+
end
|
52
|
+
|
53
|
+
attr_reader :config, :descriptions
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Capybara
|
5
|
+
# Extracts cop descriptions from YARD docstrings
|
6
|
+
class DescriptionExtractor
|
7
|
+
def initialize(yardocs)
|
8
|
+
@code_objects = yardocs.map(&CodeObject.public_method(:new))
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_h
|
12
|
+
code_objects
|
13
|
+
.select(&:cop?)
|
14
|
+
.map(&:configuration)
|
15
|
+
.reduce(:merge)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :code_objects
|
21
|
+
|
22
|
+
# Decorator of a YARD code object for working with documented cops
|
23
|
+
class CodeObject
|
24
|
+
RUBOCOP_COP_CLASS_NAME = 'RuboCop::Cop::Base'
|
25
|
+
|
26
|
+
def initialize(yardoc)
|
27
|
+
@yardoc = yardoc
|
28
|
+
end
|
29
|
+
|
30
|
+
# Test if the YARD code object documents a concrete cop class
|
31
|
+
#
|
32
|
+
# @return [Boolean]
|
33
|
+
def cop?
|
34
|
+
cop_subclass? && !abstract?
|
35
|
+
end
|
36
|
+
|
37
|
+
# Configuration for the documented cop that would live in default.yml
|
38
|
+
#
|
39
|
+
# @return [Hash]
|
40
|
+
def configuration
|
41
|
+
{ cop_name => { 'Description' => description } }
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def cop_name
|
47
|
+
Object.const_get(documented_constant).cop_name
|
48
|
+
end
|
49
|
+
|
50
|
+
def description
|
51
|
+
yardoc.docstring.split("\n\n").first.to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
def documented_constant
|
55
|
+
yardoc.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
def cop_subclass?
|
59
|
+
yardoc.superclass.path == RUBOCOP_COP_CLASS_NAME
|
60
|
+
end
|
61
|
+
|
62
|
+
def abstract?
|
63
|
+
yardoc.tags.any? { |tag| tag.tag_name.eql?('abstract') }
|
64
|
+
end
|
65
|
+
|
66
|
+
attr_reader :yardoc
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Capybara
|
6
|
+
# Checks that no expectations are set on Capybara's `current_path`.
|
7
|
+
#
|
8
|
+
# The
|
9
|
+
# https://www.rubydoc.info/github/teamcapybara/capybara/main/Capybara/RSpecMatchers#have_current_path-instance_method[`have_current_path` matcher]
|
10
|
+
# should be used on `page` to set expectations on Capybara's
|
11
|
+
# current path, since it uses
|
12
|
+
# https://github.com/teamcapybara/capybara/blob/main/README.md#asynchronous-javascript-ajax-and-friends[Capybara's waiting functionality]
|
13
|
+
# which ensures that preceding actions (like `click_link`) have
|
14
|
+
# completed.
|
15
|
+
#
|
16
|
+
# This cop does not support autocorrection in some cases.
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# # bad
|
20
|
+
# expect(current_path).to eq('/callback')
|
21
|
+
#
|
22
|
+
# # good
|
23
|
+
# expect(page).to have_current_path('/callback')
|
24
|
+
#
|
25
|
+
# # bad (does not support autocorrection)
|
26
|
+
# expect(page.current_path).to match(variable)
|
27
|
+
#
|
28
|
+
# # good
|
29
|
+
# expect(page).to have_current_path('/callback')
|
30
|
+
#
|
31
|
+
class CurrentPathExpectation < ::RuboCop::Cop::Base
|
32
|
+
extend AutoCorrector
|
33
|
+
|
34
|
+
MSG = 'Do not set an RSpec expectation on `current_path` in ' \
|
35
|
+
'Capybara feature specs - instead, use the ' \
|
36
|
+
'`have_current_path` matcher on `page`'
|
37
|
+
|
38
|
+
RESTRICT_ON_SEND = %i[expect].freeze
|
39
|
+
|
40
|
+
# @!method expectation_set_on_current_path(node)
|
41
|
+
def_node_matcher :expectation_set_on_current_path, <<-PATTERN
|
42
|
+
(send nil? :expect (send {(send nil? :page) nil?} :current_path))
|
43
|
+
PATTERN
|
44
|
+
|
45
|
+
# Supported matchers: eq(...) / match(/regexp/) / match('regexp')
|
46
|
+
# @!method as_is_matcher(node)
|
47
|
+
def_node_matcher :as_is_matcher, <<-PATTERN
|
48
|
+
(send
|
49
|
+
#expectation_set_on_current_path ${:to :to_not :not_to}
|
50
|
+
${(send nil? :eq ...) (send nil? :match (regexp ...))})
|
51
|
+
PATTERN
|
52
|
+
|
53
|
+
# @!method regexp_str_matcher(node)
|
54
|
+
def_node_matcher :regexp_str_matcher, <<-PATTERN
|
55
|
+
(send
|
56
|
+
#expectation_set_on_current_path ${:to :to_not :not_to}
|
57
|
+
$(send nil? :match (str $_)))
|
58
|
+
PATTERN
|
59
|
+
|
60
|
+
def self.autocorrect_incompatible_with
|
61
|
+
[Style::TrailingCommaInArguments]
|
62
|
+
end
|
63
|
+
|
64
|
+
def on_send(node)
|
65
|
+
expectation_set_on_current_path(node) do
|
66
|
+
add_offense(node.loc.selector) do |corrector|
|
67
|
+
next unless node.chained?
|
68
|
+
|
69
|
+
autocorrect(corrector, node)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def autocorrect(corrector, node)
|
77
|
+
as_is_matcher(node.parent) do |to_sym, matcher_node|
|
78
|
+
rewrite_expectation(corrector, node, to_sym, matcher_node)
|
79
|
+
end
|
80
|
+
|
81
|
+
regexp_str_matcher(node.parent) do |to_sym, matcher_node, regexp|
|
82
|
+
rewrite_expectation(corrector, node, to_sym, matcher_node)
|
83
|
+
convert_regexp_str_to_literal(corrector, matcher_node, regexp)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def rewrite_expectation(corrector, node, to_symbol, matcher_node)
|
88
|
+
current_path_node = node.first_argument
|
89
|
+
corrector.replace(current_path_node, 'page')
|
90
|
+
corrector.replace(node.parent.loc.selector, 'to')
|
91
|
+
matcher_method = if to_symbol == :to
|
92
|
+
'have_current_path'
|
93
|
+
else
|
94
|
+
'have_no_current_path'
|
95
|
+
end
|
96
|
+
corrector.replace(matcher_node.loc.selector, matcher_method)
|
97
|
+
add_ignore_query_options(corrector, node)
|
98
|
+
end
|
99
|
+
|
100
|
+
def convert_regexp_str_to_literal(corrector, matcher_node, regexp_str)
|
101
|
+
str_node = matcher_node.first_argument
|
102
|
+
regexp_expr = Regexp.new(regexp_str).inspect
|
103
|
+
corrector.replace(str_node, regexp_expr)
|
104
|
+
end
|
105
|
+
|
106
|
+
# `have_current_path` with no options will include the querystring
|
107
|
+
# while `page.current_path` does not.
|
108
|
+
# This ensures the option `ignore_query: true` is added
|
109
|
+
# except when the expectation is a regexp or string
|
110
|
+
def add_ignore_query_options(corrector, node)
|
111
|
+
expectation_node = node.parent.last_argument
|
112
|
+
expectation_last_child = expectation_node.children.last
|
113
|
+
return if %i[regexp str].include?(expectation_last_child.type)
|
114
|
+
|
115
|
+
corrector.insert_after(
|
116
|
+
expectation_last_child,
|
117
|
+
', ignore_query: true'
|
118
|
+
)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Capybara
|
6
|
+
# Checks for usage of deprecated style methods.
|
7
|
+
#
|
8
|
+
# @example when using `assert_style`
|
9
|
+
# # bad
|
10
|
+
# page.find(:css, '#first').assert_style(display: 'block')
|
11
|
+
#
|
12
|
+
# # good
|
13
|
+
# page.find(:css, '#first').assert_matches_style(display: 'block')
|
14
|
+
#
|
15
|
+
# @example when using `has_style?`
|
16
|
+
# # bad
|
17
|
+
# expect(page.find(:css, 'first')
|
18
|
+
# .has_style?(display: 'block')).to be true
|
19
|
+
#
|
20
|
+
# # good
|
21
|
+
# expect(page.find(:css, 'first')
|
22
|
+
# .matches_style?(display: 'block')).to be true
|
23
|
+
#
|
24
|
+
# @example when using `have_style`
|
25
|
+
# # bad
|
26
|
+
# expect(page).to have_style(display: 'block')
|
27
|
+
#
|
28
|
+
# # good
|
29
|
+
# expect(page).to match_style(display: 'block')
|
30
|
+
#
|
31
|
+
class MatchStyle < ::RuboCop::Cop::Base
|
32
|
+
extend AutoCorrector
|
33
|
+
|
34
|
+
MSG = 'Use `%<good>s` instead of `%<bad>s`.'
|
35
|
+
RESTRICT_ON_SEND = %i[assert_style has_style? have_style].freeze
|
36
|
+
PREFERRED_METHOD = {
|
37
|
+
'assert_style' => 'assert_matches_style',
|
38
|
+
'has_style?' => 'matches_style?',
|
39
|
+
'have_style' => 'match_style'
|
40
|
+
}.freeze
|
41
|
+
|
42
|
+
def on_send(node)
|
43
|
+
method_node = node.loc.selector
|
44
|
+
add_offense(method_node) do |corrector|
|
45
|
+
corrector.replace(method_node,
|
46
|
+
PREFERRED_METHOD[method_node.source])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def message(node)
|
53
|
+
format(MSG, good: PREFERRED_METHOD[node.source], bad: node.source)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
# Help methods for capybara.
|
6
|
+
module CapybaraHelp
|
7
|
+
module_function
|
8
|
+
|
9
|
+
# @param node [RuboCop::AST::SendNode]
|
10
|
+
# @param locator [String]
|
11
|
+
# @param element [String]
|
12
|
+
# @return [Boolean]
|
13
|
+
def specific_option?(node, locator, element)
|
14
|
+
attrs = CssSelector.attributes(locator).keys
|
15
|
+
return false unless replaceable_element?(node, element, attrs)
|
16
|
+
|
17
|
+
attrs.all? do |attr|
|
18
|
+
CssSelector.specific_options?(element, attr)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param locator [String]
|
23
|
+
# @return [Boolean]
|
24
|
+
def specific_pseudo_classes?(locator)
|
25
|
+
CssSelector.pseudo_classes(locator).all? do |pseudo_class|
|
26
|
+
replaceable_pseudo_class?(pseudo_class, locator)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @param pseudo_class [String]
|
31
|
+
# @param locator [String]
|
32
|
+
# @return [Boolean]
|
33
|
+
def replaceable_pseudo_class?(pseudo_class, locator)
|
34
|
+
return false unless CssSelector.specific_pesudo_classes?(pseudo_class)
|
35
|
+
|
36
|
+
case pseudo_class
|
37
|
+
when 'not()' then replaceable_pseudo_class_not?(locator)
|
38
|
+
else true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param locator [String]
|
43
|
+
# @return [Boolean]
|
44
|
+
def replaceable_pseudo_class_not?(locator)
|
45
|
+
locator.scan(/not\(.*?\)/).all? do |negation|
|
46
|
+
CssSelector.attributes(negation).values.all? do |v|
|
47
|
+
v.is_a?(TrueClass) || v.is_a?(FalseClass)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param node [RuboCop::AST::SendNode]
|
53
|
+
# @param element [String]
|
54
|
+
# @param attrs [Array<String>]
|
55
|
+
# @return [Boolean]
|
56
|
+
def replaceable_element?(node, element, attrs)
|
57
|
+
case element
|
58
|
+
when 'link' then replaceable_to_link?(node, attrs)
|
59
|
+
else true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param node [RuboCop::AST::SendNode]
|
64
|
+
# @param attrs [Array<String>]
|
65
|
+
# @return [Boolean]
|
66
|
+
def replaceable_to_link?(node, attrs)
|
67
|
+
include_option?(node, :href) || attrs.include?('href')
|
68
|
+
end
|
69
|
+
|
70
|
+
# @param node [RuboCop::AST::SendNode]
|
71
|
+
# @param option [Symbol]
|
72
|
+
# @return [Boolean]
|
73
|
+
def include_option?(node, option)
|
74
|
+
node.each_descendant(:sym).find { |opt| opt.value == option }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
# Helps parsing css selector.
|
6
|
+
module CssSelector
|
7
|
+
COMMON_OPTIONS = %w[
|
8
|
+
above below left_of right_of near count minimum maximum between text
|
9
|
+
id class style visible obscured exact exact_text normalize_ws match
|
10
|
+
wait filter_set focused
|
11
|
+
].freeze
|
12
|
+
SPECIFIC_OPTIONS = {
|
13
|
+
'button' => (
|
14
|
+
COMMON_OPTIONS + %w[disabled name value title type]
|
15
|
+
).freeze,
|
16
|
+
'link' => (
|
17
|
+
COMMON_OPTIONS + %w[href alt title download]
|
18
|
+
).freeze,
|
19
|
+
'table' => (
|
20
|
+
COMMON_OPTIONS + %w[
|
21
|
+
caption with_cols cols with_rows rows
|
22
|
+
]
|
23
|
+
).freeze,
|
24
|
+
'select' => (
|
25
|
+
COMMON_OPTIONS + %w[
|
26
|
+
disabled name placeholder options enabled_options
|
27
|
+
disabled_options selected with_selected multiple with_options
|
28
|
+
]
|
29
|
+
).freeze,
|
30
|
+
'field' => (
|
31
|
+
COMMON_OPTIONS + %w[
|
32
|
+
checked unchecked disabled valid name placeholder
|
33
|
+
validation_message readonly with type multiple
|
34
|
+
]
|
35
|
+
).freeze
|
36
|
+
}.freeze
|
37
|
+
SPECIFIC_PSEUDO_CLASSES = %w[
|
38
|
+
not() disabled enabled checked unchecked
|
39
|
+
].freeze
|
40
|
+
|
41
|
+
module_function
|
42
|
+
|
43
|
+
# @param element [String]
|
44
|
+
# @param attribute [String]
|
45
|
+
# @return [Boolean]
|
46
|
+
# @example
|
47
|
+
# specific_pesudo_classes?('button', 'name') # => true
|
48
|
+
# specific_pesudo_classes?('link', 'invalid') # => false
|
49
|
+
def specific_options?(element, attribute)
|
50
|
+
SPECIFIC_OPTIONS.fetch(element, []).include?(attribute)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param pseudo_class [String]
|
54
|
+
# @return [Boolean]
|
55
|
+
# @example
|
56
|
+
# specific_pesudo_classes?('disabled') # => true
|
57
|
+
# specific_pesudo_classes?('first-of-type') # => false
|
58
|
+
def specific_pesudo_classes?(pseudo_class)
|
59
|
+
SPECIFIC_PSEUDO_CLASSES.include?(pseudo_class)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param selector [String]
|
63
|
+
# @return [Boolean]
|
64
|
+
# @example
|
65
|
+
# id?('#some-id') # => true
|
66
|
+
# id?('.some-class') # => false
|
67
|
+
def id?(selector)
|
68
|
+
selector.start_with?('#')
|
69
|
+
end
|
70
|
+
|
71
|
+
# @param selector [String]
|
72
|
+
# @return [Boolean]
|
73
|
+
# @example
|
74
|
+
# attribute?('[attribute]') # => true
|
75
|
+
# attribute?('attribute') # => false
|
76
|
+
def attribute?(selector)
|
77
|
+
selector.start_with?('[')
|
78
|
+
end
|
79
|
+
|
80
|
+
# @param selector [String]
|
81
|
+
# @return [Array<String>]
|
82
|
+
# @example
|
83
|
+
# attributes('a[foo-bar_baz]') # => {"foo-bar_baz=>true}
|
84
|
+
# attributes('button[foo][bar]') # => {"foo"=>true, "bar"=>true}
|
85
|
+
# attributes('table[foo=bar]') # => {"foo"=>"'bar'"}
|
86
|
+
def attributes(selector)
|
87
|
+
selector.scan(/\[(.*?)\]/).flatten.to_h do |attr|
|
88
|
+
key, value = attr.split('=')
|
89
|
+
[key, normalize_value(value)]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# @param selector [String]
|
94
|
+
# @return [Boolean]
|
95
|
+
# @example
|
96
|
+
# common_attributes?('a[focused]') # => true
|
97
|
+
# common_attributes?('button[focused][visible]') # => true
|
98
|
+
# common_attributes?('table[id=some-id]') # => true
|
99
|
+
# common_attributes?('h1[invalid]') # => false
|
100
|
+
def common_attributes?(selector)
|
101
|
+
attributes(selector).keys.difference(COMMON_OPTIONS).none?
|
102
|
+
end
|
103
|
+
|
104
|
+
# @param selector [String]
|
105
|
+
# @return [Array<String>]
|
106
|
+
# @example
|
107
|
+
# pseudo_classes('button:not([disabled])') # => ['not()']
|
108
|
+
# pseudo_classes('a:enabled:not([valid])') # => ['enabled', 'not()']
|
109
|
+
def pseudo_classes(selector)
|
110
|
+
# Attributes must be excluded or else the colon in the `href`s URL
|
111
|
+
# will also be picked up as pseudo classes.
|
112
|
+
# "a:not([href='http://example.com']):enabled" => "a:not():enabled"
|
113
|
+
ignored_attribute = selector.gsub(/\[.*?\]/, '')
|
114
|
+
# "a:not():enabled" => ["not()", "enabled"]
|
115
|
+
ignored_attribute.scan(/:([^:]*)/).flatten
|
116
|
+
end
|
117
|
+
|
118
|
+
# @param selector [String]
|
119
|
+
# @return [Boolean]
|
120
|
+
# @example
|
121
|
+
# multiple_selectors?('a.cls b#id') # => true
|
122
|
+
# multiple_selectors?('a.cls') # => false
|
123
|
+
def multiple_selectors?(selector)
|
124
|
+
selector.match?(/[ >,+~]/)
|
125
|
+
end
|
126
|
+
|
127
|
+
# @param value [String]
|
128
|
+
# @return [Boolean, String]
|
129
|
+
# @example
|
130
|
+
# normalize_value('true') # => true
|
131
|
+
# normalize_value('false') # => false
|
132
|
+
# normalize_value(nil) # => false
|
133
|
+
# normalize_value("foo") # => "'foo'"
|
134
|
+
def normalize_value(value)
|
135
|
+
case value
|
136
|
+
when 'true' then true
|
137
|
+
when 'false' then false
|
138
|
+
when nil then true
|
139
|
+
else "'#{value}'"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Capybara
|
6
|
+
# Enforces use of `have_no_*` or `not_to` for negated expectations.
|
7
|
+
#
|
8
|
+
# @example EnforcedStyle: not_to (default)
|
9
|
+
# # bad
|
10
|
+
# expect(page).to have_no_selector
|
11
|
+
# expect(page).to have_no_css('a')
|
12
|
+
#
|
13
|
+
# # good
|
14
|
+
# expect(page).not_to have_selector
|
15
|
+
# expect(page).not_to have_css('a')
|
16
|
+
#
|
17
|
+
# @example EnforcedStyle: have_no
|
18
|
+
# # bad
|
19
|
+
# expect(page).not_to have_selector
|
20
|
+
# expect(page).not_to have_css('a')
|
21
|
+
#
|
22
|
+
# # good
|
23
|
+
# expect(page).to have_no_selector
|
24
|
+
# expect(page).to have_no_css('a')
|
25
|
+
#
|
26
|
+
class NegationMatcher < ::RuboCop::Cop::Base
|
27
|
+
extend AutoCorrector
|
28
|
+
include ConfigurableEnforcedStyle
|
29
|
+
|
30
|
+
MSG = 'Use `expect(...).%<runner>s %<matcher>s`.'
|
31
|
+
CAPYBARA_MATCHERS = %w[
|
32
|
+
selector css xpath text title current_path link button
|
33
|
+
field checked_field unchecked_field select table
|
34
|
+
sibling ancestor
|
35
|
+
].freeze
|
36
|
+
POSITIVE_MATCHERS =
|
37
|
+
Set.new(CAPYBARA_MATCHERS) { |element| :"have_#{element}" }.freeze
|
38
|
+
NEGATIVE_MATCHERS =
|
39
|
+
Set.new(CAPYBARA_MATCHERS) { |element| :"have_no_#{element}" }
|
40
|
+
.freeze
|
41
|
+
RESTRICT_ON_SEND = (POSITIVE_MATCHERS + NEGATIVE_MATCHERS).freeze
|
42
|
+
|
43
|
+
# @!method not_to?(node)
|
44
|
+
def_node_matcher :not_to?, <<~PATTERN
|
45
|
+
(send ... :not_to
|
46
|
+
(send nil? %POSITIVE_MATCHERS ...))
|
47
|
+
PATTERN
|
48
|
+
|
49
|
+
# @!method have_no?(node)
|
50
|
+
def_node_matcher :have_no?, <<~PATTERN
|
51
|
+
(send ... :to
|
52
|
+
(send nil? %NEGATIVE_MATCHERS ...))
|
53
|
+
PATTERN
|
54
|
+
|
55
|
+
def on_send(node)
|
56
|
+
return unless offense?(node.parent)
|
57
|
+
|
58
|
+
matcher = node.method_name.to_s
|
59
|
+
add_offense(offense_range(node),
|
60
|
+
message: message(matcher)) do |corrector|
|
61
|
+
corrector.replace(node.parent.loc.selector, replaced_runner)
|
62
|
+
corrector.replace(node.loc.selector,
|
63
|
+
replaced_matcher(matcher))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def offense?(node)
|
70
|
+
(style == :have_no && not_to?(node)) ||
|
71
|
+
(style == :not_to && have_no?(node))
|
72
|
+
end
|
73
|
+
|
74
|
+
def offense_range(node)
|
75
|
+
node.parent.loc.selector.with(end_pos: node.loc.selector.end_pos)
|
76
|
+
end
|
77
|
+
|
78
|
+
def message(matcher)
|
79
|
+
format(MSG,
|
80
|
+
runner: replaced_runner,
|
81
|
+
matcher: replaced_matcher(matcher))
|
82
|
+
end
|
83
|
+
|
84
|
+
def replaced_runner
|
85
|
+
case style
|
86
|
+
when :have_no
|
87
|
+
'to'
|
88
|
+
when :not_to
|
89
|
+
'not_to'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def replaced_matcher(matcher)
|
94
|
+
case style
|
95
|
+
when :have_no
|
96
|
+
matcher.sub('have_', 'have_no_')
|
97
|
+
when :not_to
|
98
|
+
matcher.sub('have_no_', 'have_')
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Capybara
|
6
|
+
# Checks for there is a more specific actions offered by Capybara.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
#
|
10
|
+
# # bad
|
11
|
+
# find('a').click
|
12
|
+
# find('button.cls').click
|
13
|
+
# find('a', exact_text: 'foo').click
|
14
|
+
# find('div button').click
|
15
|
+
#
|
16
|
+
# # good
|
17
|
+
# click_link
|
18
|
+
# click_button(class: 'cls')
|
19
|
+
# click_link(exact_text: 'foo')
|
20
|
+
# find('div').click_button
|
21
|
+
#
|
22
|
+
class SpecificActions < ::RuboCop::Cop::Base
|
23
|
+
MSG = "Prefer `%<good_action>s` over `find('%<selector>s').click`."
|
24
|
+
RESTRICT_ON_SEND = %i[click].freeze
|
25
|
+
SPECIFIC_ACTION = {
|
26
|
+
'button' => 'button',
|
27
|
+
'a' => 'link'
|
28
|
+
}.freeze
|
29
|
+
|
30
|
+
# @!method click_on_selector(node)
|
31
|
+
def_node_matcher :click_on_selector, <<-PATTERN
|
32
|
+
(send _ :find (str $_) ...)
|
33
|
+
PATTERN
|
34
|
+
|
35
|
+
def on_send(node)
|
36
|
+
click_on_selector(node.receiver) do |arg|
|
37
|
+
next unless supported_selector?(arg)
|
38
|
+
# Always check the last selector in the case of multiple selectors
|
39
|
+
# separated by whitespace.
|
40
|
+
# because the `.click` is executed on the element to
|
41
|
+
# which the last selector points.
|
42
|
+
next unless (selector = last_selector(arg))
|
43
|
+
next unless (action = specific_action(selector))
|
44
|
+
next unless CapybaraHelp.specific_option?(node.receiver, arg,
|
45
|
+
action)
|
46
|
+
next unless CapybaraHelp.specific_pseudo_classes?(arg)
|
47
|
+
|
48
|
+
range = offense_range(node, node.receiver)
|
49
|
+
add_offense(range, message: message(action, selector))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def specific_action(selector)
|
56
|
+
SPECIFIC_ACTION[last_selector(selector)]
|
57
|
+
end
|
58
|
+
|
59
|
+
def supported_selector?(selector)
|
60
|
+
!selector.match?(/[>,+~]/)
|
61
|
+
end
|
62
|
+
|
63
|
+
def last_selector(arg)
|
64
|
+
arg.split.last[/^\w+/, 0]
|
65
|
+
end
|
66
|
+
|
67
|
+
def offense_range(node, receiver)
|
68
|
+
receiver.loc.selector.with(end_pos: node.loc.expression.end_pos)
|
69
|
+
end
|
70
|
+
|
71
|
+
def message(action, selector)
|
72
|
+
format(MSG,
|
73
|
+
good_action: good_action(action),
|
74
|
+
selector: selector)
|
75
|
+
end
|
76
|
+
|
77
|
+
def good_action(action)
|
78
|
+
"click_#{action}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Capybara
|
6
|
+
# Checks if there is a more specific finder offered by Capybara.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# # bad
|
10
|
+
# find('#some-id')
|
11
|
+
# find('[visible][id=some-id]')
|
12
|
+
#
|
13
|
+
# # good
|
14
|
+
# find_by_id('some-id')
|
15
|
+
# find_by_id('some-id', visible: true)
|
16
|
+
#
|
17
|
+
class SpecificFinders < ::RuboCop::Cop::Base
|
18
|
+
extend AutoCorrector
|
19
|
+
|
20
|
+
include RangeHelp
|
21
|
+
|
22
|
+
MSG = 'Prefer `find_by` over `find`.'
|
23
|
+
RESTRICT_ON_SEND = %i[find].freeze
|
24
|
+
|
25
|
+
# @!method find_argument(node)
|
26
|
+
def_node_matcher :find_argument, <<~PATTERN
|
27
|
+
(send _ :find (str $_) ...)
|
28
|
+
PATTERN
|
29
|
+
|
30
|
+
def on_send(node)
|
31
|
+
find_argument(node) do |arg|
|
32
|
+
next if CssSelector.multiple_selectors?(arg)
|
33
|
+
|
34
|
+
on_attr(node, arg) if attribute?(arg)
|
35
|
+
on_id(node, arg) if CssSelector.id?(arg)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def on_attr(node, arg)
|
42
|
+
return unless (id = CssSelector.attributes(arg)['id'])
|
43
|
+
|
44
|
+
register_offense(node, replaced_arguments(arg, id))
|
45
|
+
end
|
46
|
+
|
47
|
+
def on_id(node, arg)
|
48
|
+
register_offense(node, "'#{arg.to_s.delete('#')}'")
|
49
|
+
end
|
50
|
+
|
51
|
+
def attribute?(arg)
|
52
|
+
CssSelector.attribute?(arg) &&
|
53
|
+
CssSelector.common_attributes?(arg)
|
54
|
+
end
|
55
|
+
|
56
|
+
def register_offense(node, arg_replacement)
|
57
|
+
add_offense(offense_range(node)) do |corrector|
|
58
|
+
corrector.replace(node.loc.selector, 'find_by_id')
|
59
|
+
corrector.replace(node.first_argument.loc.expression,
|
60
|
+
arg_replacement)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def replaced_arguments(arg, id)
|
65
|
+
options = to_options(CssSelector.attributes(arg))
|
66
|
+
options.empty? ? id : "#{id}, #{options}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_options(attrs)
|
70
|
+
attrs.each.map do |key, value|
|
71
|
+
next if key == 'id'
|
72
|
+
|
73
|
+
"#{key}: #{value}"
|
74
|
+
end.compact.join(', ')
|
75
|
+
end
|
76
|
+
|
77
|
+
def offense_range(node)
|
78
|
+
range_between(node.loc.selector.begin_pos, end_pos(node))
|
79
|
+
end
|
80
|
+
|
81
|
+
def end_pos(node)
|
82
|
+
if node.loc.end
|
83
|
+
node.loc.end.end_pos
|
84
|
+
else
|
85
|
+
node.loc.expression.end_pos
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Capybara
|
6
|
+
# Checks for there is a more specific matcher offered by Capybara.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
#
|
10
|
+
# # bad
|
11
|
+
# expect(page).to have_selector('button')
|
12
|
+
# expect(page).to have_no_selector('button.cls')
|
13
|
+
# expect(page).to have_css('button')
|
14
|
+
# expect(page).to have_no_css('a.cls', href: 'http://example.com')
|
15
|
+
# expect(page).to have_css('table.cls')
|
16
|
+
# expect(page).to have_css('select')
|
17
|
+
# expect(page).to have_css('input', exact_text: 'foo')
|
18
|
+
#
|
19
|
+
# # good
|
20
|
+
# expect(page).to have_button
|
21
|
+
# expect(page).to have_no_button(class: 'cls')
|
22
|
+
# expect(page).to have_button
|
23
|
+
# expect(page).to have_no_link('foo', class: 'cls', href: 'http://example.com')
|
24
|
+
# expect(page).to have_table(class: 'cls')
|
25
|
+
# expect(page).to have_select
|
26
|
+
# expect(page).to have_field('foo')
|
27
|
+
#
|
28
|
+
class SpecificMatcher < ::RuboCop::Cop::Base
|
29
|
+
MSG = 'Prefer `%<good_matcher>s` over `%<bad_matcher>s`.'
|
30
|
+
RESTRICT_ON_SEND = %i[have_selector have_no_selector have_css
|
31
|
+
have_no_css].freeze
|
32
|
+
SPECIFIC_MATCHER = {
|
33
|
+
'button' => 'button',
|
34
|
+
'a' => 'link',
|
35
|
+
'table' => 'table',
|
36
|
+
'select' => 'select',
|
37
|
+
'input' => 'field'
|
38
|
+
}.freeze
|
39
|
+
|
40
|
+
# @!method first_argument(node)
|
41
|
+
def_node_matcher :first_argument, <<-PATTERN
|
42
|
+
(send nil? _ (str $_) ... )
|
43
|
+
PATTERN
|
44
|
+
|
45
|
+
def on_send(node)
|
46
|
+
first_argument(node) do |arg|
|
47
|
+
next unless (matcher = specific_matcher(arg))
|
48
|
+
next if CssSelector.multiple_selectors?(arg)
|
49
|
+
next unless CapybaraHelp.specific_option?(node, arg, matcher)
|
50
|
+
next unless CapybaraHelp.specific_pseudo_classes?(arg)
|
51
|
+
|
52
|
+
add_offense(node, message: message(node, matcher))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def specific_matcher(arg)
|
59
|
+
splitted_arg = arg[/^\w+/, 0]
|
60
|
+
SPECIFIC_MATCHER[splitted_arg]
|
61
|
+
end
|
62
|
+
|
63
|
+
def message(node, matcher)
|
64
|
+
format(MSG,
|
65
|
+
good_matcher: good_matcher(node, matcher),
|
66
|
+
bad_matcher: node.method_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
def good_matcher(node, matcher)
|
70
|
+
node.method_name
|
71
|
+
.to_s
|
72
|
+
.gsub(/selector|css/, matcher.to_s)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Capybara
|
6
|
+
# Checks for boolean visibility in Capybara finders.
|
7
|
+
#
|
8
|
+
# Capybara lets you find elements that match a certain visibility using
|
9
|
+
# the `:visible` option. `:visible` accepts both boolean and symbols as
|
10
|
+
# values, however using booleans can have unwanted effects. `visible:
|
11
|
+
# false` does not find just invisible elements, but both visible and
|
12
|
+
# invisible elements. For expressiveness and clarity, use one of the
|
13
|
+
# symbol values, `:all`, `:hidden` or `:visible`.
|
14
|
+
# Read more in
|
15
|
+
# https://www.rubydoc.info/gems/capybara/Capybara%2FNode%2FFinders:all[the documentation].
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# # bad
|
19
|
+
# expect(page).to have_selector('.foo', visible: false)
|
20
|
+
# expect(page).to have_css('.foo', visible: true)
|
21
|
+
# expect(page).to have_link('my link', visible: false)
|
22
|
+
#
|
23
|
+
# # good
|
24
|
+
# expect(page).to have_selector('.foo', visible: :visible)
|
25
|
+
# expect(page).to have_css('.foo', visible: :all)
|
26
|
+
# expect(page).to have_link('my link', visible: :hidden)
|
27
|
+
#
|
28
|
+
class VisibilityMatcher < ::RuboCop::Cop::Base
|
29
|
+
MSG_FALSE = 'Use `:all` or `:hidden` instead of `false`.'
|
30
|
+
MSG_TRUE = 'Use `:visible` instead of `true`.'
|
31
|
+
CAPYBARA_MATCHER_METHODS = %w[
|
32
|
+
button
|
33
|
+
checked_field
|
34
|
+
css
|
35
|
+
field
|
36
|
+
link
|
37
|
+
select
|
38
|
+
selector
|
39
|
+
table
|
40
|
+
unchecked_field
|
41
|
+
xpath
|
42
|
+
].flat_map do |element|
|
43
|
+
["have_#{element}".to_sym, "have_no_#{element}".to_sym]
|
44
|
+
end
|
45
|
+
|
46
|
+
RESTRICT_ON_SEND = CAPYBARA_MATCHER_METHODS
|
47
|
+
|
48
|
+
# @!method visible_true?(node)
|
49
|
+
def_node_matcher :visible_true?, <<~PATTERN
|
50
|
+
(send nil? #capybara_matcher? ... (hash <$(pair (sym :visible) true) ...>))
|
51
|
+
PATTERN
|
52
|
+
|
53
|
+
# @!method visible_false?(node)
|
54
|
+
def_node_matcher :visible_false?, <<~PATTERN
|
55
|
+
(send nil? #capybara_matcher? ... (hash <$(pair (sym :visible) false) ...>))
|
56
|
+
PATTERN
|
57
|
+
|
58
|
+
def on_send(node)
|
59
|
+
visible_false?(node) { |arg| add_offense(arg, message: MSG_FALSE) }
|
60
|
+
visible_true?(node) { |arg| add_offense(arg, message: MSG_TRUE) }
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def capybara_matcher?(method_name)
|
66
|
+
CAPYBARA_MATCHER_METHODS.include? method_name
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'capybara/current_path_expectation'
|
4
|
+
require_relative 'capybara/match_style'
|
5
|
+
require_relative 'capybara/negation_matcher'
|
6
|
+
require_relative 'capybara/specific_actions'
|
7
|
+
require_relative 'capybara/specific_finders'
|
8
|
+
require_relative 'capybara/specific_matcher'
|
9
|
+
require_relative 'capybara/visibility_matcher'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
require 'rubocop'
|
7
|
+
|
8
|
+
require_relative 'rubocop/cop/capybara/mixin/capybara_help'
|
9
|
+
require_relative 'rubocop/cop/capybara/mixin/css_selector'
|
10
|
+
|
11
|
+
require_relative 'rubocop/cop/capybara_cops'
|
12
|
+
|
13
|
+
project_root = File.join(__dir__, '..')
|
14
|
+
RuboCop::ConfigLoader.inject_defaults!(project_root)
|
15
|
+
obsoletion = File.join(project_root, 'config', 'obsoletion.yml')
|
16
|
+
RuboCop::ConfigObsoletion.files << obsoletion if File.exist?(obsoletion)
|
17
|
+
|
18
|
+
RuboCop::Cop::Style::TrailingCommaInArguments.singleton_class.prepend(
|
19
|
+
Module.new do
|
20
|
+
def autocorrect_incompatible_with
|
21
|
+
super.push(RuboCop::Cop::Capybara::CurrentPathExpectation)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
)
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rubocop-capybara
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.17.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yudai Takada
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-12-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rubocop
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.41'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.41'
|
27
|
+
description: |2
|
28
|
+
Code style checking for Capybara test files (RSpec, Cucumber, Minitest).
|
29
|
+
A plugin for the RuboCop code style enforcing & linting tool.
|
30
|
+
email:
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files:
|
34
|
+
- MIT-LICENSE.md
|
35
|
+
- README.md
|
36
|
+
files:
|
37
|
+
- CHANGELOG.md
|
38
|
+
- CODE_OF_CONDUCT.md
|
39
|
+
- MIT-LICENSE.md
|
40
|
+
- README.md
|
41
|
+
- config/default.yml
|
42
|
+
- lib/rubocop-capybara.rb
|
43
|
+
- lib/rubocop/capybara/config_formatter.rb
|
44
|
+
- lib/rubocop/capybara/description_extractor.rb
|
45
|
+
- lib/rubocop/capybara/version.rb
|
46
|
+
- lib/rubocop/cop/capybara/current_path_expectation.rb
|
47
|
+
- lib/rubocop/cop/capybara/match_style.rb
|
48
|
+
- lib/rubocop/cop/capybara/mixin/capybara_help.rb
|
49
|
+
- lib/rubocop/cop/capybara/mixin/css_selector.rb
|
50
|
+
- lib/rubocop/cop/capybara/negation_matcher.rb
|
51
|
+
- lib/rubocop/cop/capybara/specific_actions.rb
|
52
|
+
- lib/rubocop/cop/capybara/specific_finders.rb
|
53
|
+
- lib/rubocop/cop/capybara/specific_matcher.rb
|
54
|
+
- lib/rubocop/cop/capybara/visibility_matcher.rb
|
55
|
+
- lib/rubocop/cop/capybara_cops.rb
|
56
|
+
homepage: https://github.com/rubocop/rubocop-capybara
|
57
|
+
licenses:
|
58
|
+
- MIT
|
59
|
+
metadata:
|
60
|
+
changelog_uri: https://github.com/rubocop/rubocop-capybara/blob/main/CHANGELOG.md
|
61
|
+
documentation_uri: https://docs.rubocop.org/rubocop-capybara/
|
62
|
+
rubygems_mfa_required: 'true'
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: 2.6.0
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
requirements: []
|
78
|
+
rubygems_version: 3.2.33
|
79
|
+
signing_key:
|
80
|
+
specification_version: 4
|
81
|
+
summary: Code style checking for Capybara test files
|
82
|
+
test_files: []
|