Skip to content

Commit f198be4

Browse files
committed
Fix an incorrect autocorrect for Capybara/RedundantWithinFind when escape required css selector
Fix: #136 Previously, the following automatic corrections were made. before: ```ruby within find_by_id("array-form-session.dates") do expect(page).to have_text(:visible, "YYYY/MM/DD") end ``` after: ```ruby within "#array-form-session.dates" do expect(page).to have_text(:visible, "YYYY/MM/DD") end ``` This is `.' in find_by_id. ` has the same meaning as the escaped id. In other words, when replacing within, the `. ` must be escaped. This PR has been modified so that escaping is correctly done in selectors that require escaping. This will prevent the behavior from changing before and after the automatic correction.
1 parent 88eb2ba commit f198be4

File tree

5 files changed

+135
-3
lines changed

5 files changed

+135
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Add `Capybara/AmbiguousClick` cop and make soft-deprecated `Capybara/ClickLinkOrButtonStyle` cop. If you want to use `EnforcedStyle: strict`, use `Capybara/AmbiguousClick` cop instead. ([@ydah])
66
- Add new `Capybara/FindAllFirst` cop. ([@ydah])
77
- Fix an error for `Capybara/RSpec/HaveSelector` when passing no arguments. ([@earlopain])
8+
- Fix an incorrect autocorrect for `Capybara/RedundantWithinFind` when escape required css selector. ([@ydah])
89

910
## 2.21.0 (2024-06-08)
1011

lib/rubocop/cop/capybara/mixin/css_selector.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require 'strscan'
4+
35
module RuboCop
46
module Cop
57
module Capybara
@@ -83,6 +85,49 @@ def multiple_selectors?(selector)
8385
normalize = selector.gsub(/(\\[>,+~]|\(.*\))/, '')
8486
normalize.match?(/[ >,+~]/)
8587
end
88+
89+
# @param selector [String]
90+
# @return [String]
91+
# @example
92+
# css_escape('some-id') # => some-id
93+
# css_escape('some-id.with-priod') # => some-id\.with-priod
94+
# @reference
95+
# https://github.com/mathiasbynens/CSS.escape/blob/master/css.escape.js
96+
def css_escape(selector) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
97+
scanner = StringScanner.new(selector)
98+
result = +''
99+
100+
# Special case: if the selector is of length 1 and
101+
# the first character is `-`
102+
if selector.length == 1 && scanner.peek(1) == '-'
103+
return "\\#{selector}"
104+
end
105+
106+
until scanner.eos?
107+
# NULL character (U+0000)
108+
if scanner.scan(/\0/)
109+
result << "\uFFFD"
110+
# Control characters (U+0001 to U+001F, U+007F)
111+
elsif scanner.scan(/[\x01-\x1F\x7F]/)
112+
result << "\\#{scanner.matched.ord.to_s(16)} "
113+
# First character is a digit (U+0030 to U+0039)
114+
elsif scanner.pos.zero? && scanner.scan(/\d/)
115+
result << "\\#{scanner.matched.ord.to_s(16)} "
116+
# Second character is a digit and first is `-`
117+
elsif scanner.pos == 1 && scanner.scan(/\d/) &&
118+
scanner.pre_match == '-'
119+
result << "\\#{scanner.matched.ord.to_s(16)} "
120+
# Alphanumeric characters, `-`, `_`
121+
elsif scanner.scan(/[a-zA-Z0-9\-_]/)
122+
result << scanner.matched
123+
# Any other characters, escape them
124+
elsif scanner.scan(/./)
125+
result << "\\#{scanner.matched}"
126+
end
127+
end
128+
129+
result
130+
end
86131
end
87132
end
88133
end

lib/rubocop/cop/capybara/redundant_within_find.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,15 @@ def msg(node)
5353
end
5454

5555
def replaced(node)
56-
replaced = node.arguments.map(&:source).join(', ')
5756
if node.method?(:find_by_id)
58-
replaced.sub(/\A(["'])/, '\1#')
57+
node.first_argument.source.match(/\A(['"])(.*)['"]\z/) do
58+
quote = ::Regexp.last_match(1)
59+
css = ::Regexp.last_match(2)
60+
return ["#{quote}##{CssSelector.css_escape(css)}#{quote}",
61+
*node.arguments[1..].map(&:source)].join(', ')
62+
end
5963
else
60-
replaced
64+
node.arguments.map(&:source).join(', ')
6165
end
6266
end
6367
end

spec/rubocop/cop/capybara/mixin/css_selector_spec.rb

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,73 @@
143143
expect(described_class.multiple_selectors?('a.cls\>b')).to be false
144144
end
145145
end
146+
147+
describe 'CssSelector.css_escape' do
148+
context 'when selector is a single hyphen' do
149+
let(:selector) { '-' }
150+
151+
it 'escapes the hyphen character' do
152+
expect(described_class.css_escape(selector)).to eq('\\-')
153+
end
154+
end
155+
156+
context 'when selector contains NULL character' do
157+
let(:selector) { "abc\0def" }
158+
159+
it 'replaces NULL character with U+FFFD' do
160+
expect(described_class.css_escape(selector)).to eq('abc�def')
161+
end
162+
end
163+
164+
context 'when selector contains control characters' do
165+
let(:selector) { "abc\x01\x1F\x7Fdef" }
166+
167+
it 'escapes control characters as hexadecimal with a trailing space' do
168+
expect(described_class.css_escape(selector))
169+
.to eq('abc\\1 \\1f \\7f def')
170+
end
171+
end
172+
173+
context 'when selector starts with a digit' do
174+
let(:selector) { '1abc' }
175+
176+
it 'escapes the starting digit as hexadecimal with a trailing space' do
177+
expect(described_class.css_escape(selector)).to eq('\\31 abc')
178+
end
179+
end
180+
181+
context 'when selector starts with a hyphen followed by a digit' do
182+
let(:selector) { '-1abc' }
183+
184+
it 'escapes the digit following a hyphen as hexadecimal with a ' \
185+
'trailing space' do
186+
expect(described_class.css_escape(selector)).to eq('-\\31 abc')
187+
end
188+
end
189+
190+
context 'when selector contains alphanumeric characters, hyphen, or ' \
191+
'underscore' do
192+
let(:selector) { 'a-Z_0-9' }
193+
194+
it 'does not escape alphanumeric characters, hyphen, or underscore' do
195+
expect(described_class.css_escape(selector)).to eq('a-Z_0-9')
196+
end
197+
end
198+
199+
context 'when selector contains special characters needing escape' do
200+
let(:selector) { 'a!b@c#d$' }
201+
202+
it 'escapes special characters' do
203+
expect(described_class.css_escape(selector)).to eq('a\\!b\\@c\\#d\\$')
204+
end
205+
end
206+
207+
context 'when selector contains mixed cases' do
208+
let(:selector) { "ab\x00\x7F" }
209+
210+
it 'handles mixed cases appropriately' do
211+
expect(described_class.css_escape(selector)).to eq('ab�\\7f ')
212+
end
213+
end
214+
end
146215
end

spec/rubocop/cop/capybara/redundant_within_find_spec.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@
6666
RUBY
6767
end
6868

69+
it 'registers an offense when using `within find_by_id("foo.bar")`' do
70+
expect_offense(<<~RUBY)
71+
within find_by_id('foo.bar') do
72+
^^^^^^^^^^^^^^^^^^^^^ Redundant `within find_by_id(...)` call detected.
73+
end
74+
RUBY
75+
76+
expect_correction(<<~'RUBY')
77+
within '#foo\.bar' do
78+
end
79+
RUBY
80+
end
81+
6982
it 'registers an offense when using `within find_by_id(...)` with ' \
7083
'other argument' do
7184
expect_offense(<<~RUBY)

0 commit comments

Comments
 (0)