Editable filter chips, uniformly-sized value controls, working field-picker search, and native date-range inputs with relative presets in place of the vendored calendar — native inputs give year-jump, manual entry and keyboard accessibility for free.
257 lines
9.7 KiB
Elixir
257 lines
9.7 KiB
Elixir
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: <field>' 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>
|
|
# ("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 `<input type="date">` 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
|