test: added tests

This commit is contained in:
carla 2025-11-26 18:14:29 +01:00
parent b509dc4ea3
commit 4313703538
5 changed files with 1068 additions and 0 deletions

View file

@ -0,0 +1,77 @@
defmodule Mv.Membership.CustomFieldShowInOverviewTest do
@moduledoc """
Tests for CustomField show_in_overview attribute.
Tests cover:
- Creating custom fields with show_in_overview: true
- Creating custom fields with show_in_overview: false (default)
- Updating show_in_overview to true
- Updating show_in_overview to false
"""
use Mv.DataCase, async: true
alias Mv.Membership.CustomField
describe "show_in_overview attribute" do
test "creates custom field with show_in_overview: true" do
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field_show",
value_type: :string,
show_in_overview: true
})
|> Ash.create()
assert custom_field.show_in_overview == true
end
test "creates custom field with show_in_overview: true (default)" do
assert {:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field_hide",
value_type: :string
})
|> Ash.create()
assert custom_field.show_in_overview == true
end
test "updates show_in_overview to true" do
{:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field_update",
value_type: :string,
show_in_overview: false
})
|> Ash.create()
assert {:ok, updated_field} =
custom_field
|> Ash.Changeset.for_update(:update, %{show_in_overview: true})
|> Ash.update()
assert updated_field.show_in_overview == true
end
test "updates show_in_overview to false" do
{:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field_update2",
value_type: :string,
show_in_overview: true
})
|> Ash.create()
assert {:ok, updated_field} =
custom_field
|> Ash.Changeset.for_update(:update, %{show_in_overview: false})
|> Ash.update()
assert updated_field.show_in_overview == false
end
end
end

View file

@ -0,0 +1,109 @@
defmodule MvWeb.MemberLive.IndexCustomFieldsAccessibilityTest do
@moduledoc """
Accessibility tests for custom field columns in the member overview.
Tests cover:
- SortHeaderComponent for custom fields has correct ARIA labels
- Tab navigation works for custom field columns
- Screen reader announcements for sorting
"""
use MvWeb.ConnCase, async: true
import Phoenix.LiveViewTest
require Ash.Query
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
setup do
# Create test member
{:ok, member} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "Alice",
last_name: "Anderson",
email: "alice@example.com"
})
|> Ash.create()
# Create custom field with show_in_overview: true
{:ok, field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "membership_number",
value_type: :string,
show_in_overview: true
})
|> Ash.create()
# Create custom field value
{:ok, _cfv} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => "A001"}
})
|> Ash.create()
%{member: member, field: field}
end
test "sort header component for custom fields has correct ARIA labels", %{
conn: conn,
field: field
} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
# Check that the sort button has aria-label
assert html =~ ~r/aria-label=["']Click to sort["']/i or
html =~ ~r/aria-label=["'].*sort.*["']/i
# Check that data-testid is present for testing
assert html =~ ~r/data-testid=["']custom_field_#{field.id}["']/
end
test "sort header component shows correct ARIA label when sorted ascending", %{
conn: conn,
field: field
} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc")
html = render(view)
# Check that aria-label indicates ascending sort
assert html =~ ~r/aria-label=["'].*ascending.*["']/i
end
test "sort header component shows correct ARIA label when sorted descending", %{
conn: conn,
field: field
} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
html = render(view)
# Check that aria-label indicates descending sort
assert html =~ ~r/aria-label=["'].*descending.*["']/i
end
test "custom field column header is keyboard accessible", %{conn: conn, field: field} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
# Check that the sort button is a button element (keyboard accessible)
assert html =~ ~r/<button[^>]*data-testid=["']custom_field_#{field.id}["']/
# Button should not have tabindex="-1" (which would remove from tab order)
refute html =~ ~r/tabindex=["']-1["']/
end
test "custom field column header has proper semantic structure", %{conn: conn, field: field} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
# Check that custom field name is displayed in the header
assert html =~ field.name
end
end

View file

@ -0,0 +1,262 @@
defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
@moduledoc """
Tests for displaying custom fields in the member overview.
Tests cover:
- Custom fields with show_in_overview: true are displayed
- Custom fields with show_in_overview: false are not displayed
- Multiple custom fields with show_in_overview: true are all displayed
- Custom field values are correctly formatted for different types
- Members without custom field values show empty cell or "-"
"""
use MvWeb.ConnCase, async: true
import Phoenix.LiveViewTest
require Ash.Query
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
setup do
# Create test members
{:ok, member1} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "Alice",
last_name: "Anderson",
email: "alice@example.com"
})
|> Ash.create()
{:ok, member2} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "Bob",
last_name: "Brown",
email: "bob@example.com"
})
|> Ash.create()
# Create custom fields
{:ok, field_show_string} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "phone_mobile",
value_type: :string,
show_in_overview: true
})
|> Ash.create()
{:ok, field_hide} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "internal_note",
value_type: :string,
show_in_overview: false
})
|> Ash.create()
{:ok, field_show_integer} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "membership_number",
value_type: :integer,
show_in_overview: true
})
|> Ash.create()
{:ok, field_show_boolean} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "newsletter",
value_type: :boolean,
show_in_overview: true
})
|> Ash.create()
{:ok, field_show_date} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "birthday",
value_type: :date,
show_in_overview: true
})
|> Ash.create()
{:ok, field_show_email} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "secondary_email",
value_type: :email,
show_in_overview: true
})
|> Ash.create()
# Create custom field values for member1
{:ok, _cfv1} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member1.id,
custom_field_id: field_show_string.id,
value: %{"_union_type" => "string", "_union_value" => "+49123456789"}
})
|> Ash.create()
{:ok, _cfv2} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member1.id,
custom_field_id: field_show_integer.id,
value: %{"_union_type" => "integer", "_union_value" => 12345}
})
|> Ash.create()
{:ok, _cfv3} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member1.id,
custom_field_id: field_show_boolean.id,
value: %{"_union_type" => "boolean", "_union_value" => true}
})
|> Ash.create()
{:ok, _cfv4} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member1.id,
custom_field_id: field_show_date.id,
value: %{"_union_type" => "date", "_union_value" => ~D[1990-05-15]}
})
|> Ash.create()
{:ok, _cfv5} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member1.id,
custom_field_id: field_show_email.id,
value: %{"_union_type" => "email", "_union_value" => "alice.private@example.com"}
})
|> Ash.create()
# Create hidden custom field value (should not be displayed)
{:ok, _cfv_hidden} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member1.id,
custom_field_id: field_hide.id,
value: %{"_union_type" => "string", "_union_value" => "Internal note"}
})
|> Ash.create()
%{
member1: member1,
member2: member2,
field_show_string: field_show_string,
field_hide: field_hide,
field_show_integer: field_show_integer,
field_show_boolean: field_show_boolean,
field_show_date: field_show_date,
field_show_email: field_show_email
}
end
test "displays custom field with show_in_overview: true", %{
conn: conn,
member1: _member1,
field_show_string: field
} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
# Check that the custom field column header is displayed
assert html =~ field.name
# Check that the value is displayed
assert html =~ "+49123456789"
end
test "does not display custom field with show_in_overview: false", %{
conn: conn,
member1: _member1,
field_hide: field
} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
# Check that the hidden custom field column header is NOT displayed
refute html =~ field.name
# Check that the value is NOT displayed
refute html =~ "Internal note"
end
test "displays multiple custom fields with show_in_overview: true", %{
conn: conn,
field_show_string: field_string,
field_show_integer: field_integer,
field_show_boolean: field_boolean
} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
# Check that all visible custom field column headers are displayed
assert html =~ field_string.name
assert html =~ field_integer.name
assert html =~ field_boolean.name
end
test "formats string custom field values correctly", %{conn: conn, member1: _member1} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
assert html =~ "+49123456789"
end
test "formats integer custom field values correctly", %{conn: conn, member1: _member1} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
assert html =~ "12345"
end
test "formats boolean custom field values correctly", %{conn: conn, member1: _member1} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
# Boolean should be displayed as "Yes" or "No" or similar
# Check for true representation
assert html =~ "true" or html =~ "Yes" or html =~ "Ja"
end
test "formats date custom field values correctly", %{conn: conn, member1: _member1} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
# Date should be displayed in readable format
assert html =~ "1990" or html =~ "1990-05-15" or html =~ "15.05.1990"
end
test "formats email custom field values correctly", %{conn: conn, member1: _member1} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
assert html =~ "alice.private@example.com"
end
test "shows empty cell or placeholder for members without custom field values", %{
conn: conn,
member2: _member2,
field_show_string: field
} do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
# The custom field column should exist
assert html =~ field.name
# Member2 should have an empty cell for this field
# We check that member2's row exists but doesn't have the value
assert html =~ "Bob Brown"
# The value should not appear for member2 (only for member1)
# We check that the value appears somewhere (for member1) but member2 row should have "-"
assert html =~ "+49123456789"
end
end

