This commit is contained in:
parent
3322efcdf6
commit
5fd7c0e7f6
13 changed files with 583 additions and 258 deletions
|
|
@ -62,18 +62,21 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do
|
|||
end
|
||||
|
||||
@tag :ui
|
||||
test "filter dropdown has aria-label", %{
|
||||
test "filter dropdown has group presence section with legend", %{
|
||||
conn: conn
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members")
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Verify filter dropdown has aria-label
|
||||
assert html =~ ~r/select.*name=["']group_filter["'].*aria-label=/ or
|
||||
html =~ ~r/aria-label=.*[Gg]roup/
|
||||
# Open filter dropdown
|
||||
view
|
||||
|> element("button[aria-label='Filter members']")
|
||||
|> render_click()
|
||||
|
||||
# Verify dropdown is present
|
||||
assert has_element?(view, "select[name='group_filter']")
|
||||
html = render(view)
|
||||
# Groups section: legend "Member has groups" and radios (Any / Yes / No)
|
||||
assert html =~ ~r/[Gg]roups/
|
||||
assert has_element?(view, "[data-testid='member-filter-form']")
|
||||
end
|
||||
|
||||
@tag :ui
|
||||
|
|
@ -92,26 +95,22 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do
|
|||
@tag :ui
|
||||
test "keyboard navigation works for filter dropdown", %{
|
||||
conn: conn,
|
||||
member1: member1,
|
||||
group1: group1
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Verify dropdown is keyboard accessible
|
||||
# Tab should focus the dropdown
|
||||
# Arrow keys should navigate options
|
||||
# Enter should select option
|
||||
assert has_element?(view, "select[name='group_filter']")
|
||||
|
||||
# Test that dropdown can be focused and changed via keyboard
|
||||
# (This is a basic accessibility check - actual keyboard testing would require browser automation)
|
||||
view
|
||||
|> element("#group-filter-form")
|
||||
|> render_change(%{"group_filter" => group1.id})
|
||||
|> element("button[aria-label='Filter members']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("[data-testid='member-filter-form']")
|
||||
|> render_change(%{"group_#{group1.id}" => "in", "payment_filter" => "all"})
|
||||
|
||||
# Verify change was applied
|
||||
html = render(view)
|
||||
assert html
|
||||
assert html =~ member1.first_name
|
||||
end
|
||||
|
||||
@tag :ui
|
||||
|
|
@ -121,18 +120,14 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Verify sort header is keyboard accessible
|
||||
# Tab should focus the sort header
|
||||
# Enter/Space should activate sorting
|
||||
assert has_element?(view, "[data-testid='groups']")
|
||||
|
||||
# Test that sort header can be activated via click (simulating keyboard)
|
||||
view
|
||||
|> element("[data-testid='groups']")
|
||||
|> render_click()
|
||||
|
||||
# Verify sort was applied
|
||||
assert_patch(view, "/members?query=&sort_field=groups&sort_order=asc")
|
||||
# Verify sort was applied (URL may include other params)
|
||||
assert has_element?(view, "[data-testid='groups'][aria-label*='ascending']")
|
||||
end
|
||||
|
||||
@tag :ui
|
||||
|
|
@ -144,19 +139,16 @@ defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Apply filter
|
||||
view
|
||||
|> element("#group-filter-form")
|
||||
|> render_change(%{"group_filter" => group1.id})
|
||||
|> element("button[aria-label='Filter members']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("[data-testid='member-filter-form']")
|
||||
|> render_change(%{"group_#{group1.id}" => "in", "payment_filter" => "all"})
|
||||
|
||||
# Verify filter change is announced (via aria-live region or similar)
|
||||
html = render(view)
|
||||
# Should show filtered results
|
||||
assert html =~ member1.first_name
|
||||
|
||||
# Verify member count or filter status is announced
|
||||
# (Implementation might use aria-live="polite" for announcements)
|
||||
assert html
|
||||
end
|
||||
|
||||
@tag :ui
|
||||
|
|
|
|||
|
|
@ -64,7 +64,11 @@ defmodule MvWeb.MemberLive.IndexGroupsDisplayTest do
|
|||
%{member1: member1, member2: member2, member3: member3, group1: group1, group2: group2}
|
||||
end
|
||||
|
||||
test "displays group badges for members in groups", %{conn: conn, group1: group1, group2: group2} do
|
||||
test "displays group badges for members in groups", %{
|
||||
conn: conn,
|
||||
group1: group1,
|
||||
group2: group2
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
defmodule MvWeb.MemberLive.IndexGroupsFilterTest do
|
||||
@moduledoc """
|
||||
Tests for filtering members by group in the member overview.
|
||||
|
||||
Uses the filter dropdown (MemberFilterComponent) with one row per group:
|
||||
All / Yes / No (per group).
|
||||
"""
|
||||
# async: false to prevent PostgreSQL deadlocks when creating members and groups
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
|
@ -53,7 +56,28 @@ defmodule MvWeb.MemberLive.IndexGroupsFilterTest do
|
|||
%{member1: member1, member2: member2, member3: member3, group1: group1, group2: group2}
|
||||
end
|
||||
|
||||
test "filter 'All groups' shows all members", %{conn: conn, member1: m1, member2: m2, member3: m3} do
|
||||
defp open_filter_and_set_group(view, group_id, value) do
|
||||
view
|
||||
|> element("button[aria-label='Filter members']")
|
||||
|> render_click()
|
||||
|
||||
key = "group_#{group_id}"
|
||||
|
||||
view
|
||||
|> element("[data-testid='member-filter-form']")
|
||||
|> render_change(%{key => value, "payment_filter" => "all"})
|
||||
|
||||
# Force LiveView to process {:group_filter_changed, ...} (render triggers mailbox processing)
|
||||
_ = render(view)
|
||||
assert_patch(view)
|
||||
end
|
||||
|
||||
test "filter All (default) shows all members", %{
|
||||
conn: conn,
|
||||
member1: m1,
|
||||
member2: m2,
|
||||
member3: m3
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
assert html =~ m1.first_name
|
||||
|
|
@ -61,7 +85,7 @@ defmodule MvWeb.MemberLive.IndexGroupsFilterTest do
|
|||
assert html =~ m3.first_name
|
||||
end
|
||||
|
||||
test "filter by specific group shows only members in that group", %{
|
||||
test "filter group1 Yes shows only members in group1", %{
|
||||
conn: conn,
|
||||
member1: m1,
|
||||
member2: m2,
|
||||
|
|
@ -71,9 +95,7 @@ defmodule MvWeb.MemberLive.IndexGroupsFilterTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
view
|
||||
|> element("#group-filter-form")
|
||||
|> render_change(%{"group_filter" => group1.id})
|
||||
open_filter_and_set_group(view, group1.id, "in")
|
||||
|
||||
html = render(view)
|
||||
assert html =~ m1.first_name
|
||||
|
|
@ -81,21 +103,45 @@ defmodule MvWeb.MemberLive.IndexGroupsFilterTest do
|
|||
refute html =~ m3.first_name
|
||||
end
|
||||
|
||||
test "filter persists in URL parameters", %{conn: conn, group1: group1, member1: m1} do
|
||||
test "filter group1 No shows only members not in group1", %{
|
||||
conn: conn,
|
||||
member1: m1,
|
||||
member2: m2,
|
||||
member3: m3,
|
||||
group1: group1
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
view
|
||||
|> element("#group-filter-form")
|
||||
|> render_change(%{"group_filter" => group1.id})
|
||||
open_filter_and_set_group(view, group1.id, "not_in")
|
||||
|
||||
html = render(view)
|
||||
refute html =~ m1.first_name
|
||||
assert html =~ m2.first_name
|
||||
assert html =~ m3.first_name
|
||||
end
|
||||
|
||||
test "filter persists in URL parameters", %{
|
||||
conn: conn,
|
||||
member1: m1,
|
||||
member2: m2,
|
||||
member3: m3,
|
||||
group1: group1
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
open_filter_and_set_group(view, group1.id, "in")
|
||||
|
||||
# Verify filter is applied
|
||||
html = render(view)
|
||||
assert html =~ m1.first_name
|
||||
refute html =~ m2.first_name
|
||||
refute html =~ m3.first_name
|
||||
|
||||
# Verify visiting with group_filter in URL shows same filtered list
|
||||
{:ok, _view2, html2} = live(conn, "/members?group_filter=#{group1.id}")
|
||||
{:ok, _view2, html2} = live(conn, "/members?group_#{group1.id}=in")
|
||||
assert html2 =~ m1.first_name
|
||||
refute html2 =~ m2.first_name
|
||||
refute html2 =~ m3.first_name
|
||||
end
|
||||
|
||||
test "filter is restored from URL on load", %{
|
||||
|
|
@ -106,7 +152,7 @@ defmodule MvWeb.MemberLive.IndexGroupsFilterTest do
|
|||
group1: group1
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members?group_filter=#{group1.id}")
|
||||
{:ok, _view, html} = live(conn, "/members?group_#{group1.id}=in")
|
||||
assert html =~ m1.first_name
|
||||
refute html =~ m2.first_name
|
||||
refute html =~ m3.first_name
|
||||
|
|
|
|||
|
|
@ -102,8 +102,12 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
|
|||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
view
|
||||
|> element("#group-filter-form")
|
||||
|> render_change(%{"group_filter" => group1.id})
|
||||
|> element("button[aria-label='Filter members']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("[data-testid='member-filter-form']")
|
||||
|> render_change(%{"group_#{group1.id}" => "in", "payment_filter" => "all"})
|
||||
|
||||
html = render(view)
|
||||
assert html =~ member1.first_name
|
||||
|
|
@ -160,13 +164,13 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
|
|||
|> Ash.create(actor: system_actor)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
# Visit with both group filter and cycle status filter in URL (cycle filter is toggled via button, not a select).
|
||||
# Cycle filter may depend on "current" cycle; we only verify the page loads with both params.
|
||||
|
||||
{:ok, _view, html} =
|
||||
live(conn, "/members?group_filter=#{group1.id}&cycle_status_filter=paid")
|
||||
live(conn, "/members?group_#{group1.id}=in&cycle_status_filter=paid")
|
||||
|
||||
assert html =~ "Members"
|
||||
assert html =~ group1.name
|
||||
# member1 has a group and a paid cycle; page should load with both filters
|
||||
assert html =~ member1.first_name
|
||||
end
|
||||
|
||||
test "groups work with existing search (not testing search integration)", %{
|
||||
|
|
@ -180,8 +184,12 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
|
|||
|
||||
# Apply group filter
|
||||
view
|
||||
|> element("#group-filter-form")
|
||||
|> render_change(%{"group_filter" => group1.id})
|
||||
|> element("button[aria-label='Filter members']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("[data-testid='member-filter-form']")
|
||||
|> render_change(%{"group_#{group1.id}" => "in", "payment_filter" => "all"})
|
||||
|
||||
# Apply search (this tests that filter and search work together;
|
||||
# search form is in SearchBarComponent with phx-submit="search")
|
||||
|
|
@ -208,8 +216,12 @@ defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
|
|||
|
||||
# Apply group filter
|
||||
view
|
||||
|> element("#group-filter-form")
|
||||
|> render_change(%{"group_filter" => group1.id})
|
||||
|> element("button[aria-label='Filter members']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("[data-testid='member-filter-form']")
|
||||
|> render_change(%{"group_#{group1.id}" => "in", "payment_filter" => "all"})
|
||||
|
||||
# Apply sorting
|
||||
view
|
||||
|
|
|
|||
|
|
@ -97,30 +97,28 @@ defmodule MvWeb.MemberLive.IndexGroupsPerformanceTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Apply filter
|
||||
# Open filter and apply "Yes" for group1 (even-indexed members are in group1)
|
||||
view
|
||||
|> element("#group-filter-form")
|
||||
|> render_change(%{"group_filter" => group1.id})
|
||||
|> element("button[aria-label='Filter members']")
|
||||
|> render_click()
|
||||
|
||||
# Verify only filtered members are shown
|
||||
view
|
||||
|> element("[data-testid='member-filter-form']")
|
||||
|> render_change(%{"group_#{group1.id}" => "in", "payment_filter" => "all"})
|
||||
|
||||
# Force LiveView to process {:group_filter_changed, ...}
|
||||
html = render(view)
|
||||
|
||||
# Members with even indices (0, 2, 4, 6, 8) are in group1
|
||||
even_members = Enum.filter(0..9, &(rem(&1, 2) == 0))
|
||||
odd_members = Enum.filter(0..9, &(rem(&1, 2) == 1))
|
||||
|
||||
Enum.each(even_members, fn i ->
|
||||
# Only even-indexed members (0,2,4,6,8) are in group1
|
||||
Enum.each([0, 2, 4, 6, 8], fn i ->
|
||||
member = Enum.at(members, i)
|
||||
assert html =~ member.first_name
|
||||
end)
|
||||
|
||||
Enum.each(odd_members, fn i ->
|
||||
Enum.each([1, 3, 5, 7, 9], fn i ->
|
||||
member = Enum.at(members, i)
|
||||
refute html =~ member.first_name
|
||||
end)
|
||||
|
||||
# If filtering was done in-memory, we'd load all members first
|
||||
# Database-level filtering is more efficient
|
||||
end
|
||||
|
||||
@tag :slow
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ defmodule MvWeb.MemberLive.IndexGroupsUrlParamsTest do
|
|||
Tests for URL parameter persistence for groups in the member overview.
|
||||
|
||||
Tests cover:
|
||||
- Group filter is written to URL (group_filter=<group_id>)
|
||||
- Group presence filter is written to URL (group_presence=has_groups|no_groups)
|
||||
- Group sorting is written to URL (sort_field=groups&sort_order=asc)
|
||||
- URL parameters are restored on load
|
||||
- URL parameters work with other parameters (query, sort_field, etc.)
|
||||
|
|
@ -53,19 +53,22 @@ defmodule MvWeb.MemberLive.IndexGroupsUrlParamsTest do
|
|||
|
||||
test "group filter is written to URL", %{
|
||||
conn: conn,
|
||||
member1: member1,
|
||||
group1: group1
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Apply group filter
|
||||
view
|
||||
|> element("#group-filter-form")
|
||||
|> render_change(%{"group_filter" => group1.id})
|
||||
|> element("button[aria-label='Filter members']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("[data-testid='member-filter-form']")
|
||||
|> render_change(%{"group_#{group1.id}" => "in", "payment_filter" => "all"})
|
||||
|
||||
# Verify filter was applied (URL is patched with group_filter and other default params)
|
||||
html = render(view)
|
||||
assert html =~ group1.name
|
||||
assert html =~ member1.first_name
|
||||
end
|
||||
|
||||
test "group sorting is written to URL", %{
|
||||
|
|
@ -92,13 +95,10 @@ defmodule MvWeb.MemberLive.IndexGroupsUrlParamsTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
{:ok, view, html} =
|
||||
live(conn, "/members?group_filter=#{group1.id}&sort_field=groups&sort_order=asc")
|
||||
live(conn, "/members?group_#{group1.id}=in&sort_field=groups&sort_order=asc")
|
||||
|
||||
# Verify filter is applied
|
||||
assert html =~ member1.first_name
|
||||
refute html =~ member2.first_name
|
||||
|
||||
# Verify sort is applied
|
||||
assert has_element?(view, "[data-testid='groups'][aria-label*='ascending']")
|
||||
end
|
||||
|
||||
|
|
@ -108,23 +108,22 @@ defmodule MvWeb.MemberLive.IndexGroupsUrlParamsTest do
|
|||
group1: group1
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members?query=Alice&group_filter=#{group1.id}")
|
||||
{:ok, _view, html} = live(conn, "/members?query=Alice&group_#{group1.id}=in")
|
||||
|
||||
# Verify both query and filter are applied (URL may include other default params)
|
||||
assert html =~ member1.first_name
|
||||
end
|
||||
|
||||
test "URL parameters work with other sort fields", %{
|
||||
conn: conn,
|
||||
member1: member1,
|
||||
group1: group1
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
{:ok, view, html} =
|
||||
live(conn, "/members?sort_field=first_name&sort_order=desc&group_filter=#{group1.id}")
|
||||
live(conn, "/members?sort_field=first_name&sort_order=desc&group_#{group1.id}=in")
|
||||
|
||||
# Verify all parameters are preserved (filter applied, sort reflected in UI)
|
||||
assert html =~ group1.name
|
||||
assert html =~ member1.first_name
|
||||
assert has_element?(view, "[data-testid='first_name'][aria-label*='descending']")
|
||||
end
|
||||
|
||||
|
|
@ -134,37 +133,28 @@ defmodule MvWeb.MemberLive.IndexGroupsUrlParamsTest do
|
|||
group1: group1
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
# Simulate bookmarking a URL with filter and sort
|
||||
bookmark_url = "/members?group_filter=#{group1.id}&sort_field=groups&sort_order=asc"
|
||||
bookmark_url = "/members?group_#{group1.id}=in&sort_field=groups&sort_order=asc"
|
||||
|
||||
{:ok, view, html} = live(conn, bookmark_url)
|
||||
|
||||
# Verify filter and sort are both applied when loading bookmarked URL
|
||||
assert html =~ member1.first_name
|
||||
assert has_element?(view, "[data-testid='groups'][aria-label*='ascending']")
|
||||
end
|
||||
|
||||
test "handles multiple group_filter parameters (uses last one)", %{
|
||||
test "handles multiple group filter parameters (uses last one)", %{
|
||||
conn: conn,
|
||||
member1: member1,
|
||||
member2: member2,
|
||||
group1: group1
|
||||
} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, group2} =
|
||||
Group
|
||||
|> Ash.Changeset.for_create(:create, %{name: "Active Members"})
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
# URL with duplicate parameters (should use last one)
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?group_filter=#{group1.id}&group_filter=#{group2.id}")
|
||||
# Duplicate param for same group: last wins. group_id=in then not_in -> not_in
|
||||
{:ok, _view, html} =
|
||||
live(conn, "/members?group_#{group1.id}=in&group_#{group1.id}=not_in")
|
||||
|
||||
# Verify the last filter value is used
|
||||
# Implementation should handle this gracefully
|
||||
html = render(view)
|
||||
# Should show members from group2 (last filter)
|
||||
assert html
|
||||
# not_in group1: member2 and member3 (member1 is in group1)
|
||||
refute html =~ member1.first_name
|
||||
assert html =~ member2.first_name
|
||||
end
|
||||
|
||||
test "handles invalid URL parameters gracefully", %{
|
||||
|
|
@ -173,11 +163,10 @@ defmodule MvWeb.MemberLive.IndexGroupsUrlParamsTest do
|
|||
member2: member2
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
# URL with invalid group_filter (non-existent UUID)
|
||||
invalid_id = Ecto.UUID.generate()
|
||||
{:ok, view, html} = live(conn, "/members?group_filter=#{invalid_id}")
|
||||
{:ok, _view, html} = live(conn, "/members?group_#{invalid_id}=in")
|
||||
|
||||
# Verify all members are shown (invalid filter ignored)
|
||||
# Unknown group id ignored, all members shown
|
||||
assert html =~ member1.first_name
|
||||
assert html =~ member2.first_name
|
||||
end
|
||||
|
|
@ -188,10 +177,8 @@ defmodule MvWeb.MemberLive.IndexGroupsUrlParamsTest do
|
|||
member2: member2
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
# URL with malformed group_filter (not a UUID)
|
||||
{:ok, view, html} = live(conn, "/members?group_filter=not-a-uuid")
|
||||
{:ok, _view, html} = live(conn, "/members?group_not-a-uuid=in")
|
||||
|
||||
# Verify all members are shown (malformed filter ignored)
|
||||
assert html =~ member1.first_name
|
||||
assert html =~ member2.first_name
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue