mitgliederverwaltung/test/mv_web/live/global_settings_live_test.exs
Simon a12888de2f
All checks were successful
continuous-integration/drone/push Build is passing
Improve member view table behavior+style, fix config settings (#493)
## Description of the implemented changes
The changes were:
- [x] Bugfixing
- [x] New Feature
- [ ] Breaking Change
- [x] Refactoring

This PR standardizes interactive table behavior and improves settings robustness.
It makes the new hover/focus-visible row highlight the default for clickable tables, keeps sticky first-column behavior configurable (and optimized for member selection UX), and tightens SMTP source-of-truth handling so ENV-based and UI-based configuration do not conflict.

## What has been changed?
- Refactored `CoreComponents.table` to expose interaction state via `data-row-interactive` and moved default row hover/focus styling to CSS.
- Made the new row highlight behavior (`hover` + `:has(:focus-visible)`) the default for clickable zebra tables.
- Kept sticky-first-column as an explicit table option and preserved sticky-specific selection accent behavior.
- Updated member overview table usage to the sticky-first-column mode and refined scrolling behavior (table scrollbar within container, not page-coupled).
- Adjusted table-related tests to validate the new interaction contract (attribute/CSS-driven behavior instead of legacy ring classes).
- Improved SMTP config handling:
  - clearer ENV-vs-Settings behavior (ENV-only mode when host env is set),
  - read-only and warning behavior in global settings UI when required env keys are missing,
  - updated related config/tests/docs.
- Updated docs and changelog (`CHANGELOG.md`, `DESIGN_GUIDELINES.md`, `CODE_GUIDELINES.md`, SMTP concept docs).
- Updated gettext catalogs (`default.pot`, `en`, `de`) for new/changed UI strings.

## Definition of Done
### Code Quality
- [x] No new technical depths
- [x] Linting passed
- [x] Documentation is added were needed

### Accessibility
- [x] New elements are properly defined with html-tags
- [x] Colour contrast follows WCAG criteria
- [x] Aria labels are added when needed
- [x] Everything is accessible by keyboard
- [x] Tab-Order is comprehensible
- [x] All interactive elements have a visible focus

### Testing
- [x] Tests for new code are written
- [x] All tests pass
- [ ] axe-core dev tools show no critical or major issues

## Additional Notes
- Branch includes 4 commits:
  - `fix: make sure smtp can be set either via env or ui`
  - `fix: make horizontal scrollbars sticky to bottom`
  - `docs: update changelog`
  - `feat: make checkbox column in member view sticky`
- Full fast suite passed (`mix test --exclude slow --exclude ui`): 2017 tests, 0 failures (plus expected non-failing warning logs in test output).
- Reviewer focus areas:
  1. **Cross-table UX consistency** after moving row interaction styling to component/CSS contract.
  2. **Sticky table behavior** (selection accent stripe, zebra background, keyboard focus visibility).
  3. **SMTP precedence and UI constraints** in global settings when ENV mode is active.
  4. **Regression risk in tests** that previously asserted ring-based row classes.
- No breaking API changes expected; behavior change is primarily visual/interaction-level and intentional.

Reviewed-on: #493
Co-authored-by: Simon <s.thiessen@local-it.org>
Co-committed-by: Simon <s.thiessen@local-it.org>
2026-05-08 15:04:53 +02:00

286 lines
9.8 KiB
Elixir

defmodule MvWeb.GlobalSettingsLiveTest do
use MvWeb.ConnCase, async: false
import Phoenix.LiveViewTest
alias Mv.Membership
defp clear_smtp_env do
[
"SMTP_HOST",
"SMTP_PORT",
"SMTP_SSL",
"SMTP_USERNAME",
"SMTP_PASSWORD",
"SMTP_PASSWORD_FILE",
"MAIL_FROM_EMAIL",
"MAIL_FROM_NAME"
]
|> Enum.each(&System.delete_env/1)
end
describe "Global Settings LiveView" do
setup %{conn: conn} do
user = create_test_user(%{email: "admin@example.com"})
conn = conn_with_oidc_user(conn, user)
{:ok, conn: conn, user: user}
end
test "renders the global settings page", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/settings")
assert html =~ "Club Settings"
assert html =~ "Settings"
end
test "displays current club name", %{conn: conn} do
# Set initial club name
{:ok, settings} = Membership.get_settings()
{:ok, _updated} = Membership.update_settings(settings, %{club_name: "Test Club"})
{:ok, _view, html} = live(conn, ~p"/settings")
assert html =~ "Test Club"
end
test "can update club name via form", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/settings")
# Submit form with new club name
assert view
|> form("#settings-form", %{setting: %{club_name: "Updated Club Name"}})
|> render_submit()
# Check for success message
assert render(view) =~ "Settings updated successfully"
assert render(view) =~ "Updated Club Name"
end
test "shows error when club_name is empty", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/settings")
# Submit form with empty club name
html =
view
|> form("#settings-form", %{setting: %{club_name: ""}})
|> render_submit()
assert html =~ "must be present"
end
test "shows error when club_name is missing", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/settings")
# Submit form with club_name explicitly set to empty string
# (Phoenix forms will keep existing value if field is omitted)
html =
view
|> form("#settings-form", %{setting: %{club_name: ""}})
|> render_submit()
assert html =~ "must be present"
end
test "shows open button for join page URL in same row as copy", %{conn: conn} do
{:ok, settings} = Membership.get_settings()
{:ok, _} = Membership.update_settings(settings, %{join_form_enabled: true})
{:ok, view, _html} = live(conn, ~p"/settings")
assert has_element?(view, "#copy-join-url-btn")
assert has_element?(
view,
"a[href][target=\"_blank\"][rel=\"noopener noreferrer\"]",
"Open"
)
end
end
describe "SMTP / E-Mail section" do
setup %{conn: conn} do
user = create_test_user(%{email: "admin@example.com"})
conn = conn_with_oidc_user(conn, user)
{:ok, conn: conn, user: user}
end
test "renders SMTP section with host/port fields and test email area", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/settings")
# Section title (Gettext key: SMTP or E-Mail per concept)
assert html =~ "SMTP" or html =~ "E-Mail"
end
test "shows Send test email button when SMTP is configured", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/settings")
# When Mv.Config.smtp_configured?() is true, button and recipient input should be present
# In test env SMTP is typically not configured; we only assert the section exists
html = render(view)
assert html =~ "SMTP" or html =~ "E-Mail"
end
test "send test email with valid address shows success or error result", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/settings")
if has_element?(view, "[data-testid='smtp-test-email-form']") do
# Submit the test-email form (phx-submit) with a valid recipient address
view
|> form("[data-testid='smtp-test-email-form']", %{"to_email" => "test@example.com"})
|> render_submit()
# Result area must appear regardless of success or error
assert has_element?(view, "[data-testid='smtp-test-result']")
else
assert render(view) =~ "Settings"
end
end
test "shows warning when SMTP is not configured in production", %{conn: conn} do
# Concept: in prod, show warning "SMTP is not configured. Transactional emails..."
# In test we only check that the section exists; warning visibility is env-dependent
{:ok, _view, html} = live(conn, ~p"/settings")
assert html =~ "SMTP" or html =~ "E-Mail" or html =~ "Settings"
end
@tag :ui
test "disables all SMTP inputs when SMTP_HOST is set", %{conn: conn} do
clear_smtp_env()
System.put_env("SMTP_HOST", "smtp.env-only.example")
on_exit(fn -> clear_smtp_env() end)
{:ok, view, _html} = live(conn, ~p"/settings")
assert has_element?(view, "#setting_smtp_host[disabled]")
assert has_element?(view, "#setting_smtp_port[disabled]")
assert has_element?(view, "#setting_smtp_ssl[disabled]")
assert has_element?(view, "#setting_smtp_username[disabled]")
assert has_element?(view, "#setting_smtp_password[disabled]")
assert has_element?(view, "#setting_smtp_from_email[disabled]")
assert has_element?(view, "#setting_smtp_from_name[disabled]")
end
@tag :ui
test "does not render SMTP save action when SMTP_HOST is set", %{conn: conn} do
clear_smtp_env()
System.put_env("SMTP_HOST", "smtp.env-only.example")
on_exit(fn -> clear_smtp_env() end)
{:ok, view, _html} = live(conn, ~p"/settings")
refute has_element?(view, "#smtp-form button", "Save SMTP Settings")
end
@tag :ui
test "shows explicit ENV-only mode hint when SMTP_HOST is set", %{conn: conn} do
clear_smtp_env()
System.put_env("SMTP_HOST", "smtp.env-only.example")
on_exit(fn -> clear_smtp_env() end)
{:ok, _view, html} = live(conn, ~p"/settings")
assert html =~ "SMTP is fully managed via environment variables"
end
@tag :ui
test "shows warning block for missing required SMTP ENV values in ENV-only mode", %{
conn: conn
} do
clear_smtp_env()
System.put_env("SMTP_HOST", "smtp.env-only.example")
on_exit(fn -> clear_smtp_env() end)
{:ok, _view, html} = live(conn, ~p"/settings")
assert html =~ "SMTP environment configuration appears incomplete"
assert html =~ "SMTP_USERNAME"
assert html =~ "SMTP_PASSWORD/SMTP_PASSWORD_FILE"
end
@tag :ui
test "does not enter ENV-only mode when SMTP_HOST is not set", %{conn: conn} do
clear_smtp_env()
System.put_env("SMTP_USERNAME", "leftover@example.com")
on_exit(fn -> clear_smtp_env() end)
{:ok, view, html} = live(conn, ~p"/settings")
refute html =~ "SMTP is fully managed via environment variables"
refute html =~ "SMTP environment configuration appears incomplete"
refute has_element?(view, "#setting_smtp_host[disabled]")
refute has_element?(view, "#setting_smtp_username[disabled]")
end
@tag :ui
test "shows effective ENV SMTP host value in disabled field", %{conn: conn} do
clear_smtp_env()
System.put_env("SMTP_HOST", "smtp.env-active.example")
on_exit(fn -> clear_smtp_env() end)
{:ok, settings} = Membership.get_settings()
{:ok, _} = Membership.update_settings(settings, %{smtp_host: "smtp.db-legacy.example"})
{:ok, _view, html} = live(conn, ~p"/settings")
assert html =~ ~s(value="smtp.env-active.example")
refute html =~ ~s(value="smtp.db-legacy.example")
end
end
describe "Authentication section when OIDC-only is enabled" do
setup %{conn: conn} do
user = create_test_user(%{email: "admin@example.com"})
conn = conn_with_oidc_user(conn, user)
{:ok, settings} = Membership.get_settings()
original_oidc_only = Map.get(settings, :oidc_only, false)
{:ok, _} = Membership.update_settings(settings, %{oidc_only: true})
{:ok, conn: conn, original_oidc_only: original_oidc_only}
end
@describetag :ui
test "registration checkbox is disabled when OIDC-only is enabled", %{
conn: conn,
original_oidc_only: original
} do
try do
{:ok, view, _html} = live(conn, ~p"/settings")
assert has_element?(view, "#registration-enabled-checkbox[disabled]")
after
{:ok, s} = Membership.get_settings()
Membership.update_settings(s, %{oidc_only: original})
end
end
@describetag :ui
test "OIDC-only hint is visible when OIDC-only is enabled", %{
conn: conn,
original_oidc_only: original
} do
try do
{:ok, view, _html} = live(conn, ~p"/settings")
assert has_element?(view, "[data-testid='oidc-only-registration-hint']")
after
{:ok, s} = Membership.get_settings()
Membership.update_settings(s, %{oidc_only: original})
end
end
test "when OIDC-only is disabled, registration checkbox is enabled and can be toggled", %{
conn: conn,
original_oidc_only: original
} do
try do
{:ok, settings} = Membership.get_settings()
Membership.update_settings(settings, %{oidc_only: false})
{:ok, view, _html} = live(conn, ~p"/settings")
refute has_element?(view, "#registration-enabled-checkbox[disabled]")
initial_checked =
view |> element("#registration-enabled-checkbox") |> render() =~ "checked"
view
|> element("#registration-enabled-checkbox")
|> render_click()
new_checked = view |> element("#registration-enabled-checkbox") |> render() =~ "checked"
assert new_checked != initial_checked
after
{:ok, s} = Membership.get_settings()
Membership.update_settings(s, %{oidc_only: original})
end
end
end
end