Merge branch 'main' into feature/223_member_checkbox
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
1b06f885bf
60 changed files with 8281 additions and 1644 deletions
|
|
@ -11,4 +11,70 @@ defmodule Mv.Membership.MemberFieldVisibilityTest do
|
|||
use Mv.DataCase, async: true
|
||||
|
||||
alias Mv.Membership.Member
|
||||
|
||||
describe "show_in_overview?/1" do
|
||||
test "returns true for all member fields by default" do
|
||||
# When no settings exist or member_field_visibility is not configured
|
||||
# Test with fields from constants
|
||||
member_fields = Mv.Constants.member_fields()
|
||||
|
||||
Enum.each(member_fields, fn field ->
|
||||
assert Member.show_in_overview?(field) == true,
|
||||
"Field #{field} should be visible by default"
|
||||
end)
|
||||
end
|
||||
|
||||
test "returns false for fields with show_in_overview: false in settings" do
|
||||
# Get or create settings
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
# Use a field that exists in member fields
|
||||
member_fields = Mv.Constants.member_fields()
|
||||
field_to_hide = List.first(member_fields)
|
||||
field_to_show = List.last(member_fields)
|
||||
|
||||
# Update settings to hide a field (use string keys for JSONB)
|
||||
{:ok, _updated_settings} =
|
||||
Mv.Membership.update_settings(settings, %{
|
||||
member_field_visibility: %{Atom.to_string(field_to_hide) => false}
|
||||
})
|
||||
|
||||
# JSONB may convert atom keys to string keys, so we check via show_in_overview? instead
|
||||
assert Member.show_in_overview?(field_to_hide) == false
|
||||
assert Member.show_in_overview?(field_to_show) == true
|
||||
end
|
||||
|
||||
test "returns true for non-configured fields (default)" do
|
||||
# Get or create settings
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
|
||||
# Use fields that exist in member fields
|
||||
member_fields = Mv.Constants.member_fields()
|
||||
fields_to_hide = Enum.take(member_fields, 2)
|
||||
fields_to_show = Enum.take(member_fields, -2)
|
||||
|
||||
# Update settings to hide some fields (use string keys for JSONB)
|
||||
visibility_config =
|
||||
Enum.reduce(fields_to_hide, %{}, fn field, acc ->
|
||||
Map.put(acc, Atom.to_string(field), false)
|
||||
end)
|
||||
|
||||
{:ok, _updated_settings} =
|
||||
Mv.Membership.update_settings(settings, %{
|
||||
member_field_visibility: visibility_config
|
||||
})
|
||||
|
||||
# Hidden fields should be false
|
||||
Enum.each(fields_to_hide, fn field ->
|
||||
assert Member.show_in_overview?(field) == false,
|
||||
"Field #{field} should be hidden"
|
||||
end)
|
||||
|
||||
# Unconfigured fields should still be true (default)
|
||||
Enum.each(fields_to_show, fn field ->
|
||||
assert Member.show_in_overview?(field) == true,
|
||||
"Field #{field} should be visible by default"
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
defmodule MvWeb.Components.FieldVisibilityDropdownComponentTest do
|
||||
use MvWeb.ConnCase, async: true
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
describe "field visibility dropdown in member view" do
|
||||
test "renders and toggles visibility", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, ~p"/members")
|
||||
|
||||
# Renders Dropdown
|
||||
assert has_element?(view, "[data-testid='dropdown-menu']")
|
||||
|
||||
# Opens Dropdown
|
||||
view |> element("[data-testid='dropdown-button']") |> render_click()
|
||||
assert has_element?(view, "#field-visibility-menu")
|
||||
assert has_element?(view, "button[phx-click='select_item'][phx-value-item='email']")
|
||||
assert has_element?(view, "button[phx-click='select_all']")
|
||||
assert has_element?(view, "button[phx-click='select_none']")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -150,35 +150,27 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
assert has_element?(view, "[data-testid='email'] .opacity-40")
|
||||
end
|
||||
|
||||
test "icon distribution is correct for all fields", %{conn: conn} do
|
||||
test "icon distribution shows exactly one active sort icon", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
# Test neutral state - all fields except first name (default) should show neutral icons
|
||||
# Test neutral state - only one field should have active sort icon
|
||||
{:ok, _view, html_neutral} = live(conn, "/members")
|
||||
|
||||
# Count neutral icons (should be 7 - one for each field)
|
||||
neutral_count =
|
||||
html_neutral |> String.split("hero-chevron-up-down") |> length() |> Kernel.-(1)
|
||||
|
||||
assert neutral_count == 7
|
||||
|
||||
# Count active icons (should be 1)
|
||||
# Count active icons (should be exactly 1 - ascending for default sort field)
|
||||
up_count = html_neutral |> String.split("hero-chevron-up ") |> length() |> Kernel.-(1)
|
||||
down_count = html_neutral |> String.split("hero-chevron-down ") |> length() |> Kernel.-(1)
|
||||
assert up_count == 1
|
||||
assert down_count == 0
|
||||
|
||||
# Test ascending state - one field active, others neutral
|
||||
{:ok, _view, html_asc} = live(conn, "/members?sort_field=first_name&sort_order=asc")
|
||||
assert up_count == 1, "Expected exactly 1 ascending icon, got #{up_count}"
|
||||
assert down_count == 0, "Expected 0 descending icons, got #{down_count}"
|
||||
|
||||
# Should have exactly 1 ascending icon and 7 neutral icons
|
||||
up_count = html_asc |> String.split("hero-chevron-up ") |> length() |> Kernel.-(1)
|
||||
neutral_count = html_asc |> String.split("hero-chevron-up-down") |> length() |> Kernel.-(1)
|
||||
down_count = html_asc |> String.split("hero-chevron-down ") |> length() |> Kernel.-(1)
|
||||
# Test descending state
|
||||
{:ok, _view, html_desc} = live(conn, "/members?sort_field=first_name&sort_order=desc")
|
||||
|
||||
assert up_count == 1
|
||||
assert neutral_count == 7
|
||||
assert down_count == 0
|
||||
up_count = html_desc |> String.split("hero-chevron-up ") |> length() |> Kernel.-(1)
|
||||
down_count = html_desc |> String.split("hero-chevron-down ") |> length() |> Kernel.-(1)
|
||||
|
||||
assert up_count == 0, "Expected 0 ascending icons, got #{up_count}"
|
||||
assert down_count == 1, "Expected exactly 1 descending icon, got #{down_count}"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
defmodule MvWeb.CustomFieldLive.DeletionTest do
|
||||
@moduledoc """
|
||||
Tests for CustomFieldLive.Index deletion modal and slug confirmation.
|
||||
Tests for CustomFieldLive.IndexComponent deletion modal and slug confirmation.
|
||||
Tests the custom field management component embedded in the settings page.
|
||||
|
||||
Tests cover:
|
||||
- Opening deletion confirmation modal
|
||||
|
|
@ -39,11 +40,11 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|||
# Create custom field value
|
||||
create_custom_field_value(member, custom_field, "test")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
{:ok, view, _html} = live(conn, ~p"/settings")
|
||||
|
||||
# Click delete button
|
||||
# Click delete button - find the delete link within the component
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> element("#custom-fields-component a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Modal should be visible
|
||||
|
|
@ -65,10 +66,10 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|||
create_custom_field_value(member1, custom_field, "test1")
|
||||
create_custom_field_value(member2, custom_field, "test2")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
{:ok, view, _html} = live(conn, ~p"/settings")
|
||||
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> element("#custom-fields-component a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Should show plural form
|
||||
|
|
@ -78,10 +79,10 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|||
test "shows 0 members for custom field without values", %{conn: conn} do
|
||||
{:ok, _custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
{:ok, view, _html} = live(conn, ~p"/settings")
|
||||
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> element("#custom-fields-component a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Should show 0 members
|
||||
|
|
@ -93,15 +94,16 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|||
test "updates confirmation state when typing", %{conn: conn} do
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
{:ok, view, _html} = live(conn, ~p"/settings")
|
||||
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> element("#custom-fields-component a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Type in slug input
|
||||
# Type in slug input - use element to find the form with phx-target
|
||||
view
|
||||
|> render_change("update_slug_confirmation", %{"slug" => custom_field.slug})
|
||||
|> element("#delete-custom-field-modal form")
|
||||
|> render_change(%{"slug" => custom_field.slug})
|
||||
|
||||
# Confirm button should be enabled now (no disabled attribute)
|
||||
html = render(view)
|
||||
|
|
@ -111,15 +113,16 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|||
test "delete button is disabled when slug doesn't match", %{conn: conn} do
|
||||
{:ok, _custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
{:ok, view, _html} = live(conn, ~p"/settings")
|
||||
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> element("#custom-fields-component a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Type wrong slug
|
||||
# Type wrong slug - use element to find the form with phx-target
|
||||
view
|
||||
|> render_change("update_slug_confirmation", %{"slug" => "wrong-slug"})
|
||||
|> element("#delete-custom-field-modal form")
|
||||
|> render_change(%{"slug" => "wrong-slug"})
|
||||
|
||||
# Button should be disabled
|
||||
html = render(view)
|
||||
|
|
@ -133,20 +136,21 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
{:ok, custom_field_value} = create_custom_field_value(member, custom_field, "test")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
{:ok, view, _html} = live(conn, ~p"/settings")
|
||||
|
||||
# Open modal
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> element("#custom-fields-component a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Enter correct slug
|
||||
# Enter correct slug - use element to find the form with phx-target
|
||||
view
|
||||
|> render_change("update_slug_confirmation", %{"slug" => custom_field.slug})
|
||||
|> element("#delete-custom-field-modal form")
|
||||
|> render_change(%{"slug" => custom_field.slug})
|
||||
|
||||
# Click confirm
|
||||
view
|
||||
|> element("button", "Delete Custom Field and All Values")
|
||||
|> element("#delete-custom-field-modal button", "Delete Custom Field and All Values")
|
||||
|> render_click()
|
||||
|
||||
# Should show success message
|
||||
|
|
@ -162,27 +166,28 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|||
assert {:ok, _} = Ash.get(Member, member.id)
|
||||
end
|
||||
|
||||
test "shows error when slug doesn't match", %{conn: conn} do
|
||||
test "button remains disabled and custom field not deleted when slug doesn't match", %{
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
{:ok, view, _html} = live(conn, ~p"/settings")
|
||||
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> element("#custom-fields-component a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Enter wrong slug
|
||||
# Enter wrong slug - use element to find the form with phx-target
|
||||
view
|
||||
|> render_change("update_slug_confirmation", %{"slug" => "wrong-slug"})
|
||||
|> element("#delete-custom-field-modal form")
|
||||
|> render_change(%{"slug" => "wrong-slug"})
|
||||
|
||||
# Try to confirm (button should be disabled, but test the handler anyway)
|
||||
view
|
||||
|> render_click("confirm_delete", %{})
|
||||
# Button should be disabled and we cannot click it
|
||||
# The test verifies that the button is properly disabled in the UI
|
||||
html = render(view)
|
||||
assert html =~ ~r/disabled(?:=""|(?!\w))/
|
||||
|
||||
# Should show error message
|
||||
assert render(view) =~ "Slug does not match"
|
||||
|
||||
# Custom field should still exist
|
||||
# Custom field should still exist since deletion couldn't proceed
|
||||
assert {:ok, _} = Ash.get(CustomField, custom_field.id)
|
||||
end
|
||||
end
|
||||
|
|
@ -191,10 +196,10 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|||
test "closes modal without deleting", %{conn: conn} do
|
||||
{:ok, custom_field} = create_custom_field("test_field", :string)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/custom_fields")
|
||||
{:ok, view, _html} = live(conn, ~p"/settings")
|
||||
|
||||
view
|
||||
|> element("a", "Delete")
|
||||
|> element("#custom-fields-component a", "Delete")
|
||||
|> render_click()
|
||||
|
||||
# Modal should be visible
|
||||
|
|
@ -202,7 +207,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
|
|||
|
||||
# Click cancel
|
||||
view
|
||||
|> element("button", "Cancel")
|
||||
|> element("#delete-custom-field-modal button", "Cancel")
|
||||
|> render_click()
|
||||
|
||||
# Modal should be gone
|
||||
|
|
|
|||
370
test/mv_web/live/member_live/index/field_selection_test.exs
Normal file
370
test/mv_web/live/member_live/index/field_selection_test.exs
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
defmodule MvWeb.MemberLive.Index.FieldSelectionTest do
|
||||
@moduledoc """
|
||||
Tests for FieldSelection module handling cookie/session/URL management.
|
||||
"""
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias MvWeb.MemberLive.Index.FieldSelection
|
||||
|
||||
describe "get_from_session/1" do
|
||||
test "returns empty map when session is empty" do
|
||||
assert FieldSelection.get_from_session(%{}) == %{}
|
||||
end
|
||||
|
||||
test "returns empty map when session key is missing" do
|
||||
session = %{"other_key" => "value"}
|
||||
assert FieldSelection.get_from_session(session) == %{}
|
||||
end
|
||||
|
||||
test "parses valid JSON from session" do
|
||||
json = Jason.encode!(%{"first_name" => true, "email" => false})
|
||||
session = %{"member_field_selection" => json}
|
||||
|
||||
result = FieldSelection.get_from_session(session)
|
||||
|
||||
assert result == %{"first_name" => true, "email" => false}
|
||||
end
|
||||
|
||||
test "handles invalid JSON gracefully" do
|
||||
session = %{"member_field_selection" => "invalid json{["}
|
||||
|
||||
result = FieldSelection.get_from_session(session)
|
||||
|
||||
assert result == %{}
|
||||
end
|
||||
|
||||
test "converts non-boolean values to true" do
|
||||
json = Jason.encode!(%{"first_name" => "true", "email" => 1, "street" => true})
|
||||
session = %{"member_field_selection" => json}
|
||||
|
||||
result = FieldSelection.get_from_session(session)
|
||||
|
||||
# All values should be booleans, non-booleans default to true
|
||||
assert result["first_name"] == true
|
||||
assert result["email"] == true
|
||||
assert result["street"] == true
|
||||
end
|
||||
|
||||
test "handles nil session" do
|
||||
assert FieldSelection.get_from_session(nil) == %{}
|
||||
end
|
||||
|
||||
test "handles non-map session" do
|
||||
assert FieldSelection.get_from_session("not a map") == %{}
|
||||
end
|
||||
end
|
||||
|
||||
describe "save_to_session/2" do
|
||||
test "saves field selection to session as JSON" do
|
||||
session = %{}
|
||||
selection = %{"first_name" => true, "email" => false}
|
||||
|
||||
result = FieldSelection.save_to_session(session, selection)
|
||||
|
||||
assert Map.has_key?(result, "member_field_selection")
|
||||
assert Jason.decode!(result["member_field_selection"]) == selection
|
||||
end
|
||||
|
||||
test "overwrites existing selection" do
|
||||
session = %{"member_field_selection" => Jason.encode!(%{"old" => true})}
|
||||
selection = %{"new" => true}
|
||||
|
||||
result = FieldSelection.save_to_session(session, selection)
|
||||
|
||||
assert Jason.decode!(result["member_field_selection"]) == selection
|
||||
end
|
||||
|
||||
test "handles empty selection" do
|
||||
session = %{}
|
||||
selection = %{}
|
||||
|
||||
result = FieldSelection.save_to_session(session, selection)
|
||||
|
||||
assert Jason.decode!(result["member_field_selection"]) == %{}
|
||||
end
|
||||
|
||||
test "handles invalid selection gracefully" do
|
||||
session = %{}
|
||||
|
||||
result = FieldSelection.save_to_session(session, "not a map")
|
||||
|
||||
assert result == session
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_from_cookie/1" do
|
||||
test "returns empty map when cookie header is missing" do
|
||||
conn = %Plug.Conn{}
|
||||
|
||||
result = FieldSelection.get_from_cookie(conn)
|
||||
|
||||
assert result == %{}
|
||||
end
|
||||
|
||||
test "returns empty map when cookie is empty string" do
|
||||
conn = Plug.Conn.put_req_header(%Plug.Conn{}, "cookie", "")
|
||||
|
||||
result = FieldSelection.get_from_cookie(conn)
|
||||
|
||||
assert result == %{}
|
||||
end
|
||||
|
||||
test "parses valid JSON from cookie" do
|
||||
selection = %{"first_name" => true, "email" => false}
|
||||
cookie_value = selection |> Jason.encode!() |> URI.encode()
|
||||
cookie_header = "member_field_selection=#{cookie_value}"
|
||||
conn = %Plug.Conn{} |> Plug.Conn.put_req_header("cookie", cookie_header)
|
||||
|
||||
result = FieldSelection.get_from_cookie(conn)
|
||||
|
||||
assert result == selection
|
||||
end
|
||||
|
||||
test "handles invalid JSON in cookie gracefully" do
|
||||
cookie_value = URI.encode("invalid{[")
|
||||
cookie_header = "member_field_selection=#{cookie_value}"
|
||||
conn = %Plug.Conn{} |> Plug.Conn.put_req_header("cookie", cookie_header)
|
||||
|
||||
result = FieldSelection.get_from_cookie(conn)
|
||||
|
||||
assert result == %{}
|
||||
end
|
||||
|
||||
test "handles cookie with other values" do
|
||||
selection = %{"street" => true}
|
||||
cookie_value = selection |> Jason.encode!() |> URI.encode()
|
||||
cookie_header = "other_cookie=value; member_field_selection=#{cookie_value}; another=test"
|
||||
conn = %Plug.Conn{} |> Plug.Conn.put_req_header("cookie", cookie_header)
|
||||
|
||||
result = FieldSelection.get_from_cookie(conn)
|
||||
|
||||
assert result == selection
|
||||
end
|
||||
end
|
||||
|
||||
describe "save_to_cookie/2" do
|
||||
test "saves field selection to cookie" do
|
||||
conn = %Plug.Conn{}
|
||||
selection = %{"first_name" => true, "email" => false}
|
||||
|
||||
result = FieldSelection.save_to_cookie(conn, selection)
|
||||
|
||||
# Check that cookie is set
|
||||
assert result.resp_cookies["member_field_selection"]
|
||||
cookie = result.resp_cookies["member_field_selection"]
|
||||
assert cookie[:max_age] == 365 * 24 * 60 * 60
|
||||
assert cookie[:same_site] == "Lax"
|
||||
assert cookie[:http_only] == true
|
||||
end
|
||||
|
||||
test "handles invalid selection gracefully" do
|
||||
conn = %Plug.Conn{}
|
||||
|
||||
result = FieldSelection.save_to_cookie(conn, "not a map")
|
||||
|
||||
assert result == conn
|
||||
end
|
||||
end
|
||||
|
||||
describe "parse_from_url/1" do
|
||||
test "returns empty map when params is empty" do
|
||||
assert FieldSelection.parse_from_url(%{}) == %{}
|
||||
end
|
||||
|
||||
test "returns empty map when fields parameter is missing" do
|
||||
params = %{"query" => "test", "sort_field" => "first_name"}
|
||||
assert FieldSelection.parse_from_url(params) == %{}
|
||||
end
|
||||
|
||||
test "parses comma-separated field names" do
|
||||
params = %{"fields" => "first_name,email,street"}
|
||||
|
||||
result = FieldSelection.parse_from_url(params)
|
||||
|
||||
assert result == %{
|
||||
"first_name" => true,
|
||||
"email" => true,
|
||||
"street" => true
|
||||
}
|
||||
end
|
||||
|
||||
test "handles custom field names" do
|
||||
params = %{"fields" => "custom_field_abc-123,custom_field_def-456"}
|
||||
|
||||
result = FieldSelection.parse_from_url(params)
|
||||
|
||||
assert result == %{
|
||||
"custom_field_abc-123" => true,
|
||||
"custom_field_def-456" => true
|
||||
}
|
||||
end
|
||||
|
||||
test "handles mixed member and custom fields" do
|
||||
params = %{"fields" => "first_name,custom_field_123,email"}
|
||||
|
||||
result = FieldSelection.parse_from_url(params)
|
||||
|
||||
assert result == %{
|
||||
"first_name" => true,
|
||||
"custom_field_123" => true,
|
||||
"email" => true
|
||||
}
|
||||
end
|
||||
|
||||
test "trims whitespace from field names" do
|
||||
params = %{"fields" => " first_name , email , street "}
|
||||
|
||||
result = FieldSelection.parse_from_url(params)
|
||||
|
||||
assert result == %{
|
||||
"first_name" => true,
|
||||
"email" => true,
|
||||
"street" => true
|
||||
}
|
||||
end
|
||||
|
||||
test "handles empty fields string" do
|
||||
params = %{"fields" => ""}
|
||||
assert FieldSelection.parse_from_url(params) == %{}
|
||||
end
|
||||
|
||||
test "handles nil fields parameter" do
|
||||
params = %{"fields" => nil}
|
||||
assert FieldSelection.parse_from_url(params) == %{}
|
||||
end
|
||||
|
||||
test "filters out empty field names" do
|
||||
params = %{"fields" => "first_name,,email,"}
|
||||
|
||||
result = FieldSelection.parse_from_url(params)
|
||||
|
||||
assert result == %{
|
||||
"first_name" => true,
|
||||
"email" => true
|
||||
}
|
||||
end
|
||||
|
||||
test "handles non-map params" do
|
||||
assert FieldSelection.parse_from_url(nil) == %{}
|
||||
assert FieldSelection.parse_from_url("not a map") == %{}
|
||||
end
|
||||
end
|
||||
|
||||
describe "merge_sources/3" do
|
||||
test "merges all sources with URL having highest priority" do
|
||||
url_selection = %{"first_name" => false}
|
||||
session_selection = %{"first_name" => true, "email" => true}
|
||||
cookie_selection = %{"first_name" => true, "street" => true}
|
||||
|
||||
result = FieldSelection.merge_sources(url_selection, session_selection, cookie_selection)
|
||||
|
||||
# URL overrides session, session overrides cookie
|
||||
assert result["first_name"] == false
|
||||
assert result["email"] == true
|
||||
assert result["street"] == true
|
||||
end
|
||||
|
||||
test "handles empty sources" do
|
||||
result = FieldSelection.merge_sources(%{}, %{}, %{})
|
||||
|
||||
assert result == %{}
|
||||
end
|
||||
|
||||
test "cookie only" do
|
||||
cookie_selection = %{"first_name" => true}
|
||||
|
||||
result = FieldSelection.merge_sources(%{}, %{}, cookie_selection)
|
||||
|
||||
assert result == %{"first_name" => true}
|
||||
end
|
||||
|
||||
test "session overrides cookie" do
|
||||
session_selection = %{"first_name" => false}
|
||||
cookie_selection = %{"first_name" => true}
|
||||
|
||||
result = FieldSelection.merge_sources(%{}, session_selection, cookie_selection)
|
||||
|
||||
assert result["first_name"] == false
|
||||
end
|
||||
|
||||
test "URL overrides everything" do
|
||||
url_selection = %{"first_name" => true}
|
||||
session_selection = %{"first_name" => false}
|
||||
cookie_selection = %{"first_name" => false}
|
||||
|
||||
result = FieldSelection.merge_sources(url_selection, session_selection, cookie_selection)
|
||||
|
||||
assert result["first_name"] == true
|
||||
end
|
||||
|
||||
test "combines fields from all sources" do
|
||||
url_selection = %{"url_field" => true}
|
||||
session_selection = %{"session_field" => true}
|
||||
cookie_selection = %{"cookie_field" => true}
|
||||
|
||||
result = FieldSelection.merge_sources(url_selection, session_selection, cookie_selection)
|
||||
|
||||
assert result["url_field"] == true
|
||||
assert result["session_field"] == true
|
||||
assert result["cookie_field"] == true
|
||||
end
|
||||
end
|
||||
|
||||
describe "to_url_param/1" do
|
||||
test "converts selection to comma-separated string" do
|
||||
selection = %{"first_name" => true, "email" => true, "street" => false}
|
||||
|
||||
result = FieldSelection.to_url_param(selection)
|
||||
|
||||
# Only visible fields should be included (order may vary)
|
||||
fields = String.split(result, ",") |> Enum.sort()
|
||||
assert fields == ["email", "first_name"]
|
||||
end
|
||||
|
||||
test "handles empty selection" do
|
||||
assert FieldSelection.to_url_param(%{}) == ""
|
||||
end
|
||||
|
||||
test "handles all fields hidden" do
|
||||
selection = %{"first_name" => false, "email" => false}
|
||||
|
||||
result = FieldSelection.to_url_param(selection)
|
||||
|
||||
assert result == ""
|
||||
end
|
||||
|
||||
test "preserves field order" do
|
||||
selection = %{
|
||||
"z_field" => true,
|
||||
"a_field" => true,
|
||||
"m_field" => true
|
||||
}
|
||||
|
||||
result = FieldSelection.to_url_param(selection)
|
||||
|
||||
# Order should be preserved (map iteration order)
|
||||
assert String.contains?(result, "z_field")
|
||||
assert String.contains?(result, "a_field")
|
||||
assert String.contains?(result, "m_field")
|
||||
end
|
||||
|
||||
test "handles custom fields" do
|
||||
selection = %{
|
||||
"first_name" => true,
|
||||
"custom_field_abc-123" => true,
|
||||
"email" => false
|
||||
}
|
||||
|
||||
result = FieldSelection.to_url_param(selection)
|
||||
|
||||
assert String.contains?(result, "first_name")
|
||||
assert String.contains?(result, "custom_field_abc-123")
|
||||
refute String.contains?(result, "email")
|
||||
end
|
||||
|
||||
test "handles invalid input" do
|
||||
assert FieldSelection.to_url_param(nil) == ""
|
||||
assert FieldSelection.to_url_param("not a map") == ""
|
||||
end
|
||||
end
|
||||
end
|
||||
336
test/mv_web/live/member_live/index/field_visibility_test.exs
Normal file
336
test/mv_web/live/member_live/index/field_visibility_test.exs
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
defmodule MvWeb.MemberLive.Index.FieldVisibilityTest do
|
||||
@moduledoc """
|
||||
Tests for FieldVisibility module handling field visibility merging logic.
|
||||
"""
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias MvWeb.MemberLive.Index.FieldVisibility
|
||||
|
||||
# Mock custom field structs for testing
|
||||
defp create_custom_field(id, name, show_in_overview \\ true) do
|
||||
%{
|
||||
id: id,
|
||||
name: name,
|
||||
show_in_overview: show_in_overview
|
||||
}
|
||||
end
|
||||
|
||||
describe "get_all_available_fields/1" do
|
||||
test "returns member fields and custom fields" do
|
||||
custom_fields = [
|
||||
create_custom_field("cf1", "Custom Field 1"),
|
||||
create_custom_field("cf2", "Custom Field 2")
|
||||
]
|
||||
|
||||
result = FieldVisibility.get_all_available_fields(custom_fields)
|
||||
|
||||
# Should include all member fields
|
||||
assert :first_name in result
|
||||
assert :email in result
|
||||
assert :street in result
|
||||
|
||||
# Should include custom fields as strings
|
||||
assert "custom_field_cf1" in result
|
||||
assert "custom_field_cf2" in result
|
||||
end
|
||||
|
||||
test "handles empty custom fields list" do
|
||||
result = FieldVisibility.get_all_available_fields([])
|
||||
|
||||
# Should only have member fields
|
||||
assert :first_name in result
|
||||
assert :email in result
|
||||
|
||||
refute Enum.any?(result, fn field ->
|
||||
is_binary(field) and String.starts_with?(field, "custom_field_")
|
||||
end)
|
||||
end
|
||||
|
||||
test "includes all member fields from constants" do
|
||||
custom_fields = []
|
||||
result = FieldVisibility.get_all_available_fields(custom_fields)
|
||||
|
||||
member_fields = Mv.Constants.member_fields()
|
||||
|
||||
Enum.each(member_fields, fn field ->
|
||||
assert field in result
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "merge_with_global_settings/3" do
|
||||
test "user selection overrides global settings" do
|
||||
user_selection = %{"first_name" => false}
|
||||
settings = %{member_field_visibility: %{first_name: true, email: true}}
|
||||
custom_fields = []
|
||||
|
||||
result = FieldVisibility.merge_with_global_settings(user_selection, settings, custom_fields)
|
||||
|
||||
assert result["first_name"] == false
|
||||
assert result["email"] == true
|
||||
end
|
||||
|
||||
test "falls back to global settings when user selection is empty" do
|
||||
user_selection = %{}
|
||||
settings = %{member_field_visibility: %{first_name: false, email: true}}
|
||||
custom_fields = []
|
||||
|
||||
result = FieldVisibility.merge_with_global_settings(user_selection, settings, custom_fields)
|
||||
|
||||
assert result["first_name"] == false
|
||||
assert result["email"] == true
|
||||
end
|
||||
|
||||
test "defaults to true when field not in settings" do
|
||||
user_selection = %{}
|
||||
settings = %{member_field_visibility: %{first_name: false}}
|
||||
custom_fields = []
|
||||
|
||||
result = FieldVisibility.merge_with_global_settings(user_selection, settings, custom_fields)
|
||||
|
||||
# first_name from settings
|
||||
assert result["first_name"] == false
|
||||
# email defaults to true (not in settings)
|
||||
assert result["email"] == true
|
||||
end
|
||||
|
||||
test "handles custom fields visibility" do
|
||||
user_selection = %{}
|
||||
settings = %{member_field_visibility: %{}}
|
||||
|
||||
custom_fields = [
|
||||
create_custom_field("cf1", "Custom 1", true),
|
||||
create_custom_field("cf2", "Custom 2", false)
|
||||
]
|
||||
|
||||
result = FieldVisibility.merge_with_global_settings(user_selection, settings, custom_fields)
|
||||
|
||||
assert result["custom_field_cf1"] == true
|
||||
assert result["custom_field_cf2"] == false
|
||||
end
|
||||
|
||||
test "user selection overrides custom field visibility" do
|
||||
user_selection = %{"custom_field_cf1" => false}
|
||||
settings = %{member_field_visibility: %{}}
|
||||
|
||||
custom_fields = [
|
||||
create_custom_field("cf1", "Custom 1", true)
|
||||
]
|
||||
|
||||
result = FieldVisibility.merge_with_global_settings(user_selection, settings, custom_fields)
|
||||
|
||||
assert result["custom_field_cf1"] == false
|
||||
end
|
||||
|
||||
test "handles string keys in settings (JSONB format)" do
|
||||
user_selection = %{}
|
||||
settings = %{member_field_visibility: %{"first_name" => false, "email" => true}}
|
||||
custom_fields = []
|
||||
|
||||
result = FieldVisibility.merge_with_global_settings(user_selection, settings, custom_fields)
|
||||
|
||||
assert result["first_name"] == false
|
||||
assert result["email"] == true
|
||||
end
|
||||
|
||||
test "handles mixed atom and string keys in settings" do
|
||||
user_selection = %{}
|
||||
# Use string keys only (as JSONB would return)
|
||||
settings = %{member_field_visibility: %{"first_name" => false, "email" => true}}
|
||||
custom_fields = []
|
||||
|
||||
result = FieldVisibility.merge_with_global_settings(user_selection, settings, custom_fields)
|
||||
|
||||
assert result["first_name"] == false
|
||||
assert result["email"] == true
|
||||
end
|
||||
|
||||
test "handles nil settings gracefully" do
|
||||
user_selection = %{}
|
||||
settings = %{member_field_visibility: nil}
|
||||
custom_fields = []
|
||||
|
||||
result = FieldVisibility.merge_with_global_settings(user_selection, settings, custom_fields)
|
||||
|
||||
# Should default all fields to true
|
||||
assert result["first_name"] == true
|
||||
assert result["email"] == true
|
||||
end
|
||||
|
||||
test "handles missing member_field_visibility key" do
|
||||
user_selection = %{}
|
||||
settings = %{}
|
||||
custom_fields = []
|
||||
|
||||
result = FieldVisibility.merge_with_global_settings(user_selection, settings, custom_fields)
|
||||
|
||||
# Should default all fields to true
|
||||
assert result["first_name"] == true
|
||||
assert result["email"] == true
|
||||
end
|
||||
|
||||
test "includes all fields in result" do
|
||||
user_selection = %{"first_name" => false}
|
||||
settings = %{member_field_visibility: %{email: true}}
|
||||
|
||||
custom_fields = [
|
||||
create_custom_field("cf1", "Custom 1", true)
|
||||
]
|
||||
|
||||
result = FieldVisibility.merge_with_global_settings(user_selection, settings, custom_fields)
|
||||
|
||||
# Should include all member fields
|
||||
member_fields = Mv.Constants.member_fields()
|
||||
|
||||
Enum.each(member_fields, fn field ->
|
||||
assert Map.has_key?(result, Atom.to_string(field))
|
||||
end)
|
||||
|
||||
# Should include custom fields
|
||||
assert Map.has_key?(result, "custom_field_cf1")
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_visible_fields/1" do
|
||||
test "returns only fields with true visibility" do
|
||||
selection = %{
|
||||
"first_name" => true,
|
||||
"email" => false,
|
||||
"street" => true,
|
||||
"custom_field_123" => false
|
||||
}
|
||||
|
||||
result = FieldVisibility.get_visible_fields(selection)
|
||||
|
||||
assert :first_name in result
|
||||
assert :street in result
|
||||
refute :email in result
|
||||
refute "custom_field_123" in result
|
||||
end
|
||||
|
||||
test "converts member field strings to atoms" do
|
||||
selection = %{"first_name" => true, "email" => true}
|
||||
|
||||
result = FieldVisibility.get_visible_fields(selection)
|
||||
|
||||
assert :first_name in result
|
||||
assert :email in result
|
||||
end
|
||||
|
||||
test "keeps custom fields as strings" do
|
||||
selection = %{"custom_field_abc-123" => true}
|
||||
|
||||
result = FieldVisibility.get_visible_fields(selection)
|
||||
|
||||
assert "custom_field_abc-123" in result
|
||||
end
|
||||
|
||||
test "handles empty selection" do
|
||||
assert FieldVisibility.get_visible_fields(%{}) == []
|
||||
end
|
||||
|
||||
test "handles all fields hidden" do
|
||||
selection = %{"first_name" => false, "email" => false}
|
||||
|
||||
assert FieldVisibility.get_visible_fields(selection) == []
|
||||
end
|
||||
|
||||
test "handles invalid input" do
|
||||
assert FieldVisibility.get_visible_fields(nil) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_visible_member_fields/1" do
|
||||
test "returns only member fields that are visible" do
|
||||
selection = %{
|
||||
"first_name" => true,
|
||||
"email" => true,
|
||||
"custom_field_123" => true,
|
||||
"street" => false
|
||||
}
|
||||
|
||||
result = FieldVisibility.get_visible_member_fields(selection)
|
||||
|
||||
assert :first_name in result
|
||||
assert :email in result
|
||||
refute :street in result
|
||||
refute "custom_field_123" in result
|
||||
end
|
||||
|
||||
test "filters out custom fields" do
|
||||
selection = %{
|
||||
"first_name" => true,
|
||||
"custom_field_123" => true,
|
||||
"custom_field_456" => true
|
||||
}
|
||||
|
||||
result = FieldVisibility.get_visible_member_fields(selection)
|
||||
|
||||
assert :first_name in result
|
||||
refute "custom_field_123" in result
|
||||
refute "custom_field_456" in result
|
||||
end
|
||||
|
||||
test "handles empty selection" do
|
||||
assert FieldVisibility.get_visible_member_fields(%{}) == []
|
||||
end
|
||||
|
||||
test "handles invalid input" do
|
||||
assert FieldVisibility.get_visible_member_fields(nil) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_visible_custom_fields/1" do
|
||||
test "returns only custom fields that are visible" do
|
||||
selection = %{
|
||||
"first_name" => true,
|
||||
"custom_field_123" => true,
|
||||
"custom_field_456" => false,
|
||||
"email" => true
|
||||
}
|
||||
|
||||
result = FieldVisibility.get_visible_custom_fields(selection)
|
||||
|
||||
assert "custom_field_123" in result
|
||||
refute "custom_field_456" in result
|
||||
refute :first_name in result
|
||||
refute :email in result
|
||||
end
|
||||
|
||||
test "filters out member fields" do
|
||||
selection = %{
|
||||
"first_name" => true,
|
||||
"email" => true,
|
||||
"custom_field_123" => true
|
||||
}
|
||||
|
||||
result = FieldVisibility.get_visible_custom_fields(selection)
|
||||
|
||||
assert "custom_field_123" in result
|
||||
refute :first_name in result
|
||||
refute :email in result
|
||||
end
|
||||
|
||||
test "handles empty selection" do
|
||||
assert FieldVisibility.get_visible_custom_fields(%{}) == []
|
||||
end
|
||||
|
||||
test "handles fields that look like custom fields but aren't" do
|
||||
selection = %{
|
||||
"custom_field_123" => true,
|
||||
"custom_field_like_name" => true,
|
||||
"not_custom_field" => true
|
||||
}
|
||||
|
||||
result = FieldVisibility.get_visible_custom_fields(selection)
|
||||
|
||||
assert "custom_field_123" in result
|
||||
assert "custom_field_like_name" in result
|
||||
refute "not_custom_field" in result
|
||||
end
|
||||
|
||||
test "handles invalid input" do
|
||||
assert FieldVisibility.get_visible_custom_fields(nil) == []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -90,8 +90,6 @@ defmodule MvWeb.ProfileNavigationTest do
|
|||
# Verify we're on the correct profile page with OIDC specific information
|
||||
{:ok, _profile_view, html} = live(conn, "/users/#{user.id}")
|
||||
assert html =~ to_string(user.email)
|
||||
# OIDC ID should be visible
|
||||
assert html =~ "oidc_123"
|
||||
# Password auth should be disabled for OIDC users
|
||||
assert html =~ "Not enabled"
|
||||
end
|
||||
|
|
@ -150,8 +148,6 @@ defmodule MvWeb.ProfileNavigationTest do
|
|||
"/members/new",
|
||||
"/custom_field_values",
|
||||
"/custom_field_values/new",
|
||||
"/custom_fields",
|
||||
"/custom_fields/new",
|
||||
"/users",
|
||||
"/users/new"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -231,8 +231,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Date should be displayed in readable format
|
||||
assert html =~ "1990" or html =~ "1990-05-15" or html =~ "15.05.1990"
|
||||
# Date should be displayed in European format (dd.mm.yyyy)
|
||||
assert html =~ "15.05.1990"
|
||||
end
|
||||
|
||||
test "formats email custom field values correctly", %{conn: conn, member1: _member1} do
|
||||
|
|
@ -242,7 +242,7 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
assert html =~ "alice.private@example.com"
|
||||
end
|
||||
|
||||
test "shows empty cell or placeholder for members without custom field values", %{
|
||||
test "shows empty cell for members without custom field values", %{
|
||||
conn: conn,
|
||||
member2: _member2,
|
||||
field_show_string: field
|
||||
|
|
@ -253,11 +253,14 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
|
|||
# The custom field column should exist
|
||||
assert html =~ field.name
|
||||
|
||||
# Member2 should have an empty cell for this field
|
||||
# We check that member2's row exists but doesn't have the value
|
||||
assert html =~ "Bob Brown"
|
||||
# The value should not appear for member2 (only for member1)
|
||||
# We check that the value appears somewhere (for member1) but member2 row should have "-"
|
||||
# Member2 should exist in the table (first_name and last_name are in separate columns)
|
||||
assert html =~ "Bob"
|
||||
assert html =~ "Brown"
|
||||
|
||||
# The value from member1 should appear (phone number)
|
||||
assert html =~ "+49123456789"
|
||||
|
||||
# Note: Member2 doesn't have this custom field value, so the cell is empty
|
||||
# The implementation shows "" for missing values, which is the expected behavior
|
||||
end
|
||||
end
|
||||
|
|
|
|||
452
test/mv_web/member_live/index_field_visibility_test.exs
Normal file
452
test/mv_web/member_live/index_field_visibility_test.exs
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
||||
@moduledoc """
|
||||
Integration tests for field visibility dropdown functionality.
|
||||
|
||||
Tests cover:
|
||||
- Field selection dropdown rendering
|
||||
- Toggling field visibility
|
||||
- URL parameter persistence
|
||||
- Select all / deselect all
|
||||
- Integration with member list display
|
||||
- Custom fields visibility
|
||||
"""
|
||||
use MvWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
require Ash.Query
|
||||
|
||||
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
|
||||
|
||||
setup do
|
||||
# Create test members
|
||||
{:ok, member1} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Alice",
|
||||
last_name: "Anderson",
|
||||
email: "alice@example.com",
|
||||
street: "Main St",
|
||||
city: "Berlin"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, member2} =
|
||||
Member
|
||||
|> Ash.Changeset.for_create(:create_member, %{
|
||||
first_name: "Bob",
|
||||
last_name: "Brown",
|
||||
email: "bob@example.com",
|
||||
street: "Second St",
|
||||
city: "Hamburg"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create custom field
|
||||
{:ok, custom_field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "membership_number",
|
||||
value_type: :string,
|
||||
show_in_overview: true
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Create custom field values
|
||||
{:ok, _cfv1} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member1.id,
|
||||
custom_field_id: custom_field.id,
|
||||
value: "M001"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
{:ok, _cfv2} =
|
||||
CustomFieldValue
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
member_id: member2.id,
|
||||
custom_field_id: custom_field.id,
|
||||
value: "M002"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
%{
|
||||
member1: member1,
|
||||
member2: member2,
|
||||
custom_field: custom_field
|
||||
}
|
||||
end
|
||||
|
||||
describe "field visibility dropdown" do
|
||||
test "renders dropdown button", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
assert html =~ "Columns"
|
||||
assert html =~ ~s(aria-controls="field-visibility-menu")
|
||||
end
|
||||
|
||||
test "opens dropdown when button is clicked", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Initially closed
|
||||
refute has_element?(view, "ul#field-visibility-menu")
|
||||
|
||||
# Click button
|
||||
view
|
||||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
# Should be open now
|
||||
assert has_element?(view, "ul#field-visibility-menu")
|
||||
end
|
||||
|
||||
test "displays all member fields in dropdown", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Check for member fields (formatted labels)
|
||||
assert html =~ "First Name" or html =~ "first_name"
|
||||
assert html =~ "Email" or html =~ "email"
|
||||
assert html =~ "Street" or html =~ "street"
|
||||
end
|
||||
|
||||
test "displays custom fields in dropdown", %{conn: conn, custom_field: custom_field} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
assert html =~ custom_field.name
|
||||
end
|
||||
end
|
||||
|
||||
describe "field visibility toggling" do
|
||||
test "hiding a field removes it from display", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Verify email is visible initially
|
||||
html = render(view)
|
||||
assert html =~ "alice@example.com"
|
||||
|
||||
# Open dropdown and hide email
|
||||
view
|
||||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
||||
|> render_click()
|
||||
|
||||
# Wait for update
|
||||
:timer.sleep(100)
|
||||
|
||||
# Email should no longer be visible
|
||||
html = render(view)
|
||||
refute html =~ "alice@example.com"
|
||||
refute html =~ "bob@example.com"
|
||||
end
|
||||
|
||||
test "hiding custom field removes it from display", %{conn: conn, custom_field: custom_field} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Verify custom field is visible initially
|
||||
html = render(view)
|
||||
assert html =~ "M001" or html =~ custom_field.name
|
||||
|
||||
# Open dropdown and hide custom field
|
||||
view
|
||||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
custom_field_id = custom_field.id
|
||||
custom_field_string = "custom_field_#{custom_field_id}"
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_item'][phx-value-item='#{custom_field_string}']")
|
||||
|> render_click()
|
||||
|
||||
# Wait for update
|
||||
:timer.sleep(100)
|
||||
|
||||
# Custom field should no longer be visible
|
||||
html = render(view)
|
||||
refute html =~ "M001"
|
||||
refute html =~ "M002"
|
||||
end
|
||||
end
|
||||
|
||||
describe "select all / deselect all" do
|
||||
test "select all makes all fields visible", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
# Start with some fields hidden
|
||||
{:ok, view, _html} = live(conn, "/members?fields=first_name")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
# Click select all
|
||||
view
|
||||
|> element("button[phx-click='select_all']")
|
||||
|> render_click()
|
||||
|
||||
# Wait for update
|
||||
:timer.sleep(100)
|
||||
|
||||
# All fields should be visible
|
||||
html = render(view)
|
||||
assert html =~ "alice@example.com"
|
||||
assert html =~ "Main St"
|
||||
assert html =~ "Berlin"
|
||||
end
|
||||
|
||||
test "deselect all hides all fields except first_name", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
# Click deselect all
|
||||
view
|
||||
|> element("button[phx-click='select_none']")
|
||||
|> render_click()
|
||||
|
||||
# Wait for update
|
||||
:timer.sleep(100)
|
||||
|
||||
# Only first_name should be visible (it's always shown)
|
||||
html = render(view)
|
||||
# Email and street should be hidden
|
||||
refute html =~ "alice@example.com"
|
||||
refute html =~ "Main St"
|
||||
end
|
||||
end
|
||||
|
||||
describe "URL parameter persistence" do
|
||||
test "field selection is persisted in URL", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown and hide email
|
||||
view
|
||||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
||||
|> render_click()
|
||||
|
||||
# Wait for URL update
|
||||
:timer.sleep(100)
|
||||
|
||||
# Check that URL contains fields parameter
|
||||
# Note: In LiveView tests, we check the rendered HTML for the updated state
|
||||
# The actual URL update happens via push_patch
|
||||
end
|
||||
|
||||
test "loading page with fields parameter applies selection", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
# Load with first_name and city explicitly set in URL
|
||||
# Note: Other fields may still be visible due to global settings
|
||||
{:ok, view, _html} = live(conn, "/members?fields=first_name,city")
|
||||
|
||||
html = render(view)
|
||||
|
||||
# first_name and city should be visible
|
||||
assert html =~ "Alice"
|
||||
assert html =~ "Berlin"
|
||||
|
||||
# Note: email and street may still be visible if global settings allow it
|
||||
# This test verifies that the URL parameters work, not that they hide other fields
|
||||
end
|
||||
|
||||
test "fields parameter works with custom fields", %{conn: conn, custom_field: custom_field} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
custom_field_id = custom_field.id
|
||||
|
||||
# Load with custom field visible
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?fields=first_name,custom_field_#{custom_field_id}")
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Custom field should be visible
|
||||
assert html =~ "M001" or html =~ custom_field.name
|
||||
end
|
||||
end
|
||||
|
||||
describe "integration with global settings" do
|
||||
test "respects global settings when no user selection", %{conn: conn} do
|
||||
# This test would require setting up global settings
|
||||
# For now, we verify that the system works with default settings
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# All fields should be visible by default
|
||||
assert html =~ "alice@example.com"
|
||||
assert html =~ "Main St"
|
||||
end
|
||||
|
||||
test "user selection overrides global settings", %{conn: conn} do
|
||||
# This would require setting up global settings first
|
||||
# Then verifying that user selection takes precedence
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Hide a field via dropdown
|
||||
view
|
||||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
||||
|> render_click()
|
||||
|
||||
:timer.sleep(100)
|
||||
|
||||
html = render(view)
|
||||
refute html =~ "alice@example.com"
|
||||
end
|
||||
end
|
||||
|
||||
describe "edge cases" do
|
||||
test "handles empty fields parameter", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members?fields=")
|
||||
|
||||
# Should fall back to global settings
|
||||
assert html =~ "alice@example.com"
|
||||
end
|
||||
|
||||
test "handles invalid field names in URL", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members?fields=invalid_field,another_invalid")
|
||||
|
||||
# Should ignore invalid fields and use defaults
|
||||
assert html =~ "alice@example.com"
|
||||
end
|
||||
|
||||
test "handles custom field that doesn't exist", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members?fields=first_name,custom_field_nonexistent")
|
||||
|
||||
# Should work without errors
|
||||
assert html =~ "Alice"
|
||||
end
|
||||
|
||||
test "handles rapid toggling", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
# Rapidly toggle a field multiple times
|
||||
for _ <- 1..5 do
|
||||
view
|
||||
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
||||
|> render_click()
|
||||
|
||||
:timer.sleep(50)
|
||||
end
|
||||
|
||||
# Should still work correctly
|
||||
html = render(view)
|
||||
assert html =~ "Alice"
|
||||
end
|
||||
end
|
||||
|
||||
describe "accessibility" do
|
||||
test "dropdown has proper ARIA attributes", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
assert html =~ ~s(aria-controls="field-visibility-menu")
|
||||
assert html =~ ~s(aria-haspopup="menu")
|
||||
assert html =~ ~s(role="button")
|
||||
end
|
||||
|
||||
test "menu items have proper ARIA attributes when open", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
assert html =~ ~s(role="menu")
|
||||
assert html =~ ~s(role="menuitemcheckbox")
|
||||
assert html =~ ~s(aria-checked)
|
||||
end
|
||||
|
||||
test "keyboard navigation works", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
# Check that elements are keyboard accessible
|
||||
html = render(view)
|
||||
assert html =~ ~s(tabindex="0")
|
||||
# Check that keyboard events are supported
|
||||
assert html =~ ~s(phx-keydown="select_item")
|
||||
assert html =~ ~s(phx-key="Enter")
|
||||
end
|
||||
|
||||
test "keyboard activation with Enter key works", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Verify email is visible initially
|
||||
html = render(view)
|
||||
assert html =~ "alice@example.com"
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
# Simulate Enter key press on email field button
|
||||
view
|
||||
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
||||
|> render_keydown(%{key: "Enter"})
|
||||
|
||||
# Wait for update
|
||||
:timer.sleep(100)
|
||||
|
||||
# Email should no longer be visible
|
||||
html = render(view)
|
||||
refute html =~ "alice@example.com"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -33,8 +33,6 @@ defmodule MvWeb.UserLive.IndexTest do
|
|||
|
||||
assert html =~ "alice@example.com"
|
||||
assert html =~ "bob@example.com"
|
||||
assert html =~ "alice123"
|
||||
assert html =~ "bob456"
|
||||
end
|
||||
|
||||
test "shows correct action links", %{conn: conn} do
|
||||
|
|
@ -386,10 +384,6 @@ defmodule MvWeb.UserLive.IndexTest do
|
|||
|
||||
# Should still show the table structure
|
||||
assert html =~ "Email"
|
||||
assert html =~ "OIDC ID"
|
||||
# Should show the authenticated user at minimum
|
||||
# Matches the generated email pattern oidc.user{unique_id}@example.com
|
||||
assert html =~ "oidc.user"
|
||||
end
|
||||
|
||||
test "handles users with missing OIDC ID", %{conn: conn} do
|
||||
|
|
|
|||
|
|
@ -123,7 +123,13 @@ defmodule MvWeb.ConnCase do
|
|||
end
|
||||
|
||||
setup tags do
|
||||
Mv.DataCase.setup_sandbox(tags)
|
||||
{:ok, conn: Phoenix.ConnTest.build_conn()}
|
||||
pid = Mv.DataCase.setup_sandbox(tags)
|
||||
|
||||
conn = Phoenix.ConnTest.build_conn()
|
||||
# Set metadata for Phoenix.Ecto.SQL.Sandbox plug to allow LiveView processes
|
||||
# to share the test's database connection in async tests
|
||||
conn = Plug.Conn.put_private(conn, :ecto_sandbox, pid)
|
||||
|
||||
{:ok, conn: conn}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -34,10 +34,12 @@ defmodule Mv.DataCase do
|
|||
|
||||
@doc """
|
||||
Sets up the sandbox based on the test tags.
|
||||
Returns the owner pid for use with Phoenix.Ecto.SQL.Sandbox.
|
||||
"""
|
||||
def setup_sandbox(tags) do
|
||||
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Mv.Repo, shared: not tags[:async])
|
||||
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
|
||||
pid
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue