Skip to content

Commit de89019

Browse files
committed
Add new RSpec/Capybara/SpecificMatcher cop
Resolves: #1248 This cop checks for there is a more specific matcher offered by Capybara. ### @example ```ruby # bad expect(page).to have_selector('button') expect(page).to have_no_selector('button.cls') expect(page).to have_css('button') expect(page).to have_no_css('a.cls', exact_text: 'foo') expect(page).to have_css('table.cls') expect(page).to have_css('select') # good expect(page).to have_button expect(page).to have_no_button(class: 'cls') expect(page).to have_button expect(page).to have_no_link('foo', class: 'cls') expect(page).to have_table(class: 'cls') expect(page).to have_select ```
1 parent 4040aba commit de89019

File tree

7 files changed

+211
-0
lines changed

7 files changed

+211
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
* Fix incorrect path suggested by `RSpec/FilePath` cop when second argument contains spaces. ([@tejasbubane][])
66
* Fix autocorrect for EmptyLineSeparation. ([@johnny-miyake][])
7+
* Add new `RSpec/Capybara/SpecificMatcher` cop. ([@ydah][])
78

89
## 2.11.1 (2022-05-18)
910

config/default.yml

+6
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,12 @@ RSpec/Capybara/FeatureMethods:
832832
VersionChanged: '2.0'
833833
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Capybara/FeatureMethods
834834

835+
RSpec/Capybara/SpecificMatcher:
836+
Description: Checks for there is a more specific matcher offered by Capybara.
837+
Enabled: pending
838+
VersionAdded: '2.12'
839+
Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Capybara/SpecificMatcher
840+
835841
RSpec/Capybara/VisibilityMatcher:
836842
Description: Checks for boolean visibility in Capybara finders.
837843
Enabled: true

docs/modules/ROOT/pages/cops.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393

9494
* xref:cops_rspec_capybara.adoc#rspeccapybara/currentpathexpectation[RSpec/Capybara/CurrentPathExpectation]
9595
* xref:cops_rspec_capybara.adoc#rspeccapybara/featuremethods[RSpec/Capybara/FeatureMethods]
96+
* xref:cops_rspec_capybara.adoc#rspeccapybara/specificmatcher[RSpec/Capybara/SpecificMatcher]
9697
* xref:cops_rspec_capybara.adoc#rspeccapybara/visibilitymatcher[RSpec/Capybara/VisibilityMatcher]
9798

9899
=== Department xref:cops_rspec_factorybot.adoc[RSpec/FactoryBot]

docs/modules/ROOT/pages/cops_rspec_capybara.adoc

+39
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,45 @@ end
106106

107107
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Capybara/FeatureMethods
108108

109+
== RSpec/Capybara/SpecificMatcher
110+
111+
|===
112+
| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed
113+
114+
| Pending
115+
| Yes
116+
| No
117+
| 2.12
118+
| -
119+
|===
120+
121+
Checks for there is a more specific matcher offered by Capybara.
122+
123+
=== Examples
124+
125+
[source,ruby]
126+
----
127+
# bad
128+
expect(page).to have_selector('button')
129+
expect(page).to have_no_selector('button.cls')
130+
expect(page).to have_css('button')
131+
expect(page).to have_no_css('a.cls', exact_text: 'foo')
132+
expect(page).to have_css('table.cls')
133+
expect(page).to have_css('select')
134+
135+
# good
136+
expect(page).to have_button
137+
expect(page).to have_no_button(class: 'cls')
138+
expect(page).to have_button
139+
expect(page).to have_no_link('foo', class: 'cls')
140+
expect(page).to have_table(class: 'cls')
141+
expect(page).to have_select
142+
----
143+
144+
=== References
145+
146+
* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Capybara/SpecificMatcher
147+
109148
== RSpec/Capybara/VisibilityMatcher
110149

111150
|===
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module RSpec
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', exact_text: 'foo')
16+
# expect(page).to have_css('table.cls')
17+
# expect(page).to have_css('select')
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')
24+
# expect(page).to have_table(class: 'cls')
25+
# expect(page).to have_select
26+
#
27+
class SpecificMatcher < Base
28+
MSG = 'Prefer `%<good_matcher>s` over `%<bad_matcher>s`.'
29+
RESTRICT_ON_SEND = %i[have_selector have_no_selector have_css
30+
have_no_css].freeze
31+
SPECIFIC_MATCHER = {
32+
'button' => 'button',
33+
'a' => 'link',
34+
'table' => 'table',
35+
'select' => 'select'
36+
}.freeze
37+
38+
# @!method first_argument(node)
39+
def_node_matcher :first_argument, <<-PATTERN
40+
(send nil? _ (str $_) ... )
41+
PATTERN
42+
43+
def on_send(node)
44+
return unless (arg = first_argument(node))
45+
return unless (matcher = specific_matcher(arg))
46+
return if acceptable_pattern?(arg)
47+
48+
add_offense(node, message: message(node, matcher))
49+
end
50+
51+
private
52+
53+
def specific_matcher(arg)
54+
splitted_arg = arg[/^\w+/, 0]
55+
SPECIFIC_MATCHER[splitted_arg]
56+
end
57+
58+
def acceptable_pattern?(arg)
59+
arg.match?(/\[.+=\w+\]/)
60+
end
61+
62+
def message(node, matcher)
63+
format(MSG,
64+
good_matcher: good_matcher(node, matcher),
65+
bad_matcher: node.method_name)
66+
end
67+
68+
def good_matcher(node, matcher)
69+
node.method_name
70+
.to_s
71+
.gsub(/selector|css/, matcher.to_s)
72+
end
73+
end
74+
end
75+
end
76+
end
77+
end

lib/rubocop/cop/rspec_cops.rb

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require_relative 'rspec/capybara/current_path_expectation'
44
require_relative 'rspec/capybara/feature_methods'
5+
require_relative 'rspec/capybara/specific_matcher'
56
require_relative 'rspec/capybara/visibility_matcher'
67

78
require_relative 'rspec/factory_bot/attribute_defined_statically'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::RSpec::Capybara::SpecificMatcher do
4+
it 'does not register an offense for abstract matcher when ' \
5+
'first argument is not a replaceable element' do
6+
expect_no_offenses(<<-RUBY)
7+
expect(page).to have_selector('article')
8+
expect(page).to have_no_selector('body')
9+
expect(page).to have_css('tbody')
10+
RUBY
11+
end
12+
13+
it 'does not register an offense for abstract matcher when ' \
14+
'first argument is not an element' do
15+
expect_no_offenses(<<-RUBY)
16+
expect(page).to have_no_css('.a')
17+
expect(page).to have_selector('#button')
18+
expect(page).to have_no_selector('[table]')
19+
RUBY
20+
end
21+
22+
it 'registers an offense when using `have_selector`' do
23+
expect_offense(<<-RUBY)
24+
expect(page).to have_selector('button')
25+
^^^^^^^^^^^^^^^^^^^^^^^ Prefer `have_button` over `have_selector`.
26+
expect(page).to have_selector('a')
27+
^^^^^^^^^^^^^^^^^^ Prefer `have_link` over `have_selector`.
28+
expect(page).to have_selector('table')
29+
^^^^^^^^^^^^^^^^^^^^^^ Prefer `have_table` over `have_selector`.
30+
expect(page).to have_selector('select')
31+
^^^^^^^^^^^^^^^^^^^^^^^ Prefer `have_select` over `have_selector`.
32+
RUBY
33+
end
34+
35+
it 'registers an offense when using `have_no_selector`' do
36+
expect_offense(<<-RUBY)
37+
expect(page).to have_no_selector('button')
38+
^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `have_no_button` over `have_no_selector`.
39+
RUBY
40+
end
41+
42+
it 'registers an offense when using `have_css`' do
43+
expect_offense(<<-RUBY)
44+
expect(page).to have_css('button')
45+
^^^^^^^^^^^^^^^^^^ Prefer `have_button` over `have_css`.
46+
RUBY
47+
end
48+
49+
it 'registers an offense when using `have_no_css`' do
50+
expect_offense(<<-RUBY)
51+
expect(page).to have_no_css('button')
52+
^^^^^^^^^^^^^^^^^^^^^ Prefer `have_no_button` over `have_no_css`.
53+
RUBY
54+
end
55+
56+
it 'registers an offense when using abstract matcher and other args' do
57+
expect_offense(<<-RUBY)
58+
expect(page).to have_css('button', exact_text: 'foo')
59+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `have_button` over `have_css`.
60+
RUBY
61+
end
62+
63+
it 'registers an offense when using abstract matcher with state' do
64+
expect_offense(<<-RUBY)
65+
expect(page).to have_css('button[disabled]', exact_text: 'foo')
66+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `have_button` over `have_css`.
67+
expect(page).to have_css('button:not([disabled])', exact_text: 'bar')
68+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `have_button` over `have_css`.
69+
RUBY
70+
end
71+
72+
it 'does not register an offense for abstract matcher when ' \
73+
'first argument is element with nonreplaceable attributes' do
74+
expect_no_offenses(<<-RUBY)
75+
expect(page).to have_css('button[foo=bar]')
76+
expect(page).to have_css('button[foo-bar=baz]', exact_text: 'foo')
77+
RUBY
78+
end
79+
80+
it 'does not register an offense for abstract matcher when ' \
81+
'first argument is dstr' do
82+
expect_no_offenses(<<-'RUBY')
83+
expect(page).to have_css(%{a[href="#{foo}"]}, text: "bar")
84+
RUBY
85+
end
86+
end

0 commit comments

Comments
 (0)