rubocop-rspec 2.17.0 → 2.18.0

Sign up to get free protection for your applications and to get access to all the features.
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