rubocop-rspec 2.17.0 → 2.18.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 803b0cb494f6f1204fdb3ec31f7d4a4113b57fb2384066fb322e84107abbc65a
4
- data.tar.gz: cb5a6a141c0af43cf179ac9d3e568e8bc3b5764abf4fda4e8a09340e7d311161
3
+ metadata.gz: ca669591a3d96ff467934951daa0d1babb0dfbab10a51ee1b170827ee297838e
4
+ data.tar.gz: 0b72afb14344b5b5913f912c5c27d061a8eaee6a8c3c94dfa176cb83c36f0e84
5
5
  SHA512:
6
- metadata.gz: 263c0f3e23a10ba54a9fc888646ab9f4e55ea4a08e0adb5b2b32db14c1fc773ed3fa956df3c7a12794c7c3ebea4415e68a42a5d7d109e2da6b88d623417e2102
7
- data.tar.gz: d976c48d81cfd2d33b4857f08c9860d3857d5f8b51017f99c5ae650102184884dc76d95096d1efc2b70472a063a1887ef4d8e72dc7a6d003b131bf4ca67c4c5e
6
+ metadata.gz: 77179cdfde546ab60481f00538ec4e18cb9db86c2a2dd7bdbcb834f6ee7ea7f7399bdf487d753df6225500783db77174c87c9f2d314a2af4870a0c71631ced5d
7
+ data.tar.gz: a5181f81b66daba503eded433c3407d4aa9be3f866310bdefc83b7af0f3783783d0f18fa1dbde1847deee267c112ba4b2c566b484369c59c67a381a53adf5421
data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Master (Unreleased)
4
4
 
5
+ ## 2.18.0 (2023-01-16)
6
+
7
+ - Extract Capybara cops to a separate repository. ([@pirj])
8
+
9
+ ## 2.17.1 (2023-01-16)
10
+
11
+ - Fix a false negative for `RSpec/Pending` when using skipped in metadata is multiline string. ([@ydah])
12
+ - Fix a false positive for `RSpec/NoExpectationExample` when using skipped in metadata is multiline string. ([@ydah])
13
+ - Fix a false positive for `RSpec/ContextMethod` when multi-line context with `#` at the beginning. ([@ydah])
14
+ - Fix an incorrect autocorrect for `RSpec/PredicateMatcher` when multiline expect and predicate method with heredoc. ([@ydah])
15
+ - Fix a false positive for `RSpec/PredicateMatcher` when `include` with multiple argument. ([@ydah])
16
+
5
17
  ## 2.17.0 (2023-01-13)
6
18
 
7
19
  - Fix a false positive for `RSpec/PendingWithoutReason` when pending/skip is argument of methods. ([@ydah])
@@ -12,3 +12,12 @@ changed_parameters:
12
12
  parameters: IgnoredPatterns
13
13
  alternative: AllowedPatterns
14
14
  severity: warning
15
+
16
+ renamed:
17
+ RSpec/Capybara/CurrentPathExpectation: Capybara/CurrentPathExpectation
18
+ RSpec/Capybara/MatchStyle: Capybara/MatchStyle
19
+ RSpec/Capybara/NegationMatcher: Capybara/NegationMatcher
20
+ RSpec/Capybara/SpecificActions: Capybara/SpecificActions
21
+ RSpec/Capybara/SpecificFinders: Capybara/SpecificFinders
22
+ RSpec/Capybara/SpecificMatcher: Capybara/SpecificMatcher
23
+ RSpec/Capybara/VisibilityMatcher: Capybara/VisibilityMatcher
@@ -4,121 +4,35 @@ module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
6
  module Capybara
