defmodule MvWeb.MemberLive.IndexPaginationTest do @moduledoc """ §1.7 — Mount loads only one keyset page, not the whole table. §1.8 — phx-viewport-bottom fetches and appends the next page; once the last page is reached no further fetch is issued. """ use MvWeb.ConnCase, async: false import Phoenix.LiveViewTest alias Mv.Helpers.SystemActor require Ash.Query # Matches the :overview default_limit / @page_limit in the LiveView. @page_limit 50 defp seed_members(n) do actor = SystemActor.get_system_actor() Enum.each(1..n, fn i -> # Zero-padded names keep first_name-ascending order deterministic. idx = String.pad_leading(Integer.to_string(i), 4, "0") {:ok, _} = Mv.Membership.create_member( %{first_name: "Page#{idx}", last_name: "Member", email: "page#{idx}@example.com"}, actor: actor ) end) end defp row_count(html) do ~r/]*id="row-/ |> Regex.scan(html) |> length() end test "mount loads exactly one page when more members exist", %{conn: conn} do seed_members(@page_limit + 10) conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/members") assert row_count(html) == @page_limit # More rows remain, so the infinite-scroll sentinel is armed. assert html =~ ~s(phx-viewport-bottom="load_more") end test "viewport-bottom appends the next page and stops at the last page", %{conn: conn} do seed_members(@page_limit + 10) conn = conn_with_oidc_user(conn) {:ok, view, html} = live(conn, ~p"/members") assert row_count(html) == @page_limit # Fetch the next page: the remaining rows are appended to the stream. html_after = render_hook(view, "load_more", %{}) assert row_count(html_after) == @page_limit + 10 # Last page reached: the sentinel is disarmed and a further fetch is a no-op. refute html_after =~ ~s(phx-viewport-bottom="load_more") html_again = render_hook(view, "load_more", %{}) assert row_count(html_again) == @page_limit + 10 end test "selecting a row from a later page re-renders it as checked", %{conn: conn} do seed_members(@page_limit + 10) conn = conn_with_oidc_user(conn) {:ok, view, _html} = live(conn, ~p"/members") # Load the second page so its rows are in the loaded window. render_hook(view, "load_more", %{}) # The highest-index member sorts last and lives on the second page. actor = SystemActor.get_system_actor() last_member = Mv.Membership.Member |> Ash.Query.sort(first_name: :asc) |> Ash.read!(actor: actor) |> List.last() html = render_click(view, "select_member", %{"id" => last_member.id}) # The re-streamed row for a later-page member reflects the selection. assert html =~ ~s(id="row-#{last_member.id}") assert has_element?(view, ~s(tr#row-#{last_member.id} input[type="checkbox"][checked])) end test "single page does not arm the infinite-scroll sentinel", %{conn: conn} do seed_members(3) conn = conn_with_oidc_user(conn) {:ok, _view, html} = live(conn, ~p"/members") assert row_count(html) == 3 refute html =~ ~s(phx-viewport-bottom="load_more") end end