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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- `attributes_table_selector`, `form_selector` and `table_selector` now ask the
model for its DOM name (`model_name.singular` / `.plural`, the same source
Arbre and ActiveAdmin use) instead of `gsub(' ', '_')`. A namespaced name
previously kept its `::` and produced `div.attributes_table.billing::employee`
— not a selector, so it aborted the example instead of failing it.
`table_selector` also accepts a model class now.

### Changed
- tested against Ruby 3.3, 3.4, 4.0 and Rails 8.0, 8.1 (EOL Rails 7.2 dropped from CI)

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,17 @@ RSpec.describe 'Users', js: true do

within_table_for('users') do
expect(page).to have_table_row(count: 2)
expect(page).to have_table_cell('John Doe')
expect(page).to have_table_cell(text: 'John Doe')

within_table_row(id: john.id) do
expect(page).to have_table_cell('John Doe', row_id: john.id)
expect(page).to have_table_cell('John Doe', row_id: john.id, col_name: 'Full Name')
expect(page).to_not have_table_cell('John Doe', row_id: john.id, col_name: 'Id')
expect(page).to have_table_cell(text: 'John Doe')
expect(page).to have_table_cell(text: 'John Doe', column: 'Full Name')
expect(page).to_not have_table_cell(text: 'John Doe', column: 'Id')
end

within_table_row(id: jane.id) do
expect(page).to_not have_table_cell('John Doe')
expect(page).to_not have_table_cell('John Doe', col_name: 'Full Name')
expect(page).to_not have_table_cell(text: 'John Doe')
expect(page).to_not have_table_cell(text: 'John Doe', column: 'Full Name')
end
end
end
Expand Down
8 changes: 6 additions & 2 deletions lib/capybara/active_admin/selectors/table.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ module Table
def table_selector(resource_name = nil)
return 'table.index_table' if resource_name.nil?

resource_name = resource_name.to_s.gsub(' ', '_').pluralize.downcase
"table#index_table_#{resource_name}"
# NOTE: ActiveAdmin builds this id from the registered resource name
# (`active_admin_config.resource_name.plural`, index_as_table.rb:240),
# which `register Model, as: 'Other Name'` detaches from the model.
# A class can only ever yield the model's own name, so a renamed
# resource has to be addressed by the name it was registered under.
"table#index_table_#{Util.parse_model_name(resource_name, singular: false)}"
end

# @param record_id [String, Integer, nil] record ID.
Expand Down
23 changes: 19 additions & 4 deletions lib/capybara/active_admin/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,27 @@ module ActiveAdmin
module Util
# Common pure utility functions

# 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). Ask the
# model the same question instead of approximating it: `gsub(' ', '_')`
# leaves the `::` of a namespaced name in place, and
# `div.attributes_table.billing::employee` is not a selector -- Nokogiri
# raises `SyntaxError` on it with nothing pointing at the cause.
def parse_model_name(model_name, singular: true)
return if model_name.nil?

model_name = model_name.model_name.singular if model_name.is_a?(Class)
model_name = model_name.to_s.gsub(' ', '_').downcase
singular ? model_name.singularize : model_name.pluralize
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)
end

def dom_model_name(name, singular: true)
name = name.to_s.underscore.tr('/', '_').tr(' ', '_')
singular ? name.singularize : name.pluralize
end

# ActiveAdmin derives the CSS class of a row or column from the
Expand All @@ -28,7 +43,7 @@ def options_with_text(text, options = {})
options.except(:exact).merge(key => text)
end

module_function :parse_model_name, :css_class_for_label, :options_with_text
module_function :parse_model_name, :dom_model_name, :css_class_for_label, :options_with_text
end
end
end
15 changes: 15 additions & 0 deletions spec/lib/selectors/attributes_table_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@
Class.new { include Capybara::ActiveAdmin::Selectors::AttributesTable }.new
end

describe '#attributes_table_selector' do
it 'asks a model class for its DOM name' do
expect(helper.attributes_table_selector(model: Billing::Employee))
.to eq('div.attributes_table.billing_employee')
end

# Arbre uses `model_name.singular` — underscore, then '/' -> '_'. The
# previous `gsub(' ', '_')` kept the '::' and Nokogiri raised on
# `div.attributes_table.billing::employee`.
it 'underscores a namespaced name given as a string' do
expect(helper.attributes_table_selector(model: 'Billing::Employee'))
.to eq('div.attributes_table.billing_employee')
end
end

describe '#attributes_row_selector' do
it 'returns a generic row selector when no label given' do
expect(helper.attributes_row_selector).to eq('tr.row > td')
Expand Down
21 changes: 21 additions & 0 deletions spec/lib/selectors/table_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@
end
end

describe '#table_selector' do
it 'returns the generic selector when no resource name given' do
expect(helper.table_selector).to eq('table.index_table')
end

it 'builds the id from a registered resource name' do
expect(helper.table_selector('Business Employee')).to eq('table#index_table_business_employees')
end

# ActiveAdmin's id comes from `resource_name.plural`, which underscores the
# namespace. `gsub(' ', '_')` left the `::` in place, and Nokogiri raises
# on `table#index_table_billing::employees`.
it 'underscores a namespaced name instead of leaving ::' do
expect(helper.table_selector('Billing::Employee')).to eq('table#index_table_billing_employees')
end

it 'asks a model class for its own plural' do
expect(helper.table_selector(Billing::Employee)).to eq('table#index_table_billing_employees')
end
end

describe '#table_cell_selector' do
it 'returns generic selector when column is nil' do
expect(helper.table_cell_selector).to eq('td.col')
Expand Down
22 changes: 22 additions & 0 deletions spec/system/namespaced_model_names_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

# ActiveAdmin and Arbre derive these DOM names from the model and the
# registered resource name. Asserting them against a real rendered page is the
# only way to catch a derivation that merely looks plausible: the previous one
# produced `billing::employee`, which is not a selector at all.
RSpec.describe 'DOM names of a namespaced resource', type: :feature, js: true do
let!(:record) { Billing::Employee.create!(full_name: 'John Doe', salary: 100) }

it 'finds the attributes table from the model class and from its name' do
visit admin_business_employee_path(record.id)

expect(page).to have_attributes_table(model: Billing::Employee)
expect(page).to have_attributes_table(model: 'Billing::Employee')
end

it 'finds the index table by the name the resource was registered under' do
visit admin_business_employees_path

expect(page).to have_table(resource_name: 'Business Employee')
end
end
Loading