All checks were successful
continuous-integration/drone/push Build is passing
Performance optimization, RFC-compliant separator, better tests
472 lines
15 KiB
Elixir
472 lines
15 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 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
|