7
- # Checks that no expectations are set on Capybara's `current_path`.
8
- #
9
- # The
10
- # https://www.rubydoc.info/github/teamcapybara/capybara/master/Capybara/RSpecMatchers#have_current_path-instance_method[`have_current_path` matcher]
11
- # should be used on `page` to set expectations on Capybara's
12
- # current path, since it uses
13
- # https://github.com/teamcapybara/capybara/blob/master/README.md#asynchronous-javascript-ajax-and-friends[Capybara's waiting functionality]
14
- # which ensures that preceding actions (like `click_link`) have
15
- # completed.
16
- #
17
- # This cop does not support autocorrection in some cases.
18
- #
19
- # @example
20
- # # bad
21
- # expect(current_path).to eq('/callback')
22
- #
23
- # # good
24
- # expect(page).to have_current_path('/callback')
25
- #
26
- # # bad (does not support autocorrection)
27
- # expect(page.current_path).to match(variable)
28
- #
29
- # # good
30
- # expect(page).to have_current_path('/callback')
31
- #
32
- class CurrentPathExpectation < ::RuboCop::Cop::Base
33
- extend AutoCorrector
34
-
35
- MSG = 'Do not set an RSpec expectation on `current_path` in ' \
36
- 'Capybara feature specs - instead, use the ' \
37
- '`have_current_path` matcher on `page`'
38
-
39
- RESTRICT_ON_SEND = %i[expect].freeze
40
-
41
- # @!method expectation_set_on_current_path(node)
42
- def_node_matcher :expectation_set_on_current_path, <<-PATTERN
43
- (send nil? :expect (send {(send nil? :page) nil?} :current_path))
44
- PATTERN
45
-
46
- # Supported matchers: eq(...) / match(/regexp/) / match('regexp')
47
- # @!method as_is_matcher(node)
48
- def_node_matcher :as_is_matcher, <<-PATTERN
49
- (send
50
- #expectation_set_on_current_path ${:to :to_not :not_to}
51
- ${(send nil? :eq ...) (send nil? :match (regexp ...))})
52
- PATTERN
53
-
54
- # @!method regexp_str_matcher(node)
55
- def_node_matcher :regexp_str_matcher, <<-PATTERN
56
- (send
57
- #expectation_set_on_current_path ${:to :to_not :not_to}
58
- $(send nil? :match (str $_)))
59
- PATTERN
60
-
61
- def self.autocorrect_incompatible_with
62
- [Style::TrailingCommaInArguments]
63
- end
64
-
65
- def on_send(node)
66
- expectation_set_on_current_path(node) do
67
- add_offense(node.loc.selector) do |corrector|
68
- next unless node.chained?
69
-
70
- autocorrect(corrector, node)
71
- end
72
- end
73
- end
74
-
75
- private
76
-
77
- def autocorrect(corrector, node)
78
- as_is_matcher(node.parent) do |to_sym, matcher_node|
79
- rewrite_expectation(corrector, node, to_sym, matcher_node)
80
- end
81
-
82
- regexp_str_matcher(node.parent) do |to_sym, matcher_node, regexp|
83
- rewrite_expectation(corrector, node, to_sym, matcher_node)
84
- convert_regexp_str_to_literal(corrector, matcher_node, regexp)
85
- end
86
- end
87
-
88
- def rewrite_expectation(corrector, node, to_symbol, matcher_node)
89
- current_path_node = node.first_argument
90
- corrector.replace(current_path_node, 'page')
91
- corrector.replace(node.parent.loc.selector, 'to')
92
- matcher_method = if to_symbol == :to
93
- 'have_current_path'
94
- else
95
- 'have_no_current_path'
96
- end
97
- corrector.replace(matcher_node.loc.selector, matcher_method)
98
- add_ignore_query_options(corrector, node)
99
- end
100
-
101
- def convert_regexp_str_to_literal(corrector, matcher_node, regexp_str)
102
- str_node = matcher_node.first_argument
103
- regexp_expr = Regexp.new(regexp_str).inspect
104
- corrector.replace(str_node, regexp_expr)
105
- end
106
-
107
- # `have_current_path` with no options will include the querystring
108
- # while `page.current_path` does not.
109
- # This ensures the option `ignore_query: true` is added
110
- # except when the expectation is a regexp or string
111
- def add_ignore_query_options(corrector, node)
112
- expectation_node = node.parent.last_argument
113
- expectation_last_child = expectation_node.children.last
114
- return if %i[regexp str].include?(expectation_last_child.type)
115
-
116
- corrector.insert_after(
117
- expectation_last_child,
118
- ', ignore_query: true'
119
- )
120
- end
121
- end
7
+ # @!parse
8
+ # # Checks that no expectations are set on Capybara's `current_path`.
9
+ # #
10
+ # # The
11
+ # # https://www.rubydoc.info/github/teamcapybara/capybara/master/Capybara/RSpecMatchers#have_current_path-instance_method[`have_current_path` matcher]
12
+ # # should be used on `page` to set expectations on Capybara's
13
+ # # current path, since it uses
14
+ # # https://github.com/teamcapybara/capybara/blob/master/README.md#asynchronous-javascript-ajax-and-friends[Capybara's waiting functionality]
15
+ # # which ensures that preceding actions (like `click_link`) have
16
+ # # completed.
17
+ # #
18
+ # # This cop does not support autocorrection in some cases.
19
+ # #
20
+ # # @example
21
+ # # # bad
22
+ # # expect(current_path).to eq('/callback')
23
+ # #
24
+ # # # good
25
+ # # expect(page).to have_current_path('/callback')
26
+ # #
27
+ # # # bad (does not support autocorrection)
28
+ # # expect(page.current_path).to match(variable)
29
+ # #
30
+ # # # good
31
+ # # expect(page).to have_current_path('/callback')
32
+ # #
33
+ # class CurrentPathExpectation < ::RuboCop::Cop::Base; end
34
+ CurrentPathExpectation =
35
+ ::RuboCop::Cop::Capybara::CurrentPathExpectation
122
36
  end
123
37
  end
124
38
  end
@@ -4,56 +4,34 @@ module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
6
  module Capybara
7
- # Checks for usage of deprecated style methods.
8
- #
9
- # @example when using `assert_style`
10
- # # bad
11
- # page.find(:css, '#first').assert_style(display: 'block')
12
- #
13
- # # good
14
- # page.find(:css, '#first').assert_matches_style(display: 'block')
15
- #
16
- # @example when using `has_style?`
17
- # # bad
18
- # expect(page.find(:css, 'first')
19
- # .has_style?(display: 'block')).to be true
20
- #
21
- # # good
22
- # expect(page.find(:css, 'first')
23
- # .matches_style?(display: 'block')).to be true
24
- #
25
- # @example when using `have_style`
26
- # # bad
27
- # expect(page).to have_style(display: 'block')
28
- #
29
- # # good
30
- # expect(page).to match_style(display: 'block')
31
- #
32
- class MatchStyle < Base
33
- extend AutoCorrector
34
-
35
- MSG = 'Use `%<good>s` instead of `%<bad>s`.'
36
- RESTRICT_ON_SEND = %i[assert_style has_style? have_style].freeze
37
- PREFERRED_METHOD = {
38
- 'assert_style' => 'assert_matches_style',
39
- 'has_style?' => 'matches_style?',
40
- 'have_style' => 'match_style'
41
- }.freeze
42
-
43
- def on_send(node)
44
- method_node = node.loc.selector
45
- add_offense(method_node) do |corrector|
46
- corrector.replace(method_node,
47
- PREFERRED_METHOD[method_node.source])
48
- end
49
- end
50
-
51
- private
52
-
53
- def message(node)
54
- format(MSG, good: PREFERRED_METHOD[node.source], bad: node.source)
55
- end
56
- end
7
+ # @!parse
8
+ # # Checks for usage of deprecated style methods.
9
+ # #
10
+ # # @example when using `assert_style`
11
+ # # # bad
12
+ # # page.find(:css, '#first').assert_style(display: 'block')
13
+ # #
14
+ # # # good
15
+ # # page.find(:css, '#first').assert_matches_style(display: 'block')
16
+ # #
17
+ # # @example when using `has_style?`
18
+ # # # bad
19
+ # # expect(page.find(:css, 'first')
20
+ # # .has_style?(display: 'block')).to be true
21
+ # #
22
+ # # # good
23
+ # # expect(page.find(:css, 'first')
24
+ # # .matches_style?(display: 'block')).to be true
25
+ # #
26
+ # # @example when using `have_style`
27
+ # # # bad
28
+ # # expect(page).to have_style(display: 'block')
29
+ # #
30
+ # # # good
31
+ # # expect(page).to match_style(display: 'block')
32
+ # #
33
+ # class MatchStyle < ::RuboCop::Cop::Base; end
34
+ MatchStyle = ::RuboCop::Cop::Capybara::MatchStyle
57
35
  end
58
36
  end
59
37
  end
@@ -4,102 +4,29 @@ module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
6
  module Capybara
7
- # Enforces use of `have_no_*` or `not_to` for negated expectations.
8
- #
9
- # @example EnforcedStyle: not_to (default)
10
- # # bad
11
- # expect(page).to have_no_selector
12
- # expect(page).to have_no_css('a')
13
- #
14
- # # good
15
- # expect(page).not_to have_selector
16
- # expect(page).not_to have_css('a')
17
- #
18
- # @example EnforcedStyle: have_no
19
- # # bad
20
- # expect(page).not_to have_selector
21
- # expect(page).not_to have_css('a')
22
- #
23
- # # good
24
- # expect(page).to have_no_selector
25
- # expect(page).to have_no_css('a')
26
- #
27
- class NegationMatcher < ::RuboCop::Cop::Base
28
- extend AutoCorrector
29
- include ConfigurableEnforcedStyle
30
-
31
- MSG = 'Use `expect(...).%<runner>s %<matcher>s`.'
32
- CAPYBARA_MATCHERS = %w[
33
- selector css xpath text title current_path link button
34
- field checked_field unchecked_field select table
35
- sibling ancestor
36
- ].freeze
37
- POSITIVE_MATCHERS =
38
- Set.new(CAPYBARA_MATCHERS) { |element| :"have_#{element}" }.freeze
39
- NEGATIVE_MATCHERS =
40
- Set.new(CAPYBARA_MATCHERS) { |element| :"have_no_#{element}" }
41
- .freeze
42
- RESTRICT_ON_SEND = (POSITIVE_MATCHERS + NEGATIVE_MATCHERS).freeze
43
-
44
- # @!method not_to?(node)
45
- def_node_matcher :not_to?, <<~PATTERN
46
- (send ... :not_to
47
- (send nil? %POSITIVE_MATCHERS ...))
48
- PATTERN
49
-
50
- # @!method have_no?(node)
51
- def_node_matcher :have_no?, <<~PATTERN
52
- (send ... :to
53
- (send nil? %NEGATIVE_MATCHERS ...))
54
- PATTERN
55
-
56
- def on_send(node)
57
- return unless offense?(node.parent)
58
-
59
- matcher = node.method_name.to_s
60
- add_offense(offense_range(node),
61
- message: message(matcher)) do |corrector|
62
- corrector.replace(node.parent.loc.selector, replaced_runner)
63
- corrector.replace(node.loc.selector,
64
- replaced_matcher(matcher))
65
- end
66
- end
67
-
68
- private
69
-
70
- def offense?(node)
71
- (style == :have_no && not_to?(node)) ||
72
- (style == :not_to && have_no?(node))
73
- end
74
-
75
- def offense_range(node)
76
- node.parent.loc.selector.with(end_pos: node.loc.selector.end_pos)
77
- end
78
-
79
- def message(matcher)
80
- format(MSG,
81
- runner: replaced_runner,
82
- matcher: replaced_matcher(matcher))
83
- end
84
-
85
- def replaced_runner
86
- case style
87
- when :have_no
88
- 'to'
89
- when :not_to
90
- 'not_to'
91
- end
92
- end
93
-
94
- def replaced_matcher(matcher)
95
- case style
96
- when :have_no
97
- matcher.sub('have_', 'have_no_')
98
- when :not_to
99
- matcher.sub('have_no_', 'have_')
100
- end
101
- end
102
- end
7
+ # @!parse
8
+ # # Enforces use of `have_no_*` or `not_to` for negated expectations.
9
+ # #
10
+ # # @example EnforcedStyle: not_to (default)
11
+ # # # bad
12
+ # # expect(page).to have_no_selector
13
+ # # expect(page).to have_no_css('a')
14
+ # #
15
+ # # # good
16
+ # # expect(page).not_to have_selector
17
+ # # expect(page).not_to have_css('a')
18
+ # #
19
+ # # @example EnforcedStyle: have_no
20
+ # # # bad
21
+ # # expect(page).not_to have_selector
22
+ # # expect(page).not_to have_css('a')
23
+ # #
24
+ # # # good
25
+ # # expect(page).to have_no_selector
26
+ # # expect(page).to have_no_css('a')
27
+ # #
28
+ # class NegationMatcher < ::RuboCop::Cop::Base; end
29
+ NegationMatcher = ::RuboCop::Cop::Capybara::NegationMatcher
103
30
  end
104
31
  end
105
32
  end
@@ -4,81 +4,25 @@ module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
6
  module Capybara
