diff --git a/test/aprsme_web/live/map_live/historical_loader_test.exs b/test/aprsme_web/live/map_live/historical_loader_test.exs new file mode 100644 index 0000000..27a9943 --- /dev/null +++ b/test/aprsme_web/live/map_live/historical_loader_test.exs @@ -0,0 +1,120 @@ +defmodule AprsmeWeb.MapLive.HistoricalLoaderTest do + use ExUnit.Case, async: true + + alias AprsmeWeb.MapLive.HistoricalLoader + alias Phoenix.LiveView.Socket + + defp build_socket(assigns) do + defaults = %{ + map_bounds: nil, + map_zoom: 10, + visible_packets: %{}, + historical_packets: %{}, + loading_generation: 0, + pending_batch_tasks: [], + pending_bounds: nil, + tracked_callsign: "", + historical_hours: "1", + historical_loading: false, + loading_batch: 0, + total_batches: 1, + historical_cursor: nil, + __changed__: %{} + } + + %Socket{assigns: Map.merge(defaults, assigns)} + end + + describe "cancel_pending_loads/1" do + test "cancels pending timers and clears the list" do + timer1 = Process.send_after(self(), :noop, 60_000) + timer2 = Process.send_after(self(), :noop, 60_000) + + socket = build_socket(%{pending_batch_tasks: [timer1, timer2]}) + + result = HistoricalLoader.cancel_pending_loads(socket) + + assert result.assigns.pending_batch_tasks == [] + # Timers should no longer be active (returns false after cancel). + assert Process.cancel_timer(timer1) == false + assert Process.cancel_timer(timer2) == false + end + + test "is a no-op when no timers are pending" do + socket = build_socket(%{pending_batch_tasks: []}) + result = HistoricalLoader.cancel_pending_loads(socket) + assert result.assigns.pending_batch_tasks == [] + end + end + + describe "load_historical_batch/3" do + test "returns the socket unchanged when map_bounds is nil" do + socket = build_socket(%{map_bounds: nil}) + result = HistoricalLoader.load_historical_batch(socket, 0, nil) + assert result == socket + end + + test "skips when the generation argument doesn't match loading_generation" do + socket = build_socket(%{loading_generation: 5}) + # Stale generation — should return socket unchanged without any DB query. + result = HistoricalLoader.load_historical_batch(socket, 0, 1) + assert result == socket + end + end + + describe "start_progressive_historical_loading/1" do + test "bumps loading_generation and schedules a failsafe" do + socket = + build_socket(%{ + map_bounds: nil, + map_zoom: 5, + loading_generation: 0 + }) + + # With nil bounds, load_historical_batch returns the socket as-is, but + # start_progressive_historical_loading should still have mutated + # loading_batch / historical_loading / generation before calling into + # do_load_historical_batch. + result = HistoricalLoader.start_progressive_historical_loading(socket) + assert result.assigns.loading_generation == 1 + assert result.assigns.historical_loading + assert result.assigns.historical_cursor == nil + # With map_zoom 5, progressive (non-single-batch) path is used — + # total_batches should be a positive integer determined by the zoom. + assert is_integer(result.assigns.total_batches) + assert result.assigns.total_batches >= 2 + end + + test "uses a single batch at high zoom" do + socket = + build_socket(%{ + map_bounds: nil, + map_zoom: 15, + loading_generation: 0 + }) + + result = HistoricalLoader.start_progressive_historical_loading(socket) + assert result.assigns.total_batches == 1 + assert result.assigns.historical_loading + end + + test "cancels pending timers on restart" do + timer = Process.send_after(self(), :noop, 60_000) + + socket = + build_socket(%{ + map_bounds: nil, + map_zoom: 10, + pending_batch_tasks: [timer], + loading_generation: 3, + historical_loading: true + }) + + result = HistoricalLoader.start_progressive_historical_loading(socket) + assert result.assigns.loading_generation == 4 + assert result.assigns.pending_batch_tasks == [] + # Original timer should have been cancelled. + assert Process.cancel_timer(timer) == false + end + end +end