659 lines
21 KiB
Elixir
659 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,
|
||
: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 toolbar
|
||
view
|
||
|> element("button[phx-click='toggle_cycle_view']")
|
||
|> 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
|