test(member-live): assert rendered behavior instead of socket internals in the index view
Replace :sys.get_state assertions on the LiveView socket with assertions on rendered output, so the tests pin user-visible behavior rather than internal state; the few sites with no observable equivalent are kept and annotated.
This commit is contained in:
parent
3bd55fbfec
commit
ccd1f81e3e
1 changed files with 169 additions and 147 deletions
|
|
@ -1,4 +1,8 @@
|
|||
defmodule MvWeb.MemberLive.IndexTest do
|
||||
# async: false on purpose: the @slow "boolean filter performance with 150 members"
|
||||
# test asserts a wall-clock budget (duration < 1000ms). Running this module in
|
||||
# parallel with others adds CPU contention that inflates that measurement and makes
|
||||
# the timing assertion flaky, so this module stays synchronous.
|
||||
use MvWeb.ConnCase, async: false
|
||||
import Phoenix.LiveViewTest
|
||||
require Ash.Query
|
||||
|
|
@ -297,10 +301,14 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|
||||
send(view.pid, {:search_changed, "Friedrich"})
|
||||
|
||||
state = :sys.get_state(view.pid)
|
||||
|
||||
assert state.socket.assigns.query == "Friedrich"
|
||||
assert is_list(state.socket.assigns.members)
|
||||
# Rationale: this exercises the handle_info(:search_changed) callback in isolation.
|
||||
# The search box value is owned by SearchBarComponent (assign_new), and scope is
|
||||
# recomputed on handle_params rather than this handle_info, so the updated :query
|
||||
# and :members assigns have no faithful rendered proxy here. The assigns are
|
||||
# asserted on internal state to preserve the original coverage of the callback.
|
||||
assigns = :sys.get_state(view.pid).socket.assigns
|
||||
assert assigns.query == "Friedrich"
|
||||
assert is_list(assigns.members)
|
||||
end
|
||||
|
||||
@tag :ui
|
||||
|
|
@ -393,6 +401,38 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|> LazyHTML.query(~s([data-testid="bulk-actions-scope-badge"]))
|
||||
end
|
||||
|
||||
# Opens the bulk-actions dropdown and returns the mailto link's BCC payload
|
||||
# (everything after "mailto:?bcc="). This is the observable carrier of the
|
||||
# recipient set / recipient_count, replacing direct socket-assign inspection.
|
||||
defp mailto_bcc(view) do
|
||||
view |> element(~s([data-testid="bulk-actions-button"])) |> render_click()
|
||||
|
||||
href =
|
||||
render(view)
|
||||
|> LazyHTML.from_fragment()
|
||||
|> LazyHTML.query(~s([data-testid="bulk-actions-mailto"]))
|
||||
|> LazyHTML.attribute("href")
|
||||
|> List.first()
|
||||
|
||||
case href do
|
||||
"mailto:?bcc=" <> bcc -> bcc
|
||||
other -> other || ""
|
||||
end
|
||||
end
|
||||
|
||||
# Opens the member-filter dropdown so its boolean filter controls are rendered.
|
||||
defp open_member_filter(view) do
|
||||
view
|
||||
|> element(~s(button[phx-click="toggle_dropdown"][aria-label="Filter members"]))
|
||||
|> render_click()
|
||||
end
|
||||
|
||||
# Returns the rendered HTML of the member-filter dropdown (with it open).
|
||||
defp member_filter_html(view) do
|
||||
open_member_filter(view)
|
||||
render(view)
|
||||
end
|
||||
|
||||
describe "copy_emails feature" do
|
||||
setup do
|
||||
system_actor = SystemActor.get_system_actor()
|
||||
|
|
@ -528,15 +568,13 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|
||||
# Select two members by sending the select_member event directly
|
||||
render_click(view, "select_member", %{"id" => member1.id})
|
||||
render_click(view, "select_member", %{"id" => member2.id})
|
||||
html = render_click(view, "select_member", %{"id" => member2.id})
|
||||
|
||||
# Get the socket state to verify the formatted email string
|
||||
state = :sys.get_state(view.pid)
|
||||
selected_members = state.socket.assigns.selected_members
|
||||
|
||||
# Verify MapSet is used
|
||||
assert %MapSet{} = selected_members
|
||||
assert MapSet.size(selected_members) == 2
|
||||
# Both selected members are observably reflected: their row checkboxes are
|
||||
# checked and the scope badge shows the selection count ("2").
|
||||
assert has_element?(view, ~s(input[role="checkbox"][name="#{member1.id}"][checked]))
|
||||
assert has_element?(view, ~s(input[role="checkbox"][name="#{member2.id}"][checked]))
|
||||
assert scope_badge(html) |> LazyHTML.text() |> String.trim() == "2"
|
||||
end
|
||||
|
||||
test "email format is 'First Last <email>' with comma separator", %{
|
||||
|
|
@ -969,26 +1007,23 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|
||||
test "scope is :all when nothing selected and no filter", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
assigns = :sys.get_state(view.pid).socket.assigns
|
||||
assert assigns.scope == :all
|
||||
# :all scope renders the muted "all" badge.
|
||||
assert scope_badge(html) |> LazyHTML.text() |> String.trim() == "all"
|
||||
end
|
||||
|
||||
test "scope is :filtered when a search term is active", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members?query=Scope")
|
||||
{:ok, _view, html} = live(conn, "/members?query=Scope")
|
||||
|
||||
assigns = :sys.get_state(view.pid).socket.assigns
|
||||
assert assigns.scope == :filtered
|
||||
# An active search narrows the list, so the scope badge reads "filtered".
|
||||
assert scope_badge(html) |> LazyHTML.text() |> String.trim() == "filtered"
|
||||
end
|
||||
|
||||
test "scope is :filtered when a non-search filter is active", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, html} = live(conn, "/members?cycle_status_filter=paid")
|
||||
|
||||
assigns = :sys.get_state(view.pid).socket.assigns
|
||||
assert assigns.scope == :filtered
|
||||
{:ok, _view, html} = live(conn, "/members?cycle_status_filter=paid")
|
||||
|
||||
badge = scope_badge(html)
|
||||
assert badge |> LazyHTML.text() |> String.trim() == "filtered"
|
||||
|
|
@ -1001,10 +1036,13 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
render_click(view, "select_member", %{"id" => member1.id})
|
||||
html = render_click(view, "select_member", %{"id" => member1.id})
|
||||
|
||||
assigns = :sys.get_state(view.pid).socket.assigns
|
||||
assert assigns.scope == :selection
|
||||
# A selection switches the badge to the emphasized (primary) variant whose
|
||||
# label is the selected count ("1"), which is the observable proxy for scope == :selection.
|
||||
badge = scope_badge(html)
|
||||
assert badge |> LazyHTML.text() |> String.trim() == "1"
|
||||
assert badge |> LazyHTML.attribute("class") |> List.first() =~ "badge-primary"
|
||||
end
|
||||
|
||||
test "with no selection, recipient_count and mailto_bcc cover all members", %{
|
||||
|
|
@ -1013,11 +1051,11 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
assigns = :sys.get_state(view.pid).socket.assigns
|
||||
# The mailto link's BCC is the observable carrier of recipient_count/mailto_bcc.
|
||||
bcc = mailto_bcc(view)
|
||||
# Both seeded members have an email, so the no-selection scope covers both.
|
||||
assert assigns.recipient_count == 2
|
||||
assert assigns.mailto_bcc =~ "scope1%40example.com"
|
||||
assert assigns.mailto_bcc =~ "scope2%40example.com"
|
||||
assert bcc =~ "scope1%40example.com"
|
||||
assert bcc =~ "scope2%40example.com"
|
||||
end
|
||||
|
||||
test "with a selection, recipient_count and mailto_bcc cover only the selection", %{
|
||||
|
|
@ -1029,10 +1067,9 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|
||||
render_click(view, "select_member", %{"id" => member1.id})
|
||||
|
||||
assigns = :sys.get_state(view.pid).socket.assigns
|
||||
assert assigns.recipient_count == 1
|
||||
assert assigns.mailto_bcc =~ "scope1%40example.com"
|
||||
refute assigns.mailto_bcc =~ "scope2%40example.com"
|
||||
bcc = mailto_bcc(view)
|
||||
assert bcc =~ "scope1%40example.com"
|
||||
refute bcc =~ "scope2%40example.com"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -1298,6 +1335,9 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Rationale: with no boolean fields and no active filter there is no rendered
|
||||
# filter control or active-count badge to observe, so the empty initial filter
|
||||
# map is asserted on internal state directly.
|
||||
state = :sys.get_state(view.pid)
|
||||
assert state.socket.assigns.boolean_custom_field_filters == %{}
|
||||
end
|
||||
|
|
@ -1309,6 +1349,9 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Rationale: the absence of boolean fields means the filter dropdown renders
|
||||
# no boolean fieldsets; "empty list" has no positive rendered signal, so it is
|
||||
# asserted on internal state directly.
|
||||
state = :sys.get_state(view.pid)
|
||||
assert state.socket.assigns.boolean_custom_fields == []
|
||||
end
|
||||
|
|
@ -1319,89 +1362,82 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
# Create boolean and non-boolean custom fields
|
||||
boolean_field1 = create_boolean_custom_field(%{name: "Active Member"})
|
||||
boolean_field2 = create_boolean_custom_field(%{name: "Newsletter Subscription"})
|
||||
_string_field = create_string_custom_field(%{name: "Phone Number"})
|
||||
string_field = create_string_custom_field(%{name: "Phone Number"})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
open_member_filter(view)
|
||||
|
||||
state = :sys.get_state(view.pid)
|
||||
boolean_custom_fields = state.socket.assigns.boolean_custom_fields
|
||||
|
||||
# Should only contain boolean fields
|
||||
assert length(boolean_custom_fields) == 2
|
||||
assert Enum.all?(boolean_custom_fields, &(&1.value_type == :boolean))
|
||||
assert Enum.any?(boolean_custom_fields, &(&1.id == boolean_field1.id))
|
||||
assert Enum.any?(boolean_custom_fields, &(&1.id == boolean_field2.id))
|
||||
# Only the boolean fields render a tri-state filter control; the string field does not.
|
||||
assert has_element?(view, "##{"custom-boolean-filter-#{boolean_field1.id}-all"}")
|
||||
assert has_element?(view, "##{"custom-boolean-filter-#{boolean_field2.id}-all"}")
|
||||
refute has_element?(view, "##{"custom-boolean-filter-#{string_field.id}-all"}")
|
||||
end
|
||||
|
||||
test "mount sorts boolean custom fields by name ascending", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
|
||||
# Create boolean fields with specific names to test sorting
|
||||
_boolean_field_z = create_boolean_custom_field(%{name: "Zebra Field"})
|
||||
_boolean_field_a = create_boolean_custom_field(%{name: "Alpha Field"})
|
||||
_boolean_field_m = create_boolean_custom_field(%{name: "Middle Field"})
|
||||
field_z = create_boolean_custom_field(%{name: "Zebra Field"})
|
||||
field_a = create_boolean_custom_field(%{name: "Alpha Field"})
|
||||
field_m = create_boolean_custom_field(%{name: "Middle Field"})
|
||||
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
html = member_filter_html(view)
|
||||
|
||||
state = :sys.get_state(view.pid)
|
||||
boolean_custom_fields = state.socket.assigns.boolean_custom_fields
|
||||
# The rendered boolean filter controls appear in name-ascending order.
|
||||
rendered_order =
|
||||
html
|
||||
|> LazyHTML.from_fragment()
|
||||
|> LazyHTML.query(~s(input[id$="-all"][name^="custom_boolean"]))
|
||||
|> LazyHTML.attribute("id")
|
||||
|> Enum.map(
|
||||
&(&1
|
||||
|> String.replace_prefix("custom-boolean-filter-", "")
|
||||
|> String.replace_suffix("-all", ""))
|
||||
)
|
||||
|
||||
# Should be sorted by name ascending
|
||||
names = Enum.map(boolean_custom_fields, & &1.name)
|
||||
assert names == ["Alpha Field", "Middle Field", "Zebra Field"]
|
||||
assert rendered_order == [field_a.id, field_m.id, field_z.id]
|
||||
end
|
||||
|
||||
test "handle_params parses bf_<id> values correctly", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
boolean_field = create_boolean_custom_field()
|
||||
|
||||
# Test true value
|
||||
{:ok, view1, _html} =
|
||||
live(conn, "/members?bf_#{boolean_field.id}=true")
|
||||
# Test true value: the "Yes" radio is checked (the boolean true, not the string "true").
|
||||
{:ok, view1, _html} = live(conn, "/members?bf_#{boolean_field.id}=true")
|
||||
open_member_filter(view1)
|
||||
assert has_element?(view1, "##{"custom-boolean-filter-#{boolean_field.id}-true"}[checked]")
|
||||
refute has_element?(view1, "##{"custom-boolean-filter-#{boolean_field.id}-all"}[checked]")
|
||||
|
||||
state1 = :sys.get_state(view1.pid)
|
||||
filters1 = state1.socket.assigns.boolean_custom_field_filters
|
||||
assert filters1[boolean_field.id] == true
|
||||
refute filters1[boolean_field.id] == "true"
|
||||
|
||||
# Test false value
|
||||
{:ok, view2, _html} =
|
||||
live(conn, "/members?bf_#{boolean_field.id}=false")
|
||||
|
||||
state2 = :sys.get_state(view2.pid)
|
||||
filters2 = state2.socket.assigns.boolean_custom_field_filters
|
||||
assert filters2[boolean_field.id] == false
|
||||
refute filters2[boolean_field.id] == "false"
|
||||
# Test false value: the "No" radio is checked.
|
||||
{:ok, view2, _html} = live(conn, "/members?bf_#{boolean_field.id}=false")
|
||||
open_member_filter(view2)
|
||||
assert has_element?(view2, "##{"custom-boolean-filter-#{boolean_field.id}-false"}[checked]")
|
||||
refute has_element?(view2, "##{"custom-boolean-filter-#{boolean_field.id}-all"}[checked]")
|
||||
end
|
||||
|
||||
test "handle_params ignores non-existent custom field IDs", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
fake_id = Ecto.UUID.generate()
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?bf_#{fake_id}=true")
|
||||
{:ok, view, _html} = live(conn, "/members?bf_#{fake_id}=true")
|
||||
open_member_filter(view)
|
||||
|
||||
state = :sys.get_state(view.pid)
|
||||
filters = state.socket.assigns.boolean_custom_field_filters
|
||||
|
||||
# Filter should not be added for non-existent custom field
|
||||
refute Map.has_key?(filters, fake_id)
|
||||
assert filters == %{}
|
||||
# No filter control exists for a non-existent field, and no active-filter badge appears.
|
||||
refute has_element?(view, "##{"custom-boolean-filter-#{fake_id}-true"}")
|
||||
refute has_element?(view, ~s(button[aria-label="Filter members"].btn-active))
|
||||
end
|
||||
|
||||
test "handle_params ignores non-boolean custom fields", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
string_field = create_string_custom_field()
|
||||
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?bf_#{string_field.id}=true")
|
||||
{:ok, view, _html} = live(conn, "/members?bf_#{string_field.id}=true")
|
||||
open_member_filter(view)
|
||||
|
||||
state = :sys.get_state(view.pid)
|
||||
filters = state.socket.assigns.boolean_custom_field_filters
|
||||
|
||||
# Filter should not be added for non-boolean custom field
|
||||
refute Map.has_key?(filters, string_field.id)
|
||||
assert filters == %{}
|
||||
# A string field is never rendered as a boolean filter, and no filter becomes active.
|
||||
refute has_element?(view, "##{"custom-boolean-filter-#{string_field.id}-true"}")
|
||||
refute has_element?(view, ~s(button[aria-label="Filter members"].btn-active))
|
||||
end
|
||||
|
||||
test "handle_params ignores invalid filter values", %{conn: conn} do
|
||||
|
|
@ -1412,15 +1448,12 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
invalid_values = ["1", "0", "yes", "no", "True", "False", "", "invalid", "null"]
|
||||
|
||||
for invalid_value <- invalid_values do
|
||||
{:ok, view, _html} =
|
||||
live(conn, "/members?bf_#{boolean_field.id}=#{invalid_value}")
|
||||
{:ok, view, _html} = live(conn, "/members?bf_#{boolean_field.id}=#{invalid_value}")
|
||||
open_member_filter(view)
|
||||
|
||||
state = :sys.get_state(view.pid)
|
||||
filters = state.socket.assigns.boolean_custom_field_filters
|
||||
|
||||
# Invalid values should not be added to filters
|
||||
refute Map.has_key?(filters, boolean_field.id),
|
||||
"Invalid value '#{invalid_value}' should not be added to filters"
|
||||
# An invalid value leaves the field's filter at "All" (no filter applied).
|
||||
assert has_element?(view, "##{"custom-boolean-filter-#{boolean_field.id}-all"}[checked]"),
|
||||
"Invalid value '#{invalid_value}' should leave the filter at All"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -1435,12 +1468,16 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
"/members?bf_#{boolean_field1.id}=true&bf_#{boolean_field2.id}=false"
|
||||
)
|
||||
|
||||
state = :sys.get_state(view.pid)
|
||||
filters = state.socket.assigns.boolean_custom_field_filters
|
||||
open_member_filter(view)
|
||||
|
||||
assert filters[boolean_field1.id] == true
|
||||
assert filters[boolean_field2.id] == false
|
||||
assert map_size(filters) == 2
|
||||
# Both filters are reflected: field1 at "Yes", field2 at "No", and the
|
||||
# active-filter count badge shows 2.
|
||||
assert has_element?(view, "##{"custom-boolean-filter-#{boolean_field1.id}-true"}[checked]")
|
||||
assert has_element?(view, "##{"custom-boolean-filter-#{boolean_field2.id}-false"}[checked]")
|
||||
|
||||
assert view
|
||||
|> element(~s(button[aria-label="Filter members"] .badge), "2")
|
||||
|> has_element?()
|
||||
end
|
||||
|
||||
test "build_query_params includes active boolean filters and excludes nil filters", %{
|
||||
|
|
@ -1514,12 +1551,12 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
"/members?cycle_status_filter=paid&bf_#{boolean_field.id}=true"
|
||||
)
|
||||
|
||||
state = :sys.get_state(view.pid)
|
||||
filters = state.socket.assigns.boolean_custom_field_filters
|
||||
open_member_filter(view)
|
||||
|
||||
# Both filters should be set
|
||||
assert filters[boolean_field.id] == true
|
||||
assert state.socket.assigns.cycle_status_filter == :paid
|
||||
# Both filters are reflected in the rendered controls: the boolean field at
|
||||
# "Yes" and the payment-status filter at "paid".
|
||||
assert has_element?(view, "##{"custom-boolean-filter-#{boolean_field.id}-true"}[checked]")
|
||||
assert has_element?(view, "#payment-filter-paid[checked]")
|
||||
|
||||
# Both should be in URL when triggering search
|
||||
view
|
||||
|
|
@ -1540,9 +1577,8 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
{:ok, view, _html} =
|
||||
live(conn, "/members?bf_#{boolean_field.id}=true")
|
||||
|
||||
state_before = :sys.get_state(view.pid)
|
||||
filters_before = state_before.socket.assigns.boolean_custom_field_filters
|
||||
assert filters_before[boolean_field.id] == true
|
||||
open_member_filter(view)
|
||||
assert has_element?(view, "##{"custom-boolean-filter-#{boolean_field.id}-true"}[checked]")
|
||||
|
||||
# Delete the custom field (requires actor with destroy permission)
|
||||
Ash.destroy!(boolean_field, actor: system_actor)
|
||||
|
|
@ -1551,12 +1587,11 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
{:ok, view2, _html} =
|
||||
live(conn, "/members?bf_#{boolean_field.id}=true")
|
||||
|
||||
state_after = :sys.get_state(view2.pid)
|
||||
filters_after = state_after.socket.assigns.boolean_custom_field_filters
|
||||
open_member_filter(view2)
|
||||
|
||||
# Filter should not be present for deleted custom field
|
||||
refute Map.has_key?(filters_after, boolean_field.id)
|
||||
assert filters_after == %{}
|
||||
# The deleted field renders no filter control and no filter is active.
|
||||
refute has_element?(view2, "##{"custom-boolean-filter-#{boolean_field.id}-true"}")
|
||||
refute has_element?(view2, ~s(button[aria-label="Filter members"].btn-active))
|
||||
end
|
||||
|
||||
test "handle_params handles URL-encoded custom field IDs correctly", %{conn: conn} do
|
||||
|
|
@ -1569,12 +1604,11 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
{:ok, view, _html} =
|
||||
live(conn, "/members?bf_#{encoded_id}=true")
|
||||
|
||||
state = :sys.get_state(view.pid)
|
||||
filters = state.socket.assigns.boolean_custom_field_filters
|
||||
open_member_filter(view)
|
||||
|
||||
# Filter should work with URL-encoded ID
|
||||
# Phoenix should decode it automatically, so we check with original ID
|
||||
assert filters[boolean_field.id] == true
|
||||
# Phoenix decodes the param, so the filter applies under the original ID:
|
||||
# the "Yes" radio for the field is checked.
|
||||
assert has_element?(view, "##{"custom-boolean-filter-#{boolean_field.id}-true"}[checked]")
|
||||
end
|
||||
|
||||
test "handle_params ignores malformed prefix (bf_bf_<uuid>)", %{conn: conn} do
|
||||
|
|
@ -1585,12 +1619,12 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
{:ok, view, _html} =
|
||||
live(conn, "/members?bf_bf_#{boolean_field.id}=true")
|
||||
|
||||
state = :sys.get_state(view.pid)
|
||||
filters = state.socket.assigns.boolean_custom_field_filters
|
||||
open_member_filter(view)
|
||||
|
||||
# Should not parse as valid filter (UUID validation should fail)
|
||||
refute Map.has_key?(filters, boolean_field.id)
|
||||
assert filters == %{}
|
||||
# The double-prefixed param is not a valid filter: the field stays at "All"
|
||||
# and no filter is active.
|
||||
assert has_element?(view, "##{"custom-boolean-filter-#{boolean_field.id}-all"}[checked]")
|
||||
refute has_element?(view, ~s(button[aria-label="Filter members"].btn-active))
|
||||
end
|
||||
|
||||
test "handle_params limits number of boolean filters to prevent DoS", %{conn: conn} do
|
||||
|
|
@ -1605,17 +1639,11 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|
||||
{:ok, view, _html} = live(conn, "/members?#{filter_params}")
|
||||
|
||||
state = :sys.get_state(view.pid)
|
||||
filters = state.socket.assigns.boolean_custom_field_filters
|
||||
|
||||
# Should limit to maximum 50 filters
|
||||
assert map_size(filters) <= 50
|
||||
# All filters in the result should be valid
|
||||
Enum.each(filters, fn {id, value} ->
|
||||
assert value in [true, false]
|
||||
# Verify the ID corresponds to one of our boolean fields
|
||||
assert id in Enum.map(boolean_fields, &to_string(&1.id))
|
||||
end)
|
||||
# The active-filter count badge is the observable carrier of the filter count.
|
||||
# With 60 requested filters, the DoS cap clamps it to exactly 50.
|
||||
assert view
|
||||
|> element(~s(button[aria-label="Filter members"] .badge), "50")
|
||||
|> has_element?()
|
||||
end
|
||||
|
||||
test "handle_params ignores extremely long custom field IDs", %{conn: conn} do
|
||||
|
|
@ -1628,14 +1656,12 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
{:ok, view, _html} =
|
||||
live(conn, "/members?bf_#{fake_long_id}=true")
|
||||
|
||||
state = :sys.get_state(view.pid)
|
||||
filters = state.socket.assigns.boolean_custom_field_filters
|
||||
open_member_filter(view)
|
||||
|
||||
# Should not accept the extremely long ID
|
||||
refute Map.has_key?(filters, fake_long_id)
|
||||
# Valid boolean field should still work
|
||||
refute Map.has_key?(filters, boolean_field.id)
|
||||
assert filters == %{}
|
||||
# The over-long ID is rejected: the real field stays at "All" and no filter
|
||||
# is active.
|
||||
assert has_element?(view, "##{"custom-boolean-filter-#{boolean_field.id}-all"}[checked]")
|
||||
refute has_element?(view, ~s(button[aria-label="Filter members"].btn-active))
|
||||
end
|
||||
|
||||
# Helper to create a member with a boolean custom field value
|
||||
|
|
@ -2283,24 +2309,20 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
|
||||
# Start with no boolean custom fields
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
open_member_filter(view)
|
||||
|
||||
state_before = :sys.get_state(view.pid)
|
||||
boolean_fields_before = state_before.socket.assigns.boolean_custom_fields
|
||||
assert boolean_fields_before == []
|
||||
# No boolean field control is rendered yet.
|
||||
refute has_element?(view, ~s(input[name^="custom_boolean"]))
|
||||
|
||||
# Create a new boolean custom field
|
||||
new_boolean_field = create_boolean_custom_field(%{name: "Newly Added Field"})
|
||||
|
||||
# Navigate again - the new field should appear
|
||||
# Navigate again - the new field should appear in the filter dropdown.
|
||||
{:ok, view2, _html} = live(conn, "/members")
|
||||
html_after = member_filter_html(view2)
|
||||
|
||||
state_after = :sys.get_state(view2.pid)
|
||||
boolean_fields_after = state_after.socket.assigns.boolean_custom_fields
|
||||
|
||||
# New boolean field should be present
|
||||
assert length(boolean_fields_after) == 1
|
||||
assert Enum.any?(boolean_fields_after, &(&1.id == new_boolean_field.id))
|
||||
assert Enum.any?(boolean_fields_after, &(&1.name == "Newly Added Field"))
|
||||
assert has_element?(view2, "##{"custom-boolean-filter-#{new_boolean_field.id}-all"}")
|
||||
assert html_after =~ "Newly Added Field"
|
||||
end
|
||||
|
||||
@tag :slow
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue