Compare commits
1 commit
91839dc426
...
c9c5062c4d
| Author | SHA1 | Date | |
|---|---|---|---|
| c9c5062c4d |
18 changed files with 474 additions and 388 deletions
|
|
@ -1264,8 +1264,6 @@ end
|
||||||
|
|
||||||
### 3.12 Internationalization: Gettext
|
### 3.12 Internationalization: Gettext
|
||||||
|
|
||||||
**German (de):** Use informal address (“duzen”). All user-facing German text should address the user as “du” (e.g. “Bitte versuche es erneut”, “Deine Einstellungen”), not “Sie”.
|
|
||||||
|
|
||||||
**Define Translations:**
|
**Define Translations:**
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,10 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
import Ash.Expr
|
|
||||||
import MvWeb.LiveHelpers, only: [current_actor: 1]
|
import MvWeb.LiveHelpers, only: [current_actor: 1]
|
||||||
import MvWeb.Authorization
|
import MvWeb.Authorization
|
||||||
|
|
||||||
alias Mv.Membership
|
alias Mv.Membership
|
||||||
alias MvWeb.Helpers.MemberHelpers, as: MemberHelpers
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
|
|
@ -31,7 +29,6 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
|> assign(:show_add_member_input, false)
|
|> assign(:show_add_member_input, false)
|
||||||
|> assign(:member_search_query, "")
|
|> assign(:member_search_query, "")
|
||||||
|> assign(:available_members, [])
|
|> assign(:available_members, [])
|
||||||
|> assign(:add_member_candidates, [])
|
|
||||||
|> assign(:selected_member_ids, [])
|
|> assign(:selected_member_ids, [])
|
||||||
|> assign(:selected_members, [])
|
|> assign(:selected_members, [])
|
||||||
|> assign(:show_member_dropdown, false)
|
|> assign(:show_member_dropdown, false)
|
||||||
|
|
@ -97,21 +94,13 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<%= if can?(@current_user, :update, @group) do %>
|
<%= if can?(@current_user, :update, Mv.Membership.Group) do %>
|
||||||
<.button
|
<.button variant="primary" navigate={~p"/groups/#{@group.slug}/edit"}>
|
||||||
variant="primary"
|
|
||||||
navigate={~p"/groups/#{@group.slug}/edit"}
|
|
||||||
data-testid="group-show-edit-btn"
|
|
||||||
>
|
|
||||||
{gettext("Edit")}
|
{gettext("Edit")}
|
||||||
</.button>
|
</.button>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= if can?(@current_user, :destroy, @group) do %>
|
<%= if can?(@current_user, :destroy, Mv.Membership.Group) do %>
|
||||||
<.button
|
<.button class="btn-error" phx-click="open_delete_modal">
|
||||||
class="btn-error"
|
|
||||||
phx-click="open_delete_modal"
|
|
||||||
data-testid="group-show-delete-btn"
|
|
||||||
>
|
|
||||||
{gettext("Delete")}
|
{gettext("Delete")}
|
||||||
</.button>
|
</.button>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
@ -134,7 +123,7 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-lg font-semibold mb-2">{gettext("Members")}</h2>
|
<h2 class="text-lg font-semibold mb-2">{gettext("Members")}</h2>
|
||||||
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
<div class="border border-base-300 rounded-lg p-4 bg-base-100">
|
||||||
<p class="mb-4" data-testid="group-show-member-count">
|
<p class="mb-4">
|
||||||
{ngettext(
|
{ngettext(
|
||||||
"Total: %{count} member",
|
"Total: %{count} member",
|
||||||
"Total: %{count} members",
|
"Total: %{count} members",
|
||||||
|
|
@ -143,7 +132,7 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<%= if can?(@current_user, :update, @group) do %>
|
<%= if can?(@current_user, :update, Mv.Membership.Group) do %>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<%= if assigns[:show_add_member_input] do %>
|
<%= if assigns[:show_add_member_input] do %>
|
||||||
<div class="join w-full">
|
<div class="join w-full">
|
||||||
|
|
@ -171,7 +160,6 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="member-search-input"
|
id="member-search-input"
|
||||||
data-testid="group-show-member-search-input"
|
|
||||||
role="combobox"
|
role="combobox"
|
||||||
phx-hook="ComboBox"
|
phx-hook="ComboBox"
|
||||||
phx-focus="show_member_dropdown"
|
phx-focus="show_member_dropdown"
|
||||||
|
|
@ -240,7 +228,6 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary join-item"
|
class="btn btn-primary join-item"
|
||||||
phx-click="add_selected_members"
|
phx-click="add_selected_members"
|
||||||
data-testid="group-show-add-selected-members-btn"
|
|
||||||
disabled={Enum.empty?(@selected_member_ids)}
|
disabled={Enum.empty?(@selected_member_ids)}
|
||||||
aria-label={gettext("Add members")}
|
aria-label={gettext("Add members")}
|
||||||
>
|
>
|
||||||
|
|
@ -268,17 +255,15 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= if Enum.empty?(@group.members || []) do %>
|
<%= if Enum.empty?(@group.members || []) do %>
|
||||||
<p class="text-base-content/50 italic" data-testid="group-show-no-members">
|
<p class="text-base-content/50 italic">{gettext("No members in this group")}</p>
|
||||||
{gettext("No members in this group")}
|
|
||||||
</p>
|
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="overflow-x-auto" data-testid="group-show-members-table">
|
<div class="overflow-x-auto">
|
||||||
<table class="table table-zebra">
|
<table class="table table-zebra">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{gettext("Name")}</th>
|
<th>{gettext("Name")}</th>
|
||||||
<th>{gettext("Email")}</th>
|
<th>{gettext("Email")}</th>
|
||||||
<%= if can?(@current_user, :update, @group) do %>
|
<%= if can?(@current_user, :update, Mv.Membership.Group) do %>
|
||||||
<th class="w-0">{gettext("Actions")}</th>
|
<th class="w-0">{gettext("Actions")}</th>
|
||||||
<% end %>
|
<% end %>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -306,14 +291,13 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
<span class="text-base-content/50 italic">—</span>
|
<span class="text-base-content/50 italic">—</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<%= if can?(@current_user, :update, @group) do %>
|
<%= if can?(@current_user, :update, Mv.Membership.Group) do %>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost btn-sm text-error"
|
class="btn btn-ghost btn-sm text-error"
|
||||||
phx-click="remove_member"
|
phx-click="remove_member"
|
||||||
phx-value-member_id={member.id}
|
phx-value-member_id={member.id}
|
||||||
data-testid="group-show-remove-member"
|
|
||||||
aria-label={gettext("Remove member from group")}
|
aria-label={gettext("Remove member from group")}
|
||||||
data-tooltip={gettext("Remove")}
|
data-tooltip={gettext("Remove")}
|
||||||
>
|
>
|
||||||
|
|
@ -447,31 +431,28 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
# Add Member Events
|
# Add Member Events
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("show_add_member_input", _params, socket) do
|
def handle_event("show_add_member_input", _params, socket) do
|
||||||
# Load candidate members once (single DB read). Search/focus then filter in memory (R2).
|
# Reload group to ensure we have the latest members list
|
||||||
socket =
|
actor = current_actor(socket)
|
||||||
socket
|
group = socket.assigns.group
|
||||||
|> assign(:show_add_member_input, true)
|
socket = reload_group(socket, group.slug, actor)
|
||||||
|> assign(:member_search_query, "")
|
|
||||||
|> assign(:selected_member_ids, [])
|
|
||||||
|> assign(:selected_members, [])
|
|
||||||
|> assign(:show_member_dropdown, false)
|
|
||||||
|> assign(:focused_member_index, nil)
|
|
||||||
|> load_add_member_candidates()
|
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:show_add_member_input, true)
|
||||||
|
|> assign(:member_search_query, "")
|
||||||
|
|> assign(:available_members, [])
|
||||||
|
|> assign(:selected_member_ids, [])
|
||||||
|
|> assign(:selected_members, [])
|
||||||
|
|> assign(:show_member_dropdown, false)
|
||||||
|
|> assign(:focused_member_index, nil)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("show_member_dropdown", _params, socket) do
|
def handle_event("show_member_dropdown", _params, socket) do
|
||||||
# Filter in memory from preloaded candidates; no DB read (R2).
|
# Use existing group.members for filtering; reload only on add/remove
|
||||||
query = socket.assigns.member_search_query || ""
|
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign(
|
|> load_available_members("")
|
||||||
:available_members,
|
|
||||||
filter_candidates_in_memory(socket.assigns.add_member_candidates, query)
|
|
||||||
)
|
|
||||||
|> assign(:show_member_dropdown, true)
|
|> assign(:show_member_dropdown, true)
|
||||||
|> assign(:focused_member_index, nil)
|
|> assign(:focused_member_index, nil)
|
||||||
|
|
||||||
|
|
@ -485,7 +466,6 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
|> assign(:show_add_member_input, false)
|
|> assign(:show_add_member_input, false)
|
||||||
|> assign(:member_search_query, "")
|
|> assign(:member_search_query, "")
|
||||||
|> assign(:available_members, [])
|
|> assign(:available_members, [])
|
||||||
|> assign(:add_member_candidates, [])
|
|
||||||
|> assign(:selected_member_ids, [])
|
|> assign(:selected_member_ids, [])
|
||||||
|> assign(:selected_members, [])
|
|> assign(:selected_members, [])
|
||||||
|> assign(:show_member_dropdown, false)
|
|> assign(:show_member_dropdown, false)
|
||||||
|
|
@ -552,13 +532,11 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("search_members", %{"member_search" => query}, socket) do
|
def handle_event("search_members", %{"member_search" => query}, socket) do
|
||||||
# Filter in memory from preloaded candidates; no DB read (R2).
|
# Use existing group.members for filtering; reload only on add/remove
|
||||||
candidates = socket.assigns.add_member_candidates || []
|
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
|> assign(:member_search_query, query)
|
|> assign(:member_search_query, query)
|
||||||
|> assign(:available_members, filter_candidates_in_memory(candidates, query))
|
|> load_available_members(query)
|
||||||
|> assign(:show_member_dropdown, true)
|
|> assign(:show_member_dropdown, true)
|
||||||
|> assign(:focused_member_index, nil)
|
|> assign(:focused_member_index, nil)
|
||||||
|
|
||||||
|
|
@ -682,69 +660,47 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Load candidate members once when opening add-member UI (single DB read).
|
defp load_available_members(socket, query) do
|
||||||
defp load_add_member_candidates(socket) do
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
group = socket.assigns.group
|
current_member_ids = group_member_ids_set(socket.assigns.group)
|
||||||
exclude_ids = group_member_ids_set(group) |> MapSet.to_list()
|
base_query = available_members_base_query(query)
|
||||||
|
|
||||||
|
# Fetch more than 10, then exclude already-in-group and take 10 (avoids empty dropdown when first N are all in group)
|
||||||
|
fetch_limit = 50
|
||||||
|
limited_query = Ash.Query.limit(base_query, fetch_limit)
|
||||||
actor = current_actor(socket)
|
actor = current_actor(socket)
|
||||||
|
|
||||||
if exclude_ids == [] do
|
case Ash.read(limited_query, actor: actor, domain: Mv.Membership) do
|
||||||
# No members in group; load first N members
|
{:ok, members} ->
|
||||||
query =
|
available =
|
||||||
Mv.Membership.Member
|
members
|
||||||
|> Ash.Query.sort([:last_name, :first_name])
|
|> Enum.reject(fn m -> MapSet.member?(current_member_ids, m.id) end)
|
||||||
|> Ash.Query.limit(300)
|
|> Enum.take(10)
|
||||||
|
|
||||||
do_load_add_member_candidates(socket, query, actor)
|
assign(socket, available_members: available)
|
||||||
else
|
|
||||||
query =
|
|
||||||
Mv.Membership.Member
|
|
||||||
|> Ash.Query.filter(expr(id not in ^exclude_ids))
|
|
||||||
|> Ash.Query.sort([:last_name, :first_name])
|
|
||||||
|> Ash.Query.limit(300)
|
|
||||||
|
|
||||||
do_load_add_member_candidates(socket, query, actor)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp do_load_add_member_candidates(socket, query, actor) do
|
|
||||||
case Ash.read(query, actor: actor, domain: Mv.Membership) do
|
|
||||||
{:ok, candidates} ->
|
|
||||||
socket
|
|
||||||
|> assign(:add_member_candidates, candidates)
|
|
||||||
|> assign(:available_members, Enum.take(candidates, 10))
|
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
Logger.warning("Failed to load add-member candidates: #{inspect(error)}")
|
Logger.warning("Failed to load available members for group: #{inspect(error)}")
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> put_flash(:error, gettext("Could not load member list. Please try again."))
|
|> put_flash(:error, gettext("Could not load member search. Please try again."))
|
||||||
|> assign(:add_member_candidates, [])
|
|
||||||
|> assign(:available_members, [])
|
|> assign(:available_members, [])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Filter preloaded candidates by query string (name/email). No DB read. R2.
|
defp available_members_base_query(query) do
|
||||||
defp filter_candidates_in_memory(candidates, query) when is_list(candidates) do
|
search_query = if query && String.trim(query) != "", do: String.trim(query), else: nil
|
||||||
q = if is_binary(query), do: String.trim(query) |> String.downcase(), else: ""
|
|
||||||
|
|
||||||
if q == "" do
|
if search_query do
|
||||||
candidates |> Enum.take(10)
|
Mv.Membership.Member
|
||||||
|
|> Ash.Query.for_read(:search, %{query: search_query})
|
||||||
else
|
else
|
||||||
candidates
|
Mv.Membership.Member
|
||||||
|> Enum.filter(fn m ->
|
|> Ash.Query.new()
|
||||||
name = MemberHelpers.display_name(m) |> String.downcase()
|
|
||||||
email = (m.email || "") |> String.downcase()
|
|
||||||
String.contains?(name, q) or String.contains?(email, q)
|
|
||||||
end)
|
|
||||||
|> Enum.take(10)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp filter_candidates_in_memory(_, _), do: []
|
|
||||||
|
|
||||||
defp group_member_ids_set(group) do
|
defp group_member_ids_set(group) do
|
||||||
members = group.members || []
|
members = group.members || []
|
||||||
members |> Enum.map(& &1.id) |> MapSet.new()
|
members |> Enum.map(& &1.id) |> MapSet.new()
|
||||||
|
|
@ -784,7 +740,6 @@ defmodule MvWeb.GroupLive.Show do
|
||||||
|> assign(:show_add_member_input, false)
|
|> assign(:show_add_member_input, false)
|
||||||
|> assign(:member_search_query, "")
|
|> assign(:member_search_query, "")
|
||||||
|> assign(:available_members, [])
|
|> assign(:available_members, [])
|
||||||
|> assign(:add_member_candidates, [])
|
|
||||||
|> assign(:selected_member_ids, [])
|
|> assign(:selected_member_ids, [])
|
||||||
|> assign(:selected_members, [])
|
|> assign(:selected_members, [])
|
||||||
|> assign(:show_member_dropdown, false)
|
|> assign(:show_member_dropdown, false)
|
||||||
|
|
|
||||||
|
|
@ -2278,6 +2278,11 @@ msgstr "Nicht berechtigt."
|
||||||
msgid "Could not load data fields. Please check your permissions."
|
msgid "Could not load data fields. Please check your permissions."
|
||||||
msgstr "Datenfelder konnten nicht geladen werden. Bitte überprüfe deine Berechtigungen."
|
msgstr "Datenfelder konnten nicht geladen werden. Bitte überprüfe deine Berechtigungen."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/group_live/show.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Could not load member search. Please try again."
|
||||||
|
msgstr "Mitgliedersuche konnte nicht geladen werden. Bitte versuchen Sie es erneut."
|
||||||
|
|
||||||
#: lib/mv_web/live/group_live/show.ex
|
#: lib/mv_web/live/group_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Add Member"
|
msgid "Add Member"
|
||||||
|
|
@ -2618,7 +2623,17 @@ msgstr "Import"
|
||||||
msgid "Value type cannot be changed after creation"
|
msgid "Value type cannot be changed after creation"
|
||||||
msgstr "Der Wertetyp kann nach dem Erstellen nicht mehr geändert werden."
|
msgstr "Der Wertetyp kann nach dem Erstellen nicht mehr geändert werden."
|
||||||
|
|
||||||
#: lib/mv_web/live/group_live/show.ex
|
#~ #: lib/mv_web/live/import_export_live.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Could not load member list. Please try again."
|
#~ msgid "Export Members (CSV)"
|
||||||
msgstr "Mitgliederliste konnte nicht geladen werden. Bitte versuche es erneut."
|
#~ msgstr "Mitglieder exportieren (CSV)"
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/import_export_live.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Export functionality will be available in a future release."
|
||||||
|
#~ msgstr "Export-Funktionalität ist im nächsten release verfügbar."
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/import_export_live.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Import members from CSV files or export member data."
|
||||||
|
#~ msgstr "Importiere Mitglieder aus CSV-Dateien oder exportiere Mitgliederdaten."
|
||||||
|
|
|
||||||
|
|
@ -2279,6 +2279,11 @@ msgstr ""
|
||||||
msgid "Could not load data fields. Please check your permissions."
|
msgid "Could not load data fields. Please check your permissions."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/group_live/show.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Could not load member search. Please try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/group_live/show.ex
|
#: lib/mv_web/live/group_live/show.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Add Member"
|
msgid "Add Member"
|
||||||
|
|
@ -2618,8 +2623,3 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Value type cannot be changed after creation"
|
msgid "Value type cannot be changed after creation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/group_live/show.ex
|
|
||||||
#, elixir-autogen, elixir-format
|
|
||||||
msgid "Could not load member list. Please try again."
|
|
||||||
msgstr ""
|
|
||||||
|
|
|
||||||
|
|
@ -2279,6 +2279,11 @@ msgstr ""
|
||||||
msgid "Could not load data fields. Please check your permissions."
|
msgid "Could not load data fields. Please check your permissions."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/group_live/show.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Could not load member search. Please try again."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/group_live/show.ex
|
#: lib/mv_web/live/group_live/show.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Add Member"
|
msgid "Add Member"
|
||||||
|
|
@ -2619,7 +2624,17 @@ msgstr ""
|
||||||
msgid "Value type cannot be changed after creation"
|
msgid "Value type cannot be changed after creation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: lib/mv_web/live/group_live/show.ex
|
#~ #: lib/mv_web/live/import_export_live.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#~ #, elixir-autogen, elixir-format, fuzzy
|
||||||
msgid "Could not load member list. Please try again."
|
#~ msgid "Export Members (CSV)"
|
||||||
msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/import_export_live.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Export functionality will be available in a future release."
|
||||||
|
#~ msgstr ""
|
||||||
|
|
||||||
|
#~ #: lib/mv_web/live/import_export_live.ex
|
||||||
|
#~ #, elixir-autogen, elixir-format
|
||||||
|
#~ msgid "Import members from CSV files or export member data."
|
||||||
|
#~ msgstr ""
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@
|
||||||
# mix run priv/repo/seeds.exs
|
# mix run priv/repo/seeds.exs
|
||||||
#
|
#
|
||||||
|
|
||||||
alias Mv.Accounts
|
|
||||||
alias Mv.Membership
|
alias Mv.Membership
|
||||||
alias Mv.MembershipFees.CycleGenerator
|
alias Mv.Accounts
|
||||||
alias Mv.MembershipFees.MembershipFeeType
|
alias Mv.MembershipFees.MembershipFeeType
|
||||||
|
alias Mv.MembershipFees.CycleGenerator
|
||||||
|
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
|
|
@ -579,39 +579,6 @@ Enum.with_index(linked_members)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
# Create example groups (idempotent: create only if name does not exist)
|
|
||||||
group_configs = [
|
|
||||||
%{name: "Vorstand", description: "Gremium Vorstand"},
|
|
||||||
%{name: "Trainer*innen", description: "Alle lizenzierten Trainer*innen"},
|
|
||||||
%{name: "Jugend", description: "Jugendbereich"},
|
|
||||||
%{name: "Newsletter", description: "Empfänger*innen Newsletter"}
|
|
||||||
]
|
|
||||||
|
|
||||||
existing_groups =
|
|
||||||
case Membership.list_groups(actor: admin_user_with_role) do
|
|
||||||
{:ok, list} -> list
|
|
||||||
{:error, _} -> []
|
|
||||||
end
|
|
||||||
|
|
||||||
existing_names_lower = MapSet.new(existing_groups, &String.downcase(&1.name))
|
|
||||||
|
|
||||||
seed_groups =
|
|
||||||
Enum.reduce(group_configs, %{}, fn config, acc ->
|
|
||||||
name = config.name
|
|
||||||
|
|
||||||
if MapSet.member?(existing_names_lower, String.downcase(name)) do
|
|
||||||
group = Enum.find(existing_groups, &(String.downcase(&1.name) == String.downcase(name)))
|
|
||||||
Map.put(acc, name, group)
|
|
||||||
else
|
|
||||||
group =
|
|
||||||
Membership.create_group!(%{name: name, description: config.description},
|
|
||||||
actor: admin_user_with_role
|
|
||||||
)
|
|
||||||
|
|
||||||
Map.put(acc, name, group)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
# Create sample custom field values for some members
|
# Create sample custom field values for some members
|
||||||
all_members = Ash.read!(Membership.Member, actor: admin_user_with_role)
|
all_members = Ash.read!(Membership.Member, actor: admin_user_with_role)
|
||||||
all_custom_fields = Ash.read!(Membership.CustomField, actor: admin_user_with_role)
|
all_custom_fields = Ash.read!(Membership.CustomField, actor: admin_user_with_role)
|
||||||
|
|
@ -620,35 +587,6 @@ all_custom_fields = Ash.read!(Membership.CustomField, actor: admin_user_with_rol
|
||||||
find_field = fn name -> Enum.find(all_custom_fields, &(&1.name == name)) end
|
find_field = fn name -> Enum.find(all_custom_fields, &(&1.name == name)) end
|
||||||
find_member = fn email -> Enum.find(all_members, &(&1.email == email)) end
|
find_member = fn email -> Enum.find(all_members, &(&1.email == email)) end
|
||||||
|
|
||||||
# Assign seed members to groups (idempotent: duplicate create_member_group is skipped)
|
|
||||||
member_group_assignments = [
|
|
||||||
{"hans.mueller@example.de", ["Vorstand", "Newsletter"]},
|
|
||||||
{"greta.schmidt@example.de", ["Jugend", "Newsletter"]},
|
|
||||||
{"friedrich.wagner@example.de", ["Trainer*innen"]},
|
|
||||||
{"maria.weber@example.de", ["Newsletter"]},
|
|
||||||
{"thomas.klein@example.de", ["Newsletter"]}
|
|
||||||
]
|
|
||||||
|
|
||||||
Enum.each(member_group_assignments, fn {email, group_names} ->
|
|
||||||
member = find_member.(email)
|
|
||||||
|
|
||||||
if member do
|
|
||||||
Enum.each(group_names, fn group_name ->
|
|
||||||
group = seed_groups[group_name]
|
|
||||||
|
|
||||||
if group do
|
|
||||||
case Membership.create_member_group(
|
|
||||||
%{member_id: member.id, group_id: group.id},
|
|
||||||
actor: admin_user_with_role
|
|
||||||
) do
|
|
||||||
{:ok, _} -> :ok
|
|
||||||
{:error, _} -> :ok
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
# Add custom field values for Hans Müller
|
# Add custom field values for Hans Müller
|
||||||
if hans = find_member.("hans.mueller@example.de") do
|
if hans = find_member.("hans.mueller@example.de") do
|
||||||
[
|
[
|
||||||
|
|
@ -793,7 +731,6 @@ IO.puts(
|
||||||
)
|
)
|
||||||
|
|
||||||
IO.puts(" - Sample members: Hans, Greta, Friedrich")
|
IO.puts(" - Sample members: Hans, Greta, Friedrich")
|
||||||
IO.puts(" - Groups: Vorstand, Trainer*innen, Jugend, Newsletter (with members assigned)")
|
|
||||||
|
|
||||||
IO.puts(
|
IO.puts(
|
||||||
" - Additional users: hans.mueller@example.de, greta.schmidt@example.de, maria.weber@example.de, thomas.klein@example.de"
|
" - Additional users: hans.mueller@example.de, greta.schmidt@example.de, maria.weber@example.de, thomas.klein@example.de"
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ defmodule MvWeb.GroupLive.FormTest do
|
||||||
test "form renders with empty fields", %{conn: conn} do
|
test "form renders with empty fields", %{conn: conn} do
|
||||||
{:ok, view, html} = live(conn, "/groups/new")
|
{:ok, view, html} = live(conn, "/groups/new")
|
||||||
|
|
||||||
# OR-chain for i18n (Create Group / Gruppe erstellen)
|
|
||||||
assert html =~ gettext("Create Group") or html =~ "create" or html =~ "Gruppe erstellen"
|
assert html =~ gettext("Create Group") or html =~ "create" or html =~ "Gruppe erstellen"
|
||||||
assert has_element?(view, "form")
|
assert has_element?(view, "form")
|
||||||
end
|
end
|
||||||
|
|
@ -66,7 +65,6 @@ defmodule MvWeb.GroupLive.FormTest do
|
||||||
|> form("#group-form", group: form_data)
|
|> form("#group-form", group: form_data)
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
# OR-chain for i18n (required/erforderlich) and validation message wording
|
|
||||||
assert html =~ gettext("required") or html =~ "name" or html =~ "error" or
|
assert html =~ gettext("required") or html =~ "name" or html =~ "error" or
|
||||||
html =~ "erforderlich"
|
html =~ "erforderlich"
|
||||||
end
|
end
|
||||||
|
|
@ -82,7 +80,6 @@ defmodule MvWeb.GroupLive.FormTest do
|
||||||
|> form("#group-form", group: form_data)
|
|> form("#group-form", group: form_data)
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
# OR-chain for i18n (length/Länge) and validation message
|
|
||||||
assert html =~ "100" or html =~ "length" or html =~ "error" or html =~ "Länge"
|
assert html =~ "100" or html =~ "length" or html =~ "error" or html =~ "Länge"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -101,7 +98,6 @@ defmodule MvWeb.GroupLive.FormTest do
|
||||||
|> form("#group-form", group: form_data)
|
|> form("#group-form", group: form_data)
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
# OR-chain for i18n (length/Länge) and validation message
|
|
||||||
assert html =~ "500" or html =~ "length" or html =~ "error" or html =~ "Länge"
|
assert html =~ "500" or html =~ "length" or html =~ "error" or html =~ "Länge"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -120,7 +116,6 @@ defmodule MvWeb.GroupLive.FormTest do
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
# Check for a validation error on the name field in a robust way
|
# Check for a validation error on the name field in a robust way
|
||||||
# OR-chain for i18n and validation message (already taken)
|
|
||||||
assert html =~ "name" or html =~ gettext("has already been taken")
|
assert html =~ "name" or html =~ gettext("has already been taken")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -136,7 +131,6 @@ defmodule MvWeb.GroupLive.FormTest do
|
||||||
|> form("#group-form", group: form_data)
|
|> form("#group-form", group: form_data)
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
# OR-chain for i18n (error/Fehler, invalid/ungültig)
|
|
||||||
assert html =~ "error" or html =~ "invalid" or html =~ "Fehler" or html =~ "ungültig"
|
assert html =~ "error" or html =~ "invalid" or html =~ "Fehler" or html =~ "ungültig"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -202,7 +196,6 @@ defmodule MvWeb.GroupLive.FormTest do
|
||||||
|> form("#group-form", group: form_data)
|
|> form("#group-form", group: form_data)
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|
|
||||||
# OR-chain for i18n (already taken / bereits vergeben) and validation wording
|
|
||||||
assert html =~ "already" or html =~ "taken" or html =~ "exists" or html =~ "error" or
|
assert html =~ "already" or html =~ "taken" or html =~ "exists" or html =~ "error" or
|
||||||
html =~ "bereits" or html =~ "vergeben"
|
html =~ "bereits" or html =~ "vergeben"
|
||||||
end
|
end
|
||||||
|
|
@ -212,7 +205,7 @@ defmodule MvWeb.GroupLive.FormTest do
|
||||||
|
|
||||||
{:ok, _view, html} = live(conn, "/groups/#{group.slug}/edit")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}/edit")
|
||||||
|
|
||||||
# Slug should not be in form (it's immutable); regex for input element
|
# Slug should not be in form (it's immutable)
|
||||||
refute html =~ ~r/slug.*input/i or html =~ ~r/input.*slug/i
|
refute html =~ ~r/slug.*input/i or html =~ ~r/input.*slug/i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -40,14 +40,13 @@ defmodule MvWeb.GroupLive.IndexTest do
|
||||||
|
|
||||||
assert html =~ "Test Group"
|
assert html =~ "Test Group"
|
||||||
assert html =~ "Test description"
|
assert html =~ "Test description"
|
||||||
# OR-chain for i18n (Members/Mitglieder) and alternate copy for count
|
# Member count should be displayed (0 for empty group)
|
||||||
assert html =~ "0" or html =~ gettext("Members") or html =~ "Mitglieder"
|
assert html =~ "0" or html =~ gettext("Members") or html =~ "Mitglieder"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "displays 'Create Group' button for admin users", %{conn: conn} do
|
test "displays 'Create Group' button for admin users", %{conn: conn} do
|
||||||
{:ok, _view, html} = live(conn, "/groups")
|
{:ok, _view, html} = live(conn, "/groups")
|
||||||
|
|
||||||
# OR-chain for i18n (Create Group / Gruppe erstellen) and alternate wording
|
|
||||||
assert html =~ gettext("Create Group") or html =~ "create" or html =~ "new" or
|
assert html =~ gettext("Create Group") or html =~ "create" or html =~ "new" or
|
||||||
html =~ "Gruppe erstellen"
|
html =~ "Gruppe erstellen"
|
||||||
end
|
end
|
||||||
|
|
@ -55,7 +54,7 @@ defmodule MvWeb.GroupLive.IndexTest do
|
||||||
test "displays empty state when no groups exist", %{conn: conn} do
|
test "displays empty state when no groups exist", %{conn: conn} do
|
||||||
{:ok, _view, html} = live(conn, "/groups")
|
{:ok, _view, html} = live(conn, "/groups")
|
||||||
|
|
||||||
# OR-chain for i18n (No groups / Keine Gruppen) and alternate empty state copy
|
# Should show empty state or empty list message
|
||||||
assert html =~ gettext("No groups") or html =~ "0" or html =~ "empty" or
|
assert html =~ gettext("No groups") or html =~ "0" or html =~ "empty" or
|
||||||
html =~ "Keine Gruppen"
|
html =~ "Keine Gruppen"
|
||||||
end
|
end
|
||||||
|
|
@ -77,7 +76,6 @@ defmodule MvWeb.GroupLive.IndexTest do
|
||||||
|
|
||||||
{:ok, _view, html} = live(conn, "/groups")
|
{:ok, _view, html} = live(conn, "/groups")
|
||||||
|
|
||||||
# Long description may be truncated in UI
|
|
||||||
assert html =~ long_description or html =~ String.slice(long_description, 0, 100)
|
assert html =~ long_description or html =~ String.slice(long_description, 0, 100)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -111,7 +109,7 @@ defmodule MvWeb.GroupLive.IndexTest do
|
||||||
# Should be able to see groups
|
# Should be able to see groups
|
||||||
assert html =~ gettext("Groups")
|
assert html =~ gettext("Groups")
|
||||||
|
|
||||||
# Read-only must not see create button (OR for i18n)
|
# Should NOT see create button
|
||||||
refute html =~ gettext("Create Group") or html =~ "create"
|
refute html =~ gettext("Create Group") or html =~ "create"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -179,7 +177,7 @@ defmodule MvWeb.GroupLive.IndexTest do
|
||||||
final_count = Agent.get(query_count_agent, & &1)
|
final_count = Agent.get(query_count_agent, & &1)
|
||||||
:telemetry.detach(handler_id)
|
:telemetry.detach(handler_id)
|
||||||
|
|
||||||
# OR-chain for i18n (Members/Mitglieder) and count display
|
# Member count should be displayed (should be 2)
|
||||||
assert html =~ "2" or html =~ gettext("Members") or html =~ "Mitglieder"
|
assert html =~ "2" or html =~ gettext("Members") or html =~ "Mitglieder"
|
||||||
|
|
||||||
# Verify query count is reasonable (member count should be calculated efficiently)
|
# Verify query count is reasonable (member count should be calculated efficiently)
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ defmodule MvWeb.GroupLive.IntegrationTest do
|
||||||
|
|
||||||
assert html =~ "Updated Workflow Test Group"
|
assert html =~ "Updated Workflow Test Group"
|
||||||
assert html =~ "Updated description"
|
assert html =~ "Updated description"
|
||||||
# OR-chain: slug may appear as UUID or normalized slug in copy
|
# Slug should remain unchanged
|
||||||
assert html =~ original_slug or html =~ "workflow-test-group"
|
assert html =~ original_slug or html =~ "workflow-test-group"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@ defmodule MvWeb.GroupLive.IntegrationTest do
|
||||||
# View group via slug
|
# View group via slug
|
||||||
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
# OR-chain for i18n (Members/Mitglieder); member names may be first or last
|
# Member count should be 2
|
||||||
assert html =~ "2" or html =~ gettext("Members") or html =~ "Mitglieder"
|
assert html =~ "2" or html =~ gettext("Members") or html =~ "Mitglieder"
|
||||||
assert html =~ member1.first_name or html =~ member1.last_name
|
assert html =~ member1.first_name or html =~ member1.last_name
|
||||||
assert html =~ member2.first_name or html =~ member2.last_name
|
assert html =~ member2.first_name or html =~ member2.last_name
|
||||||
|
|
|
||||||
|
|
@ -22,13 +22,12 @@ defmodule MvWeb.GroupLive.ShowAccessibilityTest do
|
||||||
|> element("button", "Add Member")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# OR-chain: at least one of these ARIA/role attributes must be present
|
html = render(view)
|
||||||
assert has_element?(view, "[data-testid=group-show-member-search-input][aria-label]") or
|
|
||||||
has_element?(
|
# Search input should have proper ARIA attributes
|
||||||
view,
|
assert html =~ ~r/aria-label/ ||
|
||||||
"[data-testid=group-show-member-search-input][aria-autocomplete]"
|
html =~ ~r/aria-autocomplete/ ||
|
||||||
) or
|
html =~ ~r/role=["']combobox["']/
|
||||||
has_element?(view, "[data-testid=group-show-member-search-input][role=combobox]")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "search input has correct aria-label and aria-autocomplete attributes", %{conn: conn} do
|
test "search input has correct aria-label and aria-autocomplete attributes", %{conn: conn} do
|
||||||
|
|
@ -36,14 +35,16 @@ defmodule MvWeb.GroupLive.ShowAccessibilityTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(
|
html = render(view)
|
||||||
view,
|
|
||||||
"[data-testid=group-show-member-search-input][aria-autocomplete=list]"
|
# Search input should have ARIA attributes
|
||||||
)
|
assert html =~ ~r/aria-label.*[Ss]earch.*member/ ||
|
||||||
|
html =~ ~r/aria-autocomplete=["']list["']/
|
||||||
end
|
end
|
||||||
|
|
||||||
test "remove button has aria-label with tooltip text", %{conn: conn} do
|
test "remove button has aria-label with tooltip text", %{conn: conn} do
|
||||||
|
|
@ -66,7 +67,11 @@ defmodule MvWeb.GroupLive.ShowAccessibilityTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-remove-member][aria-label]")
|
html = render(view)
|
||||||
|
|
||||||
|
# Remove button should have aria-label
|
||||||
|
assert html =~ ~r/aria-label.*[Rr]emove/ ||
|
||||||
|
html =~ ~r/aria-label.*member/i
|
||||||
end
|
end
|
||||||
|
|
||||||
test "add button has correct aria-label", %{conn: conn} do
|
test "add button has correct aria-label", %{conn: conn} do
|
||||||
|
|
@ -74,11 +79,16 @@ defmodule MvWeb.GroupLive.ShowAccessibilityTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-add-selected-members-btn][aria-label]")
|
html = render(view)
|
||||||
|
|
||||||
|
# Add button should have aria-label
|
||||||
|
assert html =~ ~r/aria-label.*[Aa]dd/ ||
|
||||||
|
html =~ ~r/button.*[Aa]dd/
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -90,11 +100,16 @@ defmodule MvWeb.GroupLive.ShowAccessibilityTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-member-search-input]")
|
html = render(view)
|
||||||
|
|
||||||
|
# Inline add member area should have focusable elements
|
||||||
|
assert html =~ ~r/input|button/ ||
|
||||||
|
html =~ "#member-search-input"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "inline input can be closed", %{conn: conn} do
|
test "inline input can be closed", %{conn: conn} do
|
||||||
|
|
@ -102,11 +117,17 @@ defmodule MvWeb.GroupLive.ShowAccessibilityTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-member-search-input]")
|
assert has_element?(view, "#member-search-input")
|
||||||
|
|
||||||
|
# Click Add Member button again to close (or add a member to close it)
|
||||||
|
# For now, we verify the input is visible when opened
|
||||||
|
html = render(view)
|
||||||
|
assert html =~ "#member-search-input" || has_element?(view, "#member-search-input")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "enter/space activates buttons when focused", %{conn: conn} do
|
test "enter/space activates buttons when focused", %{conn: conn} do
|
||||||
|
|
@ -127,14 +148,17 @@ defmodule MvWeb.GroupLive.ShowAccessibilityTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
|
# Select member
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-member-search-input]")
|
|> element("#member-search-input")
|
||||||
|> render_focus()
|
|> render_focus()
|
||||||
|
|
||||||
|
# phx-change is on the form, so we need to trigger it via the form
|
||||||
view
|
view
|
||||||
|> element("form[phx-change='search_members']")
|
|> element("form[phx-change='search_members']")
|
||||||
|> render_change(%{"member_search" => "Bob"})
|
|> render_change(%{"member_search" => "Bob"})
|
||||||
|
|
@ -143,11 +167,14 @@ defmodule MvWeb.GroupLive.ShowAccessibilityTest do
|
||||||
|> element("[data-member-id='#{member.id}']")
|
|> element("[data-member-id='#{member.id}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
|
# Add button should be enabled and clickable
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-add-selected-members-btn]")
|
|> element("button[phx-click='add_selected_members']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Bob")
|
# Should succeed (member should appear in list)
|
||||||
|
html = render(view)
|
||||||
|
assert html =~ "Bob"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "focus management: focus is set to input when opened", %{conn: conn} do
|
test "focus management: focus is set to input when opened", %{conn: conn} do
|
||||||
|
|
@ -157,11 +184,16 @@ defmodule MvWeb.GroupLive.ShowAccessibilityTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-member-search-input]")
|
html = render(view)
|
||||||
|
|
||||||
|
# Input should be visible and focusable
|
||||||
|
assert html =~ "#member-search-input" ||
|
||||||
|
html =~ ~r/autofocus|tabindex/
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -171,11 +203,16 @@ defmodule MvWeb.GroupLive.ShowAccessibilityTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-member-search-input][aria-label]")
|
html = render(view)
|
||||||
|
|
||||||
|
# Input should have aria-label
|
||||||
|
assert html =~ ~r/aria-label.*[Ss]earch.*member/ ||
|
||||||
|
html =~ ~r/aria-label/
|
||||||
end
|
end
|
||||||
|
|
||||||
test "search results are properly announced", %{conn: conn} do
|
test "search results are properly announced", %{conn: conn} do
|
||||||
|
|
@ -194,20 +231,27 @@ defmodule MvWeb.GroupLive.ShowAccessibilityTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
|
# Search
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-member-search-input]")
|
|> element("#member-search-input")
|
||||||
|> render_focus()
|
|> render_focus()
|
||||||
|
|
||||||
|
# phx-change is on the form, so we need to trigger it via the form
|
||||||
view
|
view
|
||||||
|> element("form[phx-change='search_members']")
|
|> element("form[phx-change='search_members']")
|
||||||
|> render_change(%{"member_search" => "Charlie"})
|
|> render_change(%{"member_search" => "Charlie"})
|
||||||
|
|
||||||
assert has_element?(view, "#member-dropdown[role=listbox]")
|
html = render(view)
|
||||||
assert has_element?(view, "#member-dropdown", "Charlie")
|
|
||||||
|
# Search results should have proper ARIA attributes
|
||||||
|
assert html =~ ~r/role=["']listbox["']/ ||
|
||||||
|
html =~ ~r/role=["']option["']/ ||
|
||||||
|
html =~ "Charlie"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "flash messages are properly announced", %{conn: conn} do
|
test "flash messages are properly announced", %{conn: conn} do
|
||||||
|
|
@ -226,14 +270,16 @@ defmodule MvWeb.GroupLive.ShowAccessibilityTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Add member
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-member-search-input]")
|
|> element("#member-search-input")
|
||||||
|> render_focus()
|
|> render_focus()
|
||||||
|
|
||||||
|
# phx-change is on the form, so we need to trigger it via the form
|
||||||
view
|
view
|
||||||
|> element("form[phx-change='search_members']")
|
|> element("form[phx-change='search_members']")
|
||||||
|> render_change(%{"member_search" => "David"})
|
|> render_change(%{"member_search" => "David"})
|
||||||
|
|
@ -243,10 +289,13 @@ defmodule MvWeb.GroupLive.ShowAccessibilityTest do
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-add-selected-members-btn]")
|
|> element("button[phx-click='add_selected_members']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "David")
|
html = render(view)
|
||||||
|
|
||||||
|
# Member should appear in list (no flash message)
|
||||||
|
assert html =~ "David"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,9 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
select_member(view, member)
|
select_member(view, member)
|
||||||
add_selected(view)
|
add_selected(view)
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Alice")
|
html = render(view)
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Johnson")
|
assert html =~ "Alice"
|
||||||
|
assert html =~ "Johnson"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "member is successfully added to group (verified in list)", %{conn: conn} do
|
test "member is successfully added to group (verified in list)", %{conn: conn} do
|
||||||
|
|
@ -54,14 +55,16 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input and add member
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-member-search-input]")
|
|> element("#member-search-input")
|
||||||
|> render_focus()
|
|> render_focus()
|
||||||
|
|
||||||
|
# phx-change is on the form, so we need to trigger it via the form
|
||||||
view
|
view
|
||||||
|> element("form[phx-change='search_members']")
|
|> element("form[phx-change='search_members']")
|
||||||
|> render_change(%{"member_search" => "Bob"})
|
|> render_change(%{"member_search" => "Bob"})
|
||||||
|
|
@ -71,11 +74,14 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-add-selected-members-btn]")
|
|> element("button[phx-click='add_selected_members']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Bob")
|
html = render(view)
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Smith")
|
|
||||||
|
# Verify member appears in group list (no success flash message)
|
||||||
|
assert html =~ "Bob"
|
||||||
|
assert html =~ "Smith"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "group member list updates automatically after add", %{conn: conn} do
|
test "group member list updates automatically after add", %{conn: conn} do
|
||||||
|
|
@ -92,18 +98,21 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
actor: system_actor
|
actor: system_actor
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
refute has_element?(view, "[data-testid=group-show-members-table]", "Charlie")
|
# Initially member should NOT be in list
|
||||||
|
refute html =~ "Charlie"
|
||||||
|
|
||||||
|
# Add member
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-member-search-input]")
|
|> element("#member-search-input")
|
||||||
|> render_focus()
|
|> render_focus()
|
||||||
|
|
||||||
|
# phx-change is on the form, so we need to trigger it via the form
|
||||||
view
|
view
|
||||||
|> element("form[phx-change='search_members']")
|
|> element("form[phx-change='search_members']")
|
||||||
|> render_change(%{"member_search" => "Charlie"})
|
|> render_change(%{"member_search" => "Charlie"})
|
||||||
|
|
@ -113,11 +122,13 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-add-selected-members-btn]")
|
|> element("button[phx-click='add_selected_members']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Charlie")
|
# Member should now appear in list
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Brown")
|
html = render(view)
|
||||||
|
assert html =~ "Charlie"
|
||||||
|
assert html =~ "Brown"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "member count updates automatically after add", %{conn: conn} do
|
test "member count updates automatically after add", %{conn: conn} do
|
||||||
|
|
@ -141,11 +152,11 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|
|
||||||
# Add member
|
# Add member
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-member-search-input]")
|
|> element("#member-search-input")
|
||||||
|> render_focus()
|
|> render_focus()
|
||||||
|
|
||||||
# phx-change is on the form, so we need to trigger it via the form
|
# phx-change is on the form, so we need to trigger it via the form
|
||||||
|
|
@ -158,7 +169,7 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-add-selected-members-btn]")
|
|> element("button[phx-click='add_selected_members']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Count should have increased
|
# Count should have increased
|
||||||
|
|
@ -185,14 +196,14 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|
|
||||||
# Open inline input
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-member-search-input]")
|
assert has_element?(view, "#member-search-input")
|
||||||
|
|
||||||
# Add member
|
# Add member
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-member-search-input]")
|
|> element("#member-search-input")
|
||||||
|> render_focus()
|
|> render_focus()
|
||||||
|
|
||||||
# phx-change is on the form, so we need to trigger it via the form
|
# phx-change is on the form, so we need to trigger it via the form
|
||||||
|
|
@ -205,10 +216,11 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-add-selected-members-btn]")
|
|> element("button[phx-click='add_selected_members']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
refute has_element?(view, "[data-testid=group-show-member-search-input]")
|
# Inline input should be closed (Add Member button should be visible again)
|
||||||
|
refute has_element?(view, "#member-search-input")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Cancel button closes inline add member area without adding", %{conn: conn} do
|
test "Cancel button closes inline add member area without adding", %{conn: conn} do
|
||||||
|
|
@ -217,7 +229,7 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
open_add_member(view)
|
open_add_member(view)
|
||||||
assert has_element?(view, "[data-testid=group-show-member-search-input]")
|
assert has_element?(view, "#member-search-input")
|
||||||
assert has_element?(view, "button[phx-click='hide_add_member_input']")
|
assert has_element?(view, "button[phx-click='hide_add_member_input']")
|
||||||
|
|
||||||
cancel_add_member(view)
|
cancel_add_member(view)
|
||||||
|
|
@ -251,7 +263,7 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|
|
||||||
# Try to add same member again
|
# Try to add same member again
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Member should not appear in search (filtered out)
|
# Member should not appear in search (filtered out)
|
||||||
|
|
@ -269,12 +281,12 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-add-selected-members-btn]")
|
|> element("button", "Add")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# OR-chain for i18n and alternate error wording (already in group / duplicate)
|
# Should show error
|
||||||
html = render(view)
|
html = render(view)
|
||||||
assert html =~ gettext("already in group") or html =~ ~r/already.*group|duplicate/i
|
assert html =~ gettext("already in group") || html =~ ~r/already.*group|duplicate/i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -288,7 +300,7 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|
|
||||||
# Open inline input
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Try to add with invalid member ID (if possible)
|
# Try to add with invalid member ID (if possible)
|
||||||
|
|
@ -319,10 +331,11 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|
|
||||||
# Open inline input
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-member-search-input]")
|
# Inline input should be open
|
||||||
|
assert has_element?(view, "#member-search-input")
|
||||||
|
|
||||||
# If error occurs, inline input should remain open
|
# If error occurs, inline input should remain open
|
||||||
# (Implementation will handle this)
|
# (Implementation will handle this)
|
||||||
|
|
@ -335,10 +348,11 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|
|
||||||
# Open inline input
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-add-selected-members-btn][disabled]")
|
# Add button should be disabled
|
||||||
|
assert has_element?(view, "button[phx-click='add_selected_members'][disabled]")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -361,11 +375,11 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|
|
||||||
# Add member to empty group
|
# Add member to empty group
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-member-search-input]")
|
|> element("#member-search-input")
|
||||||
|> render_focus()
|
|> render_focus()
|
||||||
|
|
||||||
# phx-change is on the form, so we need to trigger it via the form
|
# phx-change is on the form, so we need to trigger it via the form
|
||||||
|
|
@ -378,10 +392,12 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-add-selected-members-btn]")
|
|> element("button[phx-click='add_selected_members']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Henry")
|
# Member should be added
|
||||||
|
html = render(view)
|
||||||
|
assert html =~ "Henry"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "add works when member is already in other groups", %{conn: conn} do
|
test "add works when member is already in other groups", %{conn: conn} do
|
||||||
|
|
@ -408,11 +424,11 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|
|
||||||
# Add same member to group2 (should work)
|
# Add same member to group2 (should work)
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-member-search-input]")
|
|> element("#member-search-input")
|
||||||
|> render_focus()
|
|> render_focus()
|
||||||
|
|
||||||
# phx-change is on the form, so we need to trigger it via the form
|
# phx-change is on the form, so we need to trigger it via the form
|
||||||
|
|
@ -425,10 +441,12 @@ defmodule MvWeb.GroupLive.ShowAddMemberTest do
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-add-selected-members-btn]")
|
|> element("button[phx-click='add_selected_members']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Isabel")
|
# Member should be added to group2
|
||||||
|
html = render(view)
|
||||||
|
assert html =~ "Isabel"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,18 +22,18 @@ defmodule MvWeb.GroupLive.ShowAddRemoveMembersTest do
|
||||||
test "Add Member button is visible for users with :update permission", %{conn: conn} do
|
test "Add Member button is visible for users with :update permission", %{conn: conn} do
|
||||||
group = Fixtures.group_fixture()
|
group = Fixtures.group_fixture()
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "button[phx-click='show_add_member_input']")
|
assert html =~ gettext("Add Member") or html =~ "Add Member"
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag role: :read_only
|
@tag role: :read_only
|
||||||
test "Add Member button is NOT visible for users without :update permission", %{conn: conn} do
|
test "Add Member button is NOT visible for users without :update permission", %{conn: conn} do
|
||||||
group = Fixtures.group_fixture()
|
group = Fixtures.group_fixture()
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
refute has_element?(view, "button[phx-click='show_add_member_input']")
|
refute html =~ gettext("Add Member")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Add Member button is positioned above member table", %{conn: conn} do
|
test "Add Member button is positioned above member table", %{conn: conn} do
|
||||||
|
|
@ -61,7 +61,11 @@ defmodule MvWeb.GroupLive.ShowAddRemoveMembersTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-remove-member]")
|
# Remove button should exist (can be icon button with trash icon)
|
||||||
|
html = render(view)
|
||||||
|
|
||||||
|
assert html =~ "Remove" or html =~ "remove" or html =~ "trash" or
|
||||||
|
html =~ ~r/hero-trash|hero-x-mark/
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag role: :read_only
|
@tag role: :read_only
|
||||||
|
|
@ -74,9 +78,10 @@ defmodule MvWeb.GroupLive.ShowAddRemoveMembersTest do
|
||||||
actor: system_actor
|
actor: system_actor
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
refute has_element?(view, "[data-testid=group-show-remove-member]")
|
# Remove button should NOT exist (check for trash icon or remove button specifically)
|
||||||
|
refute html =~ "hero-trash" or html =~ ~r/<button[^>]*remove_member/
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -105,7 +110,10 @@ defmodule MvWeb.GroupLive.ShowAddRemoveMembersTest do
|
||||||
|> element("button", gettext("Add Member"))
|
|> element("button", gettext("Add Member"))
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-member-search-input]")
|
html = render(view)
|
||||||
|
|
||||||
|
assert html =~ gettext("Search for a member...") ||
|
||||||
|
html =~ ~r/search.*member/i
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Add button (plus icon) is disabled until member selected", %{conn: conn} do
|
test "Add button (plus icon) is disabled until member selected", %{conn: conn} do
|
||||||
|
|
@ -113,11 +121,15 @@ defmodule MvWeb.GroupLive.ShowAddRemoveMembersTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", gettext("Add Member"))
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-add-selected-members-btn][disabled]")
|
html = render(view)
|
||||||
|
# Add button should exist and be disabled initially
|
||||||
|
assert has_element?(view, "button[phx-click='add_selected_members'][disabled]") ||
|
||||||
|
html =~ ~r/disabled/
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,8 @@ defmodule MvWeb.GroupLive.ShowAuthorizationTest do
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Should succeed (admin has :update permission, member should appear in list)
|
# Should succeed (admin has :update permission, member should appear in list)
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Alice")
|
html = render(view)
|
||||||
|
assert html =~ "Alice"
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag role: :read_only
|
@tag role: :read_only
|
||||||
|
|
@ -77,7 +78,9 @@ defmodule MvWeb.GroupLive.ShowAuthorizationTest do
|
||||||
# Note: If button is hidden, we can't click it, but we test the event handler
|
# Note: If button is hidden, we can't click it, but we test the event handler
|
||||||
# by trying to send the event directly if possible
|
# by trying to send the event directly if possible
|
||||||
|
|
||||||
refute has_element?(view, "button[phx-click='show_add_member_input']")
|
# For now, we verify that the button is not visible
|
||||||
|
html = render(view)
|
||||||
|
refute html =~ "Add Member"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "remove member event handler checks :update permission", %{conn: conn} do
|
test "remove member event handler checks :update permission", %{conn: conn} do
|
||||||
|
|
@ -100,11 +103,14 @@ defmodule MvWeb.GroupLive.ShowAuthorizationTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Remove member (should succeed for admin)
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-remove-member][phx-value-member_id='#{member.id}']")
|
|> element("button[phx-click='remove_member'][phx-value-member_id='#{member.id}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
refute has_element?(view, "[data-testid=group-show-members-table]", "Charlie")
|
# Should succeed (member should no longer be in list)
|
||||||
|
html = render(view)
|
||||||
|
refute html =~ "Charlie"
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag role: :read_only
|
@tag role: :read_only
|
||||||
|
|
@ -128,7 +134,11 @@ defmodule MvWeb.GroupLive.ShowAuthorizationTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
refute has_element?(view, "[data-testid=group-show-remove-member]")
|
# Remove button should not be visible
|
||||||
|
html = render(view)
|
||||||
|
|
||||||
|
# Read-only user should NOT see Remove button (check for trash icon or remove button specifically)
|
||||||
|
refute html =~ "hero-trash" or html =~ ~r/<button[^>]*remove_member/
|
||||||
end
|
end
|
||||||
|
|
||||||
test "error flash message on unauthorized access", %{conn: conn} do
|
test "error flash message on unauthorized access", %{conn: conn} do
|
||||||
|
|
@ -164,10 +174,10 @@ defmodule MvWeb.GroupLive.ShowAuthorizationTest do
|
||||||
actor: system_actor
|
actor: system_actor
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "button[phx-click='show_add_member_input']")
|
# Admin should see buttons
|
||||||
assert has_element?(view, "[data-testid=group-show-remove-member]")
|
assert html =~ "Add Member" || html =~ "Remove"
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag role: :read_only
|
@tag role: :read_only
|
||||||
|
|
@ -175,9 +185,10 @@ defmodule MvWeb.GroupLive.ShowAuthorizationTest do
|
||||||
_system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
_system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
group = Fixtures.group_fixture()
|
group = Fixtures.group_fixture()
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
refute has_element?(view, "button[phx-click='show_add_member_input']")
|
# Read-only user should NOT see Add Member button
|
||||||
|
refute html =~ "Add Member"
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag role: :read_only
|
@tag role: :read_only
|
||||||
|
|
@ -199,18 +210,21 @@ defmodule MvWeb.GroupLive.ShowAuthorizationTest do
|
||||||
actor: system_actor
|
actor: system_actor
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
refute has_element?(view, "[data-testid=group-show-remove-member]")
|
# Read-only user should NOT see Remove button (check for trash icon or remove button specifically)
|
||||||
|
refute html =~ "hero-trash" or html =~ ~r/<button[^>]*remove_member/
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag role: :read_only
|
@tag role: :read_only
|
||||||
test "inline add member area cannot be opened for unauthorized users", %{conn: conn} do
|
test "inline add member area cannot be opened for unauthorized users", %{conn: conn} do
|
||||||
group = Fixtures.group_fixture()
|
group = Fixtures.group_fixture()
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
refute has_element?(view, "button[phx-click='show_add_member_input']")
|
# Inline input should not be accessible (button hidden)
|
||||||
|
refute html =~ "Add Member"
|
||||||
|
refute html =~ "#member-search-input"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -305,8 +305,9 @@ defmodule MvWeb.GroupLive.ShowIntegrationTest do
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Both members should be in list
|
# Both members should be in list
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Frank")
|
html = render(view)
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Grace")
|
assert html =~ "Frank"
|
||||||
|
assert html =~ "Grace"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "multiple members can be removed sequentially", %{conn: conn} do
|
test "multiple members can be removed sequentially", %{conn: conn} do
|
||||||
|
|
@ -342,11 +343,11 @@ defmodule MvWeb.GroupLive.ShowIntegrationTest do
|
||||||
actor: system_actor
|
actor: system_actor
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
# Both should be in list initially
|
# Both should be in list initially
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Henry")
|
assert html =~ "Henry"
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Isabel")
|
assert html =~ "Isabel"
|
||||||
|
|
||||||
# Remove first member
|
# Remove first member
|
||||||
view
|
view
|
||||||
|
|
@ -359,8 +360,9 @@ defmodule MvWeb.GroupLive.ShowIntegrationTest do
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Both should be removed
|
# Both should be removed
|
||||||
refute has_element?(view, "[data-testid=group-show-members-table]", "Henry")
|
html = render(view)
|
||||||
refute has_element?(view, "[data-testid=group-show-members-table]", "Isabel")
|
refute html =~ "Henry"
|
||||||
|
refute html =~ "Isabel"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "add and remove can be mixed", %{conn: conn} do
|
test "add and remove can be mixed", %{conn: conn} do
|
||||||
|
|
@ -422,8 +424,9 @@ defmodule MvWeb.GroupLive.ShowIntegrationTest do
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Only member2 should remain
|
# Only member2 should remain
|
||||||
refute has_element?(view, "[data-testid=group-show-members-table]", "Jack")
|
html = render(view)
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Kate")
|
refute html =~ "Jack"
|
||||||
|
assert html =~ "Kate"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -34,16 +34,21 @@ defmodule MvWeb.GroupLive.ShowMemberSearchTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
|
# Type exact name
|
||||||
|
# phx-change is on the form, so we need to trigger it via the form
|
||||||
view
|
view
|
||||||
|> element("form[phx-change='search_members']")
|
|> element("form[phx-change='search_members']")
|
||||||
|> render_change(%{"member_search" => "Jonathan"})
|
|> render_change(%{"member_search" => "Jonathan"})
|
||||||
|
|
||||||
assert has_element?(view, "#member-dropdown", "Jonathan")
|
html = render(view)
|
||||||
assert has_element?(view, "#member-dropdown", "Smith")
|
|
||||||
|
assert html =~ "Jonathan"
|
||||||
|
assert html =~ "Smith"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "search finds member by partial name (fuzzy)", %{conn: conn} do
|
test "search finds member by partial name (fuzzy)", %{conn: conn} do
|
||||||
|
|
@ -63,16 +68,22 @@ defmodule MvWeb.GroupLive.ShowMemberSearchTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
|
# Type partial name
|
||||||
|
# phx-change is on the form, so we need to trigger it via the form
|
||||||
view
|
view
|
||||||
|> element("form[phx-change='search_members']")
|
|> element("form[phx-change='search_members']")
|
||||||
|> render_change(%{"member_search" => "Jon"})
|
|> render_change(%{"member_search" => "Jon"})
|
||||||
|
|
||||||
assert has_element?(view, "#member-dropdown", "Jonathan")
|
html = render(view)
|
||||||
assert has_element?(view, "#member-dropdown", "Smith")
|
|
||||||
|
# Fuzzy search should find Jonathan
|
||||||
|
assert html =~ "Jonathan"
|
||||||
|
assert html =~ "Smith"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "search finds member by email", %{conn: conn} do
|
test "search finds member by email", %{conn: conn} do
|
||||||
|
|
@ -92,17 +103,22 @@ defmodule MvWeb.GroupLive.ShowMemberSearchTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
|
# Search by email
|
||||||
|
# phx-change is on the form, so we need to trigger it via the form
|
||||||
view
|
view
|
||||||
|> element("form[phx-change='search_members']")
|
|> element("form[phx-change='search_members']")
|
||||||
|> render_change(%{"member_search" => "alice.johnson"})
|
|> render_change(%{"member_search" => "alice.johnson"})
|
||||||
|
|
||||||
assert has_element?(view, "#member-dropdown", "Alice")
|
html = render(view)
|
||||||
assert has_element?(view, "#member-dropdown", "Johnson")
|
|
||||||
assert has_element?(view, "#member-dropdown", "alice.johnson@example.com")
|
assert html =~ "Alice"
|
||||||
|
assert html =~ "Johnson"
|
||||||
|
assert html =~ "alice.johnson@example.com"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "dropdown shows member name and email", %{conn: conn} do
|
test "dropdown shows member name and email", %{conn: conn} do
|
||||||
|
|
@ -137,9 +153,11 @@ defmodule MvWeb.GroupLive.ShowMemberSearchTest do
|
||||||
|> element("form[phx-change='search_members']")
|
|> element("form[phx-change='search_members']")
|
||||||
|> render_change(%{"member_search" => "Bob"})
|
|> render_change(%{"member_search" => "Bob"})
|
||||||
|
|
||||||
assert has_element?(view, "#member-dropdown", "Bob")
|
html = render(view)
|
||||||
assert has_element?(view, "#member-dropdown", "Williams")
|
|
||||||
assert has_element?(view, "#member-dropdown", "bob@example.com")
|
assert html =~ "Bob"
|
||||||
|
assert html =~ "Williams"
|
||||||
|
assert html =~ "bob@example.com"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "ComboBox hook works (focus opens dropdown)", %{conn: conn} do
|
test "ComboBox hook works (focus opens dropdown)", %{conn: conn} do
|
||||||
|
|
@ -159,15 +177,20 @@ defmodule MvWeb.GroupLive.ShowMemberSearchTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
|
# Focus input
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-member-search-input]")
|
|> element("#member-search-input")
|
||||||
|> render_focus()
|
|> render_focus()
|
||||||
|
|
||||||
assert has_element?(view, "#member-dropdown[role=listbox]")
|
html = render(view)
|
||||||
|
|
||||||
|
# Dropdown should be visible
|
||||||
|
assert html =~ ~r/role="listbox"/ || html =~ "listbox"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -205,16 +228,21 @@ defmodule MvWeb.GroupLive.ShowMemberSearchTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
|
# Search for "David"
|
||||||
|
# phx-change is on the form, so we need to trigger it via the form
|
||||||
view
|
view
|
||||||
|> element("form[phx-change='search_members']")
|
|> element("form[phx-change='search_members']")
|
||||||
|> render_change(%{"member_search" => "David"})
|
|> render_change(%{"member_search" => "David"})
|
||||||
|
|
||||||
assert has_element?(view, "#member-dropdown", "Anderson")
|
# Assert only on dropdown (available members), not the members table
|
||||||
refute has_element?(view, "#member-dropdown", "Miller")
|
dropdown_html = view |> element("#member-dropdown") |> render()
|
||||||
|
assert dropdown_html =~ "Anderson"
|
||||||
|
refute dropdown_html =~ "Miller"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "search filters correctly when group has many members", %{conn: conn} do
|
test "search filters correctly when group has many members", %{conn: conn} do
|
||||||
|
|
@ -252,18 +280,23 @@ defmodule MvWeb.GroupLive.ShowMemberSearchTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
|
# Search
|
||||||
|
# phx-change is on the form, so we need to trigger it via the form
|
||||||
view
|
view
|
||||||
|> element("form[phx-change='search_members']")
|
|> element("form[phx-change='search_members']")
|
||||||
|> render_change(%{"member_search" => "Available"})
|
|> render_change(%{"member_search" => "Available"})
|
||||||
|
|
||||||
assert has_element?(view, "#member-dropdown", "Available")
|
# Assert only on dropdown (available members), not the members table
|
||||||
assert has_element?(view, "#member-dropdown", "Member")
|
dropdown_html = view |> element("#member-dropdown") |> render()
|
||||||
refute has_element?(view, "#member-dropdown", "Member1")
|
assert dropdown_html =~ "Available"
|
||||||
refute has_element?(view, "#member-dropdown", "Member2")
|
assert dropdown_html =~ "Member"
|
||||||
|
refute dropdown_html =~ "Member1"
|
||||||
|
refute dropdown_html =~ "Member2"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "search shows no results when all available members are already in group", %{conn: conn} do
|
test "search shows no results when all available members are already in group", %{conn: conn} do
|
||||||
|
|
@ -288,14 +321,18 @@ defmodule MvWeb.GroupLive.ShowMemberSearchTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Open inline input
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
|
# Search
|
||||||
|
# phx-change is on the form, so we need to trigger it via the form
|
||||||
view
|
view
|
||||||
|> element("form[phx-change='search_members']")
|
|> element("form[phx-change='search_members']")
|
||||||
|> render_change(%{"member_search" => "Only"})
|
|> render_change(%{"member_search" => "Only"})
|
||||||
|
|
||||||
|
# When no available members, dropdown is not rendered (length(@available_members) == 0)
|
||||||
refute has_element?(view, "#member-dropdown")
|
refute has_element?(view, "#member-dropdown")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -31,15 +31,19 @@ defmodule MvWeb.GroupLive.ShowRemoveMemberTest do
|
||||||
actor: system_actor
|
actor: system_actor
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Alice")
|
# Member should be in list initially
|
||||||
|
assert html =~ "Alice"
|
||||||
|
|
||||||
|
# Click Remove button
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-remove-member][phx-value-member_id='#{member.id}']")
|
|> element("button[phx-click='remove_member'][phx-value-member_id='#{member.id}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
refute has_element?(view, "[data-testid=group-show-members-table]", "Alice")
|
# Member should no longer be in list (no success flash message)
|
||||||
|
html = render(view)
|
||||||
|
refute html =~ "Alice"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "member is successfully removed from group (verified in list)", %{conn: conn} do
|
test "member is successfully removed from group (verified in list)", %{conn: conn} do
|
||||||
|
|
@ -60,15 +64,20 @@ defmodule MvWeb.GroupLive.ShowRemoveMemberTest do
|
||||||
actor: system_actor
|
actor: system_actor
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Bob")
|
# Member should be in list initially
|
||||||
|
assert html =~ "Bob"
|
||||||
|
|
||||||
|
# Remove member
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-remove-member][phx-value-member_id='#{member.id}']")
|
|> element("button[phx-click='remove_member'][phx-value-member_id='#{member.id}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
refute has_element?(view, "[data-testid=group-show-members-table]", "Bob")
|
html = render(view)
|
||||||
|
|
||||||
|
# Member should no longer be in list (no success flash message)
|
||||||
|
refute html =~ "Bob"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "group member list updates automatically after remove", %{conn: conn} do
|
test "group member list updates automatically after remove", %{conn: conn} do
|
||||||
|
|
@ -89,15 +98,19 @@ defmodule MvWeb.GroupLive.ShowRemoveMemberTest do
|
||||||
actor: system_actor
|
actor: system_actor
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Charlie")
|
# Member should be in list initially
|
||||||
|
assert html =~ "Charlie"
|
||||||
|
|
||||||
|
# Remove member
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-remove-member][phx-value-member_id='#{member.id}']")
|
|> element("button[phx-click='remove_member'][phx-value-member_id='#{member.id}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
refute has_element?(view, "[data-testid=group-show-members-table]", "Charlie")
|
# Member should no longer be in list
|
||||||
|
html = render(view)
|
||||||
|
refute html =~ "Charlie"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "member count updates automatically after remove", %{conn: conn} do
|
test "member count updates automatically after remove", %{conn: conn} do
|
||||||
|
|
@ -145,7 +158,7 @@ defmodule MvWeb.GroupLive.ShowRemoveMemberTest do
|
||||||
# Extract first member ID from the rendered HTML or use a different approach
|
# Extract first member ID from the rendered HTML or use a different approach
|
||||||
# Since we have member1 and member2, we can target member1 specifically
|
# Since we have member1 and member2, we can target member1 specifically
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-remove-member][phx-value-member_id='#{member1.id}']")
|
|> element("button[phx-click='remove_member'][phx-value-member_id='#{member1.id}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
# Count should have decreased
|
# Count should have decreased
|
||||||
|
|
@ -174,11 +187,17 @@ defmodule MvWeb.GroupLive.ShowRemoveMemberTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Click Remove - should remove immediately without confirmation
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-remove-member][phx-value-member_id='#{member.id}']")
|
|> element("button[phx-click='remove_member'][phx-value-member_id='#{member.id}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
refute has_element?(view, "[data-testid=group-show-members-table]", "Frank")
|
# No confirmation dialog should appear (immediate removal)
|
||||||
|
# This is verified by the member being removed without any dialog
|
||||||
|
|
||||||
|
# Member should be removed
|
||||||
|
html = render(view)
|
||||||
|
refute html =~ "Frank"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -201,17 +220,23 @@ defmodule MvWeb.GroupLive.ShowRemoveMemberTest do
|
||||||
actor: system_actor
|
actor: system_actor
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Grace")
|
# Member should be in list
|
||||||
|
assert html =~ "Grace"
|
||||||
|
|
||||||
|
# Remove last member
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-remove-member][phx-value-member_id='#{member.id}']")
|
|> element("button[phx-click='remove_member'][phx-value-member_id='#{member.id}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-no-members]")
|
# Group should show empty state
|
||||||
|
|
||||||
html = render(view)
|
html = render(view)
|
||||||
|
|
||||||
|
assert html =~ gettext("No members in this group") ||
|
||||||
|
html =~ ~r/no.*members/i
|
||||||
|
|
||||||
|
# Count should be 0
|
||||||
count = extract_member_count(html)
|
count = extract_member_count(html)
|
||||||
assert count == 0
|
assert count == 0
|
||||||
end
|
end
|
||||||
|
|
@ -244,14 +269,18 @@ defmodule MvWeb.GroupLive.ShowRemoveMemberTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group1.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group1.slug}")
|
||||||
|
|
||||||
|
# Remove from group1
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-remove-member][phx-value-member_id='#{member.id}']")
|
|> element("button[phx-click='remove_member'][phx-value-member_id='#{member.id}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
refute has_element?(view, "[data-testid=group-show-members-table]", "Henry")
|
# Member should be removed from group1
|
||||||
|
html = render(view)
|
||||||
|
refute html =~ "Henry"
|
||||||
|
|
||||||
{:ok, view2, _html2} = live(conn, "/groups/#{group2.slug}")
|
# Verify member is still in group2
|
||||||
assert has_element?(view2, "[data-testid=group-show-members-table]", "Henry")
|
{:ok, _view2, html2} = live(conn, "/groups/#{group2.slug}")
|
||||||
|
assert html2 =~ "Henry"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "remove is idempotent (no error if member already removed)", %{conn: conn} do
|
test "remove is idempotent (no error if member already removed)", %{conn: conn} do
|
||||||
|
|
@ -274,15 +303,22 @@ defmodule MvWeb.GroupLive.ShowRemoveMemberTest do
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
|
# Remove member first time
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-remove-member][phx-value-member_id='#{member.id}']")
|
|> element("button[phx-click='remove_member'][phx-value-member_id='#{member.id}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
if has_element?(view, "[data-testid=group-show-members-table]", "Isabel") do
|
# Try to remove again (should not error, just be idempotent)
|
||||||
|
# Note: Implementation should handle this gracefully
|
||||||
|
# If button is still visible somehow, try to click again
|
||||||
|
html = render(view)
|
||||||
|
|
||||||
|
if html =~ "Isabel" do
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-remove-member][phx-value-member_id='#{member.id}']")
|
|> element("button[phx-click='remove_member'][phx-value-member_id='#{member.id}']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
|
# Should not crash
|
||||||
assert render(view)
|
assert render(view)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -22,33 +22,34 @@ defmodule MvWeb.GroupLive.ShowTest do
|
||||||
test "page renders successfully", %{conn: conn} do
|
test "page renders successfully", %{conn: conn} do
|
||||||
group = Fixtures.group_fixture()
|
group = Fixtures.group_fixture()
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "h1", group.name)
|
assert html =~ group.name
|
||||||
end
|
end
|
||||||
|
|
||||||
test "displays group name", %{conn: conn} do
|
test "displays group name", %{conn: conn} do
|
||||||
group = Fixtures.group_fixture(%{name: "Test Group Name"})
|
group = Fixtures.group_fixture(%{name: "Test Group Name"})
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "h1", "Test Group Name")
|
assert html =~ "Test Group Name"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "displays group description when present", %{conn: conn} do
|
test "displays group description when present", %{conn: conn} do
|
||||||
group = Fixtures.group_fixture(%{description: "This is a test description"})
|
group = Fixtures.group_fixture(%{description: "This is a test description"})
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "p", "This is a test description")
|
assert html =~ "This is a test description"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "displays member count", %{conn: conn} do
|
test "displays member count", %{conn: conn} do
|
||||||
group = Fixtures.group_fixture()
|
group = Fixtures.group_fixture()
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-member-count]")
|
# Member count should be displayed (might be 0 or more)
|
||||||
|
assert html =~ "0" or html =~ gettext("Members") or html =~ "member" or html =~ "Mitglied"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "displays list of members in group", %{conn: conn} do
|
test "displays list of members in group", %{conn: conn} do
|
||||||
|
|
@ -66,26 +67,26 @@ defmodule MvWeb.GroupLive.ShowTest do
|
||||||
actor: system_actor
|
actor: system_actor
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Alice")
|
assert html =~ "Alice" or html =~ "Smith"
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", "Bob")
|
assert html =~ "Bob" or html =~ "Jones"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "displays edit button for admin users", %{conn: conn} do
|
test "displays edit button for admin users", %{conn: conn} do
|
||||||
group = Fixtures.group_fixture()
|
group = Fixtures.group_fixture()
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-edit-btn]")
|
assert html =~ gettext("Edit") or html =~ "edit" or html =~ "Bearbeiten"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "displays delete button for admin users", %{conn: conn} do
|
test "displays delete button for admin users", %{conn: conn} do
|
||||||
group = Fixtures.group_fixture()
|
group = Fixtures.group_fixture()
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-delete-btn]")
|
assert html =~ gettext("Delete") or html =~ "delete" or html =~ "Löschen"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -93,17 +94,19 @@ defmodule MvWeb.GroupLive.ShowTest do
|
||||||
test "route /groups/:slug works correctly", %{conn: conn} do
|
test "route /groups/:slug works correctly", %{conn: conn} do
|
||||||
group = Fixtures.group_fixture(%{name: "Board Members"})
|
group = Fixtures.group_fixture(%{name: "Board Members"})
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "h1", "Board Members")
|
assert html =~ "Board Members"
|
||||||
|
# Verify slug is in URL
|
||||||
|
assert html =~ group.slug or html =~ "board-members"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "group is found by slug via unique_slug identity", %{conn: conn} do
|
test "group is found by slug via unique_slug identity", %{conn: conn} do
|
||||||
group = Fixtures.group_fixture(%{name: "Test Group"})
|
group = Fixtures.group_fixture(%{name: "Test Group"})
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "h1", group.name)
|
assert html =~ group.name
|
||||||
end
|
end
|
||||||
|
|
||||||
test "non-existent slug returns 404", %{conn: conn} do
|
test "non-existent slug returns 404", %{conn: conn} do
|
||||||
|
|
@ -142,26 +145,28 @@ defmodule MvWeb.GroupLive.ShowTest do
|
||||||
test "displays empty group correctly (0 members)", %{conn: conn} do
|
test "displays empty group correctly (0 members)", %{conn: conn} do
|
||||||
group = Fixtures.group_fixture()
|
group = Fixtures.group_fixture()
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "[data-testid=group-show-no-members]")
|
assert html =~ "0" or html =~ gettext("No members") or html =~ "empty" or
|
||||||
|
html =~ "Keine Mitglieder"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles group without description correctly", %{conn: conn} do
|
test "handles group without description correctly", %{conn: conn} do
|
||||||
group = Fixtures.group_fixture(%{description: nil})
|
group = Fixtures.group_fixture(%{description: nil})
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "h1", group.name)
|
# Should not crash, description should be optional
|
||||||
|
assert html =~ group.name
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles slug with special characters correctly", %{conn: conn} do
|
test "handles slug with special characters correctly", %{conn: conn} do
|
||||||
# Create group with name that generates slug with hyphens
|
# Create group with name that generates slug with hyphens
|
||||||
group = Fixtures.group_fixture(%{name: "Test-Group-Name"})
|
group = Fixtures.group_fixture(%{name: "Test-Group-Name"})
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "h1", group.name)
|
assert html =~ "Test-Group-Name" or html =~ group.name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -172,11 +177,11 @@ defmodule MvWeb.GroupLive.ShowTest do
|
||||||
read_only_user = Fixtures.user_with_role_fixture("read_only")
|
read_only_user = Fixtures.user_with_role_fixture("read_only")
|
||||||
conn = conn_with_password_user(conn, read_only_user)
|
conn = conn_with_password_user(conn, read_only_user)
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "h1", group.name)
|
assert html =~ group.name
|
||||||
refute has_element?(view, "[data-testid=group-show-edit-btn]")
|
# Should NOT see edit/delete buttons
|
||||||
refute has_element?(view, "[data-testid=group-show-delete-btn]")
|
refute html =~ gettext("Edit") or html =~ gettext("Delete")
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag role: :unauthenticated
|
@tag role: :unauthenticated
|
||||||
|
|
@ -241,14 +246,14 @@ defmodule MvWeb.GroupLive.ShowTest do
|
||||||
handler_id = "test-query-counter-#{System.unique_integer([:positive])}"
|
handler_id = "test-query-counter-#{System.unique_integer([:positive])}"
|
||||||
:telemetry.attach(handler_id, [:ash, :query, :start], handler, nil)
|
:telemetry.attach(handler_id, [:ash, :query, :start], handler, nil)
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
final_count = Agent.get(query_count_agent, & &1)
|
final_count = Agent.get(query_count_agent, & &1)
|
||||||
:telemetry.detach(handler_id)
|
:telemetry.detach(handler_id)
|
||||||
|
|
||||||
|
# All members should be displayed
|
||||||
Enum.each(members, fn member ->
|
Enum.each(members, fn member ->
|
||||||
assert has_element?(view, "[data-testid=group-show-members-table]", member.first_name) or
|
assert html =~ member.first_name or html =~ member.last_name
|
||||||
has_element?(view, "[data-testid=group-show-members-table]", member.last_name)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
# Verify query count is reasonable (should avoid N+1 queries)
|
# Verify query count is reasonable (should avoid N+1 queries)
|
||||||
|
|
@ -262,9 +267,10 @@ defmodule MvWeb.GroupLive.ShowTest do
|
||||||
test "slug lookup is efficient (uses unique_slug index)", %{conn: conn} do
|
test "slug lookup is efficient (uses unique_slug index)", %{conn: conn} do
|
||||||
group = Fixtures.group_fixture()
|
group = Fixtures.group_fixture()
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/groups/#{group.slug}")
|
# Should use index for fast lookup
|
||||||
|
{:ok, _view, html} = live(conn, "/groups/#{group.slug}")
|
||||||
|
|
||||||
assert has_element?(view, "h1", group.name)
|
assert html =~ group.name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ defmodule MvWeb.GroupLiveHelpers do
|
||||||
"""
|
"""
|
||||||
def open_add_member(view) do
|
def open_add_member(view) do
|
||||||
view
|
view
|
||||||
|> element("button[phx-click='show_add_member_input']")
|
|> element("button", "Add Member")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ defmodule MvWeb.GroupLiveHelpers do
|
||||||
"""
|
"""
|
||||||
def search_member(view, query) do
|
def search_member(view, query) do
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-member-search-input]")
|
|> element("#member-search-input")
|
||||||
|> render_focus()
|
|> render_focus()
|
||||||
|
|
||||||
view
|
view
|
||||||
|
|
@ -44,7 +44,7 @@ defmodule MvWeb.GroupLiveHelpers do
|
||||||
"""
|
"""
|
||||||
def add_selected(view) do
|
def add_selected(view) do
|
||||||
view
|
view
|
||||||
|> element("[data-testid=group-show-add-selected-members-btn]")
|
|> element("button[phx-click='add_selected_members']")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue