Merge branch 'main' into feature/209_hide_field_dropdown
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
commit
f0613fe1e5
29 changed files with 1661 additions and 405 deletions
|
|
@ -9,7 +9,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
- Custom field values are correctly formatted for different types
|
||||
- Members without custom field values show empty cell or "-"
|
||||
"""
|
||||
use MvWeb.ConnCase, async: true
|
||||
# async: false to prevent PostgreSQL deadlocks when creating members and custom fields
|
||||
use MvWeb.ConnCase, async: false
|
||||
import Phoenix.LiveViewTest
|
||||
require Ash.Query
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ defmodule MvWeb.MemberLive.IndexMemberFieldsDisplayTest do
|
|||
|
||||
{:ok, _} =
|
||||
Mv.Membership.update_settings(settings, %{
|
||||
member_field_visibility: Map.new(fields_to_hide, &{&1, false})
|
||||
member_field_visibility: Map.new(fields_to_hide, &{Atom.to_string(&1), false})
|
||||
})
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
|
|
|||
|
|
@ -249,4 +249,441 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
# 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
|
||||
view
|
||||
|> element("[phx-click='select_member'][phx-value-id='#{member1.id}']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("[phx-click='select_member'][phx-value-id='#{member2.id}']")
|
||||
|> render_click()
|
||||
|
||||
# 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
|
||||
view
|
||||
|> element("[phx-click='select_member'][phx-value-id='#{member3.id}']")
|
||||
|> render_click()
|
||||
|
||||
# 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
|
||||
view
|
||||
|> element("[phx-click='select_member'][phx-value-id='#{member1.id}']")
|
||||
|> render_click()
|
||||
|
||||
# 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
|
||||
view
|
||||
|> element("[phx-click='select_member'][phx-value-id='#{member1.id}']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("[phx-click='select_member'][phx-value-id='#{member2.id}']")
|
||||
|> render_click()
|
||||
|
||||
# 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
|
||||
view
|
||||
|> element("[phx-click='select_member'][phx-value-id='#{test_member.id}']")
|
||||
|> render_click()
|
||||
|
||||
# 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 not visible when no members are selected", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Ensure no members are selected (default state)
|
||||
refute has_element?(view, "#copy-emails-btn")
|
||||
end
|
||||
|
||||
test "copy button is visible when members are selected", %{
|
||||
conn: conn,
|
||||
member1: member1
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Select a member
|
||||
view
|
||||
|> element("[phx-click='select_member'][phx-value-id='#{member1.id}']")
|
||||
|> render_click()
|
||||
|
||||
# Button should now be visible
|
||||
assert has_element?(view, "#copy-emails-btn")
|
||||
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
|
||||
view
|
||||
|> element("[phx-click='select_member'][phx-value-id='#{member1.id}']")
|
||||
|> render_click()
|
||||
|
||||
# Click copy button
|
||||
view |> element("#copy-emails-btn") |> render_click()
|
||||
|
||||
# Flash message should appear
|
||||
assert has_element?(view, "#flash-group")
|
||||
end
|
||||
end
|
||||
|
||||
describe "payment filter integration" do
|
||||
setup do
|
||||
# Create members with different payment status
|
||||
# Use unique names that won't appear elsewhere in the HTML
|
||||
{:ok, paid_member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Zahler",
|
||||
last_name: "Mitglied",
|
||||
email: "zahler@example.com",
|
||||
paid: true
|
||||
})
|
||||
|
||||
{:ok, unpaid_member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Nichtzahler",
|
||||
last_name: "Mitglied",
|
||||
email: "nichtzahler@example.com",
|
||||
paid: false
|
||||
})
|
||||
|
||||
{:ok, nil_paid_member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Unbestimmt",
|
||||
last_name: "Mitglied",
|
||||
email: "unbestimmt@example.com"
|
||||
# paid is nil by default
|
||||
})
|
||||
|
||||
%{paid_member: paid_member, unpaid_member: unpaid_member, nil_paid_member: nil_paid_member}
|
||||
end
|
||||
|
||||
test "filter shows all members when no filter is active", %{
|
||||
conn: conn,
|
||||
paid_member: paid_member,
|
||||
unpaid_member: unpaid_member,
|
||||
nil_paid_member: nil_paid_member
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
assert html =~ paid_member.first_name
|
||||
assert html =~ unpaid_member.first_name
|
||||
assert html =~ nil_paid_member.first_name
|
||||
end
|
||||
|
||||
test "filter shows only paid members when paid filter is active", %{
|
||||
conn: conn,
|
||||
paid_member: paid_member,
|
||||
unpaid_member: unpaid_member,
|
||||
nil_paid_member: nil_paid_member
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members?paid_filter=paid")
|
||||
|
||||
assert html =~ paid_member.first_name
|
||||
refute html =~ unpaid_member.first_name
|
||||
refute html =~ nil_paid_member.first_name
|
||||
end
|
||||
|
||||
test "filter shows only unpaid members (including nil) when not_paid filter is active", %{
|
||||
conn: conn,
|
||||
paid_member: paid_member,
|
||||
unpaid_member: unpaid_member,
|
||||
nil_paid_member: nil_paid_member
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members?paid_filter=not_paid")
|
||||
|
||||
refute html =~ paid_member.first_name
|
||||
assert html =~ unpaid_member.first_name
|
||||
assert html =~ nil_paid_member.first_name
|
||||
end
|
||||
|
||||
test "filter combines with search query (AND)", %{
|
||||
conn: conn,
|
||||
paid_member: paid_member
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members?query=Zahler&paid_filter=paid")
|
||||
|
||||
assert html =~ paid_member.first_name
|
||||
end
|
||||
|
||||
test "filter combines with sorting", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?paid_filter=paid&sort_field=first_name&sort_order=asc")
|
||||
|
||||
# Click on email sort header
|
||||
view
|
||||
|> element("[data-testid='email']")
|
||||
|> render_click()
|
||||
|
||||
# Filter should be preserved in URL
|
||||
path = assert_patch(view)
|
||||
assert path =~ "paid_filter=paid"
|
||||
assert path =~ "sort_field=email"
|
||||
end
|
||||
|
||||
test "URL parameter paid_filter is set when selecting filter", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open filter dropdown
|
||||
view
|
||||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Select "Paid" option
|
||||
view
|
||||
|> element("#payment-filter button[phx-value-filter='paid']")
|
||||
|> render_click()
|
||||
|
||||
path = assert_patch(view)
|
||||
assert path =~ "paid_filter=paid"
|
||||
end
|
||||
|
||||
test "URL parameter is correctly read on page load", %{
|
||||
conn: conn,
|
||||
paid_member: paid_member
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members?paid_filter=paid")
|
||||
|
||||
# Only paid member should be visible
|
||||
assert html =~ paid_member.first_name
|
||||
# Filter badge should be visible
|
||||
assert html =~ "badge"
|
||||
end
|
||||
|
||||
test "invalid URL parameter is ignored", %{
|
||||
conn: conn,
|
||||
paid_member: paid_member,
|
||||
unpaid_member: unpaid_member
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members?paid_filter=invalid_value")
|
||||
|
||||
# All members should be visible (filter not applied)
|
||||
assert html =~ paid_member.first_name
|
||||
assert html =~ unpaid_member.first_name
|
||||
end
|
||||
|
||||
test "search maintains filter state", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?paid_filter=paid")
|
||||
|
||||
# Perform search
|
||||
view
|
||||
|> element("[data-testid='search-input']")
|
||||
|> render_change(%{"query" => "test"})
|
||||
|
||||
# Filter state should be maintained in URL
|
||||
path = assert_patch(view)
|
||||
assert path =~ "paid_filter=paid"
|
||||
end
|
||||
end
|
||||
|
||||
describe "paid column in table" do
|
||||
setup do
|
||||
{:ok, paid_member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Paid",
|
||||
last_name: "Member",
|
||||
email: "paid.column@example.com",
|
||||
paid: true
|
||||
})
|
||||
|
||||
{:ok, unpaid_member} =
|
||||
Mv.Membership.create_member(%{
|
||||
first_name: "Unpaid",
|
||||
last_name: "Member",
|
||||
email: "unpaid.column@example.com",
|
||||
paid: false
|
||||
})
|
||||
|
||||
%{paid_member: paid_member, unpaid_member: unpaid_member}
|
||||
end
|
||||
|
||||
test "paid column shows green badge for paid members", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Check for success badge (green)
|
||||
assert html =~ "badge-success"
|
||||
end
|
||||
|
||||
test "paid column shows red badge for unpaid members", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Check for error badge (red)
|
||||
assert html =~ "badge-error"
|
||||
end
|
||||
|
||||
test "paid column shows 'Yes' for paid members", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
Gettext.put_locale(MvWeb.Gettext, "en")
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# The table should contain "Yes" text inside badge
|
||||
assert html =~ "badge-success"
|
||||
assert html =~ "Yes"
|
||||
end
|
||||
|
||||
test "paid column shows 'No' for unpaid members", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
Gettext.put_locale(MvWeb.Gettext, "en")
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# The table should contain "No" text inside badge
|
||||
assert html =~ "badge-error"
|
||||
assert html =~ "No"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue