defmodule MvWeb.MemberLive.IndexValueControlTest do @moduledoc """ §1.20 / §1.21 / §1.22 / §1.23 — the reworked value controls of the add-filter builder: * no "All"/"Alle" option in the boolean / group value controls (§1.20); * the field picker's search input is a live type-ahead (§1.21); * the payment control reveals a two-sided open-cycle count range under "Has unpaid" and offers a suspended-status option (§1.22, §1.23). """ use MvWeb.ConnCase, async: false import Phoenix.LiveViewTest alias Mv.Membership.CustomField alias Mv.Membership.Group setup %{conn: conn} do actor = Mv.Helpers.SystemActor.get_system_actor() {:ok, _member} = Mv.Membership.create_member( %{first_name: "Val", last_name: "Member", email: "val@example.com"}, actor: actor ) {:ok, group} = Group |> Ash.Changeset.for_create(:create, %{name: "Board"}) |> Ash.create(actor: actor) {:ok, field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "newsletter_#{System.unique_integer([:positive])}", value_type: :boolean, show_in_overview: true }) |> Ash.create(actor: actor) %{conn: conn_with_oidc_user(conn), group: group, field: field} end describe "unified popover shell (§3.11)" do test "every value control shares the 'Filter: ' shell title", %{ conn: conn, field: field } do {:ok, view, _html} = live(conn, ~p"/members") pick_field(view, "group") assert has_element?(view, "[data-testid='control-title']", "Filter:") pick_field(view, field.id) assert has_element?(view, "[data-testid='control-title']", "Filter:") pick_field(view, "payment") assert has_element?(view, "[data-testid='control-title']", "Filter:") end end describe "no All option (§1.20)" do test "the group control offers is / is-not but no 'all' option", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/members") pick_field(view, "group") assert has_element?(view, "[data-testid='value-control-group'] input[value='in']") assert has_element?(view, "[data-testid='value-control-group'] input[value='not_in']") refute has_element?(view, "[data-testid='value-control-group'] input[value='all']") end test "the group operator uses the boolean-consistent Ja/Nein check/x toggle", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/members") pick_field(view, "group") # Same join-toggle presentation as the boolean custom-field control: a # check icon on the ":in" (Ja) option and an x-mark on ":not_in" (Nein), # with a visible selected state. assert has_element?(view, "[data-testid='value-control-group'] .join .hero-check-circle") assert has_element?(view, "[data-testid='value-control-group'] .join .hero-x-circle") assert has_element?( view, "[data-testid='value-control-group'] label.has-\\[\\:checked\\]\\:btn-primary" ) end test "the fee-type operator uses the boolean-consistent Ja/Nein check/x toggle", %{conn: conn} do _fee_type = Mv.Fixtures.create_fee_type() {:ok, view, _html} = live(conn, ~p"/members") pick_field(view, "fee_type") assert has_element?(view, "[data-testid='value-control-fee-type'] .join .hero-check-circle") assert has_element?(view, "[data-testid='value-control-fee-type'] .join .hero-x-circle") end test "the boolean control offers Yes / No but no 'all' option", %{conn: conn, field: field} do {:ok, view, _html} = live(conn, ~p"/members") pick_field(view, field.id) assert has_element?(view, "[data-testid='value-control-boolean'] input[value='true']") assert has_element?(view, "[data-testid='value-control-boolean'] input[value='false']") refute has_element?(view, "[data-testid='value-control-boolean'] input[value='all']") end test "the boolean control renders a check-icon Yes and an x-icon No join toggle", %{ conn: conn, field: field } do {:ok, view, _html} = live(conn, ~p"/members") pick_field(view, field.id) # Join-toggle with a check icon on Yes and an x-mark icon on No (Task 5). assert has_element?(view, "[data-testid='value-control-boolean'] .join .hero-check-circle") assert has_element?(view, "[data-testid='value-control-boolean'] .join .hero-x-circle") # A checked option shows a visible selected state (btn-primary via has-[:checked]). assert has_element?( view, "[data-testid='value-control-boolean'] label.has-\\[\\:checked\\]\\:btn-primary" ) end end describe "field picker search (§1.21)" do test "the search input is a live type-ahead hook and narrows the field list", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/members") open_picker(view) assert has_element?(view, "[data-testid='field-combobox'][phx-hook='FilterComboboxSearch']") # The search input must live inside a form carrying the change binding — # LiveView rejects `phx-change` on an input that is not inside a
# ("form events require the input to be inside a form"), which silently # broke the live filtering in the browser (§1.21). assert has_element?(view, "form[phx-change='filter_fields'] [data-testid='field-combobox']") # A non-matching query removes the Group option from the listbox. view |> form("[data-testid='field-search-form']", %{"picker_query" => "zzzznomatch"}) |> render_change() refute has_element?(view, "#member-filter-field-opt-group") end end describe "payment status labels" do test "the toggle reads 'All paid' / 'Open contributions'", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/members") pick_field(view, "payment") assert has_element?(view, "[data-testid='value-control-payment']", "All paid") assert has_element?(view, "[data-testid='value-control-payment']", "Open contributions") refute has_element?(view, "[data-testid='value-control-payment']", "Fully paid") refute has_element?(view, "[data-testid='value-control-payment']", "Has unpaid") end test "the applied fully-paid chip reads 'All paid'", %{conn: conn} do {:ok, view, _html} = live(conn, "/members?pay_filter=fully_paid") assert has_element?(view, "[data-testid='filter-chip']", "All paid") refute render(view) =~ "Fully paid" end end describe "payment control (§1.22, §1.23)" do test "'Has unpaid' reveals the two-sided open-cycle count range", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/members") pick_field(view, "payment") refute has_element?(view, "[data-testid='payment-count-range']") view |> element("[data-testid='value-control-payment']") |> render_change(%{ "__payment" => "1", "pay" => "has_unpaid", "pay_min" => "1", "pay_max" => "" }) _ = render(view) assert_patch(view) assert has_element?(view, "[data-testid='payment-count-range'] input[name='pay_min']") assert has_element?(view, "[data-testid='payment-count-range'] input[name='pay_max']") end test "the suspended option activates the suspended-status filter in the URL", %{conn: conn} do {:ok, view, _html} = live(conn, ~p"/members") pick_field(view, "payment") assert has_element?(view, "[data-testid='payment-suspended'] input[name='suspended']") view |> element("[data-testid='value-control-payment']") |> render_change(%{"__payment" => "1", "suspended" => "1"}) _ = render(view) path = assert_patch(view) assert path =~ "suspended=1" end end describe "unified date-range controls" do # Every date-range value control (payment period, join date, exit date, and # custom date fields) renders the same shared native Von/Bis row: a # non-wrapping flex row whose two `` are each wrapped in # `min-w-0 flex-1` so they share width equally and stay on one line. test "payment period, join, exit and custom date all render the shared row", %{conn: conn} do actor = Mv.Helpers.SystemActor.get_system_actor() {:ok, date_field} = CustomField |> Ash.Changeset.for_create(:create, %{ name: "birthday_#{System.unique_integer([:positive])}", value_type: :date, show_in_overview: true }) |> Ash.create(actor: actor) {:ok, view, _html} = live(conn, ~p"/members") assert_shared_range(view, "payment", "payment-period-range", "pay_from", "pay_to") assert_shared_range(view, "join_date", "custom-range", "jd_from", "jd_to") assert_shared_range(view, "active_former", "exit-custom-range", "ed_from", "ed_to") cd_from = "cdf_#{date_field.id}_from" cd_to = "cdf_#{date_field.id}_to" assert_shared_range(view, date_field.id, "custom-range", cd_from, cd_to) end end # Opens the given field's value control and asserts its date-range row is the # shared native Von/Bis layout: a `flex` (non-wrapping) row whose two date # inputs are each wrapped in `min-w-0 flex-1` (equal width, single line). defp assert_shared_range(view, field, testid, from_name, to_name) do pick_field(view, field) assert has_element?(view, "[data-testid='#{testid}'].flex") refute has_element?(view, "[data-testid='#{testid}'].flex-wrap") assert has_element?( view, "[data-testid='#{testid}'] .min-w-0.flex-1 input[type='date'][name='#{from_name}']" ) assert has_element?( view, "[data-testid='#{testid}'] .min-w-0.flex-1 input[type='date'][name='#{to_name}']" ) end end