feat(member): condense the overview into composite name and address cells
This commit is contained in:
parent
b79d7ac9ea
commit
af2cc2e0d4
17 changed files with 716 additions and 93 deletions
|
|
@ -2,10 +2,14 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
use MvWeb.ConnCase, async: true
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
# The curated default columns hide the individual name/address sort headers,
|
||||
# so these component tests make every sortable field visible via ?fields=.
|
||||
@cols "first_name,email,street,house_number,postal_code,city,country,join_date"
|
||||
|
||||
describe "rendering" do
|
||||
test "renders with correct attributes", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
|
||||
|
||||
# Test that the component renders with correct attributes
|
||||
assert has_element?(view, "[data-testid='first_name']")
|
||||
|
|
@ -15,7 +19,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
|
||||
test "renders all sortable headers", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
|
||||
|
||||
sortable_fields = [
|
||||
:first_name,
|
||||
|
|
@ -35,7 +39,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
|
||||
test "renders correct labels", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
|
||||
|
||||
# Test specific labels
|
||||
assert has_element?(view, "button[phx-value-field='first_name']", "First name")
|
||||
|
|
@ -47,7 +51,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
describe "sort icons" do
|
||||
test "shows neutral icon for specific field when not sorted", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
|
||||
|
||||
# The neutral icon has the opcity class we can test for
|
||||
# Test that EMAIL field specifically shows neutral icon
|
||||
|
|
@ -59,7 +63,9 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
|
||||
test "shows ascending icon for specific field when sorted ascending", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members?query=&sort_field=city&sort_order=asc")
|
||||
|
||||
{:ok, view, html} =
|
||||
live(conn, "/members?fields=#{@cols}&query=&sort_field=city&sort_order=asc")
|
||||
|
||||
# Test that FIRST_NAME field specifically shows ascending icon
|
||||
# Test CSS classes - no opacity for active state
|
||||
|
|
@ -80,7 +86,9 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
|
||||
test "shows descending icon for specific field when sorted descending", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members?query=&sort_field=email&sort_order=desc")
|
||||
|
||||
{:ok, _view, html} =
|
||||
live(conn, "/members?fields=#{@cols}&query=&sort_field=email&sort_order=desc")
|
||||
|
||||
# Count occurrences to ensure only one descending sort icon. Dropdown
|
||||
# triggers carry their own trailing "hero-chevron-down size-4" chevron, so
|
||||
|
|
@ -92,7 +100,9 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
|
||||
test "multiple fields can have different icon states", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=&sort_field=city&sort_order=asc")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?fields=#{@cols}&query=&sort_field=city&sort_order=asc")
|
||||
|
||||
# CITY field should be active (ascending)
|
||||
refute has_element?(view, "[data-testid='city'] .opacity-40")
|
||||
|
|
@ -109,7 +119,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
|
||||
test "icon state changes correctly when clicking different fields", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
|
||||
|
||||
# Start: all fields neutral except first name as default
|
||||
assert has_element?(view, "[data-testid='city'] .opacity-40")
|
||||
|
|
@ -139,15 +149,19 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
# Test EMAIL field specifically
|
||||
{:ok, view, html_asc} = live(conn, "/members?sort_field=email&sort_order=asc")
|
||||
{:ok, view, html_asc} =
|
||||
live(conn, "/members?fields=#{@cols}&sort_field=email&sort_order=asc")
|
||||
|
||||
assert html_asc =~ "hero-chevron-up"
|
||||
refute has_element?(view, "[data-testid='email'] .opacity-40")
|
||||
|
||||
{:ok, view, html_desc} = live(conn, "/members?sort_field=email&sort_order=desc")
|
||||
{:ok, view, html_desc} =
|
||||
live(conn, "/members?fields=#{@cols}&sort_field=email&sort_order=desc")
|
||||
|
||||
assert html_desc =~ "hero-chevron-down"
|
||||
refute has_element?(view, "[data-testid='email'] .opacity-40")
|
||||
|
||||
{:ok, view, html_neutral} = live(conn, "/members")
|
||||
{:ok, view, html_neutral} = live(conn, "/members?fields=#{@cols}")
|
||||
assert html_neutral =~ "hero-chevron-up-down"
|
||||
assert has_element?(view, "[data-testid='email'] .opacity-40")
|
||||
end
|
||||
|
|
@ -156,7 +170,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
# Test neutral state - only one field should have active sort icon
|
||||
{:ok, _view, html_neutral} = live(conn, "/members")
|
||||
{:ok, _view, html_neutral} = live(conn, "/members?fields=#{@cols}")
|
||||
|
||||
# Count active icons (should be exactly 1 - ascending for default sort field)
|
||||
up_count = html_neutral |> String.split("hero-chevron-up ") |> length() |> Kernel.-(1)
|
||||
|
|
@ -166,7 +180,8 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
assert down_count == 0, "Expected 0 descending icons, got #{down_count}"
|
||||
|
||||
# Test descending state
|
||||
{:ok, _view, html_desc} = live(conn, "/members?sort_field=first_name&sort_order=desc")
|
||||
{:ok, _view, html_desc} =
|
||||
live(conn, "/members?fields=#{@cols}&sort_field=first_name&sort_order=desc")
|
||||
|
||||
up_count = html_desc |> String.split("hero-chevron-up ") |> length() |> Kernel.-(1)
|
||||
down_count = active_sort_down_count(html_desc)
|
||||
|
|
@ -190,7 +205,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
describe "accessibility" do
|
||||
test "sets aria-label correctly for unsorted state", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
|
||||
|
||||
# Check aria-label for unsorted state
|
||||
assert has_element?(view, "button[phx-value-field='city'][aria-label='Click to sort']")
|
||||
|
|
@ -198,7 +213,9 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
|
||||
test "sets aria-label correctly for ascending sort", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?sort_field=first_name&sort_order=asc")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?fields=#{@cols}&sort_field=first_name&sort_order=asc")
|
||||
|
||||
# Check aria-label for ascending sort
|
||||
assert has_element?(view, "button[phx-value-field='first_name'][aria-label='ascending']")
|
||||
|
|
@ -206,7 +223,9 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
|
||||
test "sets aria-label correctly for descending sort", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?sort_field=first_name&sort_order=desc")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?fields=#{@cols}&sort_field=first_name&sort_order=desc")
|
||||
|
||||
# Check aria-label for descending sort
|
||||
assert has_element?(view, "button[phx-value-field='first_name'][aria-label='descending']")
|
||||
|
|
@ -214,7 +233,9 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
|
||||
test "includes tooltip with correct aria-label", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?sort_field=first_name&sort_order=asc")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?fields=#{@cols}&sort_field=first_name&sort_order=asc")
|
||||
|
||||
# Check that tooltip div exists with correct data-tip
|
||||
assert has_element?(view, "[data-testid='first_name']")
|
||||
|
|
@ -223,7 +244,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
|
||||
test "aria-labels work for all sortable fields", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?sort_field=email&sort_order=desc")
|
||||
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}&sort_field=email&sort_order=desc")
|
||||
|
||||
# Test aria-labels for different fields
|
||||
assert has_element?(view, "button[phx-value-field='email'][aria-label='descending']")
|
||||
|
|
@ -240,7 +261,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
describe "component behavior" do
|
||||
test "clicking triggers sort event on parent LiveView", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
|
||||
|
||||
# Click on the first name sort header
|
||||
view
|
||||
|
|
@ -253,7 +274,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
|
||||
test "component handles different field types correctly", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
|
||||
|
||||
# Test that different field types render correctly
|
||||
assert has_element?(view, "button[phx-value-field='first_name']")
|
||||
|
|
@ -265,7 +286,9 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
describe "edge cases" do
|
||||
test "handles invalid sort field gracefully", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members?sort_field=invalid_field&sort_order=asc")
|
||||
|
||||
{:ok, view, html} =
|
||||
live(conn, "/members?fields=#{@cols}&sort_field=invalid_field&sort_order=asc")
|
||||
|
||||
# Should not crash and should default sorting for first name
|
||||
assert html =~ "hero-chevron-up-down"
|
||||
|
|
@ -274,7 +297,9 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
|
||||
test "handles invalid sort order gracefully", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members?sort_field=first_name&sort_order=invalid")
|
||||
|
||||
{:ok, view, html} =
|
||||
live(conn, "/members?fields=#{@cols}&sort_field=first_name&sort_order=invalid")
|
||||
|
||||
# Should default to ascending
|
||||
assert html =~ "hero-chevron-up"
|
||||
|
|
@ -283,7 +308,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
|
||||
test "handles empty sort parameters", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members?sort_field=&sort_order=")
|
||||
{:ok, view, html} = live(conn, "/members?fields=#{@cols}&sort_field=&sort_order=")
|
||||
|
||||
# Should show neutral icons
|
||||
assert html =~ "hero-chevron-up-down"
|
||||
|
|
@ -294,7 +319,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
describe "icon state transitions" do
|
||||
test "icon changes when sorting state changes", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
{:ok, view, _html} = live(conn, "/members?fields=#{@cols}")
|
||||
|
||||
# Start with neutral state
|
||||
assert has_element?(view, "[data-testid='city'] .opacity-40")
|
||||
|
|
@ -310,7 +335,7 @@ defmodule MvWeb.Components.SortHeaderComponentTest do
|
|||
|
||||
test "multiple fields can be tested for icon states", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members?sort_field=email&sort_order=desc")
|
||||
{:ok, view, html} = live(conn, "/members?fields=#{@cols}&sort_field=email&sort_order=desc")
|
||||
|
||||
# Email should be active (descending)
|
||||
assert html =~ "hero-chevron-down"
|
||||
|
|
|
|||
|
|
@ -89,17 +89,19 @@ defmodule MvWeb.MemberLive.Index.FieldVisibilityTest do
|
|||
assert result["email"] == true
|
||||
end
|
||||
|
||||
test "defaults to true when field not in settings" do
|
||||
test "defaults to the curated set when field not in settings" do
|
||||
user_selection = %{}
|
||||
settings = %{member_field_visibility: %{first_name: false}}
|
||||
settings = %{member_field_visibility: %{join_date: false}}
|
||||
custom_fields = []
|
||||
|
||||
result = FieldVisibility.merge_with_global_settings(user_selection, settings, custom_fields)
|
||||
|
||||
# first_name from settings
|
||||
assert result["first_name"] == false
|
||||
# email defaults to true (not in settings)
|
||||
assert result["email"] == true
|
||||
# join_date from settings overrides its curated default
|
||||
assert result["join_date"] == false
|
||||
# name is in the curated default set -> visible
|
||||
assert result["name"] == true
|
||||
# email is not in the curated default set -> hidden
|
||||
assert result["email"] == false
|
||||
end
|
||||
|
||||
test "handles custom fields visibility" do
|
||||
|
|
@ -160,9 +162,9 @@ defmodule MvWeb.MemberLive.Index.FieldVisibilityTest do
|
|||
|
||||
result = FieldVisibility.merge_with_global_settings(user_selection, settings, custom_fields)
|
||||
|
||||
# Should default all fields to true
|
||||
assert result["first_name"] == true
|
||||
assert result["email"] == true
|
||||
# Falls back to the curated default set (no crash on nil)
|
||||
assert result["join_date"] == true
|
||||
assert result["email"] == false
|
||||
end
|
||||
|
||||
test "handles missing member_field_visibility key" do
|
||||
|
|
@ -172,9 +174,9 @@ defmodule MvWeb.MemberLive.Index.FieldVisibilityTest do
|
|||
|
||||
result = FieldVisibility.merge_with_global_settings(user_selection, settings, custom_fields)
|
||||
|
||||
# Should default all fields to true
|
||||
assert result["first_name"] == true
|
||||
assert result["email"] == true
|
||||
# Falls back to the curated default set
|
||||
assert result["join_date"] == true
|
||||
assert result["email"] == false
|
||||
end
|
||||
|
||||
test "includes all fields in result" do
|
||||
|
|
|
|||
87
test/mv_web/member_live/index_address_cell_test.exs
Normal file
87
test/mv_web/member_live/index_address_cell_test.exs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
defmodule MvWeb.MemberLive.IndexAddressCellTest do
|
||||
@moduledoc """
|
||||
§1.1 — The address renders as one composite cell: line 1 = street + house
|
||||
number, line 2 = postal code + city.
|
||||
§1.12 — City and postal code stay findable via search despite the composite
|
||||
cell.
|
||||
"""
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.Helpers.SystemActor
|
||||
|
||||
defp create_member(attrs) do
|
||||
{:ok, member} =
|
||||
Mv.Membership.create_member(
|
||||
Map.merge(
|
||||
%{first_name: "Addr", last_name: "Tester", email: "addr@example.com"},
|
||||
attrs
|
||||
),
|
||||
actor: SystemActor.get_system_actor()
|
||||
)
|
||||
|
||||
member
|
||||
end
|
||||
|
||||
test "address shows as one cell with street/house on line 1 and postal/city on line 2", %{
|
||||
conn: conn
|
||||
} do
|
||||
member =
|
||||
create_member(%{
|
||||
street: "Hauptstraße",
|
||||
house_number: "12a",
|
||||
postal_code: "10115",
|
||||
city: "Berlin"
|
||||
})
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, ~p"/members")
|
||||
|
||||
cell =
|
||||
view
|
||||
|> element("#row-#{member.id} [data-testid='member-address']")
|
||||
|> render()
|
||||
|
||||
assert cell =~ "Hauptstraße"
|
||||
assert cell =~ "12a"
|
||||
assert cell =~ "10115"
|
||||
assert cell =~ "Berlin"
|
||||
|
||||
# Composite, not four separate sortable address columns.
|
||||
refute has_element?(view, "[data-testid='street']")
|
||||
refute has_element?(view, "[data-testid='city']")
|
||||
end
|
||||
|
||||
test "member with no address parts renders a screen-reader label, not a blank cell", %{
|
||||
conn: conn
|
||||
} do
|
||||
member = create_member(%{email: "no-address@example.com"})
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, ~p"/members")
|
||||
|
||||
row = view |> element("#row-#{member.id}") |> render()
|
||||
|
||||
assert row =~ "No address"
|
||||
refute has_element?(view, "#row-#{member.id} [data-testid='member-address']")
|
||||
end
|
||||
|
||||
test "members stay findable by city via search", %{conn: conn} do
|
||||
member = create_member(%{city: "Hamburg", email: "city-search@example.com"})
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, ~p"/members?query=Hamburg")
|
||||
|
||||
assert has_element?(view, "#row-#{member.id}")
|
||||
end
|
||||
|
||||
test "members stay findable by postal code via search", %{conn: conn} do
|
||||
member = create_member(%{postal_code: "99999", email: "postal-search@example.com"})
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, ~p"/members?query=99999")
|
||||
|
||||
assert has_element?(view, "#row-#{member.id}")
|
||||
end
|
||||
end
|
||||
|
|
@ -206,14 +206,20 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
|
||||
live(
|
||||
conn,
|
||||
"/members?fields=email&query=&sort_field=custom_field_#{field.id}&sort_order=desc"
|
||||
)
|
||||
|
||||
# Click on email column
|
||||
view
|
||||
|> element("[data-testid='email']")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(view, "/members?query=&sort_field=email&sort_order=asc")
|
||||
# The fields param rides along on the patch, so assert the sort outcome.
|
||||
path = assert_patch(view)
|
||||
assert path =~ "sort_field=email"
|
||||
assert path =~ "sort_order=asc"
|
||||
end
|
||||
|
||||
test "clicking custom field column after regular column works", %{
|
||||
|
|
|
|||
58
test/mv_web/member_live/index_default_columns_test.exs
Normal file
58
test/mv_web/member_live/index_default_columns_test.exs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
defmodule MvWeb.MemberLive.IndexDefaultColumnsTest do
|
||||
@moduledoc """
|
||||
§1.2 — With no persisted column selection, exactly the curated default columns
|
||||
are visible: selection checkbox, Name (name + email), Address (composite),
|
||||
fee type, fee status, groups, join date.
|
||||
"""
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.Helpers.SystemActor
|
||||
|
||||
setup %{conn: conn} do
|
||||
{:ok, _} =
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Col",
|
||||
last_name: "Default",
|
||||
email: "col@example.com",
|
||||
street: "Musterweg",
|
||||
house_number: "1",
|
||||
postal_code: "10115",
|
||||
city: "Berlin"
|
||||
},
|
||||
actor: SystemActor.get_system_actor()
|
||||
)
|
||||
|
||||
%{conn: conn_with_oidc_user(conn)}
|
||||
end
|
||||
|
||||
test "curated columns are visible by default", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/members")
|
||||
|
||||
# Composite Name + Address cells.
|
||||
assert has_element?(view, "[data-testid='member-name']")
|
||||
assert has_element?(view, "[data-testid='member-address']")
|
||||
# Fee type, fee status, groups, join date headers.
|
||||
assert has_element?(view, "[data-testid='membership_fee_type']")
|
||||
assert has_element?(view, "[data-testid='join_date']")
|
||||
assert has_element?(view, "th", "Membership Fee Status")
|
||||
assert has_element?(view, "[data-testid='groups']")
|
||||
end
|
||||
|
||||
test "the individual name/address sub-fields are hidden by default", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/members")
|
||||
|
||||
for field <- ~w(first_name last_name email city street house_number postal_code country) do
|
||||
refute has_element?(view, "[data-testid='#{field}']")
|
||||
end
|
||||
end
|
||||
|
||||
test "an explicit field selection still overrides the curated default", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/members?fields=email")
|
||||
|
||||
assert has_element?(view, "[data-testid='email']")
|
||||
refute has_element?(view, "[data-testid='member-name']")
|
||||
end
|
||||
end
|
||||
|
|
@ -148,20 +148,20 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Verify email is visible initially
|
||||
# The curated Name column carries the email, so it is visible initially.
|
||||
html = render(view)
|
||||
assert html =~ "alice@example.com"
|
||||
|
||||
# Open dropdown and hide email
|
||||
# Open dropdown and hide the Name column
|
||||
view
|
||||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
||||
|> element("button[phx-click='select_item'][phx-value-item='name']")
|
||||
|> render_click()
|
||||
|
||||
# Email should no longer be visible
|
||||
# The Name column (and the email it carries) is no longer visible
|
||||
html = render(view)
|
||||
refute html =~ "alice@example.com"
|
||||
refute html =~ "bob@example.com"
|
||||
|
|
@ -309,13 +309,13 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Hide a field via dropdown
|
||||
# Hide the curated Name column (which carries the email) via dropdown
|
||||
view
|
||||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
view
|
||||
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
||||
|> element("button[phx-click='select_item'][phx-value-item='name']")
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
|
@ -429,7 +429,7 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Verify email is visible initially
|
||||
# The curated Name column carries the email, so it is visible initially.
|
||||
html = render(view)
|
||||
assert html =~ "alice@example.com"
|
||||
|
||||
|
|
@ -438,12 +438,12 @@ defmodule MvWeb.MemberLive.IndexFieldVisibilityTest do
|
|||
|> element("button[aria-controls='field-visibility-menu']")
|
||||
|> render_click()
|
||||
|
||||
# Simulate Enter key press on email field button
|
||||
# Simulate Enter key press on the Name field button
|
||||
view
|
||||
|> element("button[phx-click='select_item'][phx-value-item='email']")
|
||||
|> element("button[phx-click='select_item'][phx-value-item='name']")
|
||||
|> render_keydown(%{key: "Enter"})
|
||||
|
||||
# Email should no longer be visible
|
||||
# The Name column (and the email it carries) is no longer visible
|
||||
html = render(view)
|
||||
refute html =~ "alice@example.com"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -125,7 +125,10 @@ defmodule MvWeb.MemberLive.IndexGroupsUrlParamsTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
{:ok, view, html} =
|
||||
live(conn, "/members?sort_field=first_name&sort_order=desc&group_#{group1.id}=in")
|
||||
live(
|
||||
conn,
|
||||
"/members?fields=first_name&sort_field=first_name&sort_order=desc&group_#{group1.id}=in"
|
||||
)
|
||||
|
||||
assert html =~ member1.first_name
|
||||
assert has_element?(view, "[data-testid='first_name'][aria-label*='descending']")
|
||||
|
|
|
|||
|
|
@ -43,22 +43,23 @@ defmodule MvWeb.MemberLive.IndexMemberFieldsDisplayTest do
|
|||
end
|
||||
end
|
||||
|
||||
test "respects show_in_overview config", %{conn: conn, member1: m} do
|
||||
test "respects show_in_overview config", %{conn: conn} do
|
||||
# Global settings still drive column visibility: hiding a curated default
|
||||
# column via settings removes it, while unaffected columns stay visible.
|
||||
{:ok, settings} = Mv.Membership.get_settings()
|
||||
fields_to_hide = [:street, :house_number]
|
||||
|
||||
{:ok, _} =
|
||||
Mv.Membership.update_settings(settings, %{
|
||||
member_field_visibility: Map.new(fields_to_hide, &{Atom.to_string(&1), false})
|
||||
member_field_visibility: %{"join_date" => false}
|
||||
})
|
||||
|
||||
conn = conn_with_oidc_user(conn)
|
||||
# Use search query to filter to only the expected member (Alice)
|
||||
# This significantly improves test performance by avoiding loading all members from other tests
|
||||
{:ok, _view, html} = live(conn, "/members?query=Alice")
|
||||
{:ok, view, _html} = live(conn, "/members?query=Alice")
|
||||
|
||||
assert html =~ "Email"
|
||||
assert html =~ m.email
|
||||
refute html =~ m.street
|
||||
refute has_element?(view, "[data-testid='join_date']")
|
||||
# The composite Name column (and its email content) is unaffected.
|
||||
assert has_element?(view, "[data-testid='member-name']")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
56
test/mv_web/member_live/index_search_clear_test.exs
Normal file
56
test/mv_web/member_live/index_search_clear_test.exs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
defmodule MvWeb.MemberLive.IndexSearchClearTest do
|
||||
@moduledoc """
|
||||
§1.3 — Activating the search clear (×) control clears the query, removes the
|
||||
URL search param, and resets the result set to the unfiltered list.
|
||||
"""
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.Helpers.SystemActor
|
||||
|
||||
setup %{conn: conn} do
|
||||
actor = SystemActor.get_system_actor()
|
||||
|
||||
{:ok, _} =
|
||||
Mv.Membership.create_member(
|
||||
%{first_name: "Findable", last_name: "One", email: "findable@example.com"},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
{:ok, other} =
|
||||
Mv.Membership.create_member(
|
||||
%{first_name: "Other", last_name: "Two", email: "other@example.com"},
|
||||
actor: actor
|
||||
)
|
||||
|
||||
%{conn: conn_with_oidc_user(conn), other: other}
|
||||
end
|
||||
|
||||
test "clear control only appears when a query is present", %{conn: conn} do
|
||||
{:ok, no_query, _} = live(conn, ~p"/members")
|
||||
refute has_element?(no_query, "[data-testid='search-clear']")
|
||||
|
||||
{:ok, with_query, _} = live(conn, ~p"/members?query=Findable")
|
||||
assert has_element?(with_query, "[data-testid='search-clear']")
|
||||
end
|
||||
|
||||
test "clear resets query, URL param and the result set", %{conn: conn, other: other} do
|
||||
{:ok, view, _html} = live(conn, ~p"/members?query=Findable")
|
||||
|
||||
# The filtered list excludes the non-matching member.
|
||||
refute has_element?(view, "#row-#{other.id}")
|
||||
|
||||
view |> element("[data-testid='search-clear']") |> render_click()
|
||||
|
||||
# Query cleared in the URL (search param reset to empty).
|
||||
path = assert_patch(view)
|
||||
assert path =~ "query="
|
||||
refute path =~ "query=Findable"
|
||||
|
||||
# Result set resets to the unfiltered list (the previously excluded member returns).
|
||||
assert has_element?(view, "#row-#{other.id}")
|
||||
# The input no longer carries the query.
|
||||
refute has_element?(view, "[data-testid='search-input'][value='Findable']")
|
||||
end
|
||||
end
|
||||
56
test/mv_web/member_live/index_sticky_pinned_test.exs
Normal file
56
test/mv_web/member_live/index_sticky_pinned_test.exs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
defmodule MvWeb.MemberLive.IndexStickyPinnedTest do
|
||||
@moduledoc """
|
||||
§1.19 — When the table scrolls vertically and horizontally, the header stays
|
||||
sticky and the identifier (Name) column stays pinned; overflowing cell content
|
||||
truncates with ellipsis and exposes a hover/focus tooltip.
|
||||
"""
|
||||
use MvWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.Helpers.SystemActor
|
||||
|
||||
@moduletag :ui
|
||||
|
||||
setup %{conn: conn} do
|
||||
{:ok, _} =
|
||||
Mv.Membership.create_member(
|
||||
%{
|
||||
first_name: "Reginald",
|
||||
last_name: "Worthington-Smythe",
|
||||
email: "reginald@example.com",
|
||||
street: "A Very Long Street Name That Overflows",
|
||||
house_number: "123",
|
||||
postal_code: "10115",
|
||||
city: "Berlin"
|
||||
},
|
||||
actor: SystemActor.get_system_actor()
|
||||
)
|
||||
|
||||
%{conn: conn_with_oidc_user(conn)}
|
||||
end
|
||||
|
||||
test "header is sticky and the identifier column is pinned", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/members")
|
||||
|
||||
# Sticky header (desktop).
|
||||
assert html =~ "lg:sticky"
|
||||
assert html =~ "lg:top-0"
|
||||
|
||||
# Checkbox column pinned at left-0, identifier (Name) column pinned beside it.
|
||||
assert html =~ "left-0"
|
||||
assert html =~ "left-12"
|
||||
end
|
||||
|
||||
test "composite cells truncate and expose a tooltip via title", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/members")
|
||||
|
||||
name_cell = view |> element("[data-testid='member-name']") |> render()
|
||||
assert name_cell =~ "truncate"
|
||||
assert name_cell =~ ~s(title="Reginald Worthington-Smythe")
|
||||
|
||||
address_cell = view |> element("[data-testid='member-address']") |> render()
|
||||
assert address_cell =~ "truncate"
|
||||
assert address_cell =~ ~s(title="A Very Long Street Name That Overflows 123")
|
||||
end
|
||||
end
|
||||
|
|
@ -154,9 +154,13 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|
||||
describe "sorting integration" do
|
||||
@describetag :ui
|
||||
# The curated default columns no longer expose the individual name/address
|
||||
# sort headers, so these tests make the relevant column visible via ?fields=.
|
||||
# That puts a `fields` param on every push_patch, hence the relaxed
|
||||
# substring assertions instead of exact patch strings.
|
||||
test "clicking a column header toggles sort order and updates the URL", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
{:ok, view, _html} = live(conn, "/members?fields=email")
|
||||
|
||||
# The component data test ids are built with the name of the field
|
||||
# First click – should sort ASC
|
||||
|
|
@ -164,32 +168,44 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|> element("[data-testid='email']")
|
||||
|> render_click()
|
||||
|
||||
# The LiveView pushes a patch with the new query params
|
||||
assert_patch(view, "/members?query=&sort_field=email&sort_order=asc")
|
||||
path_asc = assert_patch(view)
|
||||
assert path_asc =~ "sort_field=email"
|
||||
assert path_asc =~ "sort_order=asc"
|
||||
|
||||
# Second click – toggles to DESC
|
||||
view
|
||||
|> element("[data-testid='email']")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(view, "/members?query=&sort_field=email&sort_order=desc")
|
||||
path_desc = assert_patch(view)
|
||||
assert path_desc =~ "sort_field=email"
|
||||
assert path_desc =~ "sort_order=desc"
|
||||
end
|
||||
|
||||
test "clicking different column header resets order to ascending", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?sort_field=email&sort_order=desc")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?fields=first_name,email&sort_field=email&sort_order=desc")
|
||||
|
||||
# Click on a different column
|
||||
view
|
||||
|> element("[data-testid='first_name']")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(view, "/members?query=&sort_field=first_name&sort_order=asc")
|
||||
path = assert_patch(view)
|
||||
assert path =~ "sort_field=first_name"
|
||||
assert path =~ "sort_order=asc"
|
||||
end
|
||||
|
||||
test "all sortable columns work correctly", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(
|
||||
conn,
|
||||
"/members?fields=first_name,email,street,house_number,postal_code,city,country,join_date"
|
||||
)
|
||||
|
||||
# default ascending sorting with first name
|
||||
assert has_element?(view, "[data-testid='first_name'][aria-label='ascending']")
|
||||
|
|
@ -209,30 +225,40 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|> element("[data-testid='#{field}']")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(view, "/members?query=&sort_field=#{field}&sort_order=asc")
|
||||
path = assert_patch(view)
|
||||
assert path =~ "sort_field=#{field}"
|
||||
assert path =~ "sort_order=asc"
|
||||
end
|
||||
end
|
||||
|
||||
test "sorting works with search query", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=test")
|
||||
{:ok, view, _html} = live(conn, "/members?fields=email&query=test")
|
||||
|
||||
view
|
||||
|> element("[data-testid='email']")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(view, "/members?query=test&sort_field=email&sort_order=asc")
|
||||
path = assert_patch(view)
|
||||
assert path =~ "query=test"
|
||||
assert path =~ "sort_field=email"
|
||||
assert path =~ "sort_order=asc"
|
||||
end
|
||||
|
||||
test "sorting maintains search query when toggling order", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=test&sort_field=email&sort_order=asc")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?fields=email&query=test&sort_field=email&sort_order=asc")
|
||||
|
||||
view
|
||||
|> element("[data-testid='email']")
|
||||
|> render_click()
|
||||
|
||||
assert_patch(view, "/members?query=test&sort_field=email&sort_order=desc")
|
||||
path = assert_patch(view)
|
||||
assert path =~ "query=test"
|
||||
assert path =~ "sort_field=email"
|
||||
assert path =~ "sort_order=desc"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -240,7 +266,9 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
@describetag :ui
|
||||
test "handle_params reads sort query and applies it", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=&sort_field=email&sort_order=desc")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?fields=email&query=&sort_field=email&sort_order=desc")
|
||||
|
||||
# Check that the sort state is correctly applied
|
||||
assert has_element?(view, "[data-testid='email'][aria-label='descending']")
|
||||
|
|
@ -248,7 +276,9 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|
||||
test "handle_params handles invalid sort field gracefully", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=&sort_field=invalid_field&sort_order=asc")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?fields=first_name&query=&sort_field=invalid_field&sort_order=asc")
|
||||
|
||||
# Should not crash and should show default first name order
|
||||
assert has_element?(view, "[data-testid='first_name'][aria-label='ascending']")
|
||||
|
|
@ -256,7 +286,9 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|
||||
test "handle_params preserves search query with sort params", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=test&sort_field=email&sort_order=desc")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?fields=email&query=test&sort_field=email&sort_order=desc")
|
||||
|
||||
# Both search and sort should be preserved
|
||||
assert has_element?(view, "[data-testid='email'][aria-label='descending']")
|
||||
|
|
@ -267,7 +299,9 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
@describetag :ui
|
||||
test "search maintains sort state", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=&sort_field=email&sort_order=desc")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?fields=email&query=&sort_field=email&sort_order=desc")
|
||||
|
||||
# Perform search
|
||||
view
|
||||
|
|
@ -280,7 +314,9 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|
||||
test "sort maintains search state", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=test&sort_field=email&sort_order=asc")
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?fields=email&query=test&sort_field=email&sort_order=asc")
|
||||
|
||||
# Perform sort
|
||||
view
|
||||
|
|
@ -288,7 +324,10 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|> render_click()
|
||||
|
||||
# Search state should be maintained
|
||||
assert_patch(view, "/members?query=test&sort_field=email&sort_order=desc")
|
||||
path = assert_patch(view)
|
||||
assert path =~ "query=test"
|
||||
assert path =~ "sort_field=email"
|
||||
assert path =~ "sort_order=desc"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -1578,7 +1617,7 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
boolean_field = create_boolean_custom_field()
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?bf_#{boolean_field.id}=true")
|
||||
live(conn, "/members?fields=email&bf_#{boolean_field.id}=true")
|
||||
|
||||
# Test sort toggle preserves filter
|
||||
view
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue