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