Replace paid_filter with cycle_status_filter that filters based on membership fee cycle status (last or current cycle). Update PaymentFilterComponent to use new filter with options All, Paid, Unpaid. Remove membership fee status filter dropdown. Extend filter_members_by_cycle_status/3 to support both paid and unpaid filtering. Update toggle_cycle_view to preserve filter state in URL.
660 lines
21 KiB
Elixir
660 lines
21 KiB
Elixir
defmodule MvWeb.MemberLive.IndexTest do
|
||
use MvWeb.ConnCase, async: true
|
||
import Phoenix.LiveViewTest
|
||
require Ash.Query
|
||
|
||
test "shows translated title in German", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
conn = Plug.Test.init_test_session(conn, locale: "de")
|
||
{:ok, _view, html} = live(conn, "/members")
|
||
# Expected German title
|
||
assert html =~ "Mitglieder"
|
||
end
|
||
|
||
test "shows translated title in English", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
Gettext.put_locale(MvWeb.Gettext, "en")
|
||
{:ok, _view, html} = live(conn, "/members")
|
||
# Expected English title
|
||
assert html =~ "Members"
|
||
end
|
||
|
||
test "shows translated button text in German", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
conn = Plug.Test.init_test_session(conn, locale: "de")
|
||
{:ok, _view, html} = live(conn, "/members/new")
|
||
assert html =~ "Speichern"
|
||
end
|
||
|
||
test "shows translated button text in English", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
Gettext.put_locale(MvWeb.Gettext, "en")
|
||
{:ok, _view, html} = live(conn, "/members/new")
|
||
assert html =~ "Save"
|
||
end
|
||
|
||
test "shows translated flash message after creating a member in German", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
conn = Plug.Test.init_test_session(conn, locale: "de")
|
||
{:ok, form_view, _html} = live(conn, "/members/new")
|
||
|
||
form_data = %{
|
||
"member[first_name]" => "Max",
|
||
"member[last_name]" => "Mustermann",
|
||
"member[email]" => "max@example.com"
|
||
}
|
||
|
||
# Submit form and follow the redirect to get the flash message
|
||
{:ok, index_view, _html} =
|
||
form_view
|
||
|> form("#member-form", form_data)
|
||
|> render_submit()
|
||
|> follow_redirect(conn, "/members")
|
||
|
||
assert has_element?(index_view, "#flash-group", "Mitglied wurde erfolgreich erstellt")
|
||
end
|
||
|
||
test "shows translated flash message after creating a member in English", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, form_view, _html} = live(conn, "/members/new")
|
||
|
||
form_data = %{
|
||
"member[first_name]" => "Max",
|
||
"member[last_name]" => "Mustermann",
|
||
"member[email]" => "max@example.com"
|
||
}
|
||
|
||
# Submit form and follow the redirect to get the flash message
|
||
{:ok, index_view, _html} =
|
||
form_view
|
||
|> form("#member-form", form_data)
|
||
|> render_submit()
|
||
|> follow_redirect(conn, "/members")
|
||
|
||
assert has_element?(index_view, "#flash-group", "Member created successfully")
|
||
end
|
||
|
||
describe "sorting integration" do
|
||
test "clicking a column header toggles sort order and updates the URL", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members")
|
||
|
||
# The component data test ids are built with the name of the field
|
||
# First click – should sort ASC
|
||
view
|
||
|> element("[data-testid='email']")
|
||
|> render_click()
|
||
|
||
# The LiveView pushes a patch with the new query params
|
||
assert_patch(view, "/members?query=&sort_field=email&sort_order=asc")
|
||
|
||
# Second click – toggles to DESC
|
||
view
|
||
|> element("[data-testid='email']")
|
||
|> render_click()
|
||
|
||
assert_patch(view, "/members?query=&sort_field=email&sort_order=desc")
|
||
end
|
||
|
||
test "clicking different column header resets order to ascending", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members?sort_field=email&sort_order=desc")
|
||
|
||
# Click on a different column
|
||
view
|
||
|> element("[data-testid='first_name']")
|
||
|> render_click()
|
||
|
||
assert_patch(view, "/members?query=&sort_field=first_name&sort_order=asc")
|
||
end
|
||
|
||
test "all sortable columns work correctly", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members")
|
||
|
||
# default ascending sorting with first name
|
||
assert has_element?(view, "[data-testid='first_name'][aria-label='ascending']")
|
||
|
||
sortable_fields = [
|
||
:email,
|
||
:street,
|
||
:house_number,
|
||
:postal_code,
|
||
:city,
|
||
:phone_number,
|
||
:join_date
|
||
]
|
||
|
||
for field <- sortable_fields do
|
||
view
|
||
|> element("[data-testid='#{field}']")
|
||
|> render_click()
|
||
|
||
assert_patch(view, "/members?query=&sort_field=#{field}&sort_order=asc")
|
||
end
|
||
end
|
||
|
||
test "sorting works with search query", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members?query=test")
|
||
|
||
view
|
||
|> element("[data-testid='email']")
|
||
|> render_click()
|
||
|
||
assert_patch(view, "/members?query=test&sort_field=email&sort_order=asc")
|
||
end
|
||
|
||
test "sorting maintains search query when toggling order", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members?query=test&sort_field=email&sort_order=asc")
|
||
|
||
view
|
||
|> element("[data-testid='email']")
|
||
|> render_click()
|
||
|
||
assert_patch(view, "/members?query=test&sort_field=email&sort_order=desc")
|
||
end
|
||
end
|
||
|
||
describe "URL param handling" do
|
||
test "handle_params reads sort query and applies it", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members?query=&sort_field=email&sort_order=desc")
|
||
|
||
# Check that the sort state is correctly applied
|
||
assert has_element?(view, "[data-testid='email'][aria-label='descending']")
|
||
end
|
||
|
||
test "handle_params handles invalid sort field gracefully", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members?query=&sort_field=invalid_field&sort_order=asc")
|
||
|
||
# Should not crash and should show default first name order
|
||
assert has_element?(view, "[data-testid='first_name'][aria-label='ascending']")
|
||
end
|
||
|
||
test "handle_params preserves search query with sort params", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members?query=test&sort_field=email&sort_order=desc")
|
||
|
||
# Both search and sort should be preserved
|
||
assert has_element?(view, "[data-testid='email'][aria-label='descending']")
|
||
end
|
||
end
|
||
|
||
describe "search and sort integration" do
|
||
test "search maintains sort state", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members?query=&sort_field=email&sort_order=desc")
|
||
|
||
# Perform search
|
||
view
|
||
|> element("[data-testid='search-input']")
|
||
|> render_change(%{value: "test"})
|
||
|
||
# Sort state should be maintained
|
||
assert has_element?(view, "[data-testid='email'][aria-label='descending']")
|
||
end
|
||
|
||
test "sort maintains search state", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members?query=test&sort_field=email&sort_order=asc")
|
||
|
||
# Perform sort
|
||
view
|
||
|> element("[data-testid='email']")
|
||
|> render_click()
|
||
|
||
# Search state should be maintained
|
||
assert_patch(view, "/members?query=test&sort_field=email&sort_order=desc")
|
||
end
|
||
end
|
||
|
||
test "handle_info(:search_changed) updates assigns with search results", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members")
|
||
|
||
send(view.pid, {:search_changed, "Friedrich"})
|
||
|
||
state = :sys.get_state(view.pid)
|
||
|
||
assert state.socket.assigns.query == "Friedrich"
|
||
assert is_list(state.socket.assigns.members)
|
||
end
|
||
|
||
test "can delete a member without error", %{conn: conn} do
|
||
# Create a test member first
|
||
{:ok, member} =
|
||
Mv.Membership.create_member(%{
|
||
first_name: "Test",
|
||
last_name: "User",
|
||
email: "test@example.com"
|
||
})
|
||
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, index_view, _html} = live(conn, "/members")
|
||
|
||
# Verify the member is displayed
|
||
assert has_element?(index_view, "#members", "Test User")
|
||
|
||
# Click the delete link for this member
|
||
index_view
|
||
|> element("a", "Delete")
|
||
|> render_click()
|
||
|
||
# Verify the member is no longer displayed
|
||
refute has_element?(index_view, "#members", "Test User")
|
||
|
||
# Verify the member was actually deleted from the database
|
||
assert not (Mv.Membership.Member |> Ash.Query.filter(id == ^member.id) |> Ash.exists?())
|
||
end
|
||
|
||
describe "copy_emails feature" do
|
||
setup do
|
||
# Create test members
|
||
{:ok, member1} =
|
||
Mv.Membership.create_member(%{
|
||
first_name: "Max",
|
||
last_name: "Mustermann",
|
||
email: "max@example.com"
|
||
})
|
||
|
||
{:ok, member2} =
|
||
Mv.Membership.create_member(%{
|
||
first_name: "Erika",
|
||
last_name: "Musterfrau",
|
||
email: "erika@example.com"
|
||
})
|
||
|
||
{:ok, member3} =
|
||
Mv.Membership.create_member(%{
|
||
first_name: "Hans",
|
||
last_name: "Müller-Lüdenscheidt",
|
||
email: "hans@example.com"
|
||
})
|
||
|
||
%{member1: member1, member2: member2, member3: member3}
|
||
end
|
||
|
||
test "copy_emails event formats selected members correctly", %{
|
||
conn: conn,
|
||
member1: member1,
|
||
member2: member2
|
||
} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members")
|
||
|
||
# Select two members by sending the select_member event directly
|
||
render_click(view, "select_member", %{"id" => member1.id})
|
||
render_click(view, "select_member", %{"id" => member2.id})
|
||
|
||
# Trigger copy_emails event
|
||
view |> element("#copy-emails-btn") |> render_click()
|
||
|
||
# Verify flash message shows correct count
|
||
assert render(view) =~ "2"
|
||
end
|
||
|
||
test "copy_emails event with no selection shows error flash", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members")
|
||
|
||
# Trigger copy_emails event directly (button not visible when no selection)
|
||
# This tests the edge case where event is triggered without selection
|
||
result = render_hook(view, "copy_emails", %{})
|
||
|
||
# Should show error flash
|
||
assert result =~ "No members selected" or result =~ "Keine Mitglieder"
|
||
end
|
||
|
||
test "copy_emails event with all members selected formats all emails", %{
|
||
conn: conn
|
||
} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members")
|
||
|
||
# Select all members via select_all
|
||
view |> element("[phx-click='select_all']") |> render_click()
|
||
|
||
# Trigger copy_emails event
|
||
view |> element("#copy-emails-btn") |> render_click()
|
||
|
||
# Verify flash message shows correct count (3 members)
|
||
assert render(view) =~ "3"
|
||
end
|
||
|
||
test "copy_emails handles members with special characters in names", %{
|
||
conn: conn,
|
||
member3: member3
|
||
} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members")
|
||
|
||
# Select member with umlauts by sending the select_member event directly
|
||
render_click(view, "select_member", %{"id" => member3.id})
|
||
|
||
# Trigger copy_emails event - should not crash
|
||
view |> element("#copy-emails-btn") |> render_click()
|
||
|
||
# Verify flash message shows success
|
||
assert render(view) =~ "1"
|
||
end
|
||
|
||
test "copy_emails handles case where selected member is deleted before copy", %{
|
||
conn: conn,
|
||
member1: member1
|
||
} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members")
|
||
|
||
# Select a member by sending the select_member event directly
|
||
render_click(view, "select_member", %{"id" => member1.id})
|
||
|
||
# Delete the member from the database
|
||
Ash.destroy!(member1)
|
||
|
||
# Trigger copy_emails event directly - selection still contains the deleted ID
|
||
# but the member is no longer in @members list after reload
|
||
result = render_hook(view, "copy_emails", %{})
|
||
|
||
# Should show error since no visible members match selection
|
||
assert result =~ "No email" or result =~ "Keine E-Mail" or result =~ "0"
|
||
end
|
||
|
||
test "copy_emails formats emails as RFC 5322 compliant comma-separated list", %{
|
||
conn: conn,
|
||
member1: member1,
|
||
member2: member2
|
||
} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members")
|
||
|
||
# Select two members by sending the select_member event directly
|
||
render_click(view, "select_member", %{"id" => member1.id})
|
||
render_click(view, "select_member", %{"id" => member2.id})
|
||
|
||
# Get the socket state to verify the formatted email string
|
||
state = :sys.get_state(view.pid)
|
||
selected_members = state.socket.assigns.selected_members
|
||
|
||
# Verify MapSet is used
|
||
assert %MapSet{} = selected_members
|
||
assert MapSet.size(selected_members) == 2
|
||
end
|
||
|
||
test "email format is 'First Last <email>' with comma separator", %{
|
||
conn: conn,
|
||
member1: _member1
|
||
} do
|
||
# Test the format_member_email function indirectly
|
||
# by checking the push_event payload structure
|
||
conn = conn_with_oidc_user(conn)
|
||
|
||
# Create a member with known data
|
||
{:ok, test_member} =
|
||
Mv.Membership.create_member(%{
|
||
first_name: "Test",
|
||
last_name: "Format",
|
||
email: "test.format@example.com"
|
||
})
|
||
|
||
{:ok, view, _html} = live(conn, "/members")
|
||
|
||
# Select the test member by sending the select_member event directly
|
||
render_click(view, "select_member", %{"id" => test_member.id})
|
||
|
||
# The format should be "Test Format <test.format@example.com>"
|
||
# We verify this by checking the flash shows 1 email was copied
|
||
view |> element("#copy-emails-btn") |> render_click()
|
||
assert render(view) =~ "1"
|
||
end
|
||
|
||
test "copy button is disabled when no members selected", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members")
|
||
|
||
# Copy button should be disabled (button element)
|
||
assert has_element?(view, "#copy-emails-btn[disabled]")
|
||
# Open email button should be disabled (link with tabindex and aria-disabled)
|
||
assert has_element?(view, "#open-email-btn[tabindex='-1'][aria-disabled='true']")
|
||
end
|
||
|
||
test "copy button is enabled after selection", %{
|
||
conn: conn,
|
||
member1: member1
|
||
} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members")
|
||
|
||
# Select a member by sending the select_member event directly
|
||
render_click(view, "select_member", %{"id" => member1.id})
|
||
|
||
# Copy button should now be enabled (no disabled attribute)
|
||
refute has_element?(view, "#copy-emails-btn[disabled]")
|
||
# Open email button should now be enabled (no tabindex=-1 or aria-disabled)
|
||
refute has_element?(view, "#open-email-btn[tabindex='-1']")
|
||
refute has_element?(view, "#open-email-btn[aria-disabled='true']")
|
||
# Counter should show correct count
|
||
assert render(view) =~ "1"
|
||
end
|
||
|
||
test "copy button click triggers event and shows flash", %{
|
||
conn: conn,
|
||
member1: member1
|
||
} do
|
||
conn = conn_with_oidc_user(conn)
|
||
{:ok, view, _html} = live(conn, "/members")
|
||
|
||
# Select a member by sending the select_member event directly
|
||
render_click(view, "select_member", %{"id" => member1.id})
|
||
|
||
# Click copy button
|
||
view |> element("#copy-emails-btn") |> render_click()
|
||
|
||
# Flash message should appear
|
||
assert has_element?(view, "#flash-group")
|
||
end
|
||
end
|
||
|
||
describe "cycle status filter" do
|
||
alias Mv.MembershipFees.MembershipFeeType
|
||
alias Mv.MembershipFees.MembershipFeeCycle
|
||
|
||
# Helper to create a membership fee type
|
||
defp create_fee_type(attrs) do
|
||
default_attrs = %{
|
||
name: "Test Fee Type #{System.unique_integer([:positive])}",
|
||
amount: Decimal.new("50.00"),
|
||
interval: :yearly
|
||
}
|
||
|
||
attrs = Map.merge(default_attrs, attrs)
|
||
|
||
MembershipFeeType
|
||
|> Ash.Changeset.for_create(:create, attrs)
|
||
|> Ash.create!()
|
||
end
|
||
|
||
# Helper to create a member
|
||
defp create_member(attrs) do
|
||
default_attrs = %{
|
||
first_name: "Test",
|
||
last_name: "Member",
|
||
email: "test.member.#{System.unique_integer([:positive])}@example.com"
|
||
}
|
||
|
||
attrs = Map.merge(default_attrs, attrs)
|
||
|
||
Mv.Membership.Member
|
||
|> Ash.Changeset.for_create(:create_member, attrs)
|
||
|> Ash.create!()
|
||
end
|
||
|
||
# Helper to create a cycle
|
||
defp create_cycle(member, fee_type, attrs) do
|
||
# Delete any auto-generated cycles first to avoid conflicts
|
||
existing_cycles =
|
||
MembershipFeeCycle
|
||
|> Ash.Query.filter(member_id == ^member.id)
|
||
|> Ash.read!()
|
||
|
||
Enum.each(existing_cycles, fn cycle -> Ash.destroy!(cycle) end)
|
||
|
||
default_attrs = %{
|
||
cycle_start: ~D[2023-01-01],
|
||
amount: Decimal.new("50.00"),
|
||
member_id: member.id,
|
||
membership_fee_type_id: fee_type.id,
|
||
status: :unpaid
|
||
}
|
||
|
||
attrs = Map.merge(default_attrs, attrs)
|
||
|
||
MembershipFeeCycle
|
||
|> Ash.Changeset.for_create(:create, attrs)
|
||
|> Ash.create!()
|
||
end
|
||
|
||
test "filter shows only members with paid status in last cycle", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
fee_type = create_fee_type(%{interval: :yearly})
|
||
today = Date.utc_today()
|
||
last_year_start = Date.new!(today.year - 1, 1, 1)
|
||
|
||
# Member with paid last cycle
|
||
paid_member =
|
||
create_member(%{
|
||
first_name: "PaidLast",
|
||
membership_fee_type_id: fee_type.id
|
||
})
|
||
|
||
create_cycle(paid_member, fee_type, %{cycle_start: last_year_start, status: :paid})
|
||
|
||
# Member with unpaid last cycle
|
||
unpaid_member =
|
||
create_member(%{
|
||
first_name: "UnpaidLast",
|
||
membership_fee_type_id: fee_type.id
|
||
})
|
||
|
||
create_cycle(unpaid_member, fee_type, %{cycle_start: last_year_start, status: :unpaid})
|
||
|
||
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=paid")
|
||
|
||
assert html =~ "PaidLast"
|
||
refute html =~ "UnpaidLast"
|
||
end
|
||
|
||
test "filter shows only members with unpaid status in last cycle", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
fee_type = create_fee_type(%{interval: :yearly})
|
||
today = Date.utc_today()
|
||
last_year_start = Date.new!(today.year - 1, 1, 1)
|
||
|
||
# Member with paid last cycle
|
||
paid_member =
|
||
create_member(%{
|
||
first_name: "PaidLast",
|
||
membership_fee_type_id: fee_type.id
|
||
})
|
||
|
||
create_cycle(paid_member, fee_type, %{cycle_start: last_year_start, status: :paid})
|
||
|
||
# Member with unpaid last cycle
|
||
unpaid_member =
|
||
create_member(%{
|
||
first_name: "UnpaidLast",
|
||
membership_fee_type_id: fee_type.id
|
||
})
|
||
|
||
create_cycle(unpaid_member, fee_type, %{cycle_start: last_year_start, status: :unpaid})
|
||
|
||
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=unpaid")
|
||
|
||
refute html =~ "PaidLast"
|
||
assert html =~ "UnpaidLast"
|
||
end
|
||
|
||
test "filter shows only members with paid status in current cycle", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
fee_type = create_fee_type(%{interval: :yearly})
|
||
today = Date.utc_today()
|
||
current_year_start = Date.new!(today.year, 1, 1)
|
||
|
||
# Member with paid current cycle
|
||
paid_member =
|
||
create_member(%{
|
||
first_name: "PaidCurrent",
|
||
membership_fee_type_id: fee_type.id
|
||
})
|
||
|
||
create_cycle(paid_member, fee_type, %{cycle_start: current_year_start, status: :paid})
|
||
|
||
# Member with unpaid current cycle
|
||
unpaid_member =
|
||
create_member(%{
|
||
first_name: "UnpaidCurrent",
|
||
membership_fee_type_id: fee_type.id
|
||
})
|
||
|
||
create_cycle(unpaid_member, fee_type, %{cycle_start: current_year_start, status: :unpaid})
|
||
|
||
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=paid&show_current_cycle=true")
|
||
|
||
assert html =~ "PaidCurrent"
|
||
refute html =~ "UnpaidCurrent"
|
||
end
|
||
|
||
test "filter shows only members with unpaid status in current cycle", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
fee_type = create_fee_type(%{interval: :yearly})
|
||
today = Date.utc_today()
|
||
current_year_start = Date.new!(today.year, 1, 1)
|
||
|
||
# Member with paid current cycle
|
||
paid_member =
|
||
create_member(%{
|
||
first_name: "PaidCurrent",
|
||
membership_fee_type_id: fee_type.id
|
||
})
|
||
|
||
create_cycle(paid_member, fee_type, %{cycle_start: current_year_start, status: :paid})
|
||
|
||
# Member with unpaid current cycle
|
||
unpaid_member =
|
||
create_member(%{
|
||
first_name: "UnpaidCurrent",
|
||
membership_fee_type_id: fee_type.id
|
||
})
|
||
|
||
create_cycle(unpaid_member, fee_type, %{cycle_start: current_year_start, status: :unpaid})
|
||
|
||
{:ok, _view, html} =
|
||
live(conn, "/members?cycle_status_filter=unpaid&show_current_cycle=true")
|
||
|
||
refute html =~ "PaidCurrent"
|
||
assert html =~ "UnpaidCurrent"
|
||
end
|
||
|
||
test "toggle cycle view updates URL and preserves filter", %{conn: conn} do
|
||
conn = conn_with_oidc_user(conn)
|
||
|
||
# Start with last cycle view and paid filter
|
||
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=paid")
|
||
|
||
# Toggle to current cycle - this should update URL and preserve filter
|
||
# Use the button in the membership fee status column header
|
||
view
|
||
|> element("button[phx-click='toggle_cycle_view'].btn-xs")
|
||
|> render_click()
|
||
|
||
# Wait for patch to complete
|
||
path = assert_patch(view)
|
||
|
||
# URL should contain both filter and show_current_cycle
|
||
assert path =~ "cycle_status_filter=paid"
|
||
assert path =~ "show_current_cycle=true"
|
||
end
|
||
end
|
||
end
|