Skip to content

Ask the model for its DOM name instead of approximating it - #56

Merged
Fivell merged 1 commit into
masterfrom
fix/model-dom-names
Oct 1, 2026
Merged

Fivell merged 1 commit into
masterfrom
fix/model-dom-names

Conversation

@Fivell

@Fivell Fivell commented Oct 1, 2026

Copy link
Copy Markdown
Member

Follow-up to #53, from the same review. #53 fixed how a label becomes a CSS class; this is the other half — how a model becomes one.

The bug

Arbre derives a record's DOM name from record.class.model_name.singular, falling back to name.underscore.gsub('/', '_') (arbre 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 underscore a namespace.

The gem used gsub(' ', '_').downcase plus an unconditional singularize/pluralize, which leaves :: in place:

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 pointing at the cause, so it reads as a driver problem rather than a bad selector.

table_selector also had no class branch at all: it called .to_s on whatever it was given, so passing a model class was broken for every namespaced model.

Secondary: the unconditional singularize mangles irregulars even on the happy path — parse_model_name('Statistics') returned statistic.

What it renders, measured

Printed from the dummy app with a real browser rather than reasoned about:

<table id="index_table_business_employees">
<div class="attributes_table billing_employee">

Note the two come from different sources: the table id follows the name the resource was registered under (register Billing::Employee, as: 'Business Employee'), while the attributes-table class follows the model class. A renamed resource therefore cannot be addressed by its class — that is ActiveAdmin's design, not something this PR can fix, so it is documented at the call site instead of papered over.

The fix

parse_model_name asks the model when it can, and otherwise mirrors Arbre's fallback:

if model_name.is_a?(Class) && model_name.respond_to?(:model_name)
  name = model_name.model_name
  return singular ? name.singular : name.plural
end

dom_model_name(model_name.is_a?(Class) ? model_name.name : model_name, singular: singular)

with dom_model_name doing underscore.tr('/', '_').tr(' ', '_'). table_selector now routes through it instead of carrying its own copy.

Verification

'Business Employee'  -> table#index_table_business_employees   (unchanged)
'Billing::Employee'  -> table#index_table_billing_employees    (was billing::employees)
Billing::Employee    -> table#index_table_billing_employees    (was billing::employees)
model: Billing::Employee  -> div.attributes_table.billing_employee   (unchanged)
model: 'Billing::Employee' -> div.attributes_table.billing_employee  (was billing::employee)

Six unit examples, plus spec/system/namespaced_model_names_spec.rb asserting both names against a real rendered page — restoring the old derivation fails it:

Failure/Error: expect(page).to have_attributes_table(model: 'Billing::Employee')
Ferrum::JavaScriptError

Suite: 42 examples, 0 failures.

Also

Six dead have_table_cell examples in README.md (lines 58-68). The matcher has always been have_table_cell(options = {}) reading :column, but the README shows have_table_cell('John Doe', row_id: …, col_name: …), which raises TypeError: no implicit conversion of Symbol into String or ArgumentError: wrong number of arguments. docs/guide/README.md already uses the correct form; only the top-level README was stale.

Depends on

Nothing, but it overlaps #55 in spirit — that one adds the missing require 'capybara/active_admin/util' to these same two selector files. Different hunks, so they do not conflict; either order merges.

Left out deliberately

The review also flagged table_scope_selector as a bare li.scope while ActiveAdmin::Scope renders its id as a class. I measured it on a real page:

<li class="scope on_off_payroll selected">   text: "On / Off payroll (1)"

have_table_scope(title) keys on exact_text: title, which cannot match while scopes_show_count is on by default. But the class is not reliably derivable from the title either — Scope#initialize takes the id from the method when one is given (scope 'Pending Review', :awaiting → awaiting), and only otherwise from the name. Fixing it properly means deciding on an API, so it wants a maintainer's call rather than a guess from me.

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.
@Fivell
Fivell merged commit 81d6891 into master Oct 1, 2026
25 checks passed
@Fivell
Fivell deleted the fix/model-dom-names branch October 1, 2026 13:35
@Fivell Fivell mentioned this pull request Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant