Skip to content

Commit fe063f6

Browse files
authored
Merge pull request #80 from rubocop/fix78-2
Fix an invalid attributes parse when name with multiple `[]`
2 parents 3a749b0 + 111dad2 commit fe063f6

File tree

7 files changed

+128
-42
lines changed

7 files changed

+128
-42
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Change to default `EnforcedStyle: link_or_button` for `Capybara/ClickLinkOrButtonStyle` cop. ([@ydah])
66
- Fix a false negative for `RSpec/HaveSelector` when first argument is dstr node. ([@ydah])
7+
- Fix an invalid attributes parse when name with multiple `[]` for `Capybara/SpecificFinders` and `Capybara/SpecificActions` and `Capybara/SpecificMatcher`. ([@ydah])
78

89
## 2.19.0 (2023-09-20)
910

lib/rubocop-capybara.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require 'rubocop'
77

88
require_relative 'rubocop/cop/capybara/mixin/capybara_help'
9+
require_relative 'rubocop/cop/capybara/mixin/css_attributes_parser'
910
require_relative 'rubocop/cop/capybara/mixin/css_selector'
1011

1112
require_relative 'rubocop/cop/capybara_cops'
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Capybara
6+
# Css selector parser.
7+
# @api private
8+
class CssAttributesParser
9+
def initialize(selector)
10+
@selector = selector
11+
@state = :initial
12+
@temp = ''
13+
@results = {}
14+
@bracket_count = 0
15+
end
16+
17+
# @return [Array<String>]
18+
def parse # rubocop:disable Metrics/MethodLength
19+
@selector.chars do |char|
20+
if char == '['
21+
on_bracket_start
22+
elsif char == ']'
23+
on_bracket_end
24+
elsif @state == :inside_attr
25+
@temp += char
26+
end
27+
end
28+
@results
29+
end
30+
31+
private
32+
33+
def on_bracket_start
34+
@bracket_count += 1
35+
if @state == :initial
36+
@state = :inside_attr
37+
else
38+
@temp += '['
39+
end
40+
end
41+
42+
def on_bracket_end
43+
@bracket_count -= 1
44+
if @bracket_count.zero?
45+
@state = :initial
46+
key, value = @temp.split('=')
47+
@results[key] = normalize_value(value)
48+
@temp.clear
49+
else
50+
@temp += ']'
51+
end
52+
end
53+
54+
# @param value [String]
55+
# @return [Boolean, String]
56+
# @example
57+
# normalize_value('true') # => true
58+
# normalize_value('false') # => false
59+
# normalize_value(nil) # => nil
60+
# normalize_value("foo") # => "'foo'"
61+
def normalize_value(value)
62+
case value
63+
when 'true' then true
64+
when 'false' then false
65+
when nil then nil
66+
else "'#{value.gsub(/"|'/, '')}'"
67+
end
68+
end
69+
end
70+
end
71+
end
72+
end

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

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,9 @@ def attribute?(selector)
5555
# attributes('a[foo-bar_baz]') # => {"foo-bar_baz=>nil}
5656
# attributes('button[foo][bar=baz]') # => {"foo"=>nil, "bar"=>"'baz'"}
5757
# attributes('table[foo=bar]') # => {"foo"=>"'bar'"}
58+
# attributes('[foo="bar[baz][qux]"]') # => {"foo"=>"'bar[baz][qux]'"}
5859
def attributes(selector)
59-
# Extract the inner strings of attributes.
60-
# For example, extract the following:
61-
# 'button[foo][bar=baz]' => 'foo][bar=baz'
62-
inside_attributes = selector.scan(/\[(.*)\]/).flatten.join
63-
inside_attributes.split('][').to_h do |attr|
64-
key, value = attr.split('=')
65-
[key, normalize_value(value)]
66-
end
60+
CssAttributesParser.new(selector).parse
6761
end
6862

6963
# @param selector [String]
@@ -89,22 +83,6 @@ def multiple_selectors?(selector)
8983
normalize = selector.gsub(/(\\[>,+~]|\(.*\))/, '')
9084
normalize.match?(/[ >,+~]/)
9185
end
92-
93-
# @param value [String]
94-
# @return [Boolean, String]
95-
# @example
96-
# normalize_value('true') # => true
97-
# normalize_value('false') # => false
98-
# normalize_value(nil) # => nil
99-
# normalize_value("foo") # => "'foo'"
100-
def normalize_value(value)
101-
case value
102-
when 'true' then true
103-
when 'false' then false
104-
when nil then nil
105-
else "'#{value.gsub(/"|'/, '')}'"
106-
end
107-
end
10886
end
10987
end
11088
end
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe RuboCop::Cop::Capybara::CssAttributesParser do
4+
describe 'CssSelector.new.parse' do
5+
it 'returns attributes hash when specify attributes' do
6+
expect(described_class.new('a[foo-bar_baz]').parse).to eq(
7+
'foo-bar_baz' => nil
8+
)
9+
expect(described_class.new('table[foo=bar]').parse).to eq(
10+
'foo' => "'bar'"
11+
)
12+
end
13+
14+
it 'returns attributes hash when specify multiple attributes' do
15+
expect(described_class.new('button[foo][bar=baz]').parse).to eq(
16+
'foo' => nil, 'bar' => "'baz'"
17+
)
18+
end
19+
20+
it 'returns attributes hash when specify nested attributes' do
21+
expect(described_class.new('[foo="bar[baz]"]').parse).to eq(
22+
'foo' => "'bar[baz]'"
23+
)
24+
end
25+
26+
it 'returns attributes hash when specify nested and include ' \
27+
'multiple bracket' do
28+
expect(described_class.new('[foo="bar[baz][qux]"]').parse).to eq(
29+
'foo' => "'bar[baz][qux]'"
30+
)
31+
end
32+
33+
it 'returns empty hash when specify not include attributes' do
34+
expect(described_class.new('h1.cls#id').parse).to eq({})
35+
end
36+
end
37+
end

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

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@
9090
)
9191
end
9292

93+
it 'returns attributes hash when specify nested and include ' \
94+
'multiple bracket' do
95+
expect(described_class.attributes('[foo="bar[baz][qux]"]')).to eq(
96+
'foo' => "'bar[baz][qux]'"
97+
)
98+
end
99+
93100
it 'returns empty hash when specify not include attributes' do
94101
expect(described_class.attributes('h1.cls#id')).to eq({})
95102
end
@@ -136,22 +143,4 @@
136143
expect(described_class.multiple_selectors?('a.cls\>b')).to be false
137144
end
138145
end
139-
140-
describe 'CssSelector.normalize_value' do
141-
it 'returns true when "true"' do
142-
expect(described_class.normalize_value('true')).to be true
143-
end
144-
145-
it 'returns false when "false"' do
146-
expect(described_class.normalize_value('false')).to be false
147-
end
148-
149-
it 'returns nil when nil' do
150-
expect(described_class.normalize_value(nil)).to be_nil
151-
end
152-
153-
it "returns \"'string'\" when 'string'" do
154-
expect(described_class.normalize_value('foo')).to eq "'foo'"
155-
end
156-
end
157146
end

spec/rubocop/cop/capybara/specific_matcher_spec.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,14 @@
171171
RUBY
172172
end
173173

174+
it 'registers an offense when using abstract matcher with ' \
175+
'first argument is element with multiple brackets' do
176+
expect_offense(<<-RUBY)
177+
expect(page).to have_css('button[name="bar[baz][qux]"]', exact_text: 'foo')
178+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `have_button` over `have_css`.
179+
RUBY
180+
end
181+
174182
it 'registers an offense when using abstract matcher with state' do
175183
expect_offense(<<-RUBY)
176184
expect(page).to have_css('button[disabled=true]', exact_text: 'foo')

0 commit comments

Comments
 (0)