test: add tdd tests for group integration in member view #373

This commit is contained in:
Simon 2026-01-29 16:43:05 +01:00
parent dce4b2cf33
commit 3b87db6ad1
7 changed files with 864 additions and 1 deletions

View 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

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -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

View 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