7
- # Checks for there is a more specific actions offered by Capybara.
8
- #
9
- # @example
10
- #
11
- # # bad
12
- # find('a').click
13
- # find('button.cls').click
14
- # find('a', exact_text: 'foo').click
15
- # find('div button').click
16
- #
17
- # # good
18
- # click_link
19
- # click_button(class: 'cls')
20
- # click_link(exact_text: 'foo')
21
- # find('div').click_button
22
- #
23
- class SpecificActions < ::RuboCop::Cop::Base
24
- MSG = "Prefer `%<good_action>s` over `find('%<selector>s').click`."
25
- RESTRICT_ON_SEND = %i[click].freeze
26
- SPECIFIC_ACTION = {
27
- 'button' => 'button',
28
- 'a' => 'link'
29
- }.freeze
30
-
31
- # @!method click_on_selector(node)
32
- def_node_matcher :click_on_selector, <<-PATTERN
33
- (send _ :find (str $_) ...)
34
- PATTERN
35
-
36
- def on_send(node)
37
- click_on_selector(node.receiver) do |arg|
38
- next unless supported_selector?(arg)
39
- # Always check the last selector in the case of multiple selectors
40
- # separated by whitespace.
41
- # because the `.click` is executed on the element to
42
- # which the last selector points.
43
- next unless (selector = last_selector(arg))
44
- next unless (action = specific_action(selector))
45
- next unless CapybaraHelp.specific_option?(node.receiver, arg,
46
- action)
47
- next unless CapybaraHelp.specific_pseudo_classes?(arg)
48
-
49
- range = offense_range(node, node.receiver)
50
- add_offense(range, message: message(action, selector))
51
- end
52
- end
53
-
54
- private
55
-
56
- def specific_action(selector)
57
- SPECIFIC_ACTION[last_selector(selector)]
58
- end
59
-
60
- def supported_selector?(selector)
61
- !selector.match?(/[>,+~]/)
62
- end
63
-
64
- def last_selector(arg)
65
- arg.split.last[/^\w+/, 0]
66
- end
67
-
68
- def offense_range(node, receiver)
69
- receiver.loc.selector.with(end_pos: node.loc.expression.end_pos)
70
- end
71
-
72
- def message(action, selector)
73
- format(MSG,
74
- good_action: good_action(action),
75
- selector: selector)
76
- end
77
-
78
- def good_action(action)
79
- "click_#{action}"
80
- end
81
- end
7
+ # @!parse
8
+ # # Checks for there is a more specific actions offered by Capybara.
9
+ # #
10
+ # # @example
11
+ # #
12
+ # # # bad
13
+ # # find('a').click
14
+ # # find('button.cls').click
15
+ # # find('a', exact_text: 'foo').click
16
+ # # find('div button').click
17
+ # #
18
+ # # # good
19
+ # # click_link
20
+ # # click_button(class: 'cls')
21
+ # # click_link(exact_text: 'foo')
22
+ # # find('div').click_button
23
+ # #
24
+ # class SpecificActions < ::RuboCop::Cop::Base; end
25
+ SpecificActions = ::RuboCop::Cop::Capybara::SpecificActions
82
26
  end
83
27
  end
84
28
  end
@@ -4,89 +4,20 @@ module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
6
  module Capybara
7
- # Checks if there is a more specific finder offered by Capybara.
8
- #
9
- # @example
10
- # # bad
11
- # find('#some-id')
12
- # find('[visible][id=some-id]')
13
- #
14
- # # good
15
- # find_by_id('some-id')
16
- # find_by_id('some-id', visible: true)
17
- #
18
- class SpecificFinders < ::RuboCop::Cop::Base
19
- extend AutoCorrector
20
-
21
- include RangeHelp
22
-
23
- MSG = 'Prefer `find_by` over `find`.'
24
- RESTRICT_ON_SEND = %i[find].freeze
25
-
26
- # @!method find_argument(node)
27
- def_node_matcher :find_argument, <<~PATTERN
28
- (send _ :find (str $_) ...)
29
- PATTERN
30
-
31
- def on_send(node)
32
- find_argument(node) do |arg|
33
- next if CssSelector.multiple_selectors?(arg)
34
-
35
- on_attr(node, arg) if attribute?(arg)
36
- on_id(node, arg) if CssSelector.id?(arg)
37
- end
38
- end
39
-
40
- private
41
-
42
- def on_attr(node, arg)
43
- return unless (id = CssSelector.attributes(arg)['id'])
44
-
45
- register_offense(node, replaced_arguments(arg, id))
46
- end
47
-
48
- def on_id(node, arg)
49
- register_offense(node, "'#{arg.to_s.delete('#')}'")
50
- end
51
-
52
- def attribute?(arg)
53
- CssSelector.attribute?(arg) &&
54
- CssSelector.common_attributes?(arg)
55
- end
56
-
57
- def register_offense(node, arg_replacement)
58
- add_offense(offense_range(node)) do |corrector|
59
- corrector.replace(node.loc.selector, 'find_by_id')
60
- corrector.replace(node.first_argument.loc.expression,
61
- arg_replacement)
62
- end
63
- end
64
-
65
- def replaced_arguments(arg, id)
66
- options = to_options(CssSelector.attributes(arg))
67
- options.empty? ? id : "#{id}, #{options}"
68
- end
69
-
70
- def to_options(attrs)
71
- attrs.each.map do |key, value|
72
- next if key == 'id'
73
-
74
- "#{key}: #{value}"
75
- end.compact.join(', ')
76
- end
77
-
78
- def offense_range(node)
79
- range_between(node.loc.selector.begin_pos, end_pos(node))
80
- end
81
-
82
- def end_pos(node)
83
- if node.loc.end
84
- node.loc.end.end_pos
85
- else
86
- node.loc.expression.end_pos
87
- end
88
- end
89
- end
7
+ # @!parse
8
+ # # Checks if there is a more specific finder offered by Capybara.
9
+ # #
10
+ # # @example
11
+ # # # bad
12
+ # # find('#some-id')
13
+ # # find('[visible][id=some-id]')
14
+ # #
15
+ # # # good
16
+ # # find_by_id('some-id')
17
+ # # find_by_id('some-id', visible: true)
18
+ # #
19
+ # class SpecificFinders < ::RuboCop::Cop::Base; end
20
+ SpecificFinders = ::RuboCop::Cop::Capybara::SpecificFinders
90
21
  end
91
22
  end
92
23
  end
@@ -4,75 +4,31 @@ module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
6
  module Capybara
7
- # Checks for there is a more specific matcher offered by Capybara.
8
- #
9
- # @example
10
- #
11
- # # bad
12
- # expect(page).to have_selector('button')
13
- # expect(page).to have_no_selector('button.cls')
14
- # expect(page).to have_css('button')
15
- # expect(page).to have_no_css('a.cls', href: 'http://example.com')
16
- # expect(page).to have_css('table.cls')
17
- # expect(page).to have_css('select')
18
- # expect(page).to have_css('input', exact_text: 'foo')
19
- #
20
- # # good
21
- # expect(page).to have_button
22
- # expect(page).to have_no_button(class: 'cls')
23
- # expect(page).to have_button
24
- # expect(page).to have_no_link('foo', class: 'cls', href: 'http://example.com')
25
- # expect(page).to have_table(class: 'cls')
26
- # expect(page).to have_select
27
- # expect(page).to have_field('foo')
28
- #
29
- class SpecificMatcher < ::RuboCop::Cop::Base
30
- MSG = 'Prefer `%<good_matcher>s` over `%<bad_matcher>s`.'
31
- RESTRICT_ON_SEND = %i[have_selector have_no_selector have_css
32
- have_no_css].freeze
33
- SPECIFIC_MATCHER = {
34
- 'button' => 'button',
35
- 'a' => 'link',
36
- 'table' => 'table',
37
- 'select' => 'select',
38
- 'input' => 'field'
39
- }.freeze
40
-
41
- # @!method first_argument(node)
42
- def_node_matcher :first_argument, <<-PATTERN
43
- (send nil? _ (str $_) ... )
44
- PATTERN
45
-
46
- def on_send(node)
47
- first_argument(node) do |arg|
48
- next unless (matcher = specific_matcher(arg))
49
- next if CssSelector.multiple_selectors?(arg)
50
- next unless CapybaraHelp.specific_option?(node, arg, matcher)
51
- next unless CapybaraHelp.specific_pseudo_classes?(arg)
52
-
53
- add_offense(node, message: message(node, matcher))
54
- end
55
- end
56
-
57
- private
58
-
59
- def specific_matcher(arg)
60
- splitted_arg = arg[/^\w+/, 0]
61
- SPECIFIC_MATCHER[splitted_arg]
62
- end
63
-
64
- def message(node, matcher)
65
- format(MSG,
66
- good_matcher: good_matcher(node, matcher),
67
- bad_matcher: node.method_name)
68
- end
69
-
70
- def good_matcher(node, matcher)
71
- node.method_name
72
- .to_s
73
- .gsub(/selector|css/, matcher.to_s)
74
- end
75
- end
7
+ # @!parse
8
+ # # Checks for there is a more specific matcher offered by Capybara.
9
+ # #
10
+ # # @example
11
+ # #
12
+ # # # bad
13
+ # # expect(page).to have_selector('button')
14
+ # # expect(page).to have_no_selector('button.cls')
15
+ # # expect(page).to have_css('button')
16
+ # # expect(page).to have_no_css('a.cls', href: 'http://example.com')
17
+ # # expect(page).to have_css('table.cls')
18
+ # # expect(page).to have_css('select')
19
+ # # expect(page).to have_css('input', exact_text: 'foo')
20
+ # #
21
+ # # # good
22
+ # # expect(page).to have_button
23
+ # # expect(page).to have_no_button(class: 'cls')
24
+ # # expect(page).to have_button
25
+ # # expect(page).to have_no_link('foo', class: 'cls', href: 'http://example.com')
26
+ # # expect(page).to have_table(class: 'cls')
27
+ # # expect(page).to have_select
28
+ # # expect(page).to have_field('foo')
29
+ # #
30
+ # class SpecificMatcher < ::RuboCop::Cop::Base; end
31
+ SpecificMatcher = ::RuboCop::Cop::Capybara::SpecificMatcher
76
32
  end
77
33
  end
78
34
  end
@@ -4,69 +4,32 @@ module RuboCop
4
4
  module Cop
5
5
  module RSpec
6
6
  module Capybara
