test: add tdd tests for group integration in member view #373
This commit is contained in:
parent
dce4b2cf33
commit
3b87db6ad1
7 changed files with 864 additions and 1 deletions
189
test/mv_web/member_live/index_groups_accessibility_test.exs
Normal file
189
test/mv_web/member_live/index_groups_accessibility_test.exs
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
defmodule MvWeb.MemberLive.IndexGroupsAccessibilityTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for accessibility of groups feature in the member overview.
|
||||||
|
|
||||||
|
Tests cover:
|
||||||
|
- Badges have role="status" and aria-label
|
||||||
|
- Filter dropdown has aria-label
|
||||||
|
- Sort header has aria-label for screen reader
|
||||||
|
- Keyboard navigation works (Tab through filter, sort header)
|
||||||
|
"""
|
||||||
|
# async: false to prevent PostgreSQL deadlocks when creating members and groups
|
||||||
|
use MvWeb.ConnCase, async: false
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
require Ash.Query
|
||||||
|
|
||||||
|
alias Mv.Membership.{Group, MemberGroup}
|
||||||
|
|
||||||
|
setup do
|
||||||
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
|
# Create test members
|
||||||
|
{:ok, member1} =
|
||||||
|
Mv.Membership.create_member(
|
||||||
|
%{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
|
||||||
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create test groups
|
||||||
|
{:ok, group1} =
|
||||||
|
Group
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: "Board Members"})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
|
# Create member-group associations
|
||||||
|
{:ok, _mg1} =
|
||||||
|
MemberGroup
|
||||||
|
|> Ash.Changeset.for_create(:create, %{member_id: member1.id, group_id: group1.id})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
|
%{
|
||||||
|
member1: member1,
|
||||||
|
group1: group1
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :ui
|
||||||
|
test "group badges have role and aria-label", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1,
|
||||||
|
group1: group1
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Verify badges have accessibility attributes
|
||||||
|
# Badges should have role="status" and aria-label describing the group
|
||||||
|
assert html =~ ~r/role=["']status["']/ or html =~ ~r/aria-label=.*#{group1.name}/
|
||||||
|
assert html =~ group1.name
|
||||||
|
|
||||||
|
# Verify member1's row contains the badge
|
||||||
|
assert html =~ member1.first_name
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :ui
|
||||||
|
test "filter dropdown has aria-label", %{
|
||||||
|
conn: conn
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{: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/
|
||||||
|
|
||||||
|
# Verify dropdown is present
|
||||||
|
assert has_element?(view, "select[name='group_filter']")
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :ui
|
||||||
|
test "sort header has aria-label for screen reader", %{
|
||||||
|
conn: conn
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Verify sort header has aria-label
|
||||||
|
# Sort header should have aria-label describing the sort state
|
||||||
|
assert html =~ ~r/aria-label=.*[Gg]roup/ or
|
||||||
|
has_element?(view, "[data-testid='sort_groups'][aria-label]")
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :ui
|
||||||
|
test "keyboard navigation works for filter dropdown", %{
|
||||||
|
conn: conn,
|
||||||
|
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("select[name='group_filter']")
|
||||||
|
|> render_change(%{"group_filter" => group1.id})
|
||||||
|
|
||||||
|
# Verify change was applied
|
||||||
|
html = render(view)
|
||||||
|
assert html
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :ui
|
||||||
|
test "keyboard navigation works for sort header", %{
|
||||||
|
conn: conn
|
||||||
|
} 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='sort_groups']")
|
||||||
|
|
||||||
|
# Test that sort header can be activated via click (simulating keyboard)
|
||||||
|
view
|
||||||
|
|> element("[data-testid='sort_groups']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Verify sort was applied
|
||||||
|
assert_patch(view, "/members?query=&sort_field=groups&sort_order=asc")
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :ui
|
||||||
|
test "screen reader announcements for filter changes", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1,
|
||||||
|
group1: group1
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Apply filter
|
||||||
|
view
|
||||||
|
|> element("select[name='group_filter']")
|
||||||
|
|> render_change(%{"group_filter" => group1.id})
|
||||||
|
|
||||||
|
# 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
|
||||||
|
test "multiple badges are announced correctly", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1
|
||||||
|
} do
|
||||||
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
|
# Create multiple groups for member1
|
||||||
|
{:ok, group2} =
|
||||||
|
Group
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: "Active Members"})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
|
{:ok, _mg} =
|
||||||
|
MemberGroup
|
||||||
|
|> Ash.Changeset.for_create(:create, %{member_id: member1.id, group_id: group2.id})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Verify multiple badges are present
|
||||||
|
assert html =~ member1.first_name
|
||||||
|
# Both groups should be visible
|
||||||
|
# Screen reader should be able to distinguish between multiple badges
|
||||||
|
assert html
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -8,6 +8,7 @@ defmodule MvWeb.MemberLive.IndexGroupsDisplayTest do
|
||||||
- No badge for members without groups
|
- No badge for members without groups
|
||||||
- Badge shows group name correctly
|
- Badge shows group name correctly
|
||||||
"""
|
"""
|
||||||
|
# async: false to prevent PostgreSQL deadlocks when creating members and groups
|
||||||
use MvWeb.ConnCase, async: false
|
use MvWeb.ConnCase, async: false
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
@ -66,6 +67,7 @@ defmodule MvWeb.MemberLive.IndexGroupsDisplayTest do
|
||||||
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)
|
conn = conn_with_oidc_user(conn)
|
||||||
{:ok, _view, html} = live(conn, "/members")
|
{:ok, _view, html} = live(conn, "/members")
|
||||||
|
|
||||||
assert html =~ group1.name
|
assert html =~ group1.name
|
||||||
assert html =~ group2.name
|
assert html =~ group2.name
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ defmodule MvWeb.MemberLive.IndexGroupsFilterTest do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Tests for filtering members by group in the member overview.
|
Tests for filtering members by group in the member overview.
|
||||||
"""
|
"""
|
||||||
|
# async: false to prevent PostgreSQL deadlocks when creating members and groups
|
||||||
use MvWeb.ConnCase, async: false
|
use MvWeb.ConnCase, async: false
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
|
||||||
262
test/mv_web/member_live/index_groups_integration_test.exs
Normal file
262
test/mv_web/member_live/index_groups_integration_test.exs
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
defmodule MvWeb.MemberLive.IndexGroupsIntegrationTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for integration of groups with existing features in the member overview.
|
||||||
|
|
||||||
|
Tests cover:
|
||||||
|
- Groups column works with Field Visibility (column can be hidden)
|
||||||
|
- Groups filter works with Custom Field filters
|
||||||
|
- Groups sorting works with other sortings
|
||||||
|
- Groups work with Membership Fee Status filter
|
||||||
|
- Groups work with existing search (but not testing search integration itself)
|
||||||
|
"""
|
||||||
|
# async: false to prevent PostgreSQL deadlocks when creating members and groups
|
||||||
|
use MvWeb.ConnCase, async: false
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
require Ash.Query
|
||||||
|
|
||||||
|
alias Mv.Membership.{Group, MemberGroup, CustomField, CustomFieldValue}
|
||||||
|
|
||||||
|
setup do
|
||||||
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
|
# Create test members
|
||||||
|
{:ok, member1} =
|
||||||
|
Mv.Membership.create_member(
|
||||||
|
%{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
|
||||||
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, member2} =
|
||||||
|
Mv.Membership.create_member(
|
||||||
|
%{first_name: "Bob", last_name: "Brown", email: "bob@example.com"},
|
||||||
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create test groups
|
||||||
|
{:ok, group1} =
|
||||||
|
Group
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: "Board Members"})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
|
# Create member-group associations
|
||||||
|
{:ok, _mg1} =
|
||||||
|
MemberGroup
|
||||||
|
|> Ash.Changeset.for_create(:create, %{member_id: member1.id, group_id: group1.id})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
|
# Create custom field for filter integration test
|
||||||
|
{:ok, custom_field} =
|
||||||
|
CustomField
|
||||||
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
|
name: "newsletter",
|
||||||
|
value_type: :boolean,
|
||||||
|
show_in_overview: false
|
||||||
|
})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
|
# Create custom field value for member1
|
||||||
|
{:ok, _cfv} =
|
||||||
|
CustomFieldValue
|
||||||
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
|
member_id: member1.id,
|
||||||
|
custom_field_id: custom_field.id,
|
||||||
|
value: %{"_union_type" => "boolean", "_union_value" => true}
|
||||||
|
})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
|
%{
|
||||||
|
member1: member1,
|
||||||
|
member2: member2,
|
||||||
|
group1: group1,
|
||||||
|
custom_field: custom_field
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "groups column works with field visibility", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1,
|
||||||
|
group1: group1
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Verify groups column is visible by default
|
||||||
|
assert html =~ group1.name
|
||||||
|
assert html =~ member1.first_name
|
||||||
|
|
||||||
|
# Hide groups column via field visibility dropdown
|
||||||
|
# (This tests integration with field visibility feature)
|
||||||
|
# Note: Actual implementation depends on how field visibility works
|
||||||
|
# For now, we verify the column exists and can be toggled
|
||||||
|
assert html
|
||||||
|
end
|
||||||
|
|
||||||
|
test "groups filter works with custom field filters", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1,
|
||||||
|
member2: member2,
|
||||||
|
group1: group1,
|
||||||
|
custom_field: custom_field
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Apply group filter
|
||||||
|
view
|
||||||
|
|> element("select[name='group_filter']")
|
||||||
|
|> render_change(%{"group_filter" => group1.id})
|
||||||
|
|
||||||
|
# Apply custom field filter (boolean filter)
|
||||||
|
view
|
||||||
|
|> element("input[type='checkbox'][name='bf_#{custom_field.id}']")
|
||||||
|
|> render_change(%{"bf_#{custom_field.id}" => "true"})
|
||||||
|
|
||||||
|
# Verify both filters are applied
|
||||||
|
# member1 is in group1 AND has newsletter=true
|
||||||
|
# member2 is in group1 but has no newsletter value
|
||||||
|
html = render(view)
|
||||||
|
assert html =~ member1.first_name
|
||||||
|
# member2 might or might not be shown depending on filter logic
|
||||||
|
# (boolean filter might require the value to be true, not just present)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "groups sorting works with other sortings", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1,
|
||||||
|
member2: member2
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members?sort_field=first_name&sort_order=asc")
|
||||||
|
|
||||||
|
# Apply groups sorting (should combine with existing sort)
|
||||||
|
view
|
||||||
|
|> element("[data-testid='sort_groups']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Verify both sorts are applied (or groups sort replaces first_name sort)
|
||||||
|
html = render(view)
|
||||||
|
assert html =~ member1.first_name
|
||||||
|
assert html =~ member2.first_name
|
||||||
|
|
||||||
|
# URL should reflect the current sort
|
||||||
|
assert_patch(view, "/members?sort_field=groups&sort_order=asc")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "groups work with membership fee status filter", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1,
|
||||||
|
group1: group1
|
||||||
|
} do
|
||||||
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
|
# Create a membership fee type and cycle for member1
|
||||||
|
{:ok, fee_type} =
|
||||||
|
Mv.MembershipFees.MembershipFeeType
|
||||||
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
|
name: "Test Fee",
|
||||||
|
amount: Decimal.new("50.00"),
|
||||||
|
interval: :yearly
|
||||||
|
})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
|
{:ok, _cycle} =
|
||||||
|
Mv.MembershipFees.MembershipFeeCycle
|
||||||
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
|
member_id: member1.id,
|
||||||
|
membership_fee_type_id: fee_type.id,
|
||||||
|
cycle_start: ~D[2024-01-01],
|
||||||
|
amount: Decimal.new("50.00"),
|
||||||
|
status: :paid
|
||||||
|
})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Apply group filter
|
||||||
|
view
|
||||||
|
|> element("select[name='group_filter']")
|
||||||
|
|> render_change(%{"group_filter" => group1.id})
|
||||||
|
|
||||||
|
# Apply membership fee status filter (paid)
|
||||||
|
view
|
||||||
|
|> element("select[name='cycle_status_filter']")
|
||||||
|
|> render_change(%{"cycle_status_filter" => "paid"})
|
||||||
|
|
||||||
|
# Verify both filters are applied
|
||||||
|
html = render(view)
|
||||||
|
assert html =~ member1.first_name
|
||||||
|
|
||||||
|
# Verify URL contains both filters
|
||||||
|
assert_patch(view, "/members?group_filter=#{group1.id}&cycle_status_filter=paid")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "groups work with existing search (not testing search integration)", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1,
|
||||||
|
member2: member2,
|
||||||
|
group1: group1
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Apply group filter
|
||||||
|
view
|
||||||
|
|> element("select[name='group_filter']")
|
||||||
|
|> render_change(%{"group_filter" => group1.id})
|
||||||
|
|
||||||
|
# Apply search (this tests that filter and search work together,
|
||||||
|
# but we're not testing the search integration itself)
|
||||||
|
view
|
||||||
|
|> element("#search-bar form")
|
||||||
|
|> render_submit(%{"query" => "Alice"})
|
||||||
|
|
||||||
|
# Verify filter and search both work
|
||||||
|
html = render(view)
|
||||||
|
assert html =~ member1.first_name
|
||||||
|
refute html =~ member2.first_name
|
||||||
|
|
||||||
|
# Note: We're not testing that group names are searchable
|
||||||
|
# (that's part of Issue #5 - Search Integration)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "all filters and sortings work together", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1,
|
||||||
|
group1: group1,
|
||||||
|
custom_field: custom_field
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Apply group filter
|
||||||
|
view
|
||||||
|
|> element("select[name='group_filter']")
|
||||||
|
|> render_change(%{"group_filter" => group1.id})
|
||||||
|
|
||||||
|
# Apply custom field filter
|
||||||
|
view
|
||||||
|
|> element("input[type='checkbox'][name='bf_#{custom_field.id}']")
|
||||||
|
|> render_change(%{"bf_#{custom_field.id}" => "true"})
|
||||||
|
|
||||||
|
# Apply sorting
|
||||||
|
view
|
||||||
|
|> element("[data-testid='sort_groups']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Apply search
|
||||||
|
view
|
||||||
|
|> element("#search-bar form")
|
||||||
|
|> render_submit(%{"query" => "Alice"})
|
||||||
|
|
||||||
|
# Verify all filters and sorting are applied
|
||||||
|
html = render(view)
|
||||||
|
assert html =~ member1.first_name
|
||||||
|
|
||||||
|
# Verify URL contains all parameters
|
||||||
|
assert_patch(
|
||||||
|
view,
|
||||||
|
"/members?query=Alice&group_filter=#{group1.id}&sort_field=groups&sort_order=asc&bf_#{custom_field.id}=true"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
209
test/mv_web/member_live/index_groups_performance_test.exs
Normal file
209
test/mv_web/member_live/index_groups_performance_test.exs
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
defmodule MvWeb.MemberLive.IndexGroupsPerformanceTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for performance and N+1 query prevention for groups in the member overview.
|
||||||
|
|
||||||
|
Tests cover:
|
||||||
|
- Groups are loaded with members in a single query (preloading)
|
||||||
|
- No N+1 queries when loading members with groups
|
||||||
|
- Filter works at database level (not in-memory)
|
||||||
|
- Sort works at database level
|
||||||
|
"""
|
||||||
|
# async: false to prevent PostgreSQL deadlocks when creating members and groups
|
||||||
|
use MvWeb.ConnCase, async: false
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
require Ash.Query
|
||||||
|
|
||||||
|
alias Mv.Membership.{Group, MemberGroup}
|
||||||
|
|
||||||
|
setup do
|
||||||
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
|
# Create test members (enough to test performance)
|
||||||
|
members =
|
||||||
|
for i <- 1..10 do
|
||||||
|
{:ok, member} =
|
||||||
|
Mv.Membership.create_member(
|
||||||
|
%{
|
||||||
|
first_name: "Member#{i}",
|
||||||
|
last_name: "Test#{i}",
|
||||||
|
email: "member#{i}@example.com"
|
||||||
|
},
|
||||||
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
|
member
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create test groups
|
||||||
|
{:ok, group1} =
|
||||||
|
Group
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: "Group 1"})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
|
{:ok, group2} =
|
||||||
|
Group
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: "Group 2"})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
|
# Assign members to groups (alternating pattern)
|
||||||
|
Enum.each(Enum.with_index(members), fn {member, index} ->
|
||||||
|
group_id = if rem(index, 2) == 0, do: group1.id, else: group2.id
|
||||||
|
|
||||||
|
{:ok, _mg} =
|
||||||
|
MemberGroup
|
||||||
|
|> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group_id})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
end)
|
||||||
|
|
||||||
|
%{
|
||||||
|
members: members,
|
||||||
|
group1: group1,
|
||||||
|
group2: group2
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :slow
|
||||||
|
test "groups are preloaded with members (no N+1 queries)", %{
|
||||||
|
conn: conn,
|
||||||
|
members: _members
|
||||||
|
} do
|
||||||
|
# This test verifies that groups are loaded efficiently
|
||||||
|
# We check query count by monitoring database queries
|
||||||
|
# Note: Actual query counting would require Ecto query logging
|
||||||
|
# For now, we verify the functionality works correctly
|
||||||
|
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Verify all members are loaded
|
||||||
|
Enum.each(1..10, fn i ->
|
||||||
|
assert html =~ "Member#{i}"
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Verify groups are displayed (if preloaded correctly, this should work)
|
||||||
|
# If N+1 queries occurred, the page might be slow or fail
|
||||||
|
assert html
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :slow
|
||||||
|
test "filter works at database level", %{
|
||||||
|
conn: conn,
|
||||||
|
group1: group1,
|
||||||
|
members: members
|
||||||
|
} do
|
||||||
|
# This test verifies that filtering happens in the database query,
|
||||||
|
# not by filtering in-memory after loading all members
|
||||||
|
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Apply filter
|
||||||
|
view
|
||||||
|
|> element("select[name='group_filter']")
|
||||||
|
|> render_change(%{"group_filter" => group1.id})
|
||||||
|
|
||||||
|
# Verify only filtered members are shown
|
||||||
|
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 ->
|
||||||
|
member = Enum.at(members, i)
|
||||||
|
assert html =~ member.first_name
|
||||||
|
end)
|
||||||
|
|
||||||
|
Enum.each(odd_members, 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
|
||||||
|
test "sorting works at database level", %{
|
||||||
|
conn: conn,
|
||||||
|
members: _members
|
||||||
|
} do
|
||||||
|
# This test verifies that sorting happens in the database query,
|
||||||
|
# not by sorting in-memory after loading all members
|
||||||
|
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Apply sorting
|
||||||
|
view
|
||||||
|
|> element("[data-testid='sort_groups']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Verify sorting is applied
|
||||||
|
html = render(view)
|
||||||
|
|
||||||
|
# Verify members are displayed (if sorting was done in-memory,
|
||||||
|
# we'd load all members first, which is less efficient)
|
||||||
|
assert html
|
||||||
|
|
||||||
|
# Database-level sorting is more efficient for large datasets
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :slow
|
||||||
|
test "handles many members with many groups efficiently", %{
|
||||||
|
conn: conn
|
||||||
|
} do
|
||||||
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
|
# Create many members (20) with multiple groups each
|
||||||
|
members =
|
||||||
|
for i <- 1..20 do
|
||||||
|
{:ok, member} =
|
||||||
|
Mv.Membership.create_member(
|
||||||
|
%{
|
||||||
|
first_name: "Member#{i}",
|
||||||
|
last_name: "Test#{i}",
|
||||||
|
email: "member#{i}@example.com"
|
||||||
|
},
|
||||||
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
|
member
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create multiple groups
|
||||||
|
groups =
|
||||||
|
for i <- 1..5 do
|
||||||
|
{:ok, group} =
|
||||||
|
Group
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: "Group #{i}"})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
|
group
|
||||||
|
end
|
||||||
|
|
||||||
|
# Assign each member to 2-3 random groups
|
||||||
|
Enum.each(members, fn member ->
|
||||||
|
selected_groups = Enum.take_random(groups, Enum.random(2..3))
|
||||||
|
|
||||||
|
Enum.each(selected_groups, fn group ->
|
||||||
|
{:ok, _mg} =
|
||||||
|
MemberGroup
|
||||||
|
|> Ash.Changeset.for_create(:create, %{member_id: member.id, group_id: group.id})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Verify all members are loaded efficiently
|
||||||
|
Enum.each(1..20, fn i ->
|
||||||
|
assert html =~ "Member#{i}"
|
||||||
|
end)
|
||||||
|
|
||||||
|
# If preloading works correctly, this should be fast
|
||||||
|
# If N+1 queries occurred, this would be very slow
|
||||||
|
assert html
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -2,6 +2,7 @@ defmodule MvWeb.MemberLive.IndexGroupsSortingTest do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Tests for sorting by groups in the member overview.
|
Tests for sorting by groups in the member overview.
|
||||||
"""
|
"""
|
||||||
|
# async: false to prevent PostgreSQL deadlocks when creating members and groups
|
||||||
use MvWeb.ConnCase, async: false
|
use MvWeb.ConnCase, async: false
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
@ -65,5 +66,4 @@ defmodule MvWeb.MemberLive.IndexGroupsSortingTest do
|
||||||
html = render(view)
|
html = render(view)
|
||||||
assert html =~ group_a.name
|
assert html =~ group_a.name
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
200
test/mv_web/member_live/index_groups_url_params_test.exs
Normal file
200
test/mv_web/member_live/index_groups_url_params_test.exs
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
defmodule MvWeb.MemberLive.IndexGroupsUrlParamsTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for URL parameter persistence for groups in the member overview.
|
||||||
|
|
||||||
|
Tests cover:
|
||||||
|
- Group filter is written to URL (group_filter=<group_id>)
|
||||||
|
- 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.)
|
||||||
|
- URL is bookmarkable (filter/sorting persist)
|
||||||
|
"""
|
||||||
|
# async: false to prevent PostgreSQL deadlocks when creating members and groups
|
||||||
|
use MvWeb.ConnCase, async: false
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
require Ash.Query
|
||||||
|
|
||||||
|
alias Mv.Membership.{Group, MemberGroup}
|
||||||
|
|
||||||
|
setup do
|
||||||
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
|
||||||
|
# Create test members
|
||||||
|
{:ok, member1} =
|
||||||
|
Mv.Membership.create_member(
|
||||||
|
%{first_name: "Alice", last_name: "Anderson", email: "alice@example.com"},
|
||||||
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, member2} =
|
||||||
|
Mv.Membership.create_member(
|
||||||
|
%{first_name: "Bob", last_name: "Brown", email: "bob@example.com"},
|
||||||
|
actor: system_actor
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create test groups
|
||||||
|
{:ok, group1} =
|
||||||
|
Group
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: "Board Members"})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
|
# Create member-group associations
|
||||||
|
{:ok, _mg1} =
|
||||||
|
MemberGroup
|
||||||
|
|> Ash.Changeset.for_create(:create, %{member_id: member1.id, group_id: group1.id})
|
||||||
|
|> Ash.create(actor: system_actor)
|
||||||
|
|
||||||
|
%{
|
||||||
|
member1: member1,
|
||||||
|
member2: member2,
|
||||||
|
group1: group1
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "group filter is written to URL", %{
|
||||||
|
conn: conn,
|
||||||
|
group1: group1
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Apply group filter
|
||||||
|
view
|
||||||
|
|> element("select[name='group_filter']")
|
||||||
|
|> render_change(%{"group_filter" => group1.id})
|
||||||
|
|
||||||
|
# Verify URL was updated with group_filter parameter
|
||||||
|
assert_patch(view, "/members?group_filter=#{group1.id}")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "group sorting is written to URL", %{
|
||||||
|
conn: conn
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, _html} = live(conn, "/members")
|
||||||
|
|
||||||
|
# Click on groups column header to sort
|
||||||
|
view
|
||||||
|
|> element("[data-testid='sort_groups']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
# Verify URL was updated with sort parameters
|
||||||
|
assert_patch(view, "/members?query=&sort_field=groups&sort_order=asc")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "URL parameters are restored on load", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1,
|
||||||
|
member2: member2,
|
||||||
|
group1: group1
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
|
||||||
|
{:ok, view, html} =
|
||||||
|
live(conn, "/members?group_filter=#{group1.id}&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='sort_groups'][aria-label*='ascending']")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "URL parameters work with query parameter", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1,
|
||||||
|
group1: group1
|
||||||
|
} do
|
||||||
|
conn = conn_with_oidc_user(conn)
|
||||||
|
{:ok, view, html} = live(conn, "/members?query=Alice&group_filter=#{group1.id}")
|
||||||
|
|
||||||
|
# Verify both query and filter are applied
|
||||||
|
assert html =~ member1.first_name
|
||||||
|
assert_patch(view, "/members?query=Alice&group_filter=#{group1.id}")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "URL parameters work with other sort fields", %{
|
||||||
|
conn: conn,
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# Verify all parameters are preserved
|
||||||
|
assert_patch(view, "/members?sort_field=first_name&sort_order=desc&group_filter=#{group1.id}")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "URL is bookmarkable with filter and sorting", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1,
|
||||||
|
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"
|
||||||
|
|
||||||
|
{:ok, view, html} = live(conn, bookmark_url)
|
||||||
|
|
||||||
|
# Verify filter and sort are both applied
|
||||||
|
assert html =~ member1.first_name
|
||||||
|
assert has_element?(view, "[data-testid='sort_groups'][aria-label*='ascending']")
|
||||||
|
|
||||||
|
# Verify URL matches bookmark
|
||||||
|
assert_patch(view, bookmark_url)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles multiple group_filter parameters (uses last one)", %{
|
||||||
|
conn: conn,
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# Verify the last filter value is used
|
||||||
|
# Implementation should handle this gracefully
|
||||||
|
html = render(view)
|
||||||
|
# Should show members from group2 (last filter)
|
||||||
|
assert html
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles invalid URL parameters gracefully", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1,
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# Verify all members are shown (invalid filter ignored)
|
||||||
|
assert html =~ member1.first_name
|
||||||
|
assert html =~ member2.first_name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles malformed URL parameters", %{
|
||||||
|
conn: conn,
|
||||||
|
member1: member1,
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Verify all members are shown (malformed filter ignored)
|
||||||
|
assert html =~ member1.first_name
|
||||||
|
assert html =~ member2.first_name
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Add a link
Reference in a new issue