feat(member): make overview columns sortable through accessible sort headers
This commit is contained in:
parent
dfc616257d
commit
0c375234c8
9 changed files with 350 additions and 51 deletions
55
test/mv_web/member_live/index_sort_a11y_test.exs
Normal file
55
test/mv_web/member_live/index_sort_a11y_test.exs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
defmodule MvWeb.MemberLive.IndexSortA11yTest do
|
||||
@moduledoc """
|
||||
§1.11 — When a column is sorted, exactly one `th` carries `aria-sort` with the
|
||||
correct direction, a non-color sort glyph is present, and the sort is reflected
|
||||
in the URL.
|
||||
"""
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.Helpers.SystemActor
|
||||
|
||||
@moduletag :ui
|
||||
|
||||
setup %{conn: conn} do
|
||||
{:ok, _} =
|
||||
Mv.Membership.create_member(
|
||||
%{first_name: "Sortable", last_name: "Member", email: "sortable@example.com"},
|
||||
actor: SystemActor.get_system_actor()
|
||||
)
|
||||
|
||||
%{conn: conn_with_oidc_user(conn)}
|
||||
end
|
||||
|
||||
defp aria_sort_count(html) do
|
||||
~r/aria-sort="/ |> Regex.scan(html) |> length()
|
||||
end
|
||||
|
||||
test "exactly one th carries aria-sort ascending with a non-color glyph", %{conn: conn} do
|
||||
{:ok, view, html} = live(conn, ~p"/members?sort_field=join_date&sort_order=asc")
|
||||
|
||||
assert aria_sort_count(html) == 1
|
||||
assert has_element?(view, "th[aria-sort='ascending']")
|
||||
# Non-color shape glyph for the active ascending sort.
|
||||
assert html =~ "hero-chevron-up"
|
||||
end
|
||||
|
||||
test "aria-sort reflects descending and stays unique", %{conn: conn} do
|
||||
{:ok, view, html} = live(conn, ~p"/members?sort_field=join_date&sort_order=desc")
|
||||
|
||||
assert aria_sort_count(html) == 1
|
||||
assert has_element?(view, "th[aria-sort='descending']")
|
||||
assert html =~ "hero-chevron-down"
|
||||
end
|
||||
|
||||
test "clicking a header reflects the sort in the URL", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/members")
|
||||
|
||||
view |> element("[data-testid='join_date']") |> render_click()
|
||||
|
||||
path = assert_patch(view)
|
||||
assert path =~ "sort_field=join_date"
|
||||
assert path =~ "sort_order=asc"
|
||||
end
|
||||
end
|
||||
106
test/mv_web/member_live/index_sort_name_address_test.exs
Normal file
106
test/mv_web/member_live/index_sort_name_address_test.exs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
defmodule MvWeb.MemberLive.IndexSortNameAddressTest do
|
||||
@moduledoc """
|
||||
§1.21 — the composite Member (Name) and Address columns are sortable DB-side:
|
||||
Name by last name → first name (email in the cell is display-only), Address by
|
||||
city → postal code → street. Ascending/descending, reflected in the URL and in
|
||||
`aria-sort` (§1.11).
|
||||
"""
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.Helpers.SystemActor
|
||||
alias MvWeb.MemberLive.Index.OverviewQuery
|
||||
|
||||
setup %{conn: conn} do
|
||||
actor = SystemActor.get_system_actor()
|
||||
|
||||
# Same last name, different first name — proves the first-name tie-breaker.
|
||||
{:ok, _} =
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Bruno",
|
||||
last_name: "Meyer",
|
||||
email: "z-bruno@example.com",
|
||||
city: "Aachen",
|
||||
postal_code: "52062",
|
||||
street: "Zebra"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, _} =
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Anna",
|
||||
last_name: "Meyer",
|
||||
email: "a-anna@example.com",
|
||||
city: "Zwickau",
|
||||
postal_code: "08056",
|
||||
street: "Alpha"
|
||||
},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
%{conn: conn_with_oidc_user(conn), actor: actor}
|
||||
end
|
||||
|
||||
defp read_names(query, actor) do
|
||||
query
|
||||
|> Ash.read!(actor: actor)
|
||||
|> Enum.map(&{&1.last_name, &1.first_name})
|
||||
end
|
||||
|
||||
test "name sort orders by last name then first name, ignoring email", %{actor: actor} do
|
||||
asc = OverviewQuery.build(%{sort_field: :name, sort_order: :asc}) |> read_names(actor)
|
||||
|
||||
# Both share last name "Meyer"; the first-name tie-breaker orders Anna before
|
||||
# Bruno, even though Bruno's email sorts first alphabetically.
|
||||
assert asc == [{"Meyer", "Anna"}, {"Meyer", "Bruno"}]
|
||||
|
||||
desc = OverviewQuery.build(%{sort_field: :name, sort_order: :desc}) |> read_names(actor)
|
||||
assert desc == [{"Meyer", "Bruno"}, {"Meyer", "Anna"}]
|
||||
end
|
||||
|
||||
test "address sort orders by city, then postal code, then street", %{actor: actor} do
|
||||
asc =
|
||||
OverviewQuery.build(%{sort_field: :address, sort_order: :asc})
|
||||
|> Ash.read!(actor: actor)
|
||||
|> Enum.map(& &1.city)
|
||||
|
||||
assert asc == ["Aachen", "Zwickau"]
|
||||
|
||||
desc =
|
||||
OverviewQuery.build(%{sort_field: :address, sort_order: :desc})
|
||||
|> Ash.read!(actor: actor)
|
||||
|> Enum.map(& &1.city)
|
||||
|
||||
assert desc == ["Zwickau", "Aachen"]
|
||||
end
|
||||
|
||||
test "clicking the Name header sorts and reflects state in URL and aria-sort", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/members")
|
||||
|
||||
view |> element("button[phx-click='sort'][data-testid='name']") |> render_click()
|
||||
|
||||
path = assert_patch(view)
|
||||
assert path =~ "sort_field=name"
|
||||
# Exactly one column carries aria-sort (§1.11).
|
||||
assert has_element?(view, "th[aria-sort='ascending']")
|
||||
assert render(view) |> aria_sort_count() == 1
|
||||
end
|
||||
|
||||
test "clicking the Address header sorts and reflects state in URL", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/members")
|
||||
|
||||
view |> element("button[phx-click='sort'][data-testid='address']") |> render_click()
|
||||
|
||||
path = assert_patch(view)
|
||||
assert path =~ "sort_field=address"
|
||||
assert has_element?(view, "th[aria-sort='ascending']")
|
||||
end
|
||||
|
||||
defp aria_sort_count(html) do
|
||||
html |> String.split("aria-sort=") |> length() |> Kernel.-(1)
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue