Repository navigation
Derive CSS classes the way ActiveAdmin does - #53
Merged
Merged
Conversation
Three selectors each hand-rolled their own approximation of how
ActiveAdmin turns a label into a CSS class:
attributes_row_selector label.gsub(/[\s\/]/, '_').downcase
table_header_selector text.tr(' ', '_').downcase
table_cell_selector text.downcase.gsub(/[^a-z0-9\s]/, '')
.gsub(/\s+/, '_').squeeze('_')
ActiveAdmin does neither. It calls `parameterize(separator: '_')` --
`AttributesTable#row` (attributes_table.rb:30) and
`TableFor::Column#html_class` (table_for.rb:152) -- so the three drift
from the rendered markup, each differently:
label ActiveAdmin row selector cell selector
'VAT / TAX Number' vat_tax_number vat___tax_number vat_tax_number
'E-mail' e-mail e-mail email
"Customer's Name" customer_s_name customer's_name customers_name
'# of DIDs' of_dids #_of_dids _of_dids
'Price, USD' price_usd price,_usd price_usd
'DID Number' did_number did__number did_number
Route all three through `Util.css_class_for_label`, which makes the
same call ActiveAdmin makes, so a label can only ever produce the class
that was actually rendered.
Covered by unit examples for the labels above and, end to end, by a
`row('VAT / TAX Number')` added to the dummy's show page: with the old
derivation that spec looks for `tr.row.row-vat___tax_number` and finds
nothing.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The changed table-header normalization path lacks direct or system-level test coverage.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Aligns label-derived selectors with ActiveAdmin’s parameterize(separator: '_') behavior.
Changes:
- Adds shared label-to-CSS-class normalization.
- Updates table and attribute-row selectors.
- Adds unit and system regression coverage.
| File | Description |
|---|---|
lib/capybara/active_admin/util.rb |
Adds shared normalization helper. |
lib/capybara/active_admin/selectors/table.rb |
Normalizes table header and cell classes. |
lib/capybara/active_admin/selectors/attributes_table.rb |
Normalizes attribute-row classes. |
spec/lib/selectors/table_spec.rb |
Covers punctuation in cell labels. |
spec/lib/selectors/attributes_table_spec.rb |
Covers separator normalization. |
spec/dummy/test_application.rb |
Adds an awkward rendered label. |
spec/system/business_employee_show_spec.rb |
Verifies matching against rendered markup. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The third selector this PR rewrote had no examples at all — not one
table_header_selector or have_table_header anywhere in the suite — so
its normalization could have regressed unnoticed while the cell and
attribute-row cases stayed green.
Seven examples: generic form, a plain label, the three labels where the
previous `tr(' ', '_').downcase` drifted from parameterize, the
`column:` override taking the same path as the visible text, and the
sortable/sorted suffixes still being appended.
Restoring the old implementation fails four of them:
expected: col-vat_tax_number got: col-vat_/_tax_number
expected: col-customer_s_name got: col-customer's_name
expected: col-of_dids got: col-#_of_dids
senid231
approved these changes
Oct 1, 2026
Fivell
added a commit
that referenced
this pull request
Oct 1, 2026
…#55) Follow-up to #53, from a review of it. `parameterize` transliterates to Latin and drops the rest, so 'Имя', '名前', '№' and a blank string all come back as ''. ActiveAdmin makes the same call, so it renders `class="row row-"` for every one of them — and #53 made the gem build `tr.row.row-` to match, which matches them all at once: attributes_row_selector('Имя') => tr.row.row- > td attributes_row_selector('Телефон') => tr.row.row- > td On a page with both rows, `have_attribute_row('Имя', exact_text: ...)` passes against the phone row, and `within_attribute_row('Имя')` raises Capybara::Ambiguous. The derivation #53 replaced produced `row-имя`, matched nothing, and failed honestly. Turning a loud failure into a silent wrong match is the worse half of the trade, so the helper now raises with the label named and a suggestion to match on text. Also here, all from the same review: * `selectors/table.rb` and `selectors/attributes_table.rb` reference `Util` without requiring it. They load today only because `capybara/active_admin.rb` happens to require util one line earlier; requiring the selectors file directly raised NameError. * `table_header_selector(nil, column: 'x')` returned the generic header selector, discarding the override — so `find_table_header` matched the first header on the page whatever column was asked for. * `table_cell_selector(:full_name)` had no example, though that is the commonest input of all and the old derivation broke it (`col-fullname`). Added, plus examples for the new guard. * Two example names still said "strips slashes" / "strips other special characters", the rule #53 deleted — they passed only because those two labels collapse the same way under both. A maintainer reading them as the contract would reinstate the stripping. * CHANGELOG had nothing for #53 at all, in a released gem whose README commits to Keep a Changelog, including the selector changes a consumer needs to know about on `bundle update`.
Fivell
added a commit
that referenced
this pull request
Oct 1, 2026
Follow-up to #53, from the same review. #53 fixed how a *label* becomes a CSS class; these are the two remaining derivations of the same kind, where a *model* becomes one. Arbre builds a record's DOM name from `record.class.model_name.singular` (html/tag.rb:164-168) and ActiveAdmin builds the index table id from `active_admin_config.resource_name.plural` (index_as_table.rb:240) — both of which underscore a namespace. The gem used `gsub(' ', '_').downcase` plus an unconditional singularize/pluralize, which leaves the `::` alone: have_attributes_table(model: 'Billing::Employee') => div.attributes_table.billing::employee have_table(resource_name: Billing::Employee) => table#index_table_billing::employees Neither is a selector. Under cuprite the first aborts the example with Ferrum::JavaScriptError, under rack_test with Nokogiri::CSS::SyntaxError — in both cases with nothing naming the cause. `table_selector` also had no class branch at all, so passing a model class was broken for every namespaced model. Verified against real rendered pages: the dummy renders `<div class="attributes_table billing_employee">` and `<table id="index_table_business_employees">`, and the new system spec asserts both. Restoring the old derivation fails it. Note the two names have different sources: the table id follows the name a resource was *registered* under (`as: 'Business Employee'`), so a renamed resource cannot be addressed by its class. Documented at the call site rather than papered over. Also fixes six dead `have_table_cell` examples in README.md, which still showed a positional-text signature the matcher has never had — they raise TypeError or ArgumentError as written.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

The bug
Three selectors each hand-rolled their own approximation of how ActiveAdmin turns a label into a CSS class:
ActiveAdmin does none of those. It calls
parameterize(separator: '_'):AttributesTable#row—attributes_table.rb:30—classes << "row-#{title.to_s.parameterize(separator: "_")}"TableFor::Column#html_class—table_for.rb:152—html_classes << "col-#{@title.to_s.parameterize(separator: "_")}"So all three drift from the markup that was actually rendered, each in its own direction:
attributes_row_selectortable_cell_selector'VAT / TAX Number'vat_tax_numbervat___tax_number✗vat_tax_number'E-mail'e-maile-mailemail✗"Customer's Name"customer_s_namecustomer's_name✗customers_name✗'# of DIDs'of_dids#_of_dids✗_of_dids✗'Setup (one-time)'setup_one-timesetup_(one-time)✗setup_onetime✗'Price, USD'price_usdprice,_usd✗price_usd'DID Number'did_numberdid__number✗did_number'ISO 3166-1'iso_3166-1iso_3166-1iso_31661✗The failure is silent in the worst way:
have_table_cell(column: 'E-mail')just doesn't match, and the spec reads as "the cell is missing".This has already cost downstream users a workaround. #43 is the same bug seen from the
table_cell_selectorside, and at least one app carries a helper that pre-normalizes labels before handing them to the gem — which then breaks again when the gem's own normalization changes, because the currenttable_cell_selectorstrips_along with everything else.The fix
One helper that makes the same call ActiveAdmin makes:
All three selectors route through it, so a label can only ever produce the class that was rendered.
parameterizeis idempotent on its own output, so apps that pre-normalize as a workaround keep working and can drop it at their leisure.Verification
Unit examples for the labels in the table above, in both
spec/lib/selectors/table_spec.rbandspec/lib/selectors/attributes_table_spec.rb.End to end, the dummy's show page now has a deliberately awkward label:
and
spec/system/business_employee_show_spec.rbasserts the gem finds it. With the previous derivation restored, that spec fails against the real rendered page:Full suite: 27 examples, 0 failures.
Compatibility
Only labels that the old code mapped to a class ActiveAdmin never rendered change behaviour — those lookups were already finding nothing. Plain labels (
'Full name','ID','Country / Region','Price (USD)') are unaffected; the existing examples for them pass untouched.