mitgliederverwaltung/test/mv_web/member_live/index_test.exs
Moritz 39d2cb7820
All checks were successful
continuous-integration/drone/push Build is passing
refactor: improve email copy with MapSet, RFC 5322 commas, and cond
Performance optimization, RFC-compliant separator, better tests
2025-12-02 12:10:59 +01:00

472 lines
15 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 erstellt erfolgreich")
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 create 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
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
end