rubocop-capybara 2.17.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Join the chat at https://gitter.im/rubocop-rspec/Lobby](https://badges.gitter.im/rubocop-rspec/Lobby.svg)](https://gitter.im/rubocop-rspec/Lobby)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/rubocop-capybara.svg)](https://rubygems.org/gems/rubocop-capybara)
|
5
|
+
![CI](https://github.com/rubocop/rubocop-capybara/workflows/CI/badge.svg)
|
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: []
|