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 """ # Kept async: false as a deferred scope decision. The deferrable-FK migration # removed the concurrent-create_member deadlock that previously forced this, so # re-flipping this members/custom-fields suite to async is a possible follow-up # rather than part of the original change. use MvWeb.ConnCase, async: false import Phoenix.LiveViewTest alias Mv.Membership.{CustomField, CustomFieldValue} require Ash.Query # Disables the compact Member/Address composites (per-browser view setting) so # the individual constituent columns (first_name/last_name/email, street/…) are # offered by the column manager and available via ?fields=. defp non_compact(conn) do Plug.Conn.put_session( conn, "member_view_settings", ~s({"compact_member":false,"compact_address":false}) ) end setup do system_actor = Mv.Helpers.SystemActor.get_system_actor() # Create test members {:ok, member1} = Mv.Membership.create_member( %{ first_name: "Alice", last_name: "Anderson", email: "alice@example.com", street: "Main St", city: "Berlin" }, actor: system_actor ) {:ok, member2} = Mv.Membership.create_member( %{ first_name: "Bob", last_name: "Brown", email: "bob@example.com", street: "Second St", city: "Hamburg" }, actor: system_actor ) # Create custom field {:ok, custom_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "membership_number", value_type: :string, show_in_overview: true }) |> Ash.create(actor: system_actor) # 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(actor: system_actor) {:ok, _cfv2} = CustomFieldValue |> Ash.Changeset.for_create(:create, %{ member_id: member2.id, custom_field_id: custom_field.id, value: "M002" }) |> Ash.create(actor: system_actor) %{ 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 # The individual name/address constituents are only offered when the # compact Member/Address composites are off, so opt out of them first. conn = conn |> conn_with_oidc_user() |> non_compact() {: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") # The curated Name column carries the member names, so they are visible. html = render(view) assert html =~ "Anderson" # Open dropdown and hide the Name column view |> element("button[aria-controls='field-visibility-menu']") |> render_click() view |> element("button[phx-click='select_item'][phx-value-item='name']") |> render_click() # The Name column (and the names it carries) is no longer visible html = render(view) refute html =~ "Anderson" refute html =~ "Brown" 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() # 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() # 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() # 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() # 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") # The curated Name and Address columns are visible by default. assert html =~ "Anderson" 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 the curated Name column (which carries the names) via dropdown view |> element("button[aria-controls='field-visibility-menu']") |> render_click() view |> element("button[phx-click='select_item'][phx-value-item='name']") |> render_click() html = render(view) refute html =~ "Anderson" 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 (curated Name column visible) assert html =~ "Anderson" 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 (curated Name column visible) assert html =~ "Anderson" 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 "URL with only custom field keeps custom field visible (no invalid fallback)", %{ conn: conn, custom_field: custom_field } do conn = conn_with_oidc_user(conn) id = custom_field.id {:ok, _view, html} = live(conn, "/members?fields=custom_field_#{id}") # Selection must not be treated as invalid; custom field column stays visible assert html =~ "M001" or html =~ custom_field.name 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() 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") # The curated Name column carries the names, so they are visible initially. html = render(view) assert html =~ "Anderson" # Open dropdown view |> element("button[aria-controls='field-visibility-menu']") |> render_click() # Simulate Enter key press on the Name field button view |> element("button[phx-click='select_item'][phx-value-item='name']") |> render_keydown(%{key: "Enter"}) # The Name column (and the names it carries) is no longer visible html = render(view) refute html =~ "Anderson" end end end