7
- # Checks for boolean visibility in Capybara finders.
8
- #
9
- # Capybara lets you find elements that match a certain visibility using
10
- # the `:visible` option. `:visible` accepts both boolean and symbols as
11
- # values, however using booleans can have unwanted effects. `visible:
12
- # false` does not find just invisible elements, but both visible and
13
- # invisible elements. For expressiveness and clarity, use one of the
14
- # symbol values, `:all`, `:hidden` or `:visible`.
15
- # Read more in
16
- # https://www.rubydoc.info/gems/capybara/Capybara%2FNode%2FFinders:all[the documentation].
17
- #
18
- # @example
19
- # # bad
20
- # expect(page).to have_selector('.foo', visible: false)
21
- # expect(page).to have_css('.foo', visible: true)
22
- # expect(page).to have_link('my link', visible: false)
23
- #
24
- # # good
25
- # expect(page).to have_selector('.foo', visible: :visible)
26
- # expect(page).to have_css('.foo', visible: :all)
27
- # expect(page).to have_link('my link', visible: :hidden)
28
- #
29
- class VisibilityMatcher < ::RuboCop::Cop::Base
30
- MSG_FALSE = 'Use `:all` or `:hidden` instead of `false`.'
31
- MSG_TRUE = 'Use `:visible` instead of `true`.'
32
- CAPYBARA_MATCHER_METHODS = %w[
33
- button
34
- checked_field
35
- css
36
- field
37
- link
38
- select
39
- selector
40
- table
41
- unchecked_field
42
- xpath
43
- ].flat_map do |element|
44
- ["have_#{element}".to_sym, "have_no_#{element}".to_sym]
45
- end
46
-
47
- RESTRICT_ON_SEND = CAPYBARA_MATCHER_METHODS
48
-
49
- # @!method visible_true?(node)
50
- def_node_matcher :visible_true?, <<~PATTERN
51
- (send nil? #capybara_matcher? ... (hash <$(pair (sym :visible) true) ...>))
52
- PATTERN
53
-
54
- # @!method visible_false?(node)
55
- def_node_matcher :visible_false?, <<~PATTERN
56
- (send nil? #capybara_matcher? ... (hash <$(pair (sym :visible) false) ...>))
57
- PATTERN
58
-
59
- def on_send(node)
60
- visible_false?(node) { |arg| add_offense(arg, message: MSG_FALSE) }
61
- visible_true?(node) { |arg| add_offense(arg, message: MSG_TRUE) }
62
- end
63
-
64
- private
65
-
66
- def capybara_matcher?(method_name)
67
- CAPYBARA_MATCHER_METHODS.include? method_name
68
- end
69
- end
7
+ # @!parse
8
+ # # Checks for boolean visibility in Capybara finders.
9
+ # #
10
+ # # Capybara lets you find elements that match a certain visibility
11
+ # # using the `:visible` option. `:visible` accepts both boolean and
12
+ # # symbols as values, however using booleans can have unwanted
13
+ # # effects. `visible: false` does not find just invisible elements,
14
+ # # but both visible and invisible elements. For expressiveness and
15
+ # # clarity, use one of the # symbol values, `:all`, `:hidden` or
16
+ # # `:visible`.
17
+ # # Read more in
18
+ # # https://www.rubydoc.info/gems/capybara/Capybara%2FNode%2FFinders:all[the documentation].
19
+ # #
20
+ # # @example
21
+ # # # bad
22
+ # # expect(page).to have_selector('.foo', visible: false)
23
+ # # expect(page).to have_css('.foo', visible: true)
24
+ # # expect(page).to have_link('my link', visible: false)
25
+ # #
26
+ # # # good
27
+ # # expect(page).to have_selector('.foo', visible: :visible)
28
+ # # expect(page).to have_css('.foo', visible: :all)
29
+ # # expect(page).to have_link('my link', visible: :hidden)
30
+ # #
31
+ # class VisibilityMatcher < ::RuboCop::Cop::Base; end
32
+ VisibilityMatcher = ::RuboCop::Cop::Capybara::VisibilityMatcher
70
33
  end
71
34
  end
72
35
  end
@@ -31,7 +31,11 @@ module RuboCop
31
31
 
32
32
  # @!method context_method(node)
33
33
  def_node_matcher :context_method, <<-PATTERN
34
- (block (send #rspec? :context $(str #method_name?) ...) ...)
34
+ (block
35
+ (send #rspec? :context
36
+ ${(str #method_name?) (dstr (str #method_name?) ...)}
37
+ ...)
38
+ ...)
35
39
  PATTERN
36
40
 
37
41
  def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
@@ -11,7 +11,7 @@ module RuboCop
11
11
  def_node_matcher :skipped_in_metadata?, <<-PATTERN
12
12
  {
13
13
  (send _ _ <#skip_or_pending? ...>)
14
- (send _ _ ... (hash <(pair #skip_or_pending? { true str }) ...>))
14
+ (send _ _ ... (hash <(pair #skip_or_pending? { true str dstr }) ...>))
15
15
  }
16
16
  PATTERN
17
17
 
@@ -118,7 +118,7 @@ module RuboCop
118
118
  end
119
119
 
120
120
  # A helper for `explicit` style
121
- module ExplicitHelper
121
+ module ExplicitHelper # rubocop:disable Metrics/ModuleLength
122
122
  include RuboCop::RSpec::Language
123
123
  extend NodePattern::Macros
124
124
 
@@ -149,12 +149,35 @@ module RuboCop
149
149
  return if part_of_ignored_node?(node)
150
150
 
151
151
  predicate_matcher?(node) do |actual, matcher|
152
+ next unless replaceable_matcher?(matcher)
153
+
152
154
  add_offense(node, message: message_explicit(matcher)) do |corrector|
155
+ next if uncorrectable_matcher?(node, matcher)
156
+
153
157
  corrector_explicit(corrector, node, actual, matcher, matcher)
154
158
  end
155
159
  end
156
160
  end
157
161
 
162
+ def replaceable_matcher?(matcher)
163
+ case matcher.method_name.to_s
164
+ when 'include'
165
+ matcher.arguments.one?
166
+ else
167
+ true
168
+ end
169
+ end
170
+
171
+ def uncorrectable_matcher?(node, matcher)
172
+ heredoc_argument?(matcher) && !same_line?(node, matcher)
173
+ end
174
+
175
+ def heredoc_argument?(matcher)
176
+ matcher.arguments.select do |arg|
177
+ %i[str dstr xstr].include?(arg.type)
178
+ end.any?(&:heredoc?)
179
+ end
180
+
158
181
  # @!method predicate_matcher?(node)
159
182
  def_node_matcher :predicate_matcher?, <<-PATTERN
160
183
  (send
@@ -271,6 +294,17 @@ module RuboCop
271
294
  # # good - the above code is rewritten to it by this cop
272
295
  # expect(foo.something?).to be(true)
273
296
  #
297
+ # # bad - no autocorrect
298
+ # expect(foo)
299
+ # .to be_something(<<~TEXT)
300
+ # bar
301
+ # TEXT
302
+ #
303
+ # # good
304
+ # expect(foo.something?(<<~TEXT)).to be(true)
305
+ # bar
306
+ # TEXT
307
+ #
274
308
  # @example Strict: false, EnforcedStyle: explicit
275
309
  # # bad
276
310
  # expect(foo).to be_something
@@ -8,6 +8,15 @@ module RuboCop
8
8
  class ConfigFormatter
9
9
  EXTENSION_ROOT_DEPARTMENT = %r{^(RSpec/)}.freeze
10
10
  SUBDEPARTMENTS = %(RSpec/Capybara RSpec/FactoryBot RSpec/Rails)
11
+ EXTRACTED_COPS = %(
12
+ RSpec/Capybara/CurrentPathExpectation
13
+ RSpec/Capybara/MatchStyle
14
+ RSpec/Capybara/NegationMatcher
15
+ RSpec/Capybara/SpecificActions
16
+ RSpec/Capybara/SpecificFinders
17
+ RSpec/Capybara/SpecificMatcher
18
+ RSpec/Capybara/VisibilityMatcher
19
+ )
11
20
  AMENDMENTS = %(Metrics/BlockLength)
12
21
  COP_DOC_BASE_URL = 'https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/'
13
22
 
@@ -29,6 +38,7 @@ module RuboCop
29
38
  def unified_config
30
39
  cops.each_with_object(config.dup) do |cop, unified|
31
40
  next if SUBDEPARTMENTS.include?(cop) || AMENDMENTS.include?(cop)
41
+ next if EXTRACTED_COPS.include?(cop)
32
42
 
33
43
  replace_nil(unified[cop])
34
44
  unified[cop].merge!(descriptions.fetch(cop))
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module RSpec
5
5
  # Version information for the RSpec RuboCop plugin.
6
6
  module Version
7
- STRING = '2.17.0'
7
+ STRING = '2.18.0'
8
8
  end
9
9
  end
10
10
  end
data/lib/rubocop-rspec.rb CHANGED
@@ -4,6 +4,7 @@ require 'pathname'
4
4
  require 'yaml'
5
5
 
6
6
  require 'rubocop'
7
+ require 'rubocop-capybara'
7
8
 
8
9
  require_relative 'rubocop/rspec'
9
10
  require_relative 'rubocop/rspec/inject'
@@ -17,8 +18,6 @@ require_relative 'rubocop/rspec/language'
17
18
 
18
19
  require_relative 'rubocop/rspec/factory_bot/language'
19
20
 
20
- require_relative 'rubocop/cop/rspec/mixin/capybara_help'
21
- require_relative 'rubocop/cop/rspec/mixin/css_selector'
22
21
  require_relative 'rubocop/cop/rspec/mixin/final_end_location'
23
22
  require_relative 'rubocop/cop/rspec/mixin/inside_example_group'
24
23
  require_relative 'rubocop/cop/rspec/mixin/metadata'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-rspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.17.0
4
+ version: 2.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Backus
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2023-01-13 00:00:00.000000000 Z
13
+ date: 2023-01-16 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rubocop
@@ -26,6 +26,20 @@ dependencies:
26
26
  - - "~>"
27
27
  - !ruby/object:Gem::Version
28
28
  version: '1.33'
29
+ - !ruby/object:Gem::Dependency
30
+ name: rubocop-capybara
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
29
43
  description: |2
30
44
  Code style checking for RSpec files.
31
45
  A plugin for the RuboCop code style enforcing & linting tool.
@@ -116,9 +130,7 @@ files:
116
130
  - lib/rubocop/cop/rspec/message_expectation.rb
117
131
  - lib/rubocop/cop/rspec/message_spies.rb
118
132
  - lib/rubocop/cop/rspec/missing_example_group_argument.rb
119
- - lib/rubocop/cop/rspec/mixin/capybara_help.rb
120
133
  - lib/rubocop/cop/rspec/mixin/comments_help.rb
121
- - lib/rubocop/cop/rspec/mixin/css_selector.rb
122
134
  - lib/rubocop/cop/rspec/mixin/empty_line_separation.rb
123
135
  - lib/rubocop/cop/rspec/mixin/final_end_location.rb
124
136
  - lib/rubocop/cop/rspec/mixin/inside_example_group.rb
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RuboCop
4
- module Cop
5
- module RSpec
6
- # Help methods for capybara.
7
- module CapybaraHelp
8
- module_function
9
-
10
- # @param node [RuboCop::AST::SendNode]
11
- # @param locator [String]
12
- # @param element [String]
13
- # @return [Boolean]
14
- def specific_option?(node, locator, element)
15
- attrs = CssSelector.attributes(locator).keys
16
- return false unless replaceable_element?(node, element, attrs)
17
-
18
- attrs.all? do |attr|
19
- CssSelector.specific_options?(element, attr)
20
- end
21
- end
22
-
23
- # @param locator [String]
24
- # @return [Boolean]
25
- def specific_pseudo_classes?(locator)
26
- CssSelector.pseudo_classes(locator).all? do |pseudo_class|
27
- replaceable_pseudo_class?(pseudo_class, locator)
28
- end
29
- end
30
-
31
- # @param pseudo_class [String]
32
- # @param locator [String]
33
- # @return [Boolean]
34
- def replaceable_pseudo_class?(pseudo_class, locator)
35
- return false unless CssSelector.specific_pesudo_classes?(pseudo_class)
36
-
37
- case pseudo_class
38
- when 'not()' then replaceable_pseudo_class_not?(locator)
39
- else true
40
- end
41
- end
42
-
43
- # @param locator [String]
44
- # @return [Boolean]
45
- def replaceable_pseudo_class_not?(locator)
46
- locator.scan(/not\(.*?\)/).all? do |negation|
47
- CssSelector.attributes(negation).values.all? do |v|
48
- v.is_a?(TrueClass) || v.is_a?(FalseClass)
49
- end
50
- end
51
- end
52
-
53
- # @param node [RuboCop::AST::SendNode]
54
- # @param element [String]
55
- # @param attrs [Array<String>]
56
- # @return [Boolean]
57
- def replaceable_element?(node, element, attrs)
58
- case element
59
- when 'link' then replaceable_to_link?(node, attrs)
60
- else true
61
- end
62
- end
63
-
64
- # @param node [RuboCop::AST::SendNode]
65
- # @param attrs [Array<String>]
66
- # @return [Boolean]
67
- def replaceable_to_link?(node, attrs)
68
- include_option?(node, :href) || attrs.include?('href')
69
- end
70
-
71
- # @param node [RuboCop::AST::SendNode]
72
- # @param option [Symbol]
73
- # @return [Boolean]
74
- def include_option?(node, option)
75
- node.each_descendant(:sym).find { |opt| opt.value == option }
76
- end
77
- end
78
- end
79
- end
80
- end
@@ -1,146 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RuboCop
4
- module Cop
5
- module RSpec
6
- # Helps parsing css selector.
7
- module CssSelector
8
- COMMON_OPTIONS = %w[
9
- above below left_of right_of near count minimum maximum between text
10
- id class style visible obscured exact exact_text normalize_ws match
11
- wait filter_set focused
12
- ].freeze
13
- SPECIFIC_OPTIONS = {
14
- 'button' => (
15
- COMMON_OPTIONS + %w[disabled name value title type]
16
- ).freeze,
17
- 'link' => (
18
- COMMON_OPTIONS + %w[href alt title download]
19
- ).freeze,
20
- 'table' => (
21
- COMMON_OPTIONS + %w[
22
- caption with_cols cols with_rows rows
23
- ]
24
- ).freeze,
25
- 'select' => (
26
- COMMON_OPTIONS + %w[
27
- disabled name placeholder options enabled_options
28
- disabled_options selected with_selected multiple with_options
29
- ]
30
- ).freeze,
31
- 'field' => (
32
- COMMON_OPTIONS + %w[
33
- checked unchecked disabled valid name placeholder
34
- validation_message readonly with type multiple
35
- ]
36
- ).freeze
37
- }.freeze
38
- SPECIFIC_PSEUDO_CLASSES = %w[
39
- not() disabled enabled checked unchecked
40
- ].freeze
41
-
42
- module_function
43
-
44
- # @param element [String]
45
- # @param attribute [String]
46
- # @return [Boolean]
47
- # @example
48
- # specific_pesudo_classes?('button', 'name') # => true
49
- # specific_pesudo_classes?('link', 'invalid') # => false
50
- def specific_options?(element, attribute)
51
- SPECIFIC_OPTIONS.fetch(element, []).include?(attribute)
52
- end
53
-
54
- # @param pseudo_class [String]
55
- # @return [Boolean]
56
- # @example
57
- # specific_pesudo_classes?('disabled') # => true
58
- # specific_pesudo_classes?('first-of-type') # => false
59
- def specific_pesudo_classes?(pseudo_class)
60
- SPECIFIC_PSEUDO_CLASSES.include?(pseudo_class)
61
- end
62
-
63
- # @param selector [String]
64
- # @return [Boolean]
65
- # @example
66
- # id?('#some-id') # => true
67
- # id?('.some-class') # => false
68
- def id?(selector)
69
- selector.start_with?('#')
70
- end
71
-
72
- # @param selector [String]
73
- # @return [Boolean]
74
- # @example
75
- # attribute?('[attribute]') # => true
76
- # attribute?('attribute') # => false
77
- def attribute?(selector)
78
- selector.start_with?('[')
79
- end
80
-
81
- # @param selector [String]
82
- # @return [Array<String>]
83
- # @example
84
- # attributes('a[foo-bar_baz]') # => {"foo-bar_baz=>true}
85
- # attributes('button[foo][bar]') # => {"foo"=>true, "bar"=>true}
86
- # attributes('table[foo=bar]') # => {"foo"=>"'bar'"}
87
- def attributes(selector)
88
- selector.scan(/\[(.*?)\]/).flatten.to_h do |attr|
89
- key, value = attr.split('=')
90
- [key, normalize_value(value)]
91
- end
92
- end
93
-
94
- # @param selector [String]
95
- # @return [Boolean]
96
- # @example
97
- # common_attributes?('a[focused]') # => true
98
- # common_attributes?('button[focused][visible]') # => true
99
- # common_attributes?('table[id=some-id]') # => true
100
- # common_attributes?('h1[invalid]') # => false
101
- def common_attributes?(selector)
102
- attributes(selector).keys.difference(COMMON_OPTIONS).none?
103
- end
104
-
105
- # @param selector [String]
106
- # @return [Array<String>]
107
- # @example
108
- # pseudo_classes('button:not([disabled])') # => ['not()']
109
- # pseudo_classes('a:enabled:not([valid])') # => ['enabled', 'not()']
110
- def pseudo_classes(selector)
111
- # Attributes must be excluded or else the colon in the `href`s URL
112
- # will also be picked up as pseudo classes.
113
- # "a:not([href='http://example.com']):enabled" => "a:not():enabled"
114
- ignored_attribute = selector.gsub(/\[.*?\]/, '')
115
- # "a:not():enabled" => ["not()", "enabled"]
116
- ignored_attribute.scan(/:([^:]*)/).flatten
117
- end
118
-
119
- # @param selector [String]
120
- # @return [Boolean]
121
- # @example
122
- # multiple_selectors?('a.cls b#id') # => true
123
- # multiple_selectors?('a.cls') # => false
124
- def multiple_selectors?(selector)
125
- selector.match?(/[ >,+~]/)
126
- end
127
-
128
- # @param value [String]
129
- # @return [Boolean, String]
130
- # @example
131
- # normalize_value('true') # => true
132
- # normalize_value('false') # => false
133
- # normalize_value(nil) # => false
134
- # normalize_value("foo") # => "'foo'"
135
- def normalize_value(value)
136
- case value
137
- when 'true' then true
138
- when 'false' then false
139
- when nil then true
140
- else "'#{value}'"
141
- end
142
- end
143
- end
144
- end
145
- end
146
- end