Merge remote-tracking branch 'origin/main' into feature/member-overview-groups
This commit is contained in:
commit
6831ba046f
48 changed files with 3516 additions and 182 deletions
|
|
@ -25,12 +25,13 @@ defmodule MvWeb.SidebarAuthorizationTest do
|
|||
end
|
||||
|
||||
describe "sidebar menu with admin user" do
|
||||
test "shows Members, Fee Types and Administration with all subitems" do
|
||||
test "shows Members, Fee Types, Statistics and Administration with all subitems" do
|
||||
user = Fixtures.user_with_role_fixture("admin")
|
||||
html = render_sidebar(sidebar_assigns(user))
|
||||
|
||||
assert html =~ ~s(href="/members")
|
||||
assert html =~ ~s(href="/membership_fee_types")
|
||||
assert html =~ ~s(href="/statistics")
|
||||
assert html =~ ~s(data-testid="sidebar-administration")
|
||||
assert html =~ ~s(href="/users")
|
||||
assert html =~ ~s(href="/groups")
|
||||
|
|
@ -41,11 +42,12 @@ defmodule MvWeb.SidebarAuthorizationTest do
|
|||
end
|
||||
|
||||
describe "sidebar menu with read_only user (Vorstand/Buchhaltung)" do
|
||||
test "shows Members and Groups (from Administration)" do
|
||||
test "shows Members, Statistics and Groups (from Administration)" do
|
||||
user = Fixtures.user_with_role_fixture("read_only")
|
||||
html = render_sidebar(sidebar_assigns(user))
|
||||
|
||||
assert html =~ ~s(href="/members")
|
||||
assert html =~ ~s(href="/statistics")
|
||||
assert html =~ ~s(href="/groups")
|
||||
end
|
||||
|
||||
|
|
@ -61,11 +63,12 @@ defmodule MvWeb.SidebarAuthorizationTest do
|
|||
end
|
||||
|
||||
describe "sidebar menu with normal_user (Kassenwart)" do
|
||||
test "shows Members and Groups" do
|
||||
test "shows Members, Statistics and Groups" do
|
||||
user = Fixtures.user_with_role_fixture("normal_user")
|
||||
html = render_sidebar(sidebar_assigns(user))
|
||||
|
||||
assert html =~ ~s(href="/members")
|
||||
assert html =~ ~s(href="/statistics")
|
||||
assert html =~ ~s(href="/groups")
|
||||
end
|
||||
|
||||
|
|
@ -88,10 +91,11 @@ defmodule MvWeb.SidebarAuthorizationTest do
|
|||
refute html =~ ~s(href="/members")
|
||||
end
|
||||
|
||||
test "does not show Fee Types or Administration" do
|
||||
test "does not show Statistics, Fee Types or Administration" do
|
||||
user = Fixtures.user_with_role_fixture("own_data")
|
||||
html = render_sidebar(sidebar_assigns(user))
|
||||
|
||||
refute html =~ ~s(href="/statistics")
|
||||
refute html =~ ~s(href="/membership_fee_types")
|
||||
refute html =~ ~s(href="/users")
|
||||
refute html =~ ~s(data-testid="sidebar-administration")
|
||||
|
|
|
|||
78
test/mv_web/live/statistics_live_test.exs
Normal file
78
test/mv_web/live/statistics_live_test.exs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
defmodule MvWeb.StatisticsLiveTest do
|
||||
@moduledoc """
|
||||
Tests for the Statistics LiveView at /statistics.
|
||||
|
||||
Uses explicit auth: conn is authenticated with a role that has access to
|
||||
the statistics page (read_only by default; override with @tag :role).
|
||||
"""
|
||||
use MvWeb.ConnCase, async: true
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
alias Mv.MembershipFees.MembershipFeeType
|
||||
|
||||
describe "statistics page" do
|
||||
@describetag role: :read_only
|
||||
test "renders statistics page with title and key labels for authenticated user with access",
|
||||
%{
|
||||
conn: conn
|
||||
} do
|
||||
{:ok, _view, html} = live(conn, ~p"/statistics")
|
||||
|
||||
assert html =~ "Statistics"
|
||||
assert html =~ "Active members"
|
||||
assert html =~ "Unpaid"
|
||||
assert html =~ "Contributions by year"
|
||||
assert html =~ "Member numbers by year"
|
||||
end
|
||||
|
||||
test "page shows overview of all relevant years without year selector", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/statistics")
|
||||
|
||||
# No year dropdown: single select for year should not be present as main control
|
||||
assert html =~ "Overview" or html =~ "overview"
|
||||
# table header or legend
|
||||
assert html =~ "Year"
|
||||
end
|
||||
|
||||
test "fee_type_id in URL updates selected filter and contributions", %{conn: conn} do
|
||||
actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
fee_types =
|
||||
MembershipFeeType
|
||||
|> Ash.Query.sort(name: :asc)
|
||||
|> Ash.read!(domain: Mv.MembershipFees, actor: actor)
|
||||
|
||||
fee_type =
|
||||
case List.first(fee_types) do
|
||||
nil ->
|
||||
MembershipFeeType
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: "Test Fee #{System.unique_integer([:positive])}",
|
||||
amount: Decimal.new("50.00"),
|
||||
interval: :yearly
|
||||
})
|
||||
|> Ash.create!(actor: actor)
|
||||
|
||||
ft ->
|
||||
ft
|
||||
end
|
||||
|
||||
path = ~p"/statistics" <> "?" <> URI.encode_query(%{"fee_type_id" => fee_type.id})
|
||||
{:ok, view, html} = live(conn, path)
|
||||
|
||||
assert view |> element("select#fee-type-filter") |> has_element?()
|
||||
assert html =~ fee_type.name
|
||||
assert html =~ "Contributions by year"
|
||||
end
|
||||
end
|
||||
|
||||
describe "statistics page with own_data role" do
|
||||
@describetag role: :member
|
||||
test "redirects when user has only own_data (no access to statistics page)", %{conn: conn} do
|
||||
# member role uses own_data permission set; /statistics is not in own_data pages
|
||||
conn = get(conn, ~p"/statistics")
|
||||
assert redirected_to(conn) != ~p"/statistics"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -522,7 +522,7 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "export to CSV" do
|
||||
describe "export dropdown" do
|
||||
setup do
|
||||
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||
|
||||
|
|
@ -535,34 +535,139 @@ defmodule MvWeb.MemberLive.IndexTest do
|
|||
%{member1: m1}
|
||||
end
|
||||
|
||||
test "export button is rendered when no selection and shows (all)", %{conn: conn} do
|
||||
test "export dropdown button is rendered when no selection and shows (all)", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
|
||||
# Dropdown button should be present
|
||||
assert html =~ ~s(data-testid="export-dropdown")
|
||||
assert html =~ ~s(data-testid="export-dropdown-button")
|
||||
assert html =~ "Export"
|
||||
# Button text shows "all" when 0 selected (locale-dependent)
|
||||
assert html =~ "Export to CSV"
|
||||
assert html =~ "all" or html =~ "All"
|
||||
end
|
||||
|
||||
test "after select_member event export button shows (1)", %{conn: conn, member1: member1} do
|
||||
test "after select_member event export dropdown shows (1)", %{conn: conn, member1: member1} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
render_click(view, "select_member", %{"id" => member1.id})
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "Export to CSV"
|
||||
assert html =~ "Export"
|
||||
assert html =~ "(1)"
|
||||
end
|
||||
|
||||
test "form has correct action and payload hidden input", %{conn: conn} do
|
||||
test "dropdown opens and closes on click", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, _view, html} = live(conn, "/members")
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Initially closed
|
||||
refute has_element?(view, ~s([data-testid="export-dropdown-menu"]))
|
||||
|
||||
# Click to open
|
||||
view
|
||||
|> element(~s([data-testid="export-dropdown-button"]))
|
||||
|> render_click()
|
||||
|
||||
# Menu should be visible
|
||||
assert has_element?(view, ~s([data-testid="export-dropdown-menu"]))
|
||||
|
||||
# Click to close
|
||||
view
|
||||
|> element(~s([data-testid="export-dropdown-button"]))
|
||||
|> render_click()
|
||||
|
||||
# Menu should be hidden
|
||||
refute has_element?(view, ~s([data-testid="export-dropdown-menu"]))
|
||||
end
|
||||
|
||||
test "dropdown has click-away and ESC handlers", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element(~s([data-testid="export-dropdown-button"]))
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
assert has_element?(view, ~s([data-testid="export-dropdown-menu"]))
|
||||
|
||||
# Check that click-away handler is present
|
||||
assert html =~ ~s(phx-click-away="close_dropdown")
|
||||
# Check that ESC handler is present
|
||||
assert html =~ ~s(phx-window-keydown="close_dropdown")
|
||||
assert html =~ ~s(phx-key="Escape")
|
||||
end
|
||||
|
||||
test "dropdown menu contains CSV and PDF export links with correct payload", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element(~s([data-testid="export-dropdown-button"]))
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Check CSV link
|
||||
assert html =~ ~s(data-testid="export-csv-link")
|
||||
assert html =~ "/members/export.csv"
|
||||
assert html =~ ~s(name="payload")
|
||||
assert html =~ ~s(type="hidden")
|
||||
assert html =~ ~s(name="_csrf_token")
|
||||
|
||||
# Check PDF link
|
||||
assert html =~ ~s(data-testid="export-pdf-link")
|
||||
assert html =~ "/members/export.pdf"
|
||||
assert html =~ ~s(name="payload")
|
||||
assert html =~ ~s(type="hidden")
|
||||
assert html =~ ~s(name="_csrf_token")
|
||||
|
||||
# Both forms should have the same payload
|
||||
csv_form_payload = extract_payload_from_form(html, "/members/export.csv")
|
||||
pdf_form_payload = extract_payload_from_form(html, "/members/export.pdf")
|
||||
|
||||
assert csv_form_payload == pdf_form_payload
|
||||
assert csv_form_payload != nil
|
||||
end
|
||||
|
||||
test "dropdown has correct ARIA attributes", %{conn: conn} do
|
||||
conn = conn_with_oidc_user(conn)
|
||||
{:ok, view, _html} = live(conn, "/members")
|
||||
|
||||
html = render(view)
|
||||
|
||||
# Button should have aria-haspopup="menu"
|
||||
assert html =~ ~s(aria-haspopup="menu")
|
||||
# Button should have aria-expanded="false" when closed
|
||||
assert html =~ ~s(aria-expanded="false")
|
||||
# Button should have aria-controls pointing to menu
|
||||
assert html =~ ~s(aria-controls="export-dropdown-menu")
|
||||
|
||||
# Open dropdown
|
||||
view
|
||||
|> element(~s([data-testid="export-dropdown-button"]))
|
||||
|> render_click()
|
||||
|
||||
html = render(view)
|
||||
# Button should have aria-expanded="true" when open
|
||||
assert html =~ ~s(aria-expanded="true")
|
||||
# Menu should have role="menu"
|
||||
assert html =~ ~s(role="menu")
|
||||
end
|
||||
|
||||
# Helper to extract payload value from form HTML
|
||||
defp extract_payload_from_form(html, action_path) do
|
||||
case Regex.run(
|
||||
~r/<form[^>]*action="#{Regex.escape(action_path)}"[^>]*>.*?<input[^>]*name="payload"[^>]*value="([^"]+)"/s,
|
||||
html
|
||||
) do
|
||||
[_, payload] -> payload
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -107,6 +107,37 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "statistics route /statistics" do
|
||||
test "read_only can access /statistics" do
|
||||
user = Fixtures.user_with_role_fixture("read_only")
|
||||
conn = conn_with_user("/statistics", user) |> CheckPagePermission.call([])
|
||||
|
||||
refute conn.halted
|
||||
end
|
||||
|
||||
test "normal_user can access /statistics" do
|
||||
user = Fixtures.user_with_role_fixture("normal_user")
|
||||
conn = conn_with_user("/statistics", user) |> CheckPagePermission.call([])
|
||||
|
||||
refute conn.halted
|
||||
end
|
||||
|
||||
test "admin can access /statistics" do
|
||||
user = Fixtures.user_with_role_fixture("admin")
|
||||
conn = conn_with_user("/statistics", user) |> CheckPagePermission.call([])
|
||||
|
||||
refute conn.halted
|
||||
end
|
||||
|
||||
test "own_data cannot access /statistics" do
|
||||
user = Fixtures.user_with_role_fixture("own_data")
|
||||
conn = conn_with_user("/statistics", user) |> CheckPagePermission.call([])
|
||||
|
||||
assert conn.halted
|
||||
assert redirected_to(conn) == "/users/#{user.id}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "read_only and normal_user denied on admin routes" do
|
||||
test "read_only cannot access /admin/roles" do
|
||||
user = Fixtures.user_with_role_fixture("read_only")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue