rubocop-capybara 2.17.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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.
@@ -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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Capybara
5
+ # Version information for the Capybara RuboCop plugin.
6
+ module Version
7
+ STRING = '2.17.0'
8
+ end
9
+ end
10
+ 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: []