feat(overview): replace filter panel with add-filter builder and aging column
Swaps the unbounded vertical filter panel for a Polaris-style add-filter builder (searchable grouped field picker, type-aware value control, applied chips) and colour-codes the fees column with each member's period-scoped unpaid-cycle count.
This commit is contained in:
parent
7d1a71b1fd
commit
c547ca19af
35 changed files with 2817 additions and 3149 deletions
|
|
@ -1,660 +0,0 @@
|
|||
defmodule MvWeb.Components.MemberFilterComponentTest do
|
||||
@moduledoc """
|
||||
Unit tests for the MemberFilterComponent.
|
||||
|
||||
Tests cover:
|
||||
- Rendering Payment Filter and Boolean Custom Fields
|
||||
- Boolean filter selection and event emission
|
||||
- Button label and badge logic
|
||||
- Filtering to show only boolean custom fields
|
||||
"""
|
||||
# Kept async: false. The deferrable-FK migration removed the concurrent
|
||||
# create_member deadlock, but this file additionally showed an async-isolation
|
||||
# failure under load (filtered members from a parallel test leaking in), so it
|
||||
# is not trivially async-safe; resolving that is a separate follow-up.
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
use Gettext, backend: MvWeb.Gettext
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.Membership.CustomField
|
||||
|
||||
# Helper to create a boolean custom field (uses system_actor - only admin can create)
|
||||
defp create_boolean_custom_field(attrs \\ %{}) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
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!(actor: system_actor)
|
||||
end
|
||||
|
||||
# Helper to create a non-boolean custom field (uses system_actor - only admin can create)
|
||||
defp create_string_custom_field(attrs \\ %{}) do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
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!(actor: system_actor)
|
||||
end
|
||||
|
||||
describe "rendering" do
|
||||
test "trigger carries a trailing chevron affordance", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Mirror the shared dropdown affordance: a trailing chevron inside the
|
||||
# bespoke filter trigger button.
|
||||
chevron =
|
||||
html
|
||||
|> LazyHTML.from_fragment()
|
||||
|> LazyHTML.query(~s(#member-filter button[aria-haspopup="true"] .hero-chevron-down))
|
||||
|
||||
assert Enum.count(chevron) == 1
|
||||
end
|
||||
|
||||
test "renders boolean custom fields when present", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
boolean_field = create_boolean_custom_field(%{name: "Active Member"})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Should show the boolean custom field name in the dropdown
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
assert html =~ boolean_field.name
|
||||
end
|
||||
|
||||
test "renders payment and custom fields groups when boolean fields exist", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
_boolean_field = create_boolean_custom_field()
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
# Should have both "Payments" and "Custom Fields" group labels
|
||||
assert html =~ gettext("Payments") || html =~ "Payment"
|
||||
assert html =~ gettext("Individual datafields")
|
||||
end
|
||||
|
||||
test "renders only payment filter when no boolean custom fields exist", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
# Create a non-boolean field to ensure it's not shown
|
||||
_string_field = create_string_custom_field()
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Component should exist with correct ID
|
||||
assert has_element?(view, "#member-filter")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Should show payment filter options (check both English and translated)
|
||||
assert html =~ "All" || html =~ gettext("All")
|
||||
assert html =~ "Paid" || html =~ gettext("Paid")
|
||||
assert html =~ "Unpaid" || html =~ gettext("Unpaid")
|
||||
|
||||
# Should not show any boolean field names (since none exist)
|
||||
# We can't easily check this without knowing field names, but the structure should be correct
|
||||
end
|
||||
end
|
||||
|
||||
describe "boolean filter selection" do
|
||||
test "selecting boolean filter sends correct event", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
boolean_field = create_boolean_custom_field(%{name: "Newsletter"})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Select "True" option for the boolean field using radio input
|
||||
# Radio inputs trigger phx-change on the form, so we use render_change on the form
|
||||
view
|
||||
|> form("#member-filter form", %{
|
||||
"custom_boolean" => %{to_string(boolean_field.id) => "true"}
|
||||
})
|
||||
|> render_change()
|
||||
|
||||
# The event should be sent to the parent LiveView
|
||||
# We verify this by checking that the URL is updated
|
||||
assert_patch(view)
|
||||
end
|
||||
|
||||
test "payment filter still works after component extension", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
_boolean_field = create_boolean_custom_field()
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Select "Paid" option using radio input
|
||||
# Radio inputs trigger phx-change on the form, so we use render_change on the form
|
||||
view
|
||||
|> form("#member-filter form", %{"payment_filter" => "paid"})
|
||||
|> render_change()
|
||||
|
||||
# URL should be updated with cycle_status_filter=paid
|
||||
path = assert_patch(view)
|
||||
assert path =~ "cycle_status_filter=paid"
|
||||
end
|
||||
end
|
||||
|
||||
describe "button label" do
|
||||
test "shows active boolean filter names in button label", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
boolean_field1 = create_boolean_custom_field(%{name: "Active Member"})
|
||||
boolean_field2 = create_boolean_custom_field(%{name: "Newsletter"})
|
||||
|
||||
# Set filters via URL
|
||||
{:ok, view, _html} =
|
||||
live(
|
||||
conn,
|
||||
"/members?bf_#{boolean_field1.id}=true&bf_#{boolean_field2.id}=false"
|
||||
)
|
||||
|
||||
# Component should exist
|
||||
assert has_element?(view, "#member-filter")
|
||||
|
||||
# Button label should contain the custom field names
|
||||
# The exact format depends on implementation, but should show active filters
|
||||
button_html =
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render()
|
||||
|
||||
assert button_html =~ boolean_field1.name || button_html =~ boolean_field2.name
|
||||
end
|
||||
|
||||
test "truncates long custom field names in button label", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
# Create field with very long name (>30 characters)
|
||||
long_name = String.duplicate("A", 50)
|
||||
boolean_field = create_boolean_custom_field(%{name: long_name})
|
||||
|
||||
# Set filter via URL
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?bf_#{boolean_field.id}=true")
|
||||
|
||||
# Component should exist
|
||||
assert has_element?(view, "#member-filter")
|
||||
|
||||
# Get button label text
|
||||
button_html =
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render()
|
||||
|
||||
# Button label should be truncated - full name should NOT appear in button
|
||||
# (it may appear in dropdown when opened, but not in the button label itself)
|
||||
# Check that button doesn't contain the full 50-character name
|
||||
refute button_html =~ long_name
|
||||
|
||||
# Button should still contain some text (truncated version or indicator)
|
||||
assert String.length(button_html) > 0
|
||||
end
|
||||
|
||||
test "date-only activation (ed_mode=all) replaces the idle label", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?ed_mode=all")
|
||||
|
||||
button_html =
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render()
|
||||
|
||||
# The idle label must not appear; some non-idle label is shown. This is
|
||||
# the same observable contract as the other filter categories — the
|
||||
# button visually communicates "a filter is active". The `btn-active`
|
||||
# CSS class is set by the parent class= attribute but the `<.button>`
|
||||
# core component currently composes its own class string and drops the
|
||||
# caller-supplied one — that is a pre-existing component constraint, not
|
||||
# specific to date filters.
|
||||
refute button_html =~ gettext("Apply filters")
|
||||
end
|
||||
|
||||
test "date-only activation (jd_from) replaces the idle label", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?jd_from=2024-01-15")
|
||||
|
||||
button_html =
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render()
|
||||
|
||||
refute button_html =~ gettext("Apply filters")
|
||||
end
|
||||
|
||||
test "date filter combined with one other filter shows '2 filters active'", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
boolean_field = create_boolean_custom_field(%{name: "Newsletter"})
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?ed_mode=all&bf_#{boolean_field.id}=true")
|
||||
|
||||
button_html =
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render()
|
||||
|
||||
# With two distinct filter categories active, the label switches to the
|
||||
# pluralized "N filters active" form. Without counting date filters as
|
||||
# a category, this would show only "1 filter active" or the boolean
|
||||
# field name.
|
||||
assert button_html =~ "2"
|
||||
assert button_html =~ gettext("filters active")
|
||||
end
|
||||
end
|
||||
|
||||
describe "badge" do
|
||||
test "shows total count of active boolean filters in badge", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
boolean_field1 = create_boolean_custom_field(%{name: "Field1"})
|
||||
boolean_field2 = create_boolean_custom_field(%{name: "Field2"})
|
||||
|
||||
# Set two filters via URL
|
||||
{:ok, view, _html} =
|
||||
live(
|
||||
conn,
|
||||
"/members?bf_#{boolean_field1.id}=true&bf_#{boolean_field2.id}=false"
|
||||
)
|
||||
|
||||
# Component should exist
|
||||
assert has_element?(view, "#member-filter")
|
||||
|
||||
# Badge should be visible when boolean filters are active
|
||||
assert has_element?(view, "#member-filter .badge")
|
||||
|
||||
# Badge should show count of active boolean filters (2 in this case)
|
||||
badge_html =
|
||||
view
|
||||
|> element("#member-filter .badge")
|
||||
|> render()
|
||||
|
||||
assert badge_html =~ "2"
|
||||
end
|
||||
end
|
||||
|
||||
describe "filtering" do
|
||||
test "only boolean custom fields are displayed, not string or integer fields", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
boolean_field = create_boolean_custom_field(%{name: "Boolean Field"})
|
||||
_string_field = create_string_custom_field(%{name: "String Field"})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Should show boolean field in the dropdown panel
|
||||
# Extract only the dropdown panel HTML to check
|
||||
dropdown_html =
|
||||
view
|
||||
|> element("#member-filter div[role='dialog']")
|
||||
|> render()
|
||||
|
||||
# Should show boolean field in dropdown
|
||||
assert dropdown_html =~ boolean_field.name
|
||||
|
||||
# Should not show string field name in the filter dropdown
|
||||
# (String fields should not appear in boolean filter section)
|
||||
refute dropdown_html =~ "String Field"
|
||||
end
|
||||
|
||||
test "renders the Dates section with exit_date and join_date controls", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
dropdown_html =
|
||||
view
|
||||
|> element("#member-filter div[role='dialog']")
|
||||
|> render()
|
||||
|
||||
assert dropdown_html =~ gettext("Dates")
|
||||
assert dropdown_html =~ gettext("Join date")
|
||||
assert dropdown_html =~ gettext("Exit date")
|
||||
# Exit-date segmented control modes.
|
||||
assert dropdown_html =~ gettext("Active only")
|
||||
assert dropdown_html =~ gettext("Inactive only")
|
||||
# Built-in date inputs (always present for join_date and the ed_mode selector).
|
||||
assert dropdown_html =~ ~s(name="jd_from")
|
||||
assert dropdown_html =~ ~s(name="jd_to")
|
||||
assert dropdown_html =~ ~s(name="ed_mode")
|
||||
end
|
||||
|
||||
test "exit_date custom mode reveals ed_from and ed_to inputs", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?ed_mode=custom")
|
||||
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
dropdown_html =
|
||||
view
|
||||
|> element("#member-filter div[role='dialog']")
|
||||
|> render()
|
||||
|
||||
assert dropdown_html =~ ~s(name="ed_from")
|
||||
assert dropdown_html =~ ~s(name="ed_to")
|
||||
end
|
||||
|
||||
test "date inputs render via MvWeb.CoreComponents.input (no raw DaisyUI input markup)",
|
||||
%{conn: conn} do
|
||||
# DESIGN_GUIDELINES §1.1 mandates that LiveViews/HEEX use the project's
|
||||
# `<.input>` wrapper rather than emitting raw `<input>` tags carrying
|
||||
# DaisyUI component classes (e.g. `input input-sm input-bordered`)
|
||||
# directly in HEEX. `<.input>` is the project's single source of truth
|
||||
# for input styling; bypassing it splits styling across many call sites.
|
||||
#
|
||||
# The recognizable structural fingerprint of `<.input>` is a wrapping
|
||||
# `<fieldset class="mb-2 fieldset">` `<label>` chain immediately
|
||||
# preceding the `<input>`. The raw inline form has no such wrapper —
|
||||
# the input sits directly inside a sibling `<label>`/`<input>` flex row.
|
||||
# We assert that fingerprint on each of the date inputs.
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?ed_mode=custom")
|
||||
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
dropdown_html =
|
||||
view
|
||||
|> element("#member-filter div[role='dialog']")
|
||||
|> render()
|
||||
|
||||
for name <- ["jd_from", "jd_to", "ed_from", "ed_to"] do
|
||||
# Match `<fieldset class="mb-2 fieldset">` followed (within a short
|
||||
# window of HTML) by an `<input>` carrying the expected `name`. The
|
||||
# window prevents the regex from spanning unrelated `mb-2` /
|
||||
# `fieldset` occurrences scattered across the dropdown. The wrapper
|
||||
# is the canonical fingerprint of `MvWeb.CoreComponents.input/1`
|
||||
# (see `lib/mv_web/components/core_components.ex` — every input
|
||||
# branch starts with `<fieldset class="mb-2 fieldset">`).
|
||||
assert Regex.match?(
|
||||
~r/<fieldset[^>]*class="mb-2 fieldset"[^>]*>\s*<label[^>]*>(?:\s*<span[^>]*>.*?<\/span>)?\s*<input[^>]*name="#{name}"/s,
|
||||
dropdown_html
|
||||
),
|
||||
"expected date input #{name} to be wrapped by MvWeb.CoreComponents.input " <>
|
||||
"(class=\"mb-2 fieldset\" fieldset wrapper), not a raw inline " <>
|
||||
"<input type=\"date\"> element"
|
||||
end
|
||||
end
|
||||
|
||||
test "exit_date defaults to :active_only in the rendered radio", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
dropdown_html =
|
||||
view
|
||||
|> element("#member-filter div[role='dialog']")
|
||||
|> render()
|
||||
|
||||
assert dropdown_html =~
|
||||
~r/name="ed_mode"[^>]*value="active_only"[^>]*checked|checked[^>]*name="ed_mode"[^>]*value="active_only"/
|
||||
end
|
||||
|
||||
test "Custom date fields section is non-scrollable with 5 or fewer fields (§3.4)", %{
|
||||
conn: conn
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
for i <- 1..5 do
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "DateField-#{i}-#{System.unique_integer([:positive])}",
|
||||
value_type: :date
|
||||
})
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
section_html = custom_date_section_html(view)
|
||||
|
||||
# With ≤ 5 fields the section must NOT carry the scrollable wrapper.
|
||||
refute section_html =~ "max-h-60"
|
||||
refute section_html =~ "overflow-y-auto"
|
||||
end
|
||||
|
||||
test "Custom date fields section becomes scrollable with more than 5 fields (§3.4)", %{
|
||||
conn: conn
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
for i <- 1..6 do
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "DateField-#{i}-#{System.unique_integer([:positive])}",
|
||||
value_type: :date
|
||||
})
|
||||
|> Ash.create!(actor: system_actor)
|
||||
end
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
section_html = custom_date_section_html(view)
|
||||
|
||||
# With more than 5 fields the section is wrapped in the scrollable container.
|
||||
assert section_html =~ "max-h-60"
|
||||
assert section_html =~ "overflow-y-auto"
|
||||
end
|
||||
|
||||
# Extract the HTML of the rendered "Custom date fields" section. Returns
|
||||
# "" if the section is not rendered. Used by the threshold tests to avoid
|
||||
# picking up scrollable classes from sibling sections.
|
||||
defp custom_date_section_html(view) do
|
||||
dropdown_html =
|
||||
view
|
||||
|> element("#member-filter div[role='dialog']")
|
||||
|> render()
|
||||
|
||||
label = gettext("Custom date fields")
|
||||
|
||||
case String.split(dropdown_html, label, parts: 2) do
|
||||
[_before, after_label] ->
|
||||
# Up to the next group header label, or the footer.
|
||||
after_label
|
||||
|> String.split(["text-xs font-semibold opacity-70 mb-2 uppercase"], parts: 2)
|
||||
|> List.first()
|
||||
|
||||
_ ->
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
test "Custom date fields section appears only when date custom fields exist", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view_no_field, _} = live(conn, "/members")
|
||||
|
||||
view_no_field
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
dropdown_html =
|
||||
view_no_field
|
||||
|> element("#member-filter div[role='dialog']")
|
||||
|> render()
|
||||
|
||||
refute dropdown_html =~ gettext("Custom date fields")
|
||||
|
||||
# Add a date-typed custom field and re-load: the section appears.
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
{:ok, field} =
|
||||
CustomField
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Birthday-#{System.unique_integer([:positive])}",
|
||||
value_type: :date
|
||||
})
|
||||
|> Ash.create(actor: system_actor)
|
||||
|
||||
{:ok, view, _} = live(conn, "/members")
|
||||
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
dropdown_html =
|
||||
view
|
||||
|> element("#member-filter div[role='dialog']")
|
||||
|> render()
|
||||
|
||||
assert dropdown_html =~ gettext("Custom date fields")
|
||||
assert dropdown_html =~ field.name
|
||||
assert dropdown_html =~ "cdf_#{field.id}_from"
|
||||
assert dropdown_html =~ "cdf_#{field.id}_to"
|
||||
end
|
||||
|
||||
test "update_filters event dispatches a date_filters_changed patch with the new jd_from", %{
|
||||
conn: conn
|
||||
} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> form("#member-filter form", %{
|
||||
"jd_from" => "2024-01-15",
|
||||
"payment_filter" => "all"
|
||||
})
|
||||
|> render_change()
|
||||
|
||||
# Parent LiveView receives {:date_filters_changed, ...} and patches the URL.
|
||||
path = assert_patch(view)
|
||||
assert path =~ "jd_from=2024-01-15"
|
||||
end
|
||||
|
||||
test "selecting ed_mode=all updates the URL and reveals former members", %{conn: conn} do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
today = Date.utc_today()
|
||||
unique_name = "Zarquon-#{System.unique_integer([:positive])}"
|
||||
|
||||
{:ok, former} =
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: unique_name,
|
||||
last_name: "Exited",
|
||||
email: "ex-#{System.unique_integer([:positive])}@example.com",
|
||||
join_date: Date.add(today, -1000),
|
||||
exit_date: Date.add(today, -30)
|
||||
},
|
||||
actor: system_actor
|
||||
)
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members")
|
||||
|
||||
# Fresh load hides the former member.
|
||||
refute html =~ former.first_name
|
||||
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> form("#member-filter form", %{
|
||||
"ed_mode" => "all",
|
||||
"payment_filter" => "all"
|
||||
})
|
||||
|> render_change()
|
||||
|
||||
path = assert_patch(view)
|
||||
assert path =~ "ed_mode=all"
|
||||
|
||||
# Now Eve appears in the rendered list.
|
||||
assert render(view) =~ former.first_name
|
||||
end
|
||||
|
||||
test "dropdown shows scrollbar when many boolean custom fields exist", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
# Create 15 boolean custom fields (more than typical, should trigger scrollbar)
|
||||
boolean_fields =
|
||||
Enum.map(1..15, fn i ->
|
||||
create_boolean_custom_field(%{name: "Field #{i}"})
|
||||
end)
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#member-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Extract dropdown panel HTML
|
||||
dropdown_html =
|
||||
view
|
||||
|> element("#member-filter div[role='dialog']")
|
||||
|> render()
|
||||
|
||||
# Should have scrollbar classes: max-h-60 overflow-y-auto pr-2
|
||||
# Check for the scrollable container (the div with max-h-60 and overflow-y-auto)
|
||||
assert dropdown_html =~ "max-h-60"
|
||||
assert dropdown_html =~ "overflow-y-auto"
|
||||
|
||||
# Verify all fields are present in the dropdown
|
||||
Enum.each(boolean_fields, fn field ->
|
||||
assert dropdown_html =~ field.name
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue