Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Changed
- `table_cell_selector`, `table_header_selector` and `attributes_row_selector`
now derive the CSS class with `parameterize(separator: '_')`, the same call
ActiveAdmin makes in `AttributesTable#row` and `TableFor::Column#html_class`,
instead of three different hand-rolled substitutions. Labels containing
punctuation change selector: `'E-mail'` was `col-email` and is now
`col-e-mail`, `"Customer's Name"` was `col-customers_name` and is now
`col-customer_s_name`, `:full_name` was `col-fullname` and is now
`col-full_name`. Previously these matched nothing (or raised
`Nokogiri::CSS::SyntaxError`), so an app carrying a workaround that
pre-normalized labels before calling these helpers should drop it.
- `table_header_selector(nil, column: 'x')` now honours the override instead
of returning the generic header selector.
- tested against Ruby 3.3, 3.4, 4.0 and Rails 8.0, 8.1 (EOL Rails 7.2 dropped from CI)

### Fixed
- a label that `parameterize` cannot transliterate (`'Имя'`, `'名前'`, `'№'`, or
a blank string) now raises `ArgumentError` instead of building a bare
`.row-` / `.col-` selector. ActiveAdmin renders the same empty class for
every such label, so that selector matched all of them at once and the
matcher could pass against the wrong row.

## [1.0.0] - 2026-04-09

### Added
Expand Down
2 changes: 2 additions & 0 deletions lib/capybara/active_admin/selectors/attributes_table.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'capybara/active_admin/util'

module Capybara
module ActiveAdmin
module Selectors
Expand Down
4 changes: 3 additions & 1 deletion lib/capybara/active_admin/selectors/table.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'capybara/active_admin/util'

module Capybara
module ActiveAdmin
module Selectors
Expand Down Expand Up @@ -29,7 +31,7 @@ def table_row_selector(record_id = nil)
# @option sort_direction [String, nil] sort direction ('asc' or 'desc').
# @return selector.
def table_header_selector(text = nil, options = {})
return 'thead > tr > th.col' if text.nil?
return 'thead > tr > th.col' if text.nil? && options[:column].nil?

column = Util.css_class_for_label(options[:column] || text)
selector = "th.col.col-#{column}"
Expand Down
15 changes: 14 additions & 1 deletion lib/capybara/active_admin/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,20 @@ def parse_model_name(model_name, singular: true)
# that call rather than approximating it, so every label lands on
# the class ActiveAdmin actually rendered.
def css_class_for_label(label)
label.to_s.parameterize(separator: '_')
css_class = label.to_s.parameterize(separator: '_')
return css_class if css_class.present?

# `parameterize` transliterates to Latin and drops the rest, so any
# label without Latin-transliterable characters -- 'Имя', '名前', '№',
# or a blank string -- comes back empty. ActiveAdmin makes the same
# call, so it renders `class="row row-"` for all of them, and a
# `.row-` selector would match every one at once: the matcher would
# pass against the wrong row instead of failing. Refuse to build it.
raise ArgumentError,
"cannot derive a CSS class from #{label.inspect}: " \
'ActiveAdmin parameterizes it to an empty string, so every such ' \
'row or column shares one class and cannot be told apart. ' \
'Match on text, or pass the attribute name instead of the label.'
end

def options_with_text(text, options = {})
Expand Down
15 changes: 14 additions & 1 deletion spec/lib/selectors/attributes_table_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@
end

describe '#attributes_row_selector' do
# ActiveAdmin derives the class with the same `parameterize` call, so a
# label it cannot transliterate renders as a bare `row-` on every such
# row. Building that selector would match them all and pass against the
# wrong one, so it has to raise instead.
it 'refuses a label that parameterizes to nothing' do
expect { helper.attributes_row_selector('Имя') }
.to raise_error(ArgumentError, /cannot derive a CSS class from "Имя"/)
end

it 'refuses a blank label' do
expect { helper.attributes_row_selector(' ') }.to raise_error(ArgumentError)
end

it 'returns a generic row selector when no label given' do
expect(helper.attributes_row_selector).to eq('tr.row > td')
end
Expand All @@ -14,7 +27,7 @@
expect(helper.attributes_row_selector('Full name')).to eq('tr.row.row-full_name > td')
end

it 'replaces slash with underscore to match ActiveAdmin class generation' do
it 'substitutes a slash with the separator' do
expect(helper.attributes_row_selector('USA/UAH')).to eq('tr.row.row-usa_uah > td')
end

Expand Down
15 changes: 13 additions & 2 deletions spec/lib/selectors/table_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
.to eq('thead > tr > th.col.col-vat_tax_number')
end

it 'honours a column override even with no visible text' do
expect(helper.table_header_selector(nil, column: 'full_name'))
.to eq('thead > tr > th.col.col-full_name')
end

it 'appends the sortable and sorted classes' do
expect(helper.table_header_selector('Full Name', sortable: true, sort_direction: 'DESC'))
.to eq('thead > tr > th.col.col-full_name.sortable.sorted-desc')
Expand All @@ -47,6 +52,12 @@
expect(helper.table_cell_selector).to eq('td.col')
end

# `column :full_name` is the commonest input of all, and the derivation
# this replaced stripped the underscore out of it: col-fullname.
it 'keeps the underscore in a symbol attribute name' do
expect(helper.table_cell_selector(:full_name)).to eq('td.col.col-full_name')
end

it 'converts spaces to underscores' do
expect(helper.table_cell_selector('Full Name')).to eq('td.col.col-full_name')
end
Expand All @@ -55,11 +66,11 @@
expect(helper.table_cell_selector('ID')).to eq('td.col.col-id')
end

it 'strips slashes from column name' do
it 'substitutes a slash with the separator' do
expect(helper.table_cell_selector('Country / Region')).to eq('td.col.col-country_region')
end

it 'strips other special characters from column name' do
it 'substitutes bracket punctuation with the separator' do
expect(helper.table_cell_selector('Price (USD)')).to eq('td.col.col-price_usd')
end

Expand Down
Loading