View file

@ -0,0 +1,174 @@
defmodule MvWeb.MemberLive.IndexCustomFieldsEdgeCasesTest do
@moduledoc """
Edge case tests for custom fields in the member overview.
Tests cover:
- Custom field without values (all members have no value)
- Very long custom field values are correctly displayed
"""
use MvWeb.ConnCase, async: true
import Phoenix.LiveViewTest
require Ash.Query
alias Mv.Membership.{CustomField, Member}
test "displays custom field column even when no members have values", %{conn: conn} do
# Create test members without custom field values
{:ok, _member1} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "Alice",
last_name: "Anderson",
email: "alice@example.com"
})
|> Ash.create()
{:ok, _member2} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "Bob",
last_name: "Brown",
email: "bob@example.com"
})
|> Ash.create()
# Create custom field with show_in_overview: true but no values
{:ok, field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "membership_number",
value_type: :string,
show_in_overview: true
})
|> Ash.create()
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
# Check that the custom field column header is still displayed
assert html =~ field.name
end
test "displays very long custom field values correctly", %{conn: conn} do
# Create test member
{:ok, member} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "Alice",
last_name: "Anderson",
email: "alice@example.com"
})
|> Ash.create()
# Create custom field
{:ok, field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "long_note",
value_type: :string,
show_in_overview: true
})
|> Ash.create()
# Create very long value (but within limits)
long_value = String.duplicate("A", 500)
{:ok, _cfv} =
Mv.Membership.CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => long_value}
})
|> Ash.create()
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
# Check that the value is displayed (may be truncated in UI, but should be present)
# We check for at least part of the value
assert html =~ "A" or html =~ long_value
end
test "handles multiple custom fields with show_in_overview correctly", %{conn: conn} do
# Create test member
{:ok, member} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "Alice",
last_name: "Anderson",
email: "alice@example.com"
})
|> Ash.create()
# Create multiple custom fields with show_in_overview: true
{:ok, field1} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "field1",
value_type: :string,
show_in_overview: true
})
|> Ash.create()
{:ok, field2} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "field2",
value_type: :string,
show_in_overview: true
})
|> Ash.create()
{:ok, field3} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "field3",
value_type: :string,
show_in_overview: true
})
|> Ash.create()
# Create values for all fields
{:ok, _cfv1} =
Mv.Membership.CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: field1.id,
value: %{"_union_type" => "string", "_union_value" => "Value1"}
})
|> Ash.create()
{:ok, _cfv2} =
Mv.Membership.CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: field2.id,
value: %{"_union_type" => "string", "_union_value" => "Value2"}
})
|> Ash.create()
{:ok, _cfv3} =
Mv.Membership.CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member.id,
custom_field_id: field3.id,
value: %{"_union_type" => "string", "_union_value" => "Value3"}
})
|> Ash.create()
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
# Check that all custom field columns are displayed
assert html =~ field1.name
assert html =~ field2.name
assert html =~ field3.name
# Check that all values are displayed
assert html =~ "Value1"
assert html =~ "Value2"
assert html =~ "Value3"
end
end

View file

@ -0,0 +1,446 @@
defmodule MvWeb.MemberLive.IndexCustomFieldsSortingTest do
@moduledoc """
Tests for sorting by custom fields in the member overview.
Tests cover:
- Sorting by custom field (ascending)
- Sorting by custom field (descending)
- Sorting by custom field works with search
- Sorting by custom field works with URL parameters
- Sorting by custom field works with other columns
"""
use MvWeb.ConnCase, async: true
import Phoenix.LiveViewTest
require Ash.Query
alias Mv.Membership.{CustomField, CustomFieldValue, Member}
setup do
# Create test members
{:ok, member1} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "Alice",
last_name: "Anderson",
email: "alice@example.com"
})
|> Ash.create()
{:ok, member2} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "Bob",
last_name: "Brown",
email: "bob@example.com"
})
|> Ash.create()
{:ok, member3} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "Charlie",
last_name: "Clark",
email: "charlie@example.com"
})
|> Ash.create()
# Create custom field with show_in_overview: true
{:ok, field_string} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "membership_number",
value_type: :string,
show_in_overview: true
})
|> Ash.create()
{:ok, field_integer} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "priority",
value_type: :integer,
show_in_overview: true
})
|> Ash.create()
# Create custom field values
{:ok, _cfv1} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member1.id,
custom_field_id: field_string.id,
value: %{"_union_type" => "string", "_union_value" => "A001"}
})
|> Ash.create()
{:ok, _cfv2} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member2.id,
custom_field_id: field_string.id,
value: %{"_union_type" => "string", "_union_value" => "C003"}
})
|> Ash.create()
{:ok, _cfv3} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member3.id,
custom_field_id: field_string.id,
value: %{"_union_type" => "string", "_union_value" => "B002"}
})
|> Ash.create()
{:ok, _cfv4} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member1.id,
custom_field_id: field_integer.id,
value: %{"_union_type" => "integer", "_union_value" => 10}
})
|> Ash.create()
{:ok, _cfv5} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member2.id,
custom_field_id: field_integer.id,
value: %{"_union_type" => "integer", "_union_value" => 30}
})
|> Ash.create()
{:ok, _cfv6} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member3.id,
custom_field_id: field_integer.id,
value: %{"_union_type" => "integer", "_union_value" => 20}
})
|> Ash.create()
%{
member1: member1,
member2: member2,
member3: member3,
field_string: field_string,
field_integer: field_integer
}
end
test "sorts by custom field ascending", %{conn: conn, field_string: field} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members")
# Click on custom field column header to sort
view
|> element("[data-testid='custom_field_#{field.id}']")
|> render_click()
# Check URL was updated
assert_patch(view, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc")
# Verify sort state
assert has_element?(view, "[data-testid='custom_field_#{field.id}'][aria-label='ascending']")
end
test "sorts by custom field descending", %{conn: conn, field_string: field} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?sort_field=custom_field_#{field.id}&sort_order=asc")
# Click again to toggle to descending
view
|> element("[data-testid='custom_field_#{field.id}']")
|> render_click()
# Check URL was updated
assert_patch(view, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
# Verify sort state
assert has_element?(view, "[data-testid='custom_field_#{field.id}'][aria-label='descending']")
end
test "sorting by custom field works with search", %{conn: conn, field_string: field} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?query=Alice")
# Click on custom field column header to sort
view
|> element("[data-testid='custom_field_#{field.id}']")
|> render_click()
# Check URL maintains search query
assert_patch(view, "/members?query=Alice&sort_field=custom_field_#{field.id}&sort_order=asc")
end
test "sorting by custom field works with URL parameters", %{conn: conn, field_string: field} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
# Check that the sort state is correctly applied
assert has_element?(view, "[data-testid='custom_field_#{field.id}'][aria-label='descending']")
end
test "clicking different custom field column resets order to ascending", %{
conn: conn,
field_string: field_string,
field_integer: field_integer
} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?query=&sort_field=custom_field_#{field_string.id}&sort_order=desc")
# Click on a different custom field column
view
|> element("[data-testid='custom_field_#{field_integer.id}']")
|> render_click()
assert_patch(view, "/members?query=&sort_field=custom_field_#{field_integer.id}&sort_order=asc")
end
test "clicking regular column after custom field column works", %{
conn: conn,
field_string: field
} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?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")
end
test "clicking custom field column after regular column works", %{
conn: conn,
field_string: field
} do
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?query=&sort_field=email&sort_order=desc")
# Click on custom field column
view
|> element("[data-testid='custom_field_#{field.id}']")
|> render_click()
assert_patch(view, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc")
end
test "NULL values and empty strings are always sorted last (ASC)", %{conn: conn} do
# Create additional members with NULL and empty string values
{:ok, member_with_value} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "WithValue",
last_name: "Test",
email: "withvalue@example.com"
})
|> Ash.create()
{:ok, member_with_empty} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "WithEmpty",
last_name: "Test",
email: "withempty@example.com"
})
|> Ash.create()
{:ok, member_with_null} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "WithNull",
last_name: "Test",
email: "withnull@example.com"
})
|> Ash.create()
{:ok, member_with_another_value} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "AnotherValue",
last_name: "Test",
email: "another@example.com"
})
|> Ash.create()
# Create custom field
{:ok, field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field",
value_type: :string,
show_in_overview: true
})
|> Ash.create()
# Create values: one with actual value, one with empty string, one with NULL (no value), another with value
{:ok, _cfv1} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member_with_value.id,
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => "Zebra"}
})
|> Ash.create()
{:ok, _cfv2} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member_with_empty.id,
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => ""}
})
|> Ash.create()
# member_with_null has no custom field value (NULL)
{:ok, _cfv3} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member_with_another_value.id,
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => "Apple"}
})
|> Ash.create()
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=asc")
html = render(view)
# Find positions of member first names in the HTML to verify sort order
apple_pos = :binary.match(html, member_with_another_value.first_name)
zebra_pos = :binary.match(html, member_with_value.first_name)
empty_pos = :binary.match(html, member_with_empty.first_name)
null_pos = :binary.match(html, member_with_null.first_name)
assert apple_pos != :nomatch, "AnotherValue (Apple) should be in HTML"
assert zebra_pos != :nomatch, "WithValue (Zebra) should be in HTML"
assert empty_pos != :nomatch, "WithEmpty should be in HTML"
assert null_pos != :nomatch, "WithNull should be in HTML"
{apple_idx, _} = apple_pos
{zebra_idx, _} = zebra_pos
{empty_idx, _} = empty_pos
{null_idx, _} = null_pos
# In ASC order: Apple should come before Zebra
assert apple_idx < zebra_idx, "Apple should come before Zebra in ASC order"
# NULL and empty should come after all values
assert apple_idx < empty_idx, "Apple should come before empty value"
assert apple_idx < null_idx, "Apple should come before NULL value"
assert zebra_idx < empty_idx, "Zebra should come before empty value"
assert zebra_idx < null_idx, "Zebra should come before NULL value"
end
test "NULL values and empty strings are always sorted last (DESC)", %{conn: conn} do
# Create additional members with NULL and empty string values
{:ok, member_with_value} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "WithValue",
last_name: "Test",
email: "withvalue@example.com"
})
|> Ash.create()
{:ok, member_with_empty} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "WithEmpty",
last_name: "Test",
email: "withempty@example.com"
})
|> Ash.create()
{:ok, member_with_null} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "WithNull",
last_name: "Test",
email: "withnull@example.com"
})
|> Ash.create()
{:ok, member_with_another_value} =
Member
|> Ash.Changeset.for_create(:create_member, %{
first_name: "AnotherValue",
last_name: "Test",
email: "another@example.com"
})
|> Ash.create()
# Create custom field
{:ok, field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
name: "test_field",
value_type: :string,
show_in_overview: true
})
|> Ash.create()
# Create values: one with actual value, one with empty string, one with NULL (no value), another with value
{:ok, _cfv1} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member_with_value.id,
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => "Apple"}
})
|> Ash.create()
{:ok, _cfv2} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member_with_empty.id,
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => ""}
})
|> Ash.create()
# member_with_null has no custom field value (NULL)
{:ok, _cfv3} =
CustomFieldValue
|> Ash.Changeset.for_create(:create, %{
member_id: member_with_another_value.id,
custom_field_id: field.id,
value: %{"_union_type" => "string", "_union_value" => "Zebra"}
})
|> Ash.create()
conn = conn_with_oidc_user(conn)
{:ok, view, _html} = live(conn, "/members?query=&sort_field=custom_field_#{field.id}&sort_order=desc")
html = render(view)
# Find positions of member first names in the HTML to verify sort order
apple_pos = :binary.match(html, member_with_value.first_name)
zebra_pos = :binary.match(html, member_with_another_value.first_name)
empty_pos = :binary.match(html, member_with_empty.first_name)
null_pos = :binary.match(html, member_with_null.first_name)
assert apple_pos != :nomatch, "WithValue (Apple) should be in HTML"
assert zebra_pos != :nomatch, "AnotherValue (Zebra) should be in HTML"
assert empty_pos != :nomatch, "WithEmpty should be in HTML"
assert null_pos != :nomatch, "WithNull should be in HTML"
{apple_idx, _} = apple_pos
{zebra_idx, _} = zebra_pos
{empty_idx, _} = empty_pos
{null_idx, _} = null_pos
# In DESC order: Zebra should come before Apple
assert zebra_idx < apple_idx, "Zebra should come before Apple in DESC order"
# NULL and empty should come after all values
assert zebra_idx < empty_idx, "Zebra should come before empty value"
assert zebra_idx < null_idx, "Zebra should come before NULL value"
assert apple_idx < empty_idx, "Apple should come before empty value"
assert apple_idx < null_idx, "Apple should come before NULL value"
end
end