From 41e3a524821c5c1105d3f8d8188d92e7e3678dfd Mon Sep 17 00:00:00 2001 From: carla Date: Fri, 24 Oct 2025 10:55:11 +0200 Subject: [PATCH] test: updated tests --- .../live/components/search_bar_component.ex | 1 + .../live/components/sort_header_component.ex | 1 + priv/repo/seeds.exs | 12 + .../components/search_bar_component_test.exs | 4 +- .../components/sort_header_component_test.exs | 299 +++++++++++++++++- test/mv_web/member_live/index_test.exs | 111 ++++++- 6 files changed, 412 insertions(+), 16 deletions(-) diff --git a/lib/mv_web/live/components/search_bar_component.ex b/lib/mv_web/live/components/search_bar_component.ex index c45a1e5..ac03a63 100644 --- a/lib/mv_web/live/components/search_bar_component.ex +++ b/lib/mv_web/live/components/search_bar_component.ex @@ -44,6 +44,7 @@ defmodule MvWeb.Components.SearchBarComponent do placeholder={@placeholder} value={@query} name="query" + data-testid="search-input" phx-change="search" phx-target={@myself} phx-debounce="300" diff --git a/lib/mv_web/live/components/sort_header_component.ex b/lib/mv_web/live/components/sort_header_component.ex index 3439d91..b847308 100644 --- a/lib/mv_web/live/components/sort_header_component.ex +++ b/lib/mv_web/live/components/sort_header_component.ex @@ -56,6 +56,7 @@ defmodule MvWeb.Components.SortHeaderComponent do case dir do :asc -> gettext("ascending") :desc -> gettext("descending") + nil -> gettext("Click to sort") end end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index d850c7c..a0299fd 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -88,6 +88,18 @@ for member_attrs <- [ city: "Berlin", street: "Kastanienallee", house_number: "8" + }, + %{ + first_name: "Marianne", + last_name: "Wagner", + email: "marianne.wagner@example.de", + birth_date: ~D[1978-11-08], + join_date: ~D[2022-11-10], + paid: true, + phone_number: "+49301122334", + city: "Berlin", + street: "Kastanienallee", + house_number: "8" } ] do # Use upsert to prevent duplicates based on email diff --git a/test/mv_web/components/search_bar_component_test.exs b/test/mv_web/components/search_bar_component_test.exs index 2c85f19..bc8bc46 100644 --- a/test/mv_web/components/search_bar_component_test.exs +++ b/test/mv_web/components/search_bar_component_test.exs @@ -18,14 +18,14 @@ defmodule MvWeb.Components.SearchBarComponentTest do html = view |> element("form[role=search]") - |> render_change(%{"query" => "Friedrich"}) + |> render_submit(%{"query" => "Friedrich"}) refute html =~ "Greta" html = view |> element("form[role=search]") - |> render_change(%{"query" => "Greta"}) + |> render_submit(%{"query" => "Greta"}) refute html =~ "Friedrich" end diff --git a/test/mv_web/components/sort_header_component_test.exs b/test/mv_web/components/sort_header_component_test.exs index 9b1c006..55c3f00 100644 --- a/test/mv_web/components/sort_header_component_test.exs +++ b/test/mv_web/components/sort_header_component_test.exs @@ -1,12 +1,301 @@ defmodule MvWeb.Components.SortHeaderComponentTest do use MvWeb.ConnCase, async: true - use Phoenix.Component import Phoenix.LiveViewTest - test "renders sort header with correct attributes", %{conn: conn} do - conn = conn_with_oidc_user(conn) - {:ok, view, _html} = live(conn, "/members") + describe "rendering" do + test "renders with correct attributes", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members") - assert view |> element("[data-testid='first_name']") + # Test that the component renders with correct attributes + assert has_element?(view, "[data-testid='first_name']") + assert has_element?(view, "button[phx-value-field='city']") + assert has_element?(view, "button[phx-value-field='first_name']", "First name") + end + + test "renders all sortable headers", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members") + + sortable_fields = [:first_name, :email, :street, :house_number, :postal_code, :city, :phone_number, :join_date] + + for field <- sortable_fields do + assert has_element?(view, "button[phx-value-field='#{field}']") + end + end + + test "renders correct labels", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members") + + # Test specific labels + assert has_element?(view, "button[phx-value-field='first_name']", "First name") + assert has_element?(view, "button[phx-value-field='email']", "Email") + assert has_element?(view, "button[phx-value-field='city']", "City") + end + end + + describe "sort icons" do + test "shows neutral icon for specific field when not sorted", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, html} = live(conn, "/members") + + # The neutral icon has the opcity class we can test for + # Test that EMAIL field specifically shows neutral icon + assert has_element?(view, "[data-testid='email'] .opacity-40") + + # Test that CITY field specifically shows neutral icon + assert has_element?(view, "[data-testid='city'] .opacity-40") + end + + test "shows ascending icon for specific field when sorted ascending", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, html} = live(conn, "/members?query=&sort_field=city&sort_order=asc") + + # Test that FIRST_NAME field specifically shows ascending icon + # Test CSS classes - no opacity for active state + refute has_element?(view, "[data-testid='city'] .opacity-40") + + # Test that OTHER fields still show neutral icons + assert has_element?(view, "[data-testid='first_name'] .opacity-40") + + # Test HTML content - should contain chevron-up AND chevron-up-down + assert html =~ "hero-chevron-up" + assert html =~ "hero-chevron-up-down" + + # Count occurrences to ensure only one ascending icon + up_count = html |> String.split("hero-chevron-up ") |> length() |> Kernel.-(1) + assert up_count == 1 # Should be exactly one chevron-up icon + end + + test "shows descending icon for specific field when sorted descending", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, html} = live(conn, "/members?query=&sort_field=email&sort_order=desc") + + # Count occurrences to ensure only one descending icon + down_count = html |> String.split("hero-chevron-down ") |> length() |> Kernel.-(1) + assert down_count == 1 # Should be exactly one chevron-down icon + end + + test "multiple fields can have different icon states", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members?query=&sort_field=city&sort_order=asc") + + # CITY field should be active (ascending) + refute has_element?(view, "[data-testid='city'] .opacity-40") + + # All other fields should be neutral + assert has_element?(view, "[data-testid='first_name'] .opacity-40") + assert has_element?(view, "[data-testid='email'] .opacity-40") + assert has_element?(view, "[data-testid='street'] .opacity-40") + assert has_element?(view, "[data-testid='house_number'] .opacity-40") + assert has_element?(view, "[data-testid='postal_code'] .opacity-40") + assert has_element?(view, "[data-testid='phone_number'] .opacity-40") + assert has_element?(view, "[data-testid='join_date'] .opacity-40") + end + + test "icon state changes correctly when clicking different fields", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members") + + # Start: all fields neutral except first name as default + assert has_element?(view, "[data-testid='city'] .opacity-40") + refute has_element?(view, "[data-testid='first_name'] .opacity-40") + assert has_element?(view, "[data-testid='email'] .opacity-40") + + # Click city - should become active + view + |> element("button[phx-value-field='city']") + |> render_click() + + # city should be active, email should still be neutral + refute has_element?(view, "[data-testid='city'] .opacity-40") + assert has_element?(view, "[data-testid='email'] .opacity-40") + + # Click email - should switch active field + view + |> element("button[phx-value-field='email']") + |> render_click() + + # email should be active, city should be neutral again + refute has_element?(view, "[data-testid='email'] .opacity-40") + assert has_element?(view, "[data-testid='city'] .opacity-40") + end + + test "specific field shows correct icon for each sort state", %{conn: conn} do + conn = conn_with_oidc_user(conn) + + # Test EMAIL field specifically + {:ok, view, html_asc} = live(conn, "/members?sort_field=email&sort_order=asc") + assert html_asc =~ "hero-chevron-up" + refute has_element?(view, "[data-testid='email'] .opacity-40") + + {:ok, view, html_desc} = live(conn, "/members?sort_field=email&sort_order=desc") + assert html_desc =~ "hero-chevron-down" + refute has_element?(view, "[data-testid='email'] .opacity-40") + + {:ok, view, html_neutral} = live(conn, "/members") + assert html_neutral =~ "hero-chevron-up-down" + assert has_element?(view, "[data-testid='email'] .opacity-40") + end + + test "icon distribution is correct for all fields", %{conn: conn} do + conn = conn_with_oidc_user(conn) + + # Test neutral state - all fields except first name (default) should show neutral icons + {:ok, _view, html_neutral} = live(conn, "/members") + + # Count neutral icons (should be 7 - one for each field) + neutral_count = html_neutral |> String.split("hero-chevron-up-down") |> length() |> Kernel.-(1) + assert neutral_count == 7 + + # Count active icons (should be 1) + up_count = html_neutral |> String.split("hero-chevron-up ") |> length() |> Kernel.-(1) + down_count = html_neutral |> String.split("hero-chevron-down ") |> length() |> Kernel.-(1) + assert up_count == 1 + assert down_count == 0 + + # Test ascending state - one field active, others neutral + {:ok, _view, html_asc} = live(conn, "/members?sort_field=first_name&sort_order=asc") + + # Should have exactly 1 ascending icon and 7 neutral icons + up_count = html_asc |> String.split("hero-chevron-up ") |> length() |> Kernel.-(1) + neutral_count = html_asc |> String.split("hero-chevron-up-down") |> length() |> Kernel.-(1) + down_count = html_asc |> String.split("hero-chevron-down ") |> length() |> Kernel.-(1) + + assert up_count == 1 + assert neutral_count == 7 + assert down_count == 0 + end + end + + describe "accessibility" do + test "sets aria-label correctly for unsorted state", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members") + + # Check aria-label for unsorted state + assert has_element?(view, "button[phx-value-field='city'][aria-label='Click to sort']") + end + + test "sets aria-label correctly for ascending sort", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members?sort_field=first_name&sort_order=asc") + + # Check aria-label for ascending sort + assert has_element?(view, "button[phx-value-field='first_name'][aria-label='ascending']") + end + + test "sets aria-label correctly for descending sort", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members?sort_field=first_name&sort_order=desc") + + # Check aria-label for descending sort + assert has_element?(view, "button[phx-value-field='first_name'][aria-label='descending']") + end + + test "includes tooltip with correct aria-label", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members?sort_field=first_name&sort_order=asc") + + # Check that tooltip div exists with correct data-tip + assert has_element?(view, "[data-testid='first_name']") + assert has_element?(view, "button[phx-value-field='first_name'][aria-label='ascending']") + end + + test "aria-labels work for all sortable fields", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members?sort_field=email&sort_order=desc") + + # Test aria-labels for different fields + assert has_element?(view, "button[phx-value-field='email'][aria-label='descending']") + assert has_element?(view, "button[phx-value-field='first_name'][aria-label='Click to sort']") + assert has_element?(view, "button[phx-value-field='city'][aria-label='Click to sort']") + end + end + + describe "component behavior" do + test "clicking sends sort message to parent", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members") + + # Click on the first_name sort header + view + |> element("button[phx-value-field='first_name']") + |> render_click() + + # The component should send a message to the parent LiveView + # This is tested indirectly through the URL change in integration tests + end + + test "component handles different field types correctly", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members") + + # Test that different field types render correctly + assert has_element?(view, "button[phx-value-field='first_name']") + assert has_element?(view, "button[phx-value-field='email']") + assert has_element?(view, "button[phx-value-field='join_date']") + end + end + + describe "edge cases" do + test "handles invalid sort field gracefully", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, html} = live(conn, "/members?sort_field=invalid_field&sort_order=asc") + + # Should not crash and should default sorting for first name + assert html =~ "hero-chevron-up-down" + refute has_element?(view, "[data-testid='first_name'] .opacity-40") + end + + test "handles invalid sort order gracefully", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, html} = live(conn, "/members?sort_field=first_name&sort_order=invalid") + + # Should default to ascending + assert html =~ "hero-chevron-up" + refute has_element?(view, "[data-testid='first_name'] [aria-label='ascending']") + end + + test "handles empty sort parameters", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, html} = live(conn, "/members?sort_field=&sort_order=") + + # Should show neutral icons + assert html =~ "hero-chevron-up-down" + assert has_element?(view, "[data-testid='city'] .opacity-40") + end + end + + describe "icon state transitions" do + test "icon changes when sorting state changes", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members") + + # Start with neutral state + assert has_element?(view, "[data-testid='city'] .opacity-40") + + # Click to sort ascending + view + |> element("button[phx-value-field='city']") + |> render_click() + + # Should now be ascending (no opacity class) + refute has_element?(view, "[data-testid='city'] .opacity-40") + end + + test "multiple fields can be tested for icon states", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, html} = live(conn, "/members?sort_field=email&sort_order=desc") + + # Email should be active (descending) + assert html =~ "hero-chevron-down" + refute has_element?(view, "[data-testid='email'] .opacity-40") + + # Other fields should be neutral + assert has_element?(view, "[data-testid='first_name'] .opacity-40") + assert has_element?(view, "[data-testid='city'] .opacity-40") + end end end diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs index f697d6e..a32d967 100644 --- a/test/mv_web/member_live/index_test.exs +++ b/test/mv_web/member_live/index_test.exs @@ -74,39 +74,132 @@ defmodule MvWeb.MemberLive.IndexTest do assert has_element?(index_view, "#flash-group", "Member create successfully") end - describe "sorting interaction" do + describe "sorting integration" do test "clicking a column header toggles sort order and updates the URL", %{conn: conn} do conn = conn_with_oidc_user(conn) {:ok, view, _html} = live(conn, "/members") - # The component data test ids are built as "" + # The component data test ids are built with the name of the field # First click – should sort ASC view |> element("[data-testid='email']") |> render_click() # The LiveView pushes a patch with the new query params - assert_patch(view, "/members?sort_field=email&sort_order=asc") + assert_patch(view, "/members?query=&sort_field=email&sort_order=asc") # Second click – toggles to DESC view |> element("[data-testid='email']") |> render_click() - assert_patch(view, "/members?sort_field=email&sort_order=desc") + assert_patch(view, "/members?query=&sort_field=email&sort_order=desc") + end + + test "clicking different column header resets order to ascending", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members?sort_field=email&sort_order=desc") + + # Click on a different column + view + |> element("[data-testid='first_name']") + |> render_click() + + assert_patch(view, "/members?query=&sort_field=first_name&sort_order=asc") + end + + test "all sortable columns work correctly", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members") + + # default ascending sorting with first name + assert has_element?(view, "[data-testid='first_name'][aria-label='ascending']") + + sortable_fields = [:email, :street, :house_number, :postal_code, :city, :phone_number, :join_date] + + for field <- sortable_fields do + view + |> element("[data-testid='#{field}']") + |> render_click() + + assert_patch(view, "/members?query=&sort_field=#{field}&sort_order=asc") + end + end + + test "sorting works with search query", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members?query=test") + + view + |> element("[data-testid='email']") + |> render_click() + + assert_patch(view, "/members?query=test&sort_field=email&sort_order=asc") + end + + test "sorting maintains search query when toggling order", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members?query=test&sort_field=email&sort_order=asc") + + view + |> element("[data-testid='email']") + |> render_click() + + assert_patch(view, "/members?query=test&sort_field=email&sort_order=desc") end end describe "URL param handling" do test "handle_params reads sort query and applies it", %{conn: conn} do conn = conn_with_oidc_user(conn) - url = "/members?sort_field=email&sort_order=desc" + {:ok, view, _html} = live(conn, "/members?query=&sort_field=email&sort_order=desc") - conn = get(conn, url) + # Check that the sort state is correctly applied + assert has_element?(view, "[data-testid='email'][aria-label='descending']") + end - # The LiveView must have parsed the params and stored them as atoms. - assert conn.assigns.sort_field == :email - assert conn.assigns.sort_order == :desc + test "handle_params handles invalid sort field gracefully", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members?query=&sort_field=invalid_field&sort_order=asc") + + # Should not crash and should show default first name order + assert has_element?(view, "[data-testid='first_name'][aria-label='ascending']") + end + + test "handle_params preserves search query with sort params", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members?query=test&sort_field=email&sort_order=desc") + + # Both search and sort should be preserved + assert has_element?(view, "[data-testid='email'][aria-label='descending']") + end + end + + describe "search and sort integration" do + test "search maintains sort state", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members?query=&sort_field=email&sort_order=desc") + + # Perform search + view + |> element("[data-testid='search-input']") + |> render_change(%{value: "test"}) + + # Sort state should be maintained + assert has_element?(view, "[data-testid='email'][aria-label='descending']") + end + + test "sort maintains search state", %{conn: conn} do + conn = conn_with_oidc_user(conn) + {:ok, view, _html} = live(conn, "/members?query=test&sort_field=email&sort_order=asc") + + # Perform sort + view + |> element("[data-testid='email']") + |> render_click() + + # Search state should be maintained + assert_patch(view, "/members?query=test&sort_field=email&sort_order=desc") end end