Merge pull request 'Add groups to member detail view closes #374' (#423) from feature/374-member-detail-groups into main
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #423
This commit is contained in:
commit
3f07de1276
8 changed files with 306 additions and 23 deletions
|
|
@ -356,9 +356,9 @@ lib/
|
||||||
- Screen readers must be able to navigate and understand the interface
|
- Screen readers must be able to navigate and understand the interface
|
||||||
- ARIA labels and roles must be properly set
|
- ARIA labels and roles must be properly set
|
||||||
|
|
||||||
**Group Badges in Member Overview:**
|
**Group Badges and Links in Member Overview / Detail:**
|
||||||
- Badges must have `role="status"` and appropriate `aria-label` attributes
|
- 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 title should indicate group membership
|
- Badge/link text or title should indicate group membership for screen readers.
|
||||||
|
|
||||||
**Clickable Group Badge (for filtering) - Optional:**
|
**Clickable Group Badge (for filtering) - Optional:**
|
||||||
|
|
||||||
|
|
@ -961,6 +961,8 @@ Each functional unit can be implemented as a **separate issue**:
|
||||||
### Issue 4: Member Detail - Groups Display
|
### Issue 4: Member Detail - Groups Display
|
||||||
**Type:** Frontend
|
**Type:** Frontend
|
||||||
**Estimation:** 1-2h
|
**Estimation:** 1-2h
|
||||||
|
**Status:** Implemented (Groups as data field in Personal Data, below Linked User; button-style links to `/groups/:slug`).
|
||||||
|
|
||||||
**Tasks:**
|
**Tasks:**
|
||||||
- Add groups section to member show
|
- Add groups section to member show
|
||||||
- Display group badges
|
- Display group badges
|
||||||
|
|
|
||||||
|
|
@ -322,7 +322,6 @@
|
||||||
<%= for group <- (member.groups || []) do %>
|
<%= for group <- (member.groups || []) do %>
|
||||||
<span
|
<span
|
||||||
class="badge badge-outline badge-primary"
|
class="badge badge-outline badge-primary"
|
||||||
role="status"
|
|
||||||
aria-label={gettext("Member of group %{name}", name: group.name)}
|
aria-label={gettext("Member of group %{name}", name: group.name)}
|
||||||
>
|
>
|
||||||
{group.name}
|
{group.name}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ defmodule MvWeb.MemberLive.Show do
|
||||||
## Sections
|
## Sections
|
||||||
- Personal Data: Name, address, contact information, membership dates, notes
|
- Personal Data: Name, address, contact information, membership dates, notes
|
||||||
- Custom Fields: Dynamic fields in uniform grid layout (sorted by name)
|
- Custom Fields: Dynamic fields in uniform grid layout (sorted by name)
|
||||||
|
- 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)
|
- Membership Fees: Tab showing all membership fee cycles with status management (via MembershipFeesComponent)
|
||||||
|
|
||||||
## Navigation
|
## Navigation
|
||||||
|
|
@ -146,6 +148,28 @@ defmodule MvWeb.MemberLive.Show do
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<%!-- Groups (in Personal Data) --%>
|
||||||
|
<% groups = @member.groups || [] %>
|
||||||
|
<div>
|
||||||
|
<.data_field label={gettext("Groups")}>
|
||||||
|
<%= if Enum.empty?(groups) do %>
|
||||||
|
<span class="text-base-content/70 italic">{gettext("No groups")}</span>
|
||||||
|
<% else %>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<%= for group <- groups do %>
|
||||||
|
<.link
|
||||||
|
navigate={~p"/groups/#{group.slug}"}
|
||||||
|
class="btn btn-xs btn-outline btn-primary"
|
||||||
|
aria-label={gettext("Member of group %{name}", name: group.name)}
|
||||||
|
>
|
||||||
|
{group.name}
|
||||||
|
</.link>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</.data_field>
|
||||||
|
</div>
|
||||||
|
|
||||||
<%!-- Notes --%>
|
<%!-- Notes --%>
|
||||||
<%= if @member.notes && String.trim(@member.notes) != "" do %>
|
<%= if @member.notes && String.trim(@member.notes) != "" do %>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -262,7 +286,8 @@ defmodule MvWeb.MemberLive.Show do
|
||||||
:user,
|
:user,
|
||||||
:membership_fee_type,
|
:membership_fee_type,
|
||||||
custom_field_values: [:custom_field],
|
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)
|
member = Ash.read_one!(query, actor: actor)
|
||||||
|
|
|
||||||
|
|
@ -2202,11 +2202,13 @@ msgstr "Gruppe erfolgreich gespeichert."
|
||||||
#: lib/mv_web/live/components/member_filter_component.ex
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#: lib/mv_web/live/group_live/index.ex
|
#: lib/mv_web/live/group_live/index.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Groups"
|
msgid "Groups"
|
||||||
msgstr "Gruppen"
|
msgstr "Gruppen"
|
||||||
|
|
||||||
#: lib/mv_web/live/group_live/index.ex
|
#: lib/mv_web/live/group_live/index.ex
|
||||||
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No groups"
|
msgid "No groups"
|
||||||
msgstr "Keine Gruppen"
|
msgstr "Keine Gruppen"
|
||||||
|
|
@ -2474,6 +2476,7 @@ msgid "unpaid"
|
||||||
msgstr "Unbezahlt"
|
msgstr "Unbezahlt"
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Member of group %{name}"
|
msgid "Member of group %{name}"
|
||||||
msgstr "Mitglied der Gruppe %{name}"
|
msgstr "Mitglied der Gruppe %{name}"
|
||||||
|
|
@ -2613,12 +2616,3 @@ msgstr "Anzahl Mitglieder:"
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "PDF"
|
msgid "PDF"
|
||||||
msgstr "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}"
|
|
||||||
|
|
|
||||||
|
|
@ -2203,11 +2203,13 @@ msgstr ""
|
||||||
#: lib/mv_web/live/components/member_filter_component.ex
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#: lib/mv_web/live/group_live/index.ex
|
#: lib/mv_web/live/group_live/index.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Groups"
|
msgid "Groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/group_live/index.ex
|
#: lib/mv_web/live/group_live/index.ex
|
||||||
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No groups"
|
msgid "No groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -2475,6 +2477,7 @@ msgid "unpaid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Member of group %{name}"
|
msgid "Member of group %{name}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
||||||
|
|
@ -2203,11 +2203,13 @@ msgstr ""
|
||||||
#: lib/mv_web/live/components/member_filter_component.ex
|
#: lib/mv_web/live/components/member_filter_component.ex
|
||||||
#: lib/mv_web/live/group_live/index.ex
|
#: lib/mv_web/live/group_live/index.ex
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Groups"
|
msgid "Groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/group_live/index.ex
|
#: lib/mv_web/live/group_live/index.ex
|
||||||
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "No groups"
|
msgid "No groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -2475,6 +2477,7 @@ msgid "unpaid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/member_live/index.html.heex
|
#: lib/mv_web/live/member_live/index.html.heex
|
||||||
|
#: lib/mv_web/live/member_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Member of group %{name}"
|
msgid "Member of group %{name}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
@ -2614,8 +2617,3 @@ msgstr "Member count:"
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "PDF"
|
msgid "PDF"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#~ #: lib/mv_web/live/global_settings_live.ex
|
|
||||||
#~ #, elixir-autogen, elixir-format, fuzzy
|
|
||||||
#~ msgid "Custom Fields in CSV Import"
|
|
||||||
#~ msgstr ""
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do
|
||||||
Tests for accessibility of groups feature in the member overview.
|
Tests for accessibility of groups feature in the member overview.
|
||||||
|
|
||||||
Tests cover:
|
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
|
- Filter dropdown has aria-label
|
||||||
- Sort header has aria-label for screen reader
|
- Sort header has aria-label for screen reader
|
||||||
- Keyboard navigation works (Tab through filter, sort header)
|
- Keyboard navigation works (Tab through filter, sort header)
|
||||||
|
|
@ -44,7 +44,7 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag :ui
|
@tag :ui
|
||||||
test "group badges have role and aria-label", %{
|
test "group badges have aria-label for screen readers", %{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
member1: member1,
|
member1: member1,
|
||||||
group1: group1
|
group1: group1
|
||||||
|
|
@ -52,8 +52,8 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do
|
||||||
conn = conn_with_oidc_user(conn)
|
conn = conn_with_oidc_user(conn)
|
||||||
{:ok, view, html} = live(conn, "/members")
|
{:ok, view, html} = live(conn, "/members")
|
||||||
|
|
||||||
# Verify badges have role="status" and aria-label containing the group name
|
# Verify badges have aria-label containing the group name (no role=status on badges)
|
||||||
assert has_element?(view, "span[role='status'][aria-label*='#{group1.name}']")
|
assert has_element?(view, "span[aria-label*='#{group1.name}']")
|
||||||
assert html =~ group1.name
|
assert html =~ group1.name
|
||||||
|
|
||||||
# Verify member1's row contains the badge
|
# Verify member1's row contains the badge
|
||||||
|
|
|
||||||
262
test/mv_web/member_live/show_groups_display_test.exs
Normal file
262
test/mv_web/member_live/show_groups_display_test.exs
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
defmodule MvWeb.MemberLive.ShowGroupsDisplayTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for displaying groups in the member detail view (Issue #374).
|
||||||
|
|
||||||
|
Tests cover:
|
||||||
|
- Groups in Personal Data (with and without groups)
|
||||||
|
- 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 aria-label for screen readers
|
||||||
|
|
||||||
|
## Note on async
|
||||||
|
async: false to avoid PostgreSQL deadlocks when creating members and groups
|
||||||
|
in the same test run (same as IndexGroupsDisplayTest).
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
|
{:ok, member} =
|
||||||
|
create_member(actor, %{
|
||||||
|
first_name: "Alice",
|
||||||
|
last_name: "Anderson",
|
||||||
|
email: "alice@example.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
%{member: member, actor: actor}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "displays Groups section when member has at least one group", %{
|
||||||
|
conn: conn,
|
||||||
|
member: member,
|
||||||
|
actor: actor
|
||||||
|
} do
|
||||||
|
{: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}")
|
||||||
|
|
||||||
|
assert html =~ gettext("Groups")
|
||||||
|
assert html =~ group.name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "displays all group links with correct names when member is in multiple groups", %{
|
||||||
|
conn: conn,
|
||||||
|
member: member,
|
||||||
|
actor: actor
|
||||||
|
} do
|
||||||
|
{: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}")
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# 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 "renders all group names when member has multiple groups", %{
|
||||||
|
conn: conn,
|
||||||
|
member: member,
|
||||||
|
actor: actor
|
||||||
|
} do
|
||||||
|
{: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}")
|
||||||
|
|
||||||
|
assert html =~ "Alpha"
|
||||||
|
assert html =~ "Beta"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "groups section links" do
|
||||||
|
setup do
|
||||||
|
actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
|
{:ok, member} =
|
||||||
|
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 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}")
|
||||||
|
|
||||||
|
# Link to group detail: /groups/:slug (slug is URL-friendly, e.g. "board-members")
|
||||||
|
assert has_element?(view, "a[href*='/groups/#{group.slug}']", group.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "clicking group link 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
|
||||||
|
actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
|
{:ok, member} =
|
||||||
|
create_member(actor, %{
|
||||||
|
first_name: "Charlie",
|
||||||
|
last_name: "Clark",
|
||||||
|
email: "charlie@example.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
%{member: member, actor: actor}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "member in exactly one group shows single link", %{
|
||||||
|
conn: conn,
|
||||||
|
member: member,
|
||||||
|
actor: actor
|
||||||
|
} do
|
||||||
|
{: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}")
|
||||||
|
|
||||||
|
assert html =~ gettext("Groups")
|
||||||
|
assert html =~ group.name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "member in many groups shows all group links", %{
|
||||||
|
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} = create_group(actor, name)
|
||||||
|
g
|
||||||
|
end)
|
||||||
|
|
||||||
|
for g <- groups do
|
||||||
|
{:ok, _mg} = add_member_to_group(member, g, 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
|
||||||
|
actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
|
{:ok, member} =
|
||||||
|
create_member(actor, %{
|
||||||
|
first_name: "Diana",
|
||||||
|
last_name: "Davis",
|
||||||
|
email: "diana@example.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, group} = create_group(actor, "Readers")
|
||||||
|
{:ok, _mg} = add_member_to_group(member, group, 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
|
||||||
|
actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
|
{:ok, member} =
|
||||||
|
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 links have 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
|
||||||
|
|
||||||
|
# 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue