From dbec2d020fe7381f2323fa9503eb7aeb455c23ae Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 20 Jan 2026 15:01:35 +0100 Subject: [PATCH] test: add tdd tests for backend state management of boolean custom filters --- test/mv_web/member_live/index_test.exs | 265 +++++++++++++++++++++++++ 1 file changed, 265 insertions(+) diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs index acca9bf..69d8972 100644 --- a/test/mv_web/member_live/index_test.exs +++ b/test/mv_web/member_live/index_test.exs @@ -656,4 +656,269 @@ defmodule MvWeb.MemberLive.IndexTest do assert path =~ "show_current_cycle=true" end end + + describe "boolean custom field filters" do + alias Mv.Membership.CustomField + + # Helper to create a boolean custom field + defp create_boolean_custom_field(attrs \\ %{}) do + default_attrs = %{ + name: "test_boolean_#{System.unique_integer([:positive])}", + value_type: :boolean + } + + attrs = Map.merge(default_attrs, attrs) + + CustomField + |> Ash.Changeset.for_create(:create, attrs) + |> Ash.create!() + end + + # Helper to create a non-boolean custom field + defp create_string_custom_field(attrs \\ %{}) do + default_attrs = %{ + name: "test_string_#{System.unique_integer([:positive])}", + value_type: :string + } + + attrs = Map.merge(default_attrs, attrs) + + CustomField + |> Ash.Changeset.for_create(:create, attrs) + |> Ash.create!() + end + + test "mount initializes boolean_custom_field_filters as empty map", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members") + + state = :sys.get_state(view.pid) + assert state.socket.assigns.boolean_custom_field_filters == %{} + end + + test "handle_params parses boolean_filter_ values correctly", %{conn: conn} do + conn = conn_with_oidc_user(conn) + boolean_field = create_boolean_custom_field() + + # Test true value + {:ok, view1, _html} = + live(conn, "/members?boolean_filter_#{boolean_field.id}=true") + + state1 = :sys.get_state(view1.pid) + filters1 = state1.socket.assigns.boolean_custom_field_filters + assert filters1[boolean_field.id] == :true + refute filters1[boolean_field.id] == "true" + + # Test false value + {:ok, view2, _html} = + live(conn, "/members?boolean_filter_#{boolean_field.id}=false") + + state2 = :sys.get_state(view2.pid) + filters2 = state2.socket.assigns.boolean_custom_field_filters + assert filters2[boolean_field.id] == :false + refute filters2[boolean_field.id] == "false" + end + + test "handle_params ignores non-existent custom field IDs", %{conn: conn} do + conn = conn_with_oidc_user(conn) + fake_id = Ecto.UUID.generate() + + {:ok, view, _html} = + live(conn, "/members?boolean_filter_#{fake_id}=true") + + state = :sys.get_state(view.pid) + filters = state.socket.assigns.boolean_custom_field_filters + + # Filter should not be added for non-existent custom field + refute Map.has_key?(filters, fake_id) + assert filters == %{} + end + + test "handle_params ignores non-boolean custom fields", %{conn: conn} do + conn = conn_with_oidc_user(conn) + string_field = create_string_custom_field() + + {:ok, view, _html} = + live(conn, "/members?boolean_filter_#{string_field.id}=true") + + state = :sys.get_state(view.pid) + filters = state.socket.assigns.boolean_custom_field_filters + + # Filter should not be added for non-boolean custom field + refute Map.has_key?(filters, string_field.id) + assert filters == %{} + end + + test "handle_params ignores invalid filter values", %{conn: conn} do + conn = conn_with_oidc_user(conn) + boolean_field = create_boolean_custom_field() + + # Test various invalid values + invalid_values = ["1", "0", "yes", "no", "True", "False", "", "invalid", "null"] + + for invalid_value <- invalid_values do + {:ok, view, _html} = + live(conn, "/members?boolean_filter_#{boolean_field.id}=#{invalid_value}") + + state = :sys.get_state(view.pid) + filters = state.socket.assigns.boolean_custom_field_filters + + # Invalid values should not be added to filters + refute Map.has_key?(filters, boolean_field.id), + "Invalid value '#{invalid_value}' should not be added to filters" + end + end + + test "handle_params handles multiple boolean filters simultaneously", %{conn: conn} do + conn = conn_with_oidc_user(conn) + boolean_field1 = create_boolean_custom_field() + boolean_field2 = create_boolean_custom_field() + + {:ok, view, _html} = + live( + conn, + "/members?boolean_filter_#{boolean_field1.id}=true&boolean_filter_#{boolean_field2.id}=false" + ) + + state = :sys.get_state(view.pid) + filters = state.socket.assigns.boolean_custom_field_filters + + assert filters[boolean_field1.id] == :true + assert filters[boolean_field2.id] == :false + assert map_size(filters) == 2 + end + + test "build_query_params includes active boolean filters and excludes nil filters", %{ + conn: conn + } do + conn = conn_with_oidc_user(conn) + boolean_field1 = create_boolean_custom_field() + boolean_field2 = create_boolean_custom_field() + + # Test with active filters + {:ok, view1, _html} = + live( + conn, + "/members?boolean_filter_#{boolean_field1.id}=true&boolean_filter_#{boolean_field2.id}=false" + ) + + # Trigger a search to see if filters are preserved in URL + view1 + |> element("[data-testid='search-input']") + |> render_change(%{value: "test"}) + + # Check that the patch includes boolean filters + path1 = assert_patch(view1) + assert path1 =~ "boolean_filter_#{boolean_field1.id}=true" + assert path1 =~ "boolean_filter_#{boolean_field2.id}=false" + + # Test without filters (nil filters should not appear in URL) + {:ok, view2, _html} = live(conn, "/members") + + # Trigger a search + view2 + |> element("[data-testid='search-input']") + |> render_change(%{value: "test"}) + + # Check that no boolean_filter params are in URL + path2 = assert_patch(view2) + refute path2 =~ "boolean_filter_" + end + + test "boolean filters are preserved during navigation actions", %{conn: conn} do + conn = conn_with_oidc_user(conn) + boolean_field = create_boolean_custom_field() + + {:ok, view, _html} = + live(conn, "/members?boolean_filter_#{boolean_field.id}=true") + + # Test sort toggle preserves filter + view + |> element("[data-testid='email']") + |> render_click() + + path1 = assert_patch(view) + assert path1 =~ "boolean_filter_#{boolean_field.id}=true" + + # Test search change preserves filter + view + |> element("[data-testid='search-input']") + |> render_change(%{value: "test"}) + + path2 = assert_patch(view) + assert path2 =~ "boolean_filter_#{boolean_field.id}=true" + end + + test "boolean filters work together with cycle_status_filter", %{conn: conn} do + conn = conn_with_oidc_user(conn) + boolean_field = create_boolean_custom_field() + + {:ok, view, _html} = + live( + conn, + "/members?cycle_status_filter=paid&boolean_filter_#{boolean_field.id}=true" + ) + + state = :sys.get_state(view.pid) + filters = state.socket.assigns.boolean_custom_field_filters + + # Both filters should be set + assert filters[boolean_field.id] == :true + assert state.socket.assigns.cycle_status_filter == :paid + + # Both should be in URL when triggering search + view + |> element("[data-testid='search-input']") + |> render_change(%{value: "test"}) + + path = assert_patch(view) + assert path =~ "cycle_status_filter=paid" + assert path =~ "boolean_filter_#{boolean_field.id}=true" + end + + test "handle_params removes filter when custom field is deleted", %{conn: conn} do + conn = conn_with_oidc_user(conn) + boolean_field = create_boolean_custom_field() + + # Set up filter via URL + {:ok, view, _html} = + live(conn, "/members?boolean_filter_#{boolean_field.id}=true") + + state_before = :sys.get_state(view.pid) + filters_before = state_before.socket.assigns.boolean_custom_field_filters + assert filters_before[boolean_field.id] == :true + + # Delete the custom field + Ash.destroy!(boolean_field) + + # Navigate again - filter should be removed since custom field no longer exists + {:ok, view2, _html} = + live(conn, "/members?boolean_filter_#{boolean_field.id}=true") + + state_after = :sys.get_state(view2.pid) + filters_after = state_after.socket.assigns.boolean_custom_field_filters + + # Filter should not be present for deleted custom field + refute Map.has_key?(filters_after, boolean_field.id) + assert filters_after == %{} + end + + test "handle_params handles URL-encoded custom field IDs correctly", %{conn: conn} do + conn = conn_with_oidc_user(conn) + boolean_field = create_boolean_custom_field() + + # URL-encode the custom field ID (though UUIDs shouldn't need encoding normally) + encoded_id = URI.encode(boolean_field.id) + + {:ok, view, _html} = + live(conn, "/members?boolean_filter_#{encoded_id}=true") + + state = :sys.get_state(view.pid) + filters = state.socket.assigns.boolean_custom_field_filters + + # Filter should work with URL-encoded ID + # Phoenix should decode it automatically, so we check with original ID + assert filters[boolean_field.id] == :true + end + end end