From 2e4d14dd607d5ae6f3ba6c42ead58b03b5999c89 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 17 Feb 2026 12:15:46 +0100 Subject: [PATCH 1/4] test: add tdd tests for groups in member detail view #374 --- .../member_live/show_groups_display_test.exs | 323 ++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 test/mv_web/member_live/show_groups_display_test.exs diff --git a/test/mv_web/member_live/show_groups_display_test.exs b/test/mv_web/member_live/show_groups_display_test.exs new file mode 100644 index 0000000..49d5796 --- /dev/null +++ b/test/mv_web/member_live/show_groups_display_test.exs @@ -0,0 +1,323 @@ +defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do + @moduledoc """ + Tests for displaying groups in the member detail view (Issue #374). + + Tests cover: + - Groups section visibility (with and without groups) + - Group badges with correct names and links to group detail pages + - Edge cases (one group, many groups) + - Security: groups section visible only when user may view member + - Accessibility: badges have role and aria-label + + ## Note on async + async: false to avoid PostgreSQL deadlocks when creating members and groups + in the same test run (same as IndexGroupsDisplayTest). + + ## Expected state + These tests fail until the Groups section is implemented on the member show page + (Issue #374: load groups in handle_params, add "Groups" section with badges and links). + """ + use MvWeb.ConnCase, async: false + import Phoenix.LiveViewTest + require Ash.Query + use Gettext, backend: MvWeb.Gettext + + alias Mv.Membership.{Group, MemberGroup} + + describe "groups section" do + setup do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + {:ok, member} = + Mv.Membership.create_member( + %{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"}, + actor: system_actor + ) + + %{member: member, actor: system_actor} + end + + test "displays Groups section when member has at least one group", %{ + conn: conn, + member: member, + actor: actor + } do + {:ok, group} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Board Members"}) + |> Ash.create(actor: actor) + + {:ok, _mg} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) + |> Ash.create(actor: actor) + + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/members/#{member}") + + assert html =~ gettext("Groups") + assert html =~ group.name + end + + test "displays all groups as badges with correct names when member is in multiple groups", %{ + conn: conn, + member: member, + actor: actor + } do + {:ok, group1} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Board Members"}) + |> Ash.create(actor: actor) + + {:ok, group2} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Active Members"}) + |> Ash.create(actor: actor) + + {:ok, _mg1} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group1.id}) + |> Ash.create(actor: actor) + + {:ok, _mg2} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group2.id}) + |> Ash.create(actor: actor) + + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/members/#{member}") + + assert html =~ gettext("Groups") + assert html =~ group1.name + assert html =~ group2.name + end + + test "displays Groups section when member has no groups (empty state)", %{ + conn: conn, + member: member + } do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, ~p"/members/#{member}") + + # Assert Groups section in main content (section with h2 "Groups"), not sidebar link + assert has_element?(view, "main section h2", gettext("Groups")) + end + + test "groups are loaded with member (single request returns all group names)", %{ + conn: conn, + member: member, + actor: actor + } do + {:ok, group1} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Alpha"}) + |> Ash.create(actor: actor) + + {:ok, group2} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Beta"}) + |> Ash.create(actor: actor) + + {:ok, _mg1} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group1.id}) + |> Ash.create(actor: actor) + + {:ok, _mg2} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group2.id}) + |> Ash.create(actor: actor) + + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/members/#{member}") + + assert html =~ "Alpha" + assert html =~ "Beta" + end + end + + describe "groups section links" do + setup do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + {:ok, member} = + Mv.Membership.create_member( + %{first_name: "Bob", last_name: "Brown", email: "bob@example.com"}, + actor: system_actor + ) + + {:ok, group} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Board Members"}) + |> Ash.create(actor: system_actor) + + {:ok, _mg} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) + |> Ash.create(actor: system_actor) + + %{member: member, group: group} + end + + test "each group badge links to group detail page with correct slug", %{ + conn: conn, + member: member, + group: group + } do + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/members/#{member}") + + # Link to group detail: /groups/:slug (slug is URL-friendly, e.g. "board-members") + assert html =~ ~r/href="[^"]*\/groups\/#{Regex.escape(group.slug)}"|navigate="[^"]*\/groups\/#{Regex.escape(group.slug)}"/ + end + + test "clicking group badge navigates to group detail page", %{ + conn: conn, + member: member, + group: group + } do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, ~p"/members/#{member}") + + view + |> element("a[href*='/groups/#{group.slug}']") + |> render_click() + + assert_redirect(view, ~p"/groups/#{group.slug}") + end + end + + describe "groups section edge cases" do + setup do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + {:ok, member} = + Mv.Membership.create_member( + %{first_name: "Charlie", last_name: "Clark", email: "charlie@example.com"}, + actor: system_actor + ) + + %{member: member, actor: system_actor} + end + + test "member in exactly one group shows single badge", %{ + conn: conn, + member: member, + actor: actor + } do + {:ok, group} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Solo Group"}) + |> Ash.create(actor: actor) + + {:ok, _mg} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) + |> Ash.create(actor: actor) + + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/members/#{member}") + + assert html =~ gettext("Groups") + assert html =~ group.name + end + + test "member in many groups shows all badges", %{ + conn: conn, + member: member, + actor: actor + } do + group_names = Enum.map(1..5, fn i -> "Group #{i}" end) + + groups = + Enum.map(group_names, fn name -> + {:ok, g} = + Group + |> Ash.Changeset.for_create(:create, %{name: name}) + |> Ash.create(actor: actor) + + g + end) + + for g <- groups do + {:ok, _mg} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: g.id}) + |> Ash.create(actor: actor) + end + + conn = conn_with_oidc_user(conn) + {:ok, _view, html} = live(conn, ~p"/members/#{member}") + + assert html =~ gettext("Groups") + for name <- group_names do + assert html =~ name + end + end + end + + describe "groups section with read_only user" do + @tag role: :read_only + test "user with read permission sees Groups section", %{conn: conn} do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + {:ok, member} = + Mv.Membership.create_member( + %{first_name: "Diana", last_name: "Davis", email: "diana@example.com"}, + actor: system_actor + ) + + {:ok, group} = + Group + |> Ash.Changeset.for_create(:create, %{name: "Readers"}) + |> Ash.create(actor: system_actor) + + {:ok, _mg} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) + |> Ash.create(actor: system_actor) + + {:ok, _view, html} = live(conn, ~p"/members/#{member}") + + assert html =~ gettext("Groups") + assert html =~ group.name + end + end + + describe "groups section accessibility" do + setup do + system_actor = Mv.Helpers.SystemActor.get_system_actor() + + {:ok, member} = + Mv.Membership.create_member( + %{first_name: "Eve", last_name: "Evans", email: "eve@example.com"}, + actor: system_actor + ) + + {:ok, group} = + Group + |> Ash.Changeset.for_create(:create, %{name: "A11y Group"}) + |> Ash.create(actor: system_actor) + + {:ok, _mg} = + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) + |> Ash.create(actor: system_actor) + + %{member: member, group: group} + end + + test "group badges have role and aria-label for screen readers", %{ + conn: conn, + member: member, + group: group + } do + conn = conn_with_oidc_user(conn) + {:ok, view, html} = live(conn, ~p"/members/#{member}") + + assert html =~ group.name + # Badge has role="status" and aria-label indicating group membership (architecture: "Member of group X") + assert has_element?(view, "[role='status'][aria-label*='#{group.name}']") + end + end +end -- 2.47.2 From 46f9094e1fabd78293229c515ed74df4023ef02b Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 17 Feb 2026 12:16:15 +0100 Subject: [PATCH 2/4] style: fix formatting --- test/mv_web/member_live/show_groups_display_test.exs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/mv_web/member_live/show_groups_display_test.exs b/test/mv_web/member_live/show_groups_display_test.exs index 49d5796..78112ae 100644 --- a/test/mv_web/member_live/show_groups_display_test.exs +++ b/test/mv_web/member_live/show_groups_display_test.exs @@ -168,7 +168,8 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do {:ok, _view, html} = live(conn, ~p"/members/#{member}") # Link to group detail: /groups/:slug (slug is URL-friendly, e.g. "board-members") - assert html =~ ~r/href="[^"]*\/groups\/#{Regex.escape(group.slug)}"|navigate="[^"]*\/groups\/#{Regex.escape(group.slug)}"/ + assert html =~ + ~r/href="[^"]*\/groups\/#{Regex.escape(group.slug)}"|navigate="[^"]*\/groups\/#{Regex.escape(group.slug)}"/ end test "clicking group badge navigates to group detail page", %{ @@ -250,6 +251,7 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do {:ok, _view, html} = live(conn, ~p"/members/#{member}") assert html =~ gettext("Groups") + for name <- group_names do assert html =~ name end @@ -316,6 +318,7 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do {:ok, view, html} = live(conn, ~p"/members/#{member}") assert html =~ group.name + # Badge has role="status" and aria-label indicating group membership (architecture: "Member of group X") assert has_element?(view, "[role='status'][aria-label*='#{group.name}']") end -- 2.47.2 From b1a9eb8b1d8fb1c3f460735eb4fb8e8d21590247 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 17 Feb 2026 14:15:43 +0100 Subject: [PATCH 3/4] feat: add groups to member detail view #374 --- docs/groups-architecture.md | 2 ++ lib/mv_web/live/member_live/show.ex | 27 ++++++++++++++++++- priv/gettext/de/LC_MESSAGES/default.po | 3 +++ priv/gettext/default.pot | 3 +++ priv/gettext/en/LC_MESSAGES/default.po | 3 +++ .../member_live/show_groups_display_test.exs | 19 ++++++------- 6 files changed, 45 insertions(+), 12 deletions(-) diff --git a/docs/groups-architecture.md b/docs/groups-architecture.md index 27d9d18..1557b6f 100644 --- a/docs/groups-architecture.md +++ b/docs/groups-architecture.md @@ -961,6 +961,8 @@ Each functional unit can be implemented as a **separate issue**: ### Issue 4: Member Detail - Groups Display **Type:** Frontend **Estimation:** 1-2h +**Status:** Implemented (Groups as data field in Personal Data, below Linked User; button-style links to `/groups/:slug`). + **Tasks:** - Add groups section to member show - Display group badges diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex index 40491cc..cc0b25e 100644 --- a/lib/mv_web/live/member_live/show.ex +++ b/lib/mv_web/live/member_live/show.ex @@ -12,6 +12,8 @@ defmodule MvWeb.MemberLive.Show do ## Sections - Personal Data: Name, address, contact information, membership dates, notes - Custom Fields: Dynamic fields in uniform grid layout (sorted by name) + - Groups: Group links (buttons) in Personal Data section, below Linked User + - Payment Data: Membership fee type and cycle status - Membership Fees: Tab showing all membership fee cycles with status management (via MembershipFeesComponent) ## Navigation @@ -146,6 +148,28 @@ defmodule MvWeb.MemberLive.Show do <% end %> + <%!-- Groups (in Personal Data, below Linked User) --%> +
+ <.data_field label={gettext("Groups")}> + <%= if Enum.any?(@member.groups || []) do %> +
+ <%= for group <- (@member.groups || []) do %> + <.link + navigate={~p"/groups/#{group.slug}"} + class="btn btn-xs btn-outline btn-primary" + role="status" + aria-label={gettext("Member of group %{name}", name: group.name)} + > + {group.name} + + <% end %> +
+ <% else %> + {gettext("No groups")} + <% end %> + +
+ <%!-- Notes --%> <%= if @member.notes && String.trim(@member.notes) != "" do %>
@@ -262,7 +286,8 @@ defmodule MvWeb.MemberLive.Show do :user, :membership_fee_type, custom_field_values: [:custom_field], - membership_fee_cycles: [:membership_fee_type] + membership_fee_cycles: [:membership_fee_type], + groups: [:id, :name, :slug] ]) member = Ash.read_one!(query, actor: actor) diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 1784d4b..57334d2 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -2202,11 +2202,13 @@ msgstr "Gruppe erfolgreich gespeichert." #: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/member_live/index.html.heex +#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Groups" msgstr "Gruppen" #: lib/mv_web/live/group_live/index.ex +#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "No groups" msgstr "Keine Gruppen" @@ -2474,6 +2476,7 @@ msgid "unpaid" msgstr "Unbezahlt" #: lib/mv_web/live/member_live/index.html.heex +#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Member of group %{name}" msgstr "Mitglied der Gruppe %{name}" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index af24afd..fb156df 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -2203,11 +2203,13 @@ msgstr "" #: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/member_live/index.html.heex +#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Groups" msgstr "" #: lib/mv_web/live/group_live/index.ex +#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "No groups" msgstr "" @@ -2475,6 +2477,7 @@ msgid "unpaid" msgstr "" #: lib/mv_web/live/member_live/index.html.heex +#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Member of group %{name}" msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 88da6ff..1bf276c 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -2203,11 +2203,13 @@ msgstr "" #: lib/mv_web/live/components/member_filter_component.ex #: lib/mv_web/live/group_live/index.ex #: lib/mv_web/live/member_live/index.html.heex +#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Groups" msgstr "" #: lib/mv_web/live/group_live/index.ex +#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "No groups" msgstr "" @@ -2475,6 +2477,7 @@ msgid "unpaid" msgstr "" #: lib/mv_web/live/member_live/index.html.heex +#: lib/mv_web/live/member_live/show.ex #, elixir-autogen, elixir-format msgid "Member of group %{name}" msgstr "" diff --git a/test/mv_web/member_live/show_groups_display_test.exs b/test/mv_web/member_live/show_groups_display_test.exs index 78112ae..bda46b3 100644 --- a/test/mv_web/member_live/show_groups_display_test.exs +++ b/test/mv_web/member_live/show_groups_display_test.exs @@ -3,19 +3,15 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do Tests for displaying groups in the member detail view (Issue #374). Tests cover: - - Groups section visibility (with and without groups) - - Group badges with correct names and links to group detail pages + - Groups in Personal Data (with and without groups) + - Group buttons with correct names and links to group detail pages - Edge cases (one group, many groups) - - Security: groups section visible only when user may view member - - Accessibility: badges have role and aria-label + - Security: groups visible only when user may view member + - Accessibility: group links have role and aria-label ## Note on async async: false to avoid PostgreSQL deadlocks when creating members and groups in the same test run (same as IndexGroupsDisplayTest). - - ## Expected state - These tests fail until the Groups section is implemented on the member show page - (Issue #374: load groups in handle_params, add "Groups" section with badges and links). """ use MvWeb.ConnCase, async: false import Phoenix.LiveViewTest @@ -97,10 +93,11 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do member: member } do conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, ~p"/members/#{member}") + {:ok, _view, html} = live(conn, ~p"/members/#{member}") - # Assert Groups section in main content (section with h2 "Groups"), not sidebar link - assert has_element?(view, "main section h2", gettext("Groups")) + # Groups are in Personal Data; label "Groups" and empty state "No groups" must be present + assert html =~ gettext("Groups") + assert html =~ gettext("No groups") end test "groups are loaded with member (single request returns all group names)", %{ -- 2.47.2 From 911f308a67c93b49dc9a3444542744b91e49fe99 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 17 Feb 2026 15:30:23 +0100 Subject: [PATCH 4/4] fix: address review comments --- docs/groups-architecture.md | 6 +- lib/mv_web/live/member_live/index.html.heex | 1 - lib/mv_web/live/member_live/show.ex | 14 +- priv/gettext/de/LC_MESSAGES/default.po | 9 - priv/gettext/en/LC_MESSAGES/default.po | 5 - .../index_groups_accessibility_test.exs | 8 +- .../member_live/show_groups_display_test.exs | 209 +++++++----------- 7 files changed, 88 insertions(+), 164 deletions(-) diff --git a/docs/groups-architecture.md b/docs/groups-architecture.md index 1557b6f..0e59409 100644 --- a/docs/groups-architecture.md +++ b/docs/groups-architecture.md @@ -356,9 +356,9 @@ lib/ - Screen readers must be able to navigate and understand the interface - ARIA labels and roles must be properly set -**Group Badges in Member Overview:** -- Badges must have `role="status"` and appropriate `aria-label` attributes -- Badge title should indicate group membership +**Group Badges and Links in Member Overview / Detail:** +- Use `aria-label` to indicate group membership (e.g. "Member of group X"). Do not use `role="status"` on badges or links—that role is for live regions (screen reader announcements), not for navigation or static labels. +- Badge/link text or title should indicate group membership for screen readers. **Clickable Group Badge (for filtering) - Optional:** diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index 311447b..b490618 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -322,7 +322,6 @@ <%= for group <- (member.groups || []) do %> {group.name} diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex index cc0b25e..47e8878 100644 --- a/lib/mv_web/live/member_live/show.ex +++ b/lib/mv_web/live/member_live/show.ex @@ -12,7 +12,7 @@ defmodule MvWeb.MemberLive.Show do ## Sections - Personal Data: Name, address, contact information, membership dates, notes - Custom Fields: Dynamic fields in uniform grid layout (sorted by name) - - Groups: Group links (buttons) in Personal Data section, below Linked User + - Groups: Links to group detail pages in Personal Data section - Payment Data: Membership fee type and cycle status - Membership Fees: Tab showing all membership fee cycles with status management (via MembershipFeesComponent) @@ -148,24 +148,24 @@ defmodule MvWeb.MemberLive.Show do
<% end %> - <%!-- Groups (in Personal Data, below Linked User) --%> + <%!-- Groups (in Personal Data) --%> + <% groups = @member.groups || [] %>
<.data_field label={gettext("Groups")}> - <%= if Enum.any?(@member.groups || []) do %> + <%= if Enum.empty?(groups) do %> + {gettext("No groups")} + <% else %>
- <%= for group <- (@member.groups || []) do %> + <%= for group <- groups do %> <.link navigate={~p"/groups/#{group.slug}"} class="btn btn-xs btn-outline btn-primary" - role="status" aria-label={gettext("Member of group %{name}", name: group.name)} > {group.name} <% end %>
- <% else %> - {gettext("No groups")} <% end %>
diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 57334d2..e40d053 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -2616,12 +2616,3 @@ msgstr "Anzahl Mitglieder:" #, elixir-autogen, elixir-format msgid "PDF" msgstr "PDF" - -#~ #: lib/mv_web/live/global_settings_live.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Custom Fields in CSV Import" -#~ msgstr "Benutzerdefinierte Felder" - -#~ #, elixir-autogen, elixir-format -#~ msgid "Failed to prepare CSV import: %{error}" -#~ msgstr "Das Vorbereiten des CSV Imports ist gescheitert: %{error}" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 1bf276c..f12d478 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -2617,8 +2617,3 @@ msgstr "Member count:" #, elixir-autogen, elixir-format msgid "PDF" msgstr "" - -#~ #: lib/mv_web/live/global_settings_live.ex -#~ #, elixir-autogen, elixir-format, fuzzy -#~ msgid "Custom Fields in CSV Import" -#~ msgstr "" diff --git a/test/mv_web/member_live/index_groups_accessibility_test.exs b/test/mv_web/member_live/index_groups_accessibility_test.exs index ab9b728..d14cd9f 100644 --- a/test/mv_web/member_live/index_groups_accessibility_test.exs +++ b/test/mv_web/member_live/index_groups_accessibility_test.exs @@ -3,7 +3,7 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do Tests for accessibility of groups feature in the member overview. Tests cover: - - Badges have role="status" and aria-label + - Badges have aria-label for group membership (no role="status"; reserved for live regions) - Filter dropdown has aria-label - Sort header has aria-label for screen reader - Keyboard navigation works (Tab through filter, sort header) @@ -44,7 +44,7 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do end @tag :ui - test "group badges have role and aria-label", %{ + test "group badges have aria-label for screen readers", %{ conn: conn, member1: member1, group1: group1 @@ -52,8 +52,8 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do conn = conn_with_oidc_user(conn) {:ok, view, html} = live(conn, "/members") - # Verify badges have role="status" and aria-label containing the group name - assert has_element?(view, "span[role='status'][aria-label*='#{group1.name}']") + # Verify badges have aria-label containing the group name (no role=status on badges) + assert has_element?(view, "span[aria-label*='#{group1.name}']") assert html =~ group1.name # Verify member1's row contains the badge diff --git a/test/mv_web/member_live/show_groups_display_test.exs b/test/mv_web/member_live/show_groups_display_test.exs index bda46b3..f8434b3 100644 --- a/test/mv_web/member_live/show_groups_display_test.exs +++ b/test/mv_web/member_live/show_groups_display_test.exs @@ -4,10 +4,10 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do Tests cover: - Groups in Personal Data (with and without groups) - - Group buttons with correct names and links to group detail pages + - Group links with correct names and links to group detail pages - Edge cases (one group, many groups) - Security: groups visible only when user may view member - - Accessibility: group links have role and aria-label + - Accessibility: group links have aria-label for screen readers ## Note on async async: false to avoid PostgreSQL deadlocks when creating members and groups @@ -22,15 +22,16 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do describe "groups section" do setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() + actor = Mv.Helpers.SystemActor.get_system_actor() {:ok, member} = - Mv.Membership.create_member( - %{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"}, - actor: system_actor - ) + create_member(actor, %{ + first_name: "Alice", + last_name: "Anderson", + email: "alice@example.com" + }) - %{member: member, actor: system_actor} + %{member: member, actor: actor} end test "displays Groups section when member has at least one group", %{ @@ -38,15 +39,8 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do member: member, actor: actor } do - {:ok, group} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Board Members"}) - |> Ash.create(actor: actor) - - {:ok, _mg} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) - |> Ash.create(actor: actor) + {:ok, group} = create_group(actor, "Board Members") + {:ok, _mg} = add_member_to_group(member, group, actor) conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/members/#{member}") @@ -55,30 +49,15 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do assert html =~ group.name end - test "displays all groups as badges with correct names when member is in multiple groups", %{ + test "displays all group links with correct names when member is in multiple groups", %{ conn: conn, member: member, actor: actor } do - {:ok, group1} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Board Members"}) - |> Ash.create(actor: actor) - - {:ok, group2} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Active Members"}) - |> Ash.create(actor: actor) - - {:ok, _mg1} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group1.id}) - |> Ash.create(actor: actor) - - {:ok, _mg2} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group2.id}) - |> Ash.create(actor: actor) + {:ok, group1} = create_group(actor, "Board Members") + {:ok, group2} = create_group(actor, "Active Members") + {:ok, _mg1} = add_member_to_group(member, group1, actor) + {:ok, _mg2} = add_member_to_group(member, group2, actor) conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/members/#{member}") @@ -100,30 +79,15 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do assert html =~ gettext("No groups") end - test "groups are loaded with member (single request returns all group names)", %{ + test "renders all group names when member has multiple groups", %{ conn: conn, member: member, actor: actor } do - {:ok, group1} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Alpha"}) - |> Ash.create(actor: actor) - - {:ok, group2} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Beta"}) - |> Ash.create(actor: actor) - - {:ok, _mg1} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group1.id}) - |> Ash.create(actor: actor) - - {:ok, _mg2} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group2.id}) - |> Ash.create(actor: actor) + {:ok, group1} = create_group(actor, "Alpha") + {:ok, group2} = create_group(actor, "Beta") + {:ok, _mg1} = add_member_to_group(member, group1, actor) + {:ok, _mg2} = add_member_to_group(member, group2, actor) conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/members/#{member}") @@ -135,41 +99,29 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do describe "groups section links" do setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() + actor = Mv.Helpers.SystemActor.get_system_actor() {:ok, member} = - Mv.Membership.create_member( - %{first_name: "Bob", last_name: "Brown", email: "bob@example.com"}, - actor: system_actor - ) - - {:ok, group} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Board Members"}) - |> Ash.create(actor: system_actor) - - {:ok, _mg} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) - |> Ash.create(actor: system_actor) + create_member(actor, %{first_name: "Bob", last_name: "Brown", email: "bob@example.com"}) + {:ok, group} = create_group(actor, "Board Members") + {:ok, _mg} = add_member_to_group(member, group, actor) %{member: member, group: group} end - test "each group badge links to group detail page with correct slug", %{ + test "each group link goes to group detail page with correct slug", %{ conn: conn, member: member, group: group } do conn = conn_with_oidc_user(conn) - {:ok, _view, html} = live(conn, ~p"/members/#{member}") + {:ok, view, _html} = live(conn, ~p"/members/#{member}") # Link to group detail: /groups/:slug (slug is URL-friendly, e.g. "board-members") - assert html =~ - ~r/href="[^"]*\/groups\/#{Regex.escape(group.slug)}"|navigate="[^"]*\/groups\/#{Regex.escape(group.slug)}"/ + assert has_element?(view, "a[href*='/groups/#{group.slug}']", group.name) end - test "clicking group badge navigates to group detail page", %{ + test "clicking group link navigates to group detail page", %{ conn: conn, member: member, group: group @@ -187,31 +139,25 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do describe "groups section edge cases" do setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() + actor = Mv.Helpers.SystemActor.get_system_actor() {:ok, member} = - Mv.Membership.create_member( - %{first_name: "Charlie", last_name: "Clark", email: "charlie@example.com"}, - actor: system_actor - ) + create_member(actor, %{ + first_name: "Charlie", + last_name: "Clark", + email: "charlie@example.com" + }) - %{member: member, actor: system_actor} + %{member: member, actor: actor} end - test "member in exactly one group shows single badge", %{ + test "member in exactly one group shows single link", %{ conn: conn, member: member, actor: actor } do - {:ok, group} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Solo Group"}) - |> Ash.create(actor: actor) - - {:ok, _mg} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) - |> Ash.create(actor: actor) + {:ok, group} = create_group(actor, "Solo Group") + {:ok, _mg} = add_member_to_group(member, group, actor) conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/members/#{member}") @@ -220,7 +166,7 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do assert html =~ group.name end - test "member in many groups shows all badges", %{ + test "member in many groups shows all group links", %{ conn: conn, member: member, actor: actor @@ -229,19 +175,12 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do groups = Enum.map(group_names, fn name -> - {:ok, g} = - Group - |> Ash.Changeset.for_create(:create, %{name: name}) - |> Ash.create(actor: actor) - + {:ok, g} = create_group(actor, name) g end) for g <- groups do - {:ok, _mg} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: g.id}) - |> Ash.create(actor: actor) + {:ok, _mg} = add_member_to_group(member, g, actor) end conn = conn_with_oidc_user(conn) @@ -258,23 +197,17 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do describe "groups section with read_only user" do @tag role: :read_only test "user with read permission sees Groups section", %{conn: conn} do - system_actor = Mv.Helpers.SystemActor.get_system_actor() + actor = Mv.Helpers.SystemActor.get_system_actor() {:ok, member} = - Mv.Membership.create_member( - %{first_name: "Diana", last_name: "Davis", email: "diana@example.com"}, - actor: system_actor - ) + create_member(actor, %{ + first_name: "Diana", + last_name: "Davis", + email: "diana@example.com" + }) - {:ok, group} = - Group - |> Ash.Changeset.for_create(:create, %{name: "Readers"}) - |> Ash.create(actor: system_actor) - - {:ok, _mg} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) - |> Ash.create(actor: system_actor) + {:ok, group} = create_group(actor, "Readers") + {:ok, _mg} = add_member_to_group(member, group, actor) {:ok, _view, html} = live(conn, ~p"/members/#{member}") @@ -285,28 +218,17 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do describe "groups section accessibility" do setup do - system_actor = Mv.Helpers.SystemActor.get_system_actor() + actor = Mv.Helpers.SystemActor.get_system_actor() {:ok, member} = - Mv.Membership.create_member( - %{first_name: "Eve", last_name: "Evans", email: "eve@example.com"}, - actor: system_actor - ) - - {:ok, group} = - Group - |> Ash.Changeset.for_create(:create, %{name: "A11y Group"}) - |> Ash.create(actor: system_actor) - - {:ok, _mg} = - MemberGroup - |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) - |> Ash.create(actor: system_actor) + create_member(actor, %{first_name: "Eve", last_name: "Evans", email: "eve@example.com"}) + {:ok, group} = create_group(actor, "A11y Group") + {:ok, _mg} = add_member_to_group(member, group, actor) %{member: member, group: group} end - test "group badges have role and aria-label for screen readers", %{ + test "group links have aria-label for screen readers", %{ conn: conn, member: member, group: group @@ -316,8 +238,25 @@ defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do assert html =~ group.name - # Badge has role="status" and aria-label indicating group membership (architecture: "Member of group X") - assert has_element?(view, "[role='status'][aria-label*='#{group.name}']") + # Group link has aria-label indicating group membership for screen readers + assert has_element?(view, "a[aria-label*='#{group.name}']") end end + + # Helpers to reduce setup duplication (create member/group, assign member to group). + defp create_member(actor, attrs) do + Mv.Membership.create_member(attrs, actor: actor) + end + + defp create_group(actor, name) do + Group + |> Ash.Changeset.for_create(:create, %{name: name}) + |> Ash.create(actor: actor) + end + + defp add_member_to_group(member, group, actor) do + MemberGroup + |> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id}) + |> Ash.create(actor: actor) + end end -- 2.47.2