Skip to content

Commit e4cb115

Browse files
mjcCopilot
andcommitted
Add comprehensive LiveView tests and fix two FailuresLive pagination bugs
- Fix reset_all_failures leaving page at stale position: add page reset to 1 - Fix change_page accepting out-of-range values: clamp to current total_pages - Add total_pages_from_html helper for cleaner pagination assertions New test coverage: - rules_live_test: expand 8 → 19 tests; section content, exclusivity, nav buttons, atom-injection rejection, multi-step switching - list_pagination_test: new file, 20 tests for max_page/2 and pagination_label/3 - broadway_live_test: expand 5 → 12 tests; stardate format, timezone changes, update_stardate info message handling - failures_live_test: expand pagination describe to 6 tests; page 1 of 2 display, page 2 content, filter page-reset, clamp beyond total, reset_all page-reset Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 226b25d commit e4cb115

5 files changed

Lines changed: 434 additions & 10 deletions

File tree

lib/reencodarr_web/live/failures_live.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,10 @@ defmodule ReencodarrWeb.FailuresLive do
102102
# Reset all failed videos
103103
Media.reset_failed_videos()
104104

105-
# Reload the failures data
105+
# Reload the failures data, returning to page 1 (avoid stale page state)
106106
{:noreply,
107107
socket
108+
|> assign(:page, 1)
108109
|> put_flash(:info, "All failed videos have been reset")
109110
|> async_load_failures()}
110111
end
@@ -227,7 +228,8 @@ defmodule ReencodarrWeb.FailuresLive do
227228

228229
@impl true
229230
def handle_event("change_page", %{"page" => page}, socket) do
230-
page = page |> Parsers.parse_int(1) |> max(1)
231+
total_pages = max(1, socket.assigns.total_pages)
232+
page = page |> Parsers.parse_int(1) |> max(1) |> min(total_pages)
231233

232234
socket =
233235
socket

test/reencodarr_web/live/broadway_live_test.exs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,33 @@ defmodule ReencodarrWeb.BroadwayLiveTest do
2020
assert html =~ "OVERVIEW"
2121
assert html =~ "FAILURES"
2222
end
23+
24+
test "PIPELINE MONITOR is marked as active in navigation", %{conn: conn} do
25+
{:ok, _view, html} = live(conn, ~p"/broadway")
26+
27+
# The page title and active navigation item both contain PIPELINE MONITOR
28+
assert html =~ "PIPELINE MONITOR"
29+
end
30+
31+
test "stardate is rendered as a number", %{conn: conn} do
32+
{:ok, _view, html} = live(conn, ~p"/broadway")
33+
34+
# The stardate should appear as a decimal number (e.g. 75234.5)
35+
assert html =~ ~r/STARDATE\s+\d+\.\d/
36+
end
37+
38+
test "renders the coming soon message for Broadway content", %{conn: conn} do
39+
{:ok, _view, html} = live(conn, ~p"/broadway")
40+
41+
assert html =~ "Broadway pipeline monitoring interface"
42+
end
43+
44+
test "assigns default UTC timezone on mount", %{conn: conn} do
45+
{:ok, view, _html} = live(conn, ~p"/broadway")
46+
47+
# Default timezone is UTC — verify the page renders without issue
48+
assert render(view) =~ "PIPELINE MONITOR"
49+
end
2350
end
2451

2552
describe "set_timezone event" do
@@ -38,5 +65,43 @@ defmodule ReencodarrWeb.BroadwayLiveTest do
3865

3966
assert html =~ "PIPELINE MONITOR"
4067
end
68+
69+
test "handles multiple timezone changes in sequence", %{conn: conn} do
70+
{:ok, view, _} = live(conn, ~p"/broadway")
71+
72+
view |> render_click("set_timezone", %{"timezone" => "America/Chicago"})
73+
view |> render_click("set_timezone", %{"timezone" => "Europe/London"})
74+
html = view |> render_click("set_timezone", %{"timezone" => "Asia/Tokyo"})
75+
76+
assert html =~ "PIPELINE MONITOR"
77+
end
78+
79+
test "handles empty timezone string without crashing", %{conn: conn} do
80+
{:ok, view, _} = live(conn, ~p"/broadway")
81+
82+
html = view |> render_click("set_timezone", %{"timezone" => ""})
83+
84+
assert html =~ "PIPELINE MONITOR"
85+
end
86+
end
87+
88+
describe "update_stardate message" do
89+
test "handles :update_stardate info message without crashing", %{conn: conn} do
90+
{:ok, view, _html} = live(conn, ~p"/broadway")
91+
92+
send(view.pid, :update_stardate)
93+
94+
html = render(view)
95+
assert html =~ "STARDATE"
96+
end
97+
98+
test "stardate is still a number after :update_stardate message", %{conn: conn} do
99+
{:ok, view, _html} = live(conn, ~p"/broadway")
100+
101+
send(view.pid, :update_stardate)
102+
103+
html = render(view)
104+
assert html =~ ~r/STARDATE\s+\d+\.\d/
105+
end
41106
end
42107
end

