Adapt specs to capybara_accessible_selectors 0.16

Refactors the custom `:list`/`:list_item` selectors onto CAS's
`add_role_selector` and drops the now-native `Capybara::Node::Simple#role`
hack. `:list_item` resolves the listitem role, so specs matching
non-listitem `<li>` are updated (`have_row` for the custom field table,
presentation dividers via CSS).

CAS 0.16 also resolves role selectors by computed role and accessible
name, which our CSS-styled tables and Primer menus do not expose through
plain markup. Affected feature specs switch to robust locators:
Capybara's `:table_row`, the FullCalendar `data-date`, and the action
menu's real accessible name ("Edit status").

The `:columnheader` `colindex` filter lost its off-by-one offset and now
uses the true 1-based `th` position. Also fixes Webhooks `RowComponent`
spec capitalization.
This commit is contained in:
Alexander Brandon Coles
2026-05-29 19:29:55 +02:00
parent 0b4bcb5d3c
commit 86e40ffffe
9 changed files with 23 additions and 51 deletions
@@ -259,8 +259,8 @@ RSpec.describe Backlogs::SprintComponent, type: :component do
rendered_component
expect(menu_items).to eq(["Edit sprint", "Add work package", "Sprint board", "Burndown chart"])
expect(page).to have_list_item position: 2, role: "presentation"
expect(page).to have_list_item position: 4, role: "presentation"
# The three item groups are separated by presentation-only dividers.
expect(page).to have_css("li[role='presentation']", count: 2)
end
end
end
@@ -67,6 +67,7 @@ RSpec.describe "OAuth Access Grant Nudge upon adding a storage to a project",
it "adds a storage, nudges the project admin to grant OAuth access" do
visit external_file_storages_project_settings_project_storages_path(project_id: project)
expect(page).to have_heading "Files"
click_on("Storage")
@@ -76,8 +77,7 @@ RSpec.describe "OAuth Access Grant Nudge upon adding a storage to a project",
expect(page).to have_checked_field("New folder with automatically managed permissions")
click_on("Add")
expect(page).to have_heading "Files"
expect(page).to have_text(storage.name)
expect(page).to have_selector(:table_row, [storage.name])
within_test_selector("oauth-access-grant-nudge-modal") do
expect(page).to be_axe_clean
@@ -69,7 +69,9 @@ module Components
def drag_wp_to_date(work_package, date)
wp_card = card(work_package)
day_header = page.find(:columnheader, exact_text: date.strftime("%02d %A"))
# The FullCalendar timeline day headers are not exposed as columnheaders
# (their table has a presentation role), so target the labelled slot by date.
day_header = page.find(".fc-timeline-slot-label[data-date='#{date.iso8601}']")
drag_n_drop_element(from: wp_card, to: day_header, offset_y: day_header.native.size.height)
end
@@ -95,8 +95,8 @@ RSpec.describe Webhooks::Outgoing::Webhooks::RowComponent, type: :component do
it "renders events list grouped by Resource", :aggregate_failures do
expect(rendered_component).to have_list do |list|
expect(list).to have_list_item count: 2
expect(list).to have_list_item "Projects (created)"
expect(list).to have_list_item "Work package comments (comment)"
expect(list).to have_list_item "Projects (Created)"
expect(list).to have_list_item "Work package comments (Comment)"
end
end
end
@@ -112,9 +112,8 @@ RSpec.describe OpPrimer::ListComponent, type: :component do
end
it "renders list items" do
expect(rendered_component).to have_list_item count: 3
expect(rendered_component).to have_list_item count: 2
expect(rendered_component).to have_list_item position: 1, text: "List item 1"
expect(rendered_component).to have_list_item position: 2, role: "presentation"
expect(rendered_component).to have_list_item position: 3, text: "List item 2"
end
end
@@ -144,7 +144,7 @@ RSpec.describe "work package custom fields of type hierarchy", :js do
# Finally, we delete the custom field ... I'm done with this ...
custom_field_index_page.visit!
expect(page).to have_list_item(hierarchy_name)
expect(page).to have_row(hierarchy_name)
within("tr", text: hierarchy_name) { accept_prompt { click_on "Delete" } }
expect(page).to have_no_text(hierarchy_name)
+5 -5
View File
@@ -167,7 +167,7 @@ RSpec.describe "Projects", "editing settings", :js do
within_section "Status" do
click_on "Edit status"
within :menu, "Not set" do
within :menu, "Edit status" do
find(:menuitem, "Not started").click
end
end
@@ -179,7 +179,7 @@ RSpec.describe "Projects", "editing settings", :js do
expect(button).to have_text "Not started"
button.click
expect(find(:menu, "Not started")).to have_selector :menuitem, "Not started", aria: { current: true }
expect(find(:menu, "Edit status")).to have_selector :menuitem, "Not started", aria: { current: true }
end
end
@@ -187,7 +187,7 @@ RSpec.describe "Projects", "editing settings", :js do
within_section "Status" do
click_on "Edit status"
within :menu, "Not set" do
within :menu, "Edit status" do
find(:menuitem, "Finished").click
end
end
@@ -197,7 +197,7 @@ RSpec.describe "Projects", "editing settings", :js do
within_section "Status" do
click_on "Edit status"
within :menu, "Finished" do
within :menu, "Edit status" do
find(:menuitem, "Not set").click
end
end
@@ -209,7 +209,7 @@ RSpec.describe "Projects", "editing settings", :js do
expect(button).to have_text "Not set"
button.click
expect(find(:menu, "Not set")).to have_selector :menuitem, "Not set", aria: { current: true }
expect(find(:menu, "Edit status")).to have_selector :menuitem, "Not set", aria: { current: true }
end
end
@@ -82,7 +82,7 @@ RSpec.describe "Work Package table configuration modal columns spec", :js do
expect(page).to have_selector :columnheader, text: /.+/, count: 3
expect(page).to have_selector :columnheader, "ID"
expect(page).to have_selector :columnheader, "Subject"
expect(page).to have_selector :columnheader, "Project", colindex: 2
expect(page).to have_selector :columnheader, "Project", colindex: 4
end
end
end
@@ -28,49 +28,20 @@
# See COPYRIGHT and LICENSE files for more details.
#++
# Workaround to support role filters in component specs. This should be fixed upstream.
Capybara::Node::Simple.class_eval do
def role
self[:role]
end
CapybaraAccessibleSelectors.add_role_selector(:list, within: true) do
filter_set(:capybara_accessible_selectors, %i[aria described_by])
end
Capybara.add_selector(:list) do
xpath do |*|
XPath.descendant[[
XPath.self(:ul),
XPath.self(:ol)
].reduce(:|)]
end
locator_filter skip_if: nil do |node, locator, exact:, **|
method = exact ? :eql? : :include?
if node[:"aria-labelledby"]
CapybaraAccessibleSelectors::Helpers.element_labelledby(node).public_send(method, locator)
elsif node[:"aria-label"]
node[:"aria-label"].public_send(method, locator.to_s)
end
end
filter_set(:capybara_accessible_selectors, %i[aria role described_by])
end
Capybara.add_selector(:list_item) do
label "list item"
xpath do |*|
XPath.descendant[XPath.self(:li)]
end
expression_filter(:position) do |xpath, position|
position ? "#{xpath}[#{position}]" : xpath
CapybaraAccessibleSelectors.add_role_selector(:list_item, role: :listitem, within: true, content_fallback: true) do
expression_filter(:position, skip_if: nil) do |xpath, position|
xpath[position]
end
describe_expression_filters do |position: nil, **|
position ? " at position #{position}" : ""
end
filter_set(:capybara_accessible_selectors, %i[aria role described_by])
filter_set(:capybara_accessible_selectors, %i[aria described_by])
end
module Capybara