defmodule AprsmeWeb.MapLive.IntegrationTest do use AprsmeWeb.ConnCase import Aprsme.PacketsFixtures import Phoenix.LiveViewTest describe "MapLive.Index with real packet data" do test "renders a map with pre-existing packets and simulates bounds_changed", %{conn: conn} do # Seed some packets in the bounds we'll scroll to. now = DateTime.utc_now() for i <- 1..3 do packet_fixture(%{ sender: "INT#{i}", base_callsign: "INT#{i}", ssid: "0", received_at: DateTime.add(now, -i * 60, :second), lat: Decimal.new("#{40 + i * 0.01}"), lon: Decimal.new("#{-74 - i * 0.01}"), has_position: true, path: "WIDE1-1" }) end {:ok, view, _html} = live(conn, "/?lat=40.0&lng=-74.0&z=11", on_error: :warn) # Fire a bounds_changed event covering the seeded region. bounds = %{ "bounds" => %{ "north" => 41.0, "south" => 39.5, "east" => -73.0, "west" => -74.5 } } assert render_hook(view, "bounds_changed", bounds) assert render_hook(view, "map_ready", %{}) # Allow async historical loading to run. Process.sleep(100) html = render(view) assert html =~ "aprs-map" end test "tracks a callsign that has a real packet", %{conn: conn} do _packet = packet_fixture(%{ sender: "TRACKME", base_callsign: "TRACKME", ssid: "0", received_at: DateTime.utc_now(), lat: Decimal.new("33.0"), lon: Decimal.new("-96.5"), has_position: true, path: "WIDE1-1" }) {:ok, view, _html} = live(conn, "/?call=TRACKME", on_error: :warn) assert render_hook(view, "map_ready", %{}) Process.sleep(100) html = render(view) assert html =~ "aprs-map" end test "callsign path route loads the tracked callsign", %{conn: conn} do _packet = packet_fixture(%{ sender: "PATH1", base_callsign: "PATH1", ssid: "0", received_at: DateTime.utc_now(), lat: Decimal.new("35.0"), lon: Decimal.new("-90.0"), has_position: true, path: "WIDE1-1" }) {:ok, _view, html} = live(conn, "/PATH1", on_error: :warn) assert html =~ "aprs-map" end test "spatial_packet arriving for an in-bounds callsign triggers display", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn) render_hook(view, "bounds_changed", %{ "bounds" => %{ "north" => 34.0, "south" => 32.0, "east" => -95.0, "west" => -97.0 } }) packet = %{ id: "sp-1", sender: "SP-1", base_callsign: "SP", ssid: "1", lat: 33.0, lon: -96.0, has_position: true, symbol_table_id: "/", symbol_code: ">", received_at: DateTime.utc_now(), path: "WIDE1-1", comment: "in bounds" } send(view.pid, {:spatial_packet, packet}) Process.sleep(50) assert render(view) =~ "aprs-map" end test "handles a batch of packets including out-of-bounds ones", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn) render_hook(view, "bounds_changed", %{ "bounds" => %{ "north" => 34.0, "south" => 32.0, "east" => -95.0, "west" => -97.0 } }) packets = [ %{ id: "b-1", sender: "IN-1", base_callsign: "IN", ssid: "1", lat: 33.5, lon: -96.0, has_position: true, symbol_table_id: "/", symbol_code: ">", received_at: DateTime.utc_now(), path: "" }, %{ id: "b-2", sender: "OUT-1", base_callsign: "OUT", ssid: "1", lat: 50.0, lon: -80.0, has_position: true, symbol_table_id: "/", symbol_code: ">", received_at: DateTime.utc_now(), path: "" } ] send(view.pid, {:packet_batch, packets}) Process.sleep(50) assert render(view) =~ "aprs-map" end end describe "MapLive handle_event coverage for less-exercised events" do test "popup_closed, get_assigns, set_slideover_state, request_geolocation, geolocation_error", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) render_hook(view, "map_ready", %{}) assert render_hook(view, "popup_closed", %{}) assert render_hook(view, "set_slideover_state", %{"open" => false}) assert render_hook(view, "set_slideover_state", %{"open" => true}) assert render_hook(view, "request_geolocation", %{}) assert render_hook(view, "geolocation_error", %{"error" => "denied"}) assert render_hook(view, "toggle_slideover", %{}) end test "marker_hover_start and marker_hover_end exercise hover handlers", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn) render_hook(view, "map_ready", %{}) assert render_hook(view, "marker_hover_start", %{ "id" => "m-1", "path" => "WIDE1-1", "lat" => 33.0, "lng" => -96.0 }) assert render_hook(view, "marker_hover_end", %{}) end test "search_callsign with empty input no-ops", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "search_callsign", %{"callsign" => " "}) end test "track_callsign with empty input clears tracking", %{conn: conn} do {:ok, view, _html} = live(conn, "/?call=ZZZ", on_error: :warn) render_hook(view, "map_ready", %{}) # Empty callsign goes through the clear-tracking branch. assert render_hook(view, "track_callsign", %{"callsign" => ""}) end test "track_callsign with non-empty callsign that has a packet zooms and adds marker", %{conn: conn} do # Seed a real packet so track_callsign exercises the latest_packet branch. packet_fixture(%{ sender: "TRACKEV", base_callsign: "TRACKEV", ssid: "0", received_at: DateTime.utc_now(), lat: Decimal.new("45.0"), lon: Decimal.new("-93.0"), has_position: true, path: "WIDE1-1" }) {:ok, view, _html} = live(conn, "/", on_error: :warn) render_hook(view, "map_ready", %{}) assert render_hook(view, "track_callsign", %{"callsign" => "TRACKEV"}) end test "marker_hover_start with a path triggers RF path drawing", %{conn: conn} do # Seed packets that match the path stations so RF path resolution finds them. packet_fixture(%{ sender: "PATHST1", base_callsign: "PATHST1", ssid: "0", received_at: DateTime.utc_now(), lat: Decimal.new("33.5"), lon: Decimal.new("-96.5"), has_position: true, path: "" }) {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn) render_hook(view, "map_ready", %{}) assert render_hook(view, "marker_hover_start", %{ "id" => "m-rf", "path" => "PATHST1*,WIDE1-1", "lat" => 33.0, "lng" => -96.0 }) end test "clear_tracking event clears the tracked callsign", %{conn: conn} do {:ok, view, _html} = live(conn, "/?call=TRACK1", on_error: :warn) render_hook(view, "map_ready", %{}) assert render_hook(view, "clear_tracking", %{}) end test "update_callsign updates overlay_callsign", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "update_callsign", %{"callsign" => "AB1XYZ"}) end test "update_trail_duration changes the threshold", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) render_hook(view, "map_ready", %{}) assert render_hook(view, "update_trail_duration", %{"trail_duration" => "6"}) end test "update_historical_hours changes assigns", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) render_hook(view, "map_ready", %{}) assert render_hook(view, "update_historical_hours", %{"historical_hours" => "3"}) end test "locate_me, set_location, clear_and_reload_markers events", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "locate_me", %{}) assert render_hook(view, "set_location", %{"lat" => 33.5, "lng" => -96.5}) assert render_hook(view, "clear_and_reload_markers", %{}) end test "update_map_state with center and zoom", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) render_hook(view, "map_ready", %{}) assert render_hook(view, "update_map_state", %{ "center" => %{"lat" => 34.0, "lng" => -95.0}, "zoom" => 11 }) end test "marker_clicked event", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "marker_clicked", %{}) end test "error_boundary_triggered event logs without crashing", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "error_boundary_triggered", %{ "message" => "test error", "stack" => "test stack", "component_id" => "comp-1" }) end end describe "MapLive handle_info coverage" do test ":cleanup_old_packets, :reload_historical_packets do not crash", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) render_hook(view, "map_ready", %{}) send(view.pid, :cleanup_old_packets) send(view.pid, :reload_historical_packets) Process.sleep(50) assert render(view) =~ "aprs-map" end test ":clear_rf_path message clears the RF path", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) send(view.pid, :clear_rf_path) Process.sleep(20) assert render(view) =~ "aprs-map" end test "{:new_deployment, _} message updates deployed_at", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) send(view.pid, {:new_deployment, %{deployed_at: DateTime.utc_now()}}) Process.sleep(20) assert render(view) =~ "aprs-map" end test "{:drain_connections, n} message processes drain", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) send(view.pid, {:drain_connections, 0}) Process.sleep(20) assert render(view) =~ "aprs-map" end test ":initialize_replay message reaches handle_info_initialize_replay", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) render_hook(view, "map_ready", %{}) send(view.pid, :initialize_replay) Process.sleep(20) assert render(view) =~ "aprs-map" end test "{:load_rf_path_station_packets, _} message hits get_latest_packets_for_callsigns", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn) send(view.pid, {:load_rf_path_station_packets, ["FOO", "BAR"]}) Process.sleep(20) assert render(view) =~ "aprs-map" end test "{:show_error, message} message sets a flash", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) send(view.pid, {:show_error, "boom"}) Process.sleep(20) assert render(view) =~ "aprs-map" end test "{:load_historical_batch, _} backward-compat message", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) render_hook(view, "map_ready", %{}) send(view.pid, {:load_historical_batch, 0}) Process.sleep(20) assert render(view) =~ "aprs-map" end test "{:load_historical_batch, _, generation} matching current generation", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) render_hook(view, "map_ready", %{}) # Force loading_generation to a known value so the matching-branch # (line 957) is exercised. :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket %{channel_state | socket: %{channel_state.socket | assigns: Map.put(assigns, :loading_generation, 7)}} end) send(view.pid, {:load_historical_batch, 0, 7}) Process.sleep(20) assert render(view) =~ "aprs-map" end test "{:load_historical_batch, _, stale_gen} with stale generation is ignored", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) render_hook(view, "map_ready", %{}) # Use a wildly mismatched generation — the handler should ignore it. send(view.pid, {:load_historical_batch, 0, 99_999}) Process.sleep(20) assert render(view) =~ "aprs-map" end test "{:historical_loading_timeout, _} for stale generation no-ops", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) send(view.pid, {:historical_loading_timeout, 99_999}) Process.sleep(20) assert render(view) =~ "aprs-map" end test "{:process_pending_bounds} message", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) send(view.pid, {:process_pending_bounds}) Process.sleep(20) assert render(view) =~ "aprs-map" end test "Logger.debug-laden bounds-update path runs format strings at :debug level", %{conn: conn} do original_level = Logger.level() Logger.configure(level: :debug) try do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) render_hook(view, "map_ready", %{}) # Send bounds events so that Logger.debug calls in process_bounds_update, # handle_info_process_bounds_update, and process_bounds_from_params get # their string interpolations evaluated. send(view.pid, {:process_bounds_update, %{north: 34.0, south: 32.0, east: -95.0, west: -97.0}}) render_hook(view, "bounds_changed", %{ "bounds" => %{"north" => 34.0, "south" => 32.0, "east" => -95.0, "west" => -97.0} }) render_hook(view, "update_map_state", %{ "center" => %{"lat" => 33.5, "lng" => -96.5}, "zoom" => 11, "bounds" => %{"north" => 34.0, "south" => 32.0, "east" => -95.0, "west" => -97.0} }) Process.sleep(50) after Logger.configure(level: original_level) end end test "postgres_packet via PubSub", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn) render_hook(view, "map_ready", %{}) packet = %{ id: "pg-1", sender: "PG-1", base_callsign: "PG", ssid: "1", lat: 33.0, lon: -96.0, has_position: true, symbol_table_id: "/", symbol_code: ">", received_at: DateTime.utc_now(), path: "WIDE1-1" } send(view.pid, {:postgres_packet, packet}) Process.sleep(50) assert render(view) =~ "aprs-map" end test "{:DOWN, ref, _, batcher_pid, _} restarts the PacketBatcher", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) %{socket: %{assigns: %{batcher_pid: original_pid}}} = :sys.get_state(view.pid) # Send a fake DOWN message to simulate the batcher dying. send(view.pid, {:DOWN, make_ref(), :process, original_pid, :test_kill}) Process.sleep(30) # The handle_info DOWN handler should have spawned a new batcher. %{socket: %{assigns: %{batcher_pid: new_pid}}} = :sys.get_state(view.pid) assert is_pid(new_pid) end test "{:process_pending_bounds} with pending_bounds set triggers handle_bounds_update", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) render_hook(view, "map_ready", %{}) :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket %{ channel_state | socket: %{ channel_state.socket | assigns: assigns |> Map.put(:pending_bounds, %{north: 35.0, south: 31.0, east: -94.0, west: -98.0}) |> Map.put(:historical_loading, false) } } end) send(view.pid, {:process_pending_bounds}) Process.sleep(50) assert render(view) =~ "aprs-map" end test "stale bounds-update Logger.debug branch with :debug log level", %{conn: conn} do original_level = Logger.level() Logger.configure(level: :debug) try do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) # The LV channel state nests the socket under :socket. Set pending_bounds # via :sys.replace_state with that wrapper structure. :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket %{ channel_state | socket: %{ channel_state.socket | assigns: Map.put(assigns, :pending_bounds, %{north: 99.0, south: 0.0, east: 1.0, west: 0.0}) } } end) # Send a different bounds — should hit the "stale" Logger.debug branch # (line 1003 in index.ex), evaluating the format string at :debug level. send(view.pid, {:process_bounds_update, %{north: 34.0, south: 32.0, east: -95.0, west: -97.0}}) Process.sleep(30) assert render(view) =~ "aprs-map" after Logger.configure(level: original_level) end end test "tracked-callsign postgres_packet path with batcher_pid forced nil", %{conn: conn} do # Force batcher_pid to nil so {:postgres_packet, _} flows through # handle_info_postgres_packet (lines 1053+). {:ok, view, _html} = live(conn, "/?call=PGTRK", on_error: :warn) render_hook(view, "map_ready", %{}) :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket %{channel_state | socket: %{channel_state.socket | assigns: Map.put(assigns, :batcher_pid, nil)}} end) # Send a packet with sender matching tracked callsign — exercises the # tracked-callsign branch in handle_info_postgres_packet. packet = %{ id: "pgtrk-1", sender: "PGTRK", base_callsign: "PGTRK", ssid: "0", lat: 33.0, lon: -96.0, has_position: true, symbol_table_id: "/", symbol_code: ">", received_at: DateTime.utc_now(), path: "WIDE1-1" } send(view.pid, {:postgres_packet, packet}) Process.sleep(50) # Send a packet with a different sender — exercises the no-match branch. send(view.pid, {:postgres_packet, %{packet | sender: "OTHER", base_callsign: "OTHER"}}) Process.sleep(50) assert render(view) =~ "aprs-map" end test "untracked postgres_packet path with batcher_pid nil hits handle_info_postgres_packet else branch", %{ conn: conn } do {:ok, view, _html} = live(conn, "/", on_error: :warn) render_hook(view, "map_ready", %{}) :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket %{channel_state | socket: %{channel_state.socket | assigns: Map.put(assigns, :batcher_pid, nil)}} end) packet = %{ id: "untracked-1", sender: "UNT", base_callsign: "UNT", ssid: "1", lat: 33.0, lon: -96.0, has_position: true, symbol_table_id: "/", symbol_code: ">", received_at: DateTime.utc_now(), path: "WIDE1-1" } send(view.pid, {:postgres_packet, packet}) Process.sleep(50) assert render(view) =~ "aprs-map" end test "spatial_packet path with batcher_pid nil hits handle_info_postgres_packet branch", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn) render_hook(view, "map_ready", %{}) :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket %{channel_state | socket: %{channel_state.socket | assigns: Map.put(assigns, :batcher_pid, nil)}} end) packet = %{ id: "spatial-x", sender: "SPX", base_callsign: "SPX", ssid: "1", lat: 33.0, lon: -96.0, has_position: true, symbol_table_id: "/", symbol_code: ">", received_at: DateTime.utc_now(), path: "WIDE1-1" } send(view.pid, {:spatial_packet, packet}) Process.sleep(50) send(view.pid, {:streaming_packet, packet}) Process.sleep(50) assert render(view) =~ "aprs-map" end test "tracked-callsign positionless packets with NaiveDateTime received_at hits newer_packet? NaiveDateTime branch", %{ conn: conn } do {:ok, view, _html} = live(conn, "/?call=NAIV&lat=33.0&lng=-96.0&z=12", on_error: :warn) render_hook(view, "map_ready", %{}) # Pre-load tracked latest packet without position with a NaiveDateTime. :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket current = %{ sender: "NAIV", base_callsign: "NAIV", ssid: "0", lat: nil, lon: nil, has_position: false, received_at: ~N[2026-01-01 00:00:00] } %{ channel_state | socket: %{channel_state.socket | assigns: Map.put(assigns, :tracked_callsign_latest_packet, current)} } end) naive_packet = %{ id: "naiv-1", sender: "NAIV", base_callsign: "NAIV", ssid: "0", lat: nil, lon: nil, has_position: false, received_at: ~N[2026-06-01 00:00:00], path: "" } send(view.pid, {:packet_batch, [naive_packet]}) Process.sleep(50) # Now exercise the unrelated-types fallback (DateTime vs nil). :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket current = %{ sender: "NAIV", base_callsign: "NAIV", ssid: "0", lat: nil, lon: nil, has_position: false, received_at: nil } %{ channel_state | socket: %{channel_state.socket | assigns: Map.put(assigns, :tracked_callsign_latest_packet, current)} } end) send(view.pid, {:packet_batch, [Map.put(naive_packet, :received_at, nil)]}) Process.sleep(50) assert render(view) =~ "aprs-map" end test "clear_and_reload_markers with tracked callsign + visible packets exercises filter_with_tracked", %{conn: conn} do packet_fixture(%{ sender: "CRMV", base_callsign: "CRMV", ssid: "0", received_at: DateTime.utc_now(), lat: Decimal.new("33.0"), lon: Decimal.new("-96.0"), has_position: true, path: "WIDE1-1" }) {:ok, view, _html} = live(conn, "/?call=CRMV&lat=33.0&lng=-96.0&z=12", on_error: :warn) render_hook(view, "map_ready", %{}) # Pre-populate visible_packets and tracked_callsign_latest_packet so the # clear_and_reload handler exercises the filter_with_tracked path. tracked = %{ sender: "CRMV", base_callsign: "CRMV", ssid: "0", lat: 33.0, lon: -96.0, has_position: true, received_at: DateTime.utc_now() } visible = %{ "CRMV-0" => tracked, "OTHER-0" => %{ sender: "OTHER", base_callsign: "OTHER", ssid: "0", lat: 33.5, lon: -96.5, has_position: true, received_at: DateTime.utc_now() } } :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket %{ channel_state | socket: %{ channel_state.socket | assigns: assigns |> Map.put(:visible_packets, visible) |> Map.put(:tracked_callsign, "CRMV") |> Map.put(:tracked_callsign_latest_packet, tracked) } } end) assert render_hook(view, "clear_and_reload_markers", %{}) # Also at low zoom — exercises the heat map branch. :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket %{channel_state | socket: %{channel_state.socket | assigns: Map.put(assigns, :map_zoom, 6)}} end) assert render_hook(view, "clear_and_reload_markers", %{}) end test "{:packet_batch, _} with out-of-bounds known marker triggers remove_markers_if_needed branch", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn) render_hook(view, "map_ready", %{}) render_hook(view, "bounds_changed", %{ "bounds" => %{"north" => 34.0, "south" => 32.0, "east" => -95.0, "west" => -97.0} }) # Pre-populate visible_packets with a marker we'll later send out-of-bounds. :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket existing_packet = %{ sender: "MOVE", base_callsign: "MOVE", ssid: "0", lat: 33.5, lon: -96.0, has_position: true, received_at: DateTime.utc_now() } %{ channel_state | socket: %{channel_state.socket | assigns: Map.put(assigns, :visible_packets, %{"MOVE" => existing_packet})} } end) # Now send a batch with the same callsign but moved out-of-bounds — this # routes through batch_dispatch({false, true}, ...) and returns # {socket, nil, callsign_key}, which feeds remove_markers_if_needed # with a non-empty list. out_of_bounds_packet = %{ id: "move-out", sender: "MOVE", base_callsign: "MOVE", ssid: "0", lat: 50.0, lon: -120.0, has_position: true, symbol_table_id: "/", symbol_code: ">", received_at: DateTime.utc_now(), path: "" } send(view.pid, {:packet_batch, [out_of_bounds_packet]}) Process.sleep(50) assert render(view) =~ "aprs-map" end test "set_location with string lat/lng exercises to_float(binary) parse paths", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) # Valid binary coords — to_float(binary) → {f, _} → f. assert render_hook(view, "set_location", %{"lat" => "33.5", "lng" => "-96.5"}) # Unparseable binaries — to_float(binary) → :error → nil. assert render_hook(view, "set_location", %{"lat" => "not-a-number", "lng" => "also-bad"}) # Non-binary, non-numeric — to_float(_) → nil fallback. assert render_hook(view, "set_location", %{"lat" => %{"junk" => "shape"}, "lng" => :weird}) end test "back-to-back bounds_changed exercises schedule_bounds_update timer-cancel branch", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) render_hook(view, "map_ready", %{}) # First bounds_changed schedules a timer. render_hook(view, "bounds_changed", %{ "bounds" => %{"north" => 34.0, "south" => 32.0, "east" => -95.0, "west" => -97.0} }) # Second bounds_changed should hit the existing-timer Process.cancel_timer # branch (lines 1968+). render_hook(view, "bounds_changed", %{ "bounds" => %{"north" => 35.0, "south" => 31.0, "east" => -94.0, "west" => -98.0} }) Process.sleep(50) assert render(view) =~ "aprs-map" end test ":initialize_replay reschedules itself when map_bounds is nil", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) render_hook(view, "map_ready", %{}) # Force map_bounds to nil so handle_info_initialize_replay hits the # else-branch (line 1042) that schedules :initialize_replay later. :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket %{ channel_state | socket: %{ channel_state.socket | assigns: assigns |> Map.put(:map_bounds, nil) |> Map.put(:historical_loaded, false) } } end) send(view.pid, :initialize_replay) Process.sleep(20) assert render(view) =~ "aprs-map" end test "process_bounds_update with out-of-bounds visible_packets exercises remove_markers_batch", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) render_hook(view, "map_ready", %{}) # Seed visible_packets with packets outside the next-bounds region. visible = %{ "FAR-0" => %{ sender: "FAR", base_callsign: "FAR", ssid: "0", lat: 60.0, lon: -120.0, has_position: true, received_at: DateTime.utc_now() }, "ALSO-0" => %{ sender: "ALSO", base_callsign: "ALSO", ssid: "0", lat: 70.0, lon: -130.0, has_position: true, received_at: DateTime.utc_now() } } :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket %{ channel_state | socket: %{ channel_state.socket | assigns: assigns |> Map.put(:visible_packets, visible) |> Map.put(:initial_bounds_loaded, true) } } end) # Send bounds that exclude the seeded packets — process_bounds_update # should call remove_markers_batch with all of them as marker_ids. send(view.pid, {:process_bounds_update, %{north: 34.0, south: 32.0, east: -95.0, west: -97.0}}) Process.sleep(50) assert render(view) =~ "aprs-map" end test "{:load_rf_path_station_packets, _} with seeded packets builds and adds markers", %{conn: conn} do packet_fixture(%{ sender: "RFST1", base_callsign: "RFST1", ssid: "0", received_at: DateTime.utc_now(), lat: Decimal.new("33.0"), lon: Decimal.new("-96.0"), has_position: true, path: "" }) packet_fixture(%{ sender: "RFST2", base_callsign: "RFST2", ssid: "0", received_at: DateTime.utc_now(), lat: Decimal.new("33.5"), lon: Decimal.new("-96.5"), has_position: true, path: "" }) {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn) send(view.pid, {:load_rf_path_station_packets, ["RFST1", "RFST2"]}) Process.sleep(50) assert render(view) =~ "aprs-map" end test "{:packet_batch, _} at low zoom (untracked) triggers heat map branch", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=6", on_error: :warn) render_hook(view, "map_ready", %{}) # Force-set map_bounds and zoom directly so the packet_batch processing # reaches update_display_for_batch with a non-empty marker_data_list at # low zoom (lines 1122-1123 heat map branch). :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket %{ channel_state | socket: %{ channel_state.socket | assigns: assigns |> Map.put(:map_bounds, %{north: 40.0, south: 30.0, east: -90.0, west: -100.0}) |> Map.put(:map_zoom, 6) |> Map.put(:initial_bounds_loaded, true) } } end) packets = [ %{ id: "lz-1", sender: "LZ-1", base_callsign: "LZ", ssid: "1", lat: 33.0, lon: -96.0, has_position: true, symbol_table_id: "/", symbol_code: ">", received_at: DateTime.utc_now(), path: "" } ] send(view.pid, {:packet_batch, packets}) Process.sleep(50) assert render(view) =~ "aprs-map" end test "{:packet_batch, _} at low zoom with tracked callsign triggers trail-line branch", %{conn: conn} do packet_fixture(%{ sender: "LZTR", base_callsign: "LZTR", ssid: "0", received_at: DateTime.utc_now(), lat: Decimal.new("33.0"), lon: Decimal.new("-96.0"), has_position: true, path: "WIDE1-1" }) {:ok, view, _html} = live(conn, "/?call=LZTR&lat=33.0&lng=-96.0&z=6", on_error: :warn) render_hook(view, "map_ready", %{}) :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket %{ channel_state | socket: %{ channel_state.socket | assigns: assigns |> Map.put(:map_bounds, %{north: 40.0, south: 30.0, east: -90.0, west: -100.0}) |> Map.put(:map_zoom, 6) |> Map.put(:initial_bounds_loaded, true) |> Map.put(:tracked_callsign, "LZTR") } } end) packets = [ %{ id: "lztr-1", sender: "LZTR", base_callsign: "LZTR", ssid: "0", lat: 33.0, lon: -96.0, has_position: true, symbol_table_id: "/", symbol_code: ">", received_at: DateTime.utc_now(), path: "" } ] send(view.pid, {:packet_batch, packets}) Process.sleep(50) assert render(view) =~ "aprs-map" end test "{:historical_loading_timeout, generation} matching with historical_loading=true forces completion", %{ conn: conn } do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) render_hook(view, "map_ready", %{}) :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket %{ channel_state | socket: %{ channel_state.socket | assigns: assigns |> Map.put(:historical_loading, true) |> Map.put(:loading_generation, 42) |> Map.put(:total_batches, 5) } } end) send(view.pid, {:historical_loading_timeout, 42}) Process.sleep(50) assert render(view) =~ "aprs-map" end test "cleanup_old_packets handler with expired packets in visible_packets prunes them", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=10", on_error: :warn) render_hook(view, "map_ready", %{}) # Seed visible_packets with one fresh and one stale packet. now = DateTime.utc_now() old = DateTime.add(now, -3600, :second) visible = %{ "FRESH-0" => %{ sender: "FRESH", base_callsign: "FRESH", ssid: "0", lat: 33.0, lon: -96.0, has_position: true, received_at: now }, "STALE-0" => %{ sender: "STALE", base_callsign: "STALE", ssid: "0", lat: 33.0, lon: -96.0, has_position: true, received_at: old } } :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket %{ channel_state | socket: %{ channel_state.socket | assigns: assigns |> Map.put(:visible_packets, visible) |> Map.put(:packet_age_threshold, DateTime.add(now, -1800, :second)) } } end) send(view.pid, :cleanup_old_packets) Process.sleep(50) assert render(view) =~ "aprs-map" end test "tracked-callsign with positionless incoming exercises preferred_tracked_packet branches", %{conn: conn} do # Set up a tracked callsign with a current packet that has position. # Send incoming packets without position to walk through the cond # branches in preferred_tracked_packet/2. {:ok, view, _html} = live(conn, "/?call=PREF&lat=33.0&lng=-96.0&z=12", on_error: :warn) render_hook(view, "map_ready", %{}) :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket # Pre-load tracked_callsign_latest_packet so preferred_tracked_packet/2 # has a non-nil current_packet to compare against. current = %{ sender: "PREF", base_callsign: "PREF", ssid: "0", lat: 33.0, lon: -96.0, has_position: true, received_at: DateTime.add(DateTime.utc_now(), -120, :second) } %{ channel_state | socket: %{channel_state.socket | assigns: Map.put(assigns, :tracked_callsign_latest_packet, current)} } end) # Incoming packet without position — exercises lines 1842, 1843 # (current has position, incoming doesn't → current returned). no_pos_packet = %{ id: "nopos-1", sender: "PREF", base_callsign: "PREF", ssid: "0", lat: nil, lon: nil, has_position: false, received_at: DateTime.utc_now(), path: "" } send(view.pid, {:packet_batch, [no_pos_packet]}) Process.sleep(50) # Both nil-position; newer DateTime returned. :sys.replace_state(view.pid, fn channel_state -> %{assigns: assigns} = channel_state.socket current_no_pos = %{ sender: "PREF", base_callsign: "PREF", ssid: "0", lat: nil, lon: nil, has_position: false, received_at: DateTime.add(DateTime.utc_now(), -1000, :second) } %{ channel_state | socket: %{channel_state.socket | assigns: Map.put(assigns, :tracked_callsign_latest_packet, current_no_pos)} } end) send(view.pid, {:packet_batch, [no_pos_packet]}) Process.sleep(50) assert render(view) =~ "aprs-map" end test "tracked-callsign packet_batch processes the matching-sender branch", %{conn: conn} do {:ok, view, _html} = live(conn, "/?call=BATCH&lat=33.0&lng=-96.0&z=12", on_error: :warn) render_hook(view, "map_ready", %{}) render_hook(view, "bounds_changed", %{ "bounds" => %{ "north" => 34.0, "south" => 32.0, "east" => -95.0, "west" => -97.0 } }) packets = [ %{ id: "btr-1", sender: "BATCH", base_callsign: "BATCH", ssid: "0", lat: 33.5, lon: -96.0, has_position: true, symbol_table_id: "/", symbol_code: ">", received_at: DateTime.utc_now(), path: "" }, %{ id: "btr-2", sender: "OTHER", base_callsign: "OTHER", ssid: "1", lat: 33.5, lon: -96.5, has_position: true, symbol_table_id: "/", symbol_code: ">", received_at: DateTime.utc_now(), path: "" } ] send(view.pid, {:packet_batch, packets}) Process.sleep(50) assert render(view) =~ "aprs-map" end end end