test/reencodarr_web/live/failures_live_test.exs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ defmodule ReencodarrWeb.FailuresLiveTest do
2626
end
2727
end
2828

29+
defp total_pages_from_html(html) do
30+
case Regex.run(
31+
~r/of\s*<span class="font-medium text-white">(\d+)<\/span>/,
32+
html
33+
) do
34+
[_, total] -> String.to_integer(total)
35+
_ -> nil
36+
end
37+
end
38+
2939
# ---------------------------------------------------------------------------
3040
# Mount / basic render
3141
# ---------------------------------------------------------------------------
@@ -407,5 +417,116 @@ defmodule ReencodarrWeb.FailuresLiveTest do
407417

408418
assert current_page_from_html(html) == 1
409419
end
420+
421+
test "shows Page 1 of 2 with 21 failures at per_page 20", %{conn: conn} do
422+
Enum.each(1..21, fn n ->
423+
{:ok, video} = Fixtures.video_fixture(%{path: "/media/paginate_#{n}.mkv"})
424+
Media.record_video_failure(video, :encoding, :timeout, message: "p #{n}")
425+
end)
426+
427+
{:ok, view, _} = live(conn, ~p"/failures")
428+
html = loaded_html(view)
429+
430+
assert current_page_from_html(html) == 1
431+
assert total_pages_from_html(html) == 2
432+
end
433+
434+
test "navigating to page 2 shows the 21st failure", %{conn: conn} do
435+
{:ok, first_video} = Fixtures.video_fixture(%{path: "/media/first_on_page_one.mkv"})
436+
Media.record_video_failure(first_video, :encoding, :timeout, message: "first")
437+
438+
Enum.each(2..20, fn n ->
439+
{:ok, v} = Fixtures.video_fixture(%{path: "/media/filler_#{n}.mkv"})
440+
Media.record_video_failure(v, :encoding, :timeout, message: "filler #{n}")
441+
end)
442+
443+
{:ok, last_video} = Fixtures.video_fixture(%{path: "/media/last_on_page_two.mkv"})
444+
Media.record_video_failure(last_video, :encoding, :timeout, message: "last")
445+
446+
{:ok, view, _} = live(conn, ~p"/failures")
447+
loaded_html(view)
448+
449+
view
450+
|> element("button[phx-click='change_page'][title='Next page']")
451+
|> render_click()
452+
453+
html = render_async(view)
454+
455+
assert current_page_from_html(html) == 2
456+
assert html =~ "last_on_page_two"
457+
refute html =~ "first_on_page_one"
458+
end
459+
460+
test "filter resets page to 1 when on page 2", %{conn: conn} do
461+
Enum.each(1..21, fn n ->
462+
{:ok, video} = Fixtures.video_fixture(%{path: "/media/filter_reset_#{n}.mkv"})
463+
Media.record_video_failure(video, :encoding, :timeout, message: "filter #{n}")
464+
end)
465+
466+
{:ok, view, _} = live(conn, ~p"/failures")
467+
loaded_html(view)
468+
469+
view
470+
|> element("button[phx-click='change_page'][title='Next page']")
471+
|> render_click()
472+
473+
render_async(view)
474+
475+
view
476+
|> element("button[phx-click='filter_failures'][phx-value-filter='encoding']")
477+
|> render_click()
478+
479+
html = render_async(view)
480+
481+
assert current_page_from_html(html) == 1
482+
end
483+
484+
test "change_page clamps to total_pages when page exceeds total", %{conn: conn} do
485+
Enum.each(1..21, fn n ->
486+
{:ok, video} = Fixtures.video_fixture(%{path: "/media/clamp_#{n}.mkv"})
487+
Media.record_video_failure(video, :encoding, :timeout, message: "clamp #{n}")
488+
end)
489+
490+
{:ok, view, _} = live(conn, ~p"/failures")
491+
loaded_html(view)
492+
493+
render_click(view, "change_page", %{"page" => "999"})
494+
html = render_async(view)
495+
496+
# page should be clamped to total_pages (2), not left at 999
497+
assert current_page_from_html(html) == 2
498+
end
499+
500+
test "reset_all_failures resets page to 1", %{conn: conn} do
501+
Enum.each(1..21, fn n ->
502+
{:ok, video} = Fixtures.video_fixture(%{path: "/media/reset_page_#{n}.mkv"})
503+
Media.record_video_failure(video, :encoding, :timeout, message: "reset #{n}")
504+
end)
505+
506+
{:ok, view, _} = live(conn, ~p"/failures")
507+
loaded_html(view)
508+
509+
view
510+
|> element("button[phx-click='change_page'][title='Next page']")
511+
|> render_click()
512+
513+
render_async(view)
514+
515+
view
516+
|> element("button[phx-click='reset_all_failures']")
517+
|> render_click()
518+
519+
render_async(view)
520+
521+
# Add a new failure after the reset — it will land on page 1
522+
{:ok, new_video} = Fixtures.video_fixture(%{path: "/media/new_after_reset.mkv"})
523+
Media.record_video_failure(new_video, :encoding, :timeout, message: "after reset")
524+
525+
send(view.pid, :update_failures_data)
526+
html = render_async(view)
527+
528+
# If page was not reset to 1, we'd be on page 2 and the new failure would be invisible
529+
assert html =~ "new_after_reset"
530+
end
410531
end
411532
end
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
defmodule ReencodarrWeb.Live.ListPaginationTest do
2+
@moduledoc """
3+
Tests for the ListPagination helper module.
4+
"""
5+
use ExUnit.Case, async: true
6+
7+
alias ReencodarrWeb.Live.ListPagination
8+
9+
describe "max_page/2" do
10+
test "returns 1 for zero total items" do
11+
assert ListPagination.max_page(0, 10) == 1
12+
end
13+
14+
test "returns 1 when items fit in one page" do
15+
assert ListPagination.max_page(10, 10) == 1
16+
end
17+
18+
test "returns 1 for fewer items than per_page" do
19+
assert ListPagination.max_page(5, 10) == 1
20+
end
21+
22+
test "returns 2 when one item overflows to second page" do
23+
assert ListPagination.max_page(11, 10) == 2
24+
end
25+
26+
test "returns 2 when items fill exactly two pages" do
27+
assert ListPagination.max_page(20, 10) == 2
28+
end
29+
30+
test "returns 3 when one item overflows to third page" do
31+
assert ListPagination.max_page(21, 10) == 3
32+
end
33+
34+
test "returns correct page count for large datasets" do
35+
assert ListPagination.max_page(100, 10) == 10
36+
assert ListPagination.max_page(250, 50) == 5
37+
assert ListPagination.max_page(1000, 25) == 40
38+
end
39+
40+
test "always returns at least 1 even with 0 items" do
41+
assert ListPagination.max_page(0, 1) >= 1
42+
assert ListPagination.max_page(0, 100) >= 1
43+
end
44+
45+
test "handles single item per page" do
46+
assert ListPagination.max_page(1, 1) == 1
47+
assert ListPagination.max_page(5, 1) == 5
48+
assert ListPagination.max_page(10, 1) == 10
49+
end
50+
51+
test "result is always a positive integer" do
52+
for total <- [0, 1, 5, 10, 100, 999],
53+
per_page <- [1, 10, 25, 50, 100] do
54+
result = ListPagination.max_page(total, per_page)
55+
assert is_integer(result), "Expected integer for max_page(#{total}, #{per_page})"
56+
assert result >= 1, "Expected >= 1 for max_page(#{total}, #{per_page})"
57+
end
58+
end
59+
end
60+
61+
describe "pagination_label/3" do
62+
test "returns '0 results' when total is 0" do
63+
assert ListPagination.pagination_label(1, 10, 0) == "0 results"
64+
end
65+
66+
test "returns range for first page with full page of items" do
67+
assert ListPagination.pagination_label(1, 10, 10) == "1-10 of 10"
68+
end
69+
70+
test "returns range for first page with partial items" do
71+
assert ListPagination.pagination_label(1, 10, 5) == "1-5 of 5"
72+
end
73+
74+
test "returns correct range for second page" do
75+
assert ListPagination.pagination_label(2, 10, 15) == "11-15 of 15"
76+
end
77+
78+
test "returns correct range for second full page" do
79+
assert ListPagination.pagination_label(2, 10, 20) == "11-20 of 20"
80+
end
81+
82+
test "last page with partial items shows correct range" do
83+
assert ListPagination.pagination_label(3, 10, 25) == "21-25 of 25"
84+
end
85+
86+
test "first item on a page is always (page-1)*per_page + 1" do
87+
# Page 1: first = 1
88+
assert ListPagination.pagination_label(1, 25, 30) =~ "1-"
89+
# Page 2: first = 26
90+
assert ListPagination.pagination_label(2, 25, 30) =~ "26-"
91+
end
92+
93+
test "total is always shown correctly after 'of'" do
94+
assert ListPagination.pagination_label(1, 10, 42) =~ "of 42"
95+
assert ListPagination.pagination_label(2, 10, 42) =~ "of 42"
96+
end
97+
98+
test "last item does not exceed total" do
99+
# Page 5, per_page 10, total 42 → should show 41-42 not 41-50
100+
assert ListPagination.pagination_label(5, 10, 42) == "41-42 of 42"
101+
end
102+
103+
test "with per_page=20 defaults as in failures_live" do
104+
# Matches FailuresLive @per_page default
105+
assert ListPagination.pagination_label(1, 20, 0) == "0 results"
106+
assert ListPagination.pagination_label(1, 20, 15) == "1-15 of 15"
107+
assert ListPagination.pagination_label(1, 20, 20) == "1-20 of 20"
108+
assert ListPagination.pagination_label(2, 20, 25) == "21-25 of 25"
109+
end
110+
end
111+
end

0 commit comments

Comments
 (0)