Merge branch 'main' into feature/335_csv_import_ui
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
commit
9ddd1a470d
11 changed files with 2725 additions and 433 deletions
300
test/mv_web/components/member_filter_component_test.exs
Normal file
300
test/mv_web/components/member_filter_component_test.exs
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
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
|
||||
"""
|
||||
# async: false to prevent PostgreSQL deadlocks when running LiveView tests against DB
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
use Gettext, backend: MvWeb.Gettext
|
||||
|
||||
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
|
||||
|
||||
describe "rendering" do
|
||||
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("Custom Fields")
|
||||
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
|
||||
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 "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
|
||||
|
|
@ -1,183 +0,0 @@
|
|||
defmodule MvWeb.Components.PaymentFilterComponentTest do
|
||||
@moduledoc """
|
||||
Unit tests for the PaymentFilterComponent.
|
||||
|
||||
Tests cover:
|
||||
- Rendering in all 3 filter states (nil, :paid, :unpaid)
|
||||
- Event emission when selecting options
|
||||
- ARIA attributes for accessibility
|
||||
- Dropdown open/close behavior
|
||||
"""
|
||||
# async: false to prevent PostgreSQL deadlocks when running LiveView tests against DB
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
describe "rendering" do
|
||||
test "renders with no filter active (nil)", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Should show "All" text and no badge
|
||||
assert has_element?(view, "#payment-filter")
|
||||
refute has_element?(view, "#payment-filter .badge")
|
||||
end
|
||||
|
||||
test "renders with paid filter active", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=paid")
|
||||
|
||||
# Should show badge when filter is active
|
||||
assert has_element?(view, "#payment-filter .badge")
|
||||
end
|
||||
|
||||
test "renders with unpaid filter active", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=unpaid")
|
||||
|
||||
# Should show badge when filter is active
|
||||
assert has_element?(view, "#payment-filter .badge")
|
||||
end
|
||||
end
|
||||
|
||||
describe "dropdown behavior" do
|
||||
test "dropdown opens on button click", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Initially dropdown is closed
|
||||
refute has_element?(view, "#payment-filter ul[role='menu']")
|
||||
|
||||
# Click to open
|
||||
view
|
||||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Dropdown should be visible
|
||||
assert has_element?(view, "#payment-filter ul[role='menu']")
|
||||
end
|
||||
|
||||
test "dropdown closes after selecting an option", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
assert has_element?(view, "#payment-filter ul[role='menu']")
|
||||
|
||||
# Select an option - this should close the dropdown
|
||||
view
|
||||
|> element("#payment-filter button[phx-value-filter='paid']")
|
||||
|> render_click()
|
||||
|
||||
# After selection, dropdown should be closed
|
||||
# Note: The dropdown closes via assign, which is reflected in the next render
|
||||
refute has_element?(view, "#payment-filter ul[role='menu']")
|
||||
end
|
||||
end
|
||||
|
||||
describe "filter selection" do
|
||||
test "selecting 'All' clears the filter and updates URL", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=paid")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Select "All" option
|
||||
view
|
||||
|> element("#payment-filter button[phx-value-filter='']")
|
||||
|> render_click()
|
||||
|
||||
# URL should not contain cycle_status_filter param - wait for patch
|
||||
assert_patch(view)
|
||||
end
|
||||
|
||||
test "selecting 'Paid' sets the filter and updates URL", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Select "Paid" option
|
||||
view
|
||||
|> element("#payment-filter button[phx-value-filter='paid']")
|
||||
|> render_click()
|
||||
|
||||
# Wait for patch and check URL contains cycle_status_filter=paid
|
||||
path = assert_patch(view)
|
||||
assert path =~ "cycle_status_filter=paid"
|
||||
end
|
||||
|
||||
test "selecting 'Unpaid' sets the filter and updates URL", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
# Select "Unpaid" option
|
||||
view
|
||||
|> element("#payment-filter button[phx-value-filter='unpaid']")
|
||||
|> render_click()
|
||||
|
||||
# Wait for patch and check URL contains cycle_status_filter=unpaid
|
||||
path = assert_patch(view)
|
||||
assert path =~ "cycle_status_filter=unpaid"
|
||||
end
|
||||
end
|
||||
|
||||
describe "accessibility" do
|
||||
test "has correct ARIA attributes", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members")
|
||||
|
||||
# Main button should have aria-haspopup and aria-expanded
|
||||
assert html =~ ~s(aria-haspopup="true")
|
||||
assert html =~ ~s(aria-expanded="false")
|
||||
assert html =~ ~s(aria-label=)
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Check aria-expanded is now true
|
||||
assert html =~ ~s(aria-expanded="true")
|
||||
|
||||
# Menu should have role="menu"
|
||||
assert html =~ ~s(role="menu")
|
||||
|
||||
# Options should have role="menuitemradio"
|
||||
assert html =~ ~s(role="menuitemradio")
|
||||
end
|
||||
|
||||
test "has aria-checked on selected option", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?cycle_status_filter=paid")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element("#payment-filter button[aria-haspopup='true']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# "Paid" option should have aria-checked="true"
|
||||
# Check both possible orderings of attributes
|
||||
assert html =~ "aria-checked=\"true\"" and html =~ "phx-value-filter=\"paid\""
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue