defmodule AprsmeWeb.MapLive.IndexTest do use AprsmeWeb.ConnCase import Aprsme.PacketsFixtures import Phoenix.ConnTest import Phoenix.LiveViewTest setup do # Reset the shared Hammer ETS so per-IP request counts don't bleed # between test cases and trigger 429s in the middle of a run. case :ets.info(Aprsme.RateLimiter) do :undefined -> :ok _ -> :ets.delete_all_objects(Aprsme.RateLimiter) end :ok end describe "Index" do test "renders map view", %{conn: conn} do {:ok, view, html} = live(conn, "/", on_error: :warn) assert html =~ "APRS Map" assert has_element?(view, "#aprs-map") end test "handles bounds_changed event with float values", %{conn: conn} do {:ok, view, _html} = live(conn, "/") # Simulate JavaScript sending float values (the problematic case) bounds_params = %{ "bounds" => %{ "north" => 61.897577621605016, "south" => 5.441022303717974, "east" => -34.45312500000001, "west" => -161.54296875000003 } } # This should not crash with ArgumentError: not a binary assert render_hook(view, "bounds_changed", bounds_params) end test "handles bounds_changed event with string values", %{conn: conn} do {:ok, view, _html} = live(conn, "/") # Simulate JavaScript sending string values (expected case) bounds_params = %{ "bounds" => %{ "north" => "61.897577621605016", "south" => "5.441022303717974", "east" => "-34.45312500000001", "west" => "-161.54296875000003" } } assert render_hook(view, "bounds_changed", bounds_params) end test "handles bounds_changed event with mixed value types", %{conn: conn} do {:ok, view, _html} = live(conn, "/") # Test mixed types (float, string, integer) bounds_params = %{ "bounds" => %{ # float "north" => 61.897577621605016, # string "south" => "5.441022303717974", # integer "east" => -34, # string "west" => "-161.54296875000003" } } assert render_hook(view, "bounds_changed", bounds_params) end test "loads historical packets when map is ready", %{conn: conn} do # Mock the Packets.get_packets_for_replay function to return empty list # since we now load all historical packets at once instead of replaying Mox.stub(Aprsme.PacketsMock, :get_packets_for_replay, fn _opts -> [] end) {:ok, view, _html} = live(conn, "/") # Simulate map initialization which should trigger historical packet loading assert render_hook(view, "map_ready", %{}) # Wait for the initialize_replay message to be processed Process.sleep(20) # The view should still be rendering without errors after loading historical packets assert render(view) =~ "aprs-map" end test "loads historical packets with correct time range", %{conn: conn} do # Create mock historical packets from the last 2 hours now = DateTime.utc_now() two_hours_ago = DateTime.add(now, -7200, :second) one_hour_ago = DateTime.add(now, -3600, :second) mock_packets = [ %{ id: 1, base_callsign: "TEST1", sender: "TEST1-1", ssid: "1", data_type: "position", symbol_table_id: "/", symbol_code: ">", lat: 39.8283, lon: -98.5795, received_at: two_hours_ago, inserted_at: two_hours_ago, has_position: true, comment: "Historical packet 1" }, %{ id: 2, base_callsign: "TEST2", sender: "TEST2-1", ssid: "1", data_type: "position", symbol_table_id: "/", symbol_code: ">", lat: 40.0, lon: -99.0, received_at: one_hour_ago, inserted_at: one_hour_ago, has_position: true, comment: "Historical packet 2" } ] # Mock the Packets.get_packets_for_replay function to return our test packets Mox.stub(Aprsme.PacketsMock, :get_packets_for_replay, fn opts -> # Verify that the time range is being passed correctly (not overridden to 1 hour) assert Map.has_key?(opts, :start_time) assert Map.has_key?(opts, :end_time) # The start_time should be 2 hours ago (historical_hours = "2") expected_start = DateTime.add(now, -7200, :second) assert_in_delta DateTime.to_unix(opts.start_time), DateTime.to_unix(expected_start), 1 mock_packets end) {:ok, view, _html} = live(conn, "/") # Set historical_hours to 2 to test the time range render_hook(view, "update_historical_hours", %{"historical_hours" => "2"}) Process.sleep(10) # Simulate map initialization which should trigger historical packet loading assert render_hook(view, "map_ready", %{}) # Wait for the initialize_replay message to be processed Process.sleep(20) # The view should still be rendering without errors after loading historical packets assert render(view) =~ "aprs-map" end test "handles clear_and_reload_markers event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") # This should not crash and should work without replay functionality assert render_hook(view, "clear_and_reload_markers", %{}) end test "sets correct page title", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert page_title(view) =~ "APRS Map" end test "handles map_ready event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") # Simulate map initialization assert render_hook(view, "map_ready", %{}) end test "handles locate_me event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "locate_me", %{}) end test "handles set_location event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "set_location", %{"lat" => 40.5, "lng" => -98.5}) end test "handles marker_clicked event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "marker_clicked", %{}) end test "handles marker_hover_end event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "marker_hover_end", %{}) end test "handles update_callsign event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "update_callsign", %{"callsign" => "K5ABC"}) end test "handles track_callsign event with a callsign", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "track_callsign", %{"callsign" => "K5ABC"}) end test "handles track_callsign event with empty callsign (clears tracking)", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "track_callsign", %{"callsign" => ""}) end test "handles clear_tracking event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "clear_tracking", %{}) end test "handles update_trail_duration event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "update_trail_duration", %{"trail_duration" => "2"}) end test "handles search_callsign event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "search_callsign", %{"callsign" => "K1ABC"}) end test "handles toggle_slideover event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "toggle_slideover", %{}) end test "handles set_slideover_state event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "set_slideover_state", %{"open" => true}) assert render_hook(view, "set_slideover_state", %{"open" => false}) end test "handles geolocation_error event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "geolocation_error", %{"error" => "permission denied"}) end test "handles request_geolocation event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "request_geolocation", %{}) end test "handles popup_closed event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "popup_closed", %{}) end test "handles update_map_state event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "update_map_state", %{ "center" => %{"lat" => 40.5, "lng" => -98.5}, "zoom" => 10 }) end test "handles marker_hover_start event with a path", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "marker_hover_start", %{ "id" => "K1ABC", "path" => "K5XYZ,WIDE1-1", "lat" => 40.0, "lng" => -98.0 }) end test "handles get_assigns event", %{conn: conn} do {:ok, view, _html} = live(conn, "/") assert render_hook(view, "get_assigns", %{}) end end describe "MapLive.Index message handlers via send/2" do test "responds to :clear_rf_path by pushing clear event", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) send(view.pid, :clear_rf_path) # Any render should still work after the message is processed. assert render(view) =~ "aprs-map" end test "responds to :cleanup_old_packets", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) send(view.pid, :cleanup_old_packets) assert render(view) =~ "aprs-map" end test "responds to :reload_historical_packets", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) send(view.pid, :reload_historical_packets) assert render(view) =~ "aprs-map" end test "responds to a postgres_packet message", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) packet = %{ id: "123", sender: "TEST-1", base_callsign: "TEST", ssid: "1", lat: 35.0, lon: -90.0, symbol_table_id: "/", symbol_code: ">", comment: "test packet", received_at: DateTime.utc_now(), path: "WIDE1-1" } send(view.pid, {:postgres_packet, packet}) assert render(view) =~ "aprs-map" end test "responds to a spatial_packet message", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) packet = %{ id: "789", sender: "SPATIAL", base_callsign: "SPATIAL", ssid: "0", lat: 41.0, lon: -97.0, symbol_table_id: "/", symbol_code: "-", comment: nil, received_at: DateTime.utc_now(), path: "" } send(view.pid, {:spatial_packet, packet}) assert render(view) =~ "aprs-map" end test "responds to a packet_batch message", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) packets = for i <- 1..3 do %{ id: "batch-#{i}", sender: "BATCH-#{i}", base_callsign: "BATCH", ssid: "#{i}", lat: 40.0 + i * 0.1, lon: -98.0 + i * 0.1, symbol_table_id: "/", symbol_code: ">", comment: "batch #{i}", received_at: DateTime.utc_now(), path: "" } end send(view.pid, {:packet_batch, packets}) assert render(view) =~ "aprs-map" end test "responds to a streaming_packet message", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) packet = %{ id: "stream-1", sender: "STREAM", base_callsign: "STREAM", ssid: "0", lat: 39.0, lon: -98.0, symbol_table_id: "/", symbol_code: ">", comment: nil, received_at: DateTime.utc_now(), path: "" } send(view.pid, {:streaming_packet, packet}) assert render(view) =~ "aprs-map" end test "responds to a drain_connections message", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) send(view.pid, {:drain_connections, 5}) assert render(view) =~ "aprs-map" end test "responds to load_rf_path_station_packets message", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) send(view.pid, {:load_rf_path_station_packets, ["K1ABC", "W5XYZ"]}) assert render(view) =~ "aprs-map" end test "responds to show_error message", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) send(view.pid, {:show_error, "something went wrong"}) assert render(view) end test "responds to historical_loading_timeout message", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) # Use a stale generation — should skip without side effects. send(view.pid, {:historical_loading_timeout, -999}) assert render(view) =~ "aprs-map" end test "postgres_packet for tracked callsign exercises preferred_tracked_packet flow", %{conn: conn} do # Mount with a tracked callsign so the tracked-callsign branch of # handle_info(:postgres_packet) runs (preferred_tracked_packet/newer_packet?). {:ok, view, _html} = live(conn, "/?call=TR-1", on_error: :warn) now = DateTime.utc_now() first = %{ id: "tr-1-old", sender: "TR-1", base_callsign: "TR", ssid: "1", lat: 35.0, lon: -90.0, symbol_table_id: "/", symbol_code: ">", comment: "first", received_at: DateTime.add(now, -120, :second), path: "" } second = %{ id: "tr-1-new", sender: "TR-1", base_callsign: "TR", ssid: "1", lat: 35.1, lon: -90.1, symbol_table_id: "/", symbol_code: ">", comment: "second", received_at: now, path: "" } send(view.pid, {:postgres_packet, first}) send(view.pid, {:postgres_packet, second}) assert render(view) =~ "aprs-map" end test "postgres_packet without coordinates exercises preferred_tracked_packet no-position fallback", %{conn: conn} do {:ok, view, _html} = live(conn, "/?call=NPOS-9", on_error: :warn) no_pos = %{ id: "npos-1", sender: "NPOS-9", base_callsign: "NPOS", ssid: "9", lat: nil, lon: nil, symbol_table_id: "/", symbol_code: ">", comment: "no position", received_at: DateTime.utc_now(), path: "" } send(view.pid, {:postgres_packet, no_pos}) assert render(view) =~ "aprs-map" end end describe "MapLive.Index with URL params" do test "mounts with lat/lng/z URL params", %{conn: conn} do {:ok, _view, html} = live(conn, "/?lat=40.5&lng=-98.5&z=10", on_error: :warn) assert html =~ "aprs-map" end test "mounts with a callsign URL param", %{conn: conn} do {:ok, _view, html} = live(conn, "/?call=K1ABC", on_error: :warn) assert html =~ "aprs-map" end test "mounts with a trail duration URL param", %{conn: conn} do {:ok, _view, html} = live(conn, "/?trail=3", on_error: :warn) assert html =~ "aprs-map" end test "mounts with a historical hours URL param", %{conn: conn} do {:ok, _view, html} = live(conn, "/?hist=6", on_error: :warn) assert html =~ "aprs-map" end test "handles patch with updated params via handle_params", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) # Patch to the same LiveView with new params — triggers handle_params. assert render_patch(view, "/?trail=2&hist=2&lat=41.0&lng=-97.0&z=8") =~ "aprs-map" end end describe "MapLive.Index callsign-based routes" do test "mounts with /:callsign path", %{conn: conn} do {:ok, _view, html} = live(conn, "/K5ABC", on_error: :warn) # credo:disable-for-next-line Jump.CredoChecks.ConditionalAssertion assert html =~ "K5ABC" or html =~ "aprs-map" # page may show callsign or map depending on render state end end describe "MapLive.Index zoom and map state changes" do test "update_map_state crossing heat map threshold (high → low zoom)", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=40.5&lng=-98.5&z=10", on_error: :warn) # Drop from zoom 10 → 6 — crosses the zoom-8 heat-map threshold. assert render_hook(view, "update_map_state", %{ "center" => %{"lat" => 40.5, "lng" => -98.5}, "zoom" => 6 }) end test "update_map_state crossing heat map threshold (low → high zoom)", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=40.5&lng=-98.5&z=5", on_error: :warn) # Jump from zoom 5 → 12 — crosses the threshold in the other direction. assert render_hook(view, "update_map_state", %{ "center" => %{"lat" => 40.5, "lng" => -98.5}, "zoom" => 12 }) end test "update_map_state with string zoom value", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "update_map_state", %{ "center" => %{"lat" => "40.5", "lng" => "-98.5"}, "zoom" => "10" }) end test "update_map_state clamps extreme zoom values", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "update_map_state", %{ "center" => %{"lat" => 0.0, "lng" => 0.0}, "zoom" => 99 }) end test "bounds_changed with very zoomed-in bounds", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "bounds_changed", %{ "bounds" => %{ "north" => 40.75, "south" => 40.74, "east" => -73.98, "west" => -73.99 } }) end end describe "MapLive.Index tracking flow" do test "track_callsign with whitespace trims and uppercases", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) # Mixed case + whitespace — should be normalized. assert render_hook(view, "track_callsign", %{"callsign" => " k5abc "}) end test "clear_tracking works after starting tracking", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) render_hook(view, "track_callsign", %{"callsign" => "W1XYZ"}) assert render_hook(view, "clear_tracking", %{}) end test "update_trail_duration with various values", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) for dur <- ["1", "2", "6", "12", "24"] do assert render_hook(view, "update_trail_duration", %{"trail_duration" => dur}) end end test "update_historical_hours with various values", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) for hrs <- ["1", "2", "6", "12", "24"] do assert render_hook(view, "update_historical_hours", %{"historical_hours" => hrs}) end end end describe "MapLive.Index error handling and edge cases" do test "error_boundary_triggered event is handled without crashing", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "error_boundary_triggered", %{ "message" => "Test error", "stack" => "at test:1", "component_id" => "test-component" }) end test "update_map_state with bounds in params", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "update_map_state", %{ "center" => %{"lat" => 33.0, "lng" => -96.0}, "zoom" => 10, "bounds" => %{ "north" => 34.0, "south" => 32.0, "east" => -95.0, "west" => -97.0 } }) end test "update_map_state with invalid/non-map center uses socket default", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0", on_error: :warn) assert render_hook(view, "update_map_state", %{ "center" => "not-a-map", "zoom" => 8 }) end test "marker_hover_start with invalid path does not crash", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "marker_hover_start", %{ "id" => "m1", "path" => "", "lat" => 33.0, "lng" => -96.0 }) end test "set_location event updates center", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "set_location", %{"lat" => 40.7, "lng" => -74.0}) end test "update_trail_duration with extreme value is clamped", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) # Very large value should be parsed and clamped assert render_hook(view, "update_trail_duration", %{"trail_duration" => "9999"}) assert render_hook(view, "update_trail_duration", %{"trail_duration" => "0"}) assert render_hook(view, "update_trail_duration", %{"trail_duration" => "invalid"}) end test "update_historical_hours with invalid input", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "update_historical_hours", %{"historical_hours" => "invalid"}) assert render_hook(view, "update_historical_hours", %{"historical_hours" => "0"}) end test "handles process_bounds_update via direct send", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) bounds = %{north: 34.0, south: 32.0, east: -95.0, west: -97.0} send(view.pid, {:process_bounds_update, bounds}) Process.sleep(50) assert render(view) =~ "aprs-map" end test "update_trail_duration at low zoom while tracking a callsign", %{conn: conn} do {:ok, view, _html} = live(conn, "/TRACK1?lat=33.0&lng=-96.0&z=5", on_error: :warn) assert render_hook(view, "update_trail_duration", %{"trail_duration" => "2"}) end test "update_historical_hours when map_ready triggers reload", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) render_hook(view, "map_ready", %{}) Process.sleep(20) assert render_hook(view, "update_historical_hours", %{"historical_hours" => "6"}) end test "clear_and_reload_markers at low zoom (heat map path)", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=5", on_error: :warn) # At zoom 5, clear_and_reload_markers uses the heat map branch. assert render_hook(view, "clear_and_reload_markers", %{}) end test "clear_and_reload_markers at high zoom (marker path)", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn) assert render_hook(view, "clear_and_reload_markers", %{}) end test "set_location with invalid coords is ignored safely", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) # Invalid-range coords should be rejected by UrlParams.valid_coordinates? assert render_hook(view, "set_location", %{"lat" => 999, "lng" => 999}) end test "set_location with string coords", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "set_location", %{"lat" => "40.7", "lng" => "-74.0"}) end test "marker_hover_start with a path of digipeaters (no positions)", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "marker_hover_start", %{ "id" => "x", "path" => "WIDE1-1,WIDE2-2,K5ABC*", "lat" => 33.0, "lng" => -96.0 }) end test "marker_hover_start followed by marker_hover_end cancels timer", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) render_hook(view, "marker_hover_start", %{ "id" => "x", "path" => "WIDE1-1", "lat" => 33.0, "lng" => -96.0 }) assert render_hook(view, "marker_hover_end", %{}) # Starting hover again cancels the pending clear timer. assert render_hook(view, "marker_hover_start", %{ "id" => "y", "path" => "WIDE2-2", "lat" => 34.0, "lng" => -97.0 }) end test "update_callsign sets overlay_callsign", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) assert render_hook(view, "update_callsign", %{"callsign" => "N0CALL"}) end test "tracked callsign with a latest packet renders other_ssids list", %{conn: conn} do now = DateTime.utc_now() _p = packet_fixture(%{ sender: "TRACKME-7", base_callsign: "TRACKME", ssid: "7", received_at: now, lat: Decimal.new("33.0"), lon: Decimal.new("-96.5"), has_position: true }) _p2 = packet_fixture(%{ sender: "TRACKME-9", base_callsign: "TRACKME", ssid: "9", received_at: DateTime.add(now, -60, :second), lat: Decimal.new("33.1"), lon: Decimal.new("-96.6"), has_position: true }) {:ok, _view, html} = live(conn, "/TRACKME-7", on_error: :warn) assert html =~ "TRACKME" end test "deployment_events broadcast updates deployed_at", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) deploy_time = DateTime.utc_now() # Send the deployment broadcast as a Phoenix.Socket.Broadcast (what the # PubSub subscriber receives in the real system). send( view.pid, %Phoenix.Socket.Broadcast{ topic: "deployment_events", event: "new_deployment", payload: {:new_deployment, %{deployed_at: deploy_time}} } ) Process.sleep(20) assert Process.alive?(view.pid) end test "handles drain_connections with small to_drain value", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) # to_drain * 10 << 100, likely no-op; still should keep process alive. send(view.pid, {:drain_connections, 1}) Process.sleep(30) assert Process.alive?(view.pid) end test "handles clear_rf_path via direct send", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) send(view.pid, :clear_rf_path) Process.sleep(30) assert Process.alive?(view.pid) end test "handles load_historical_batch with stale generation", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) # Send with a generation that probably doesn't match — should just be ignored. send(view.pid, {:load_historical_batch, 0, 99_999}) Process.sleep(30) assert Process.alive?(view.pid) end test "handles load_historical_batch with backward-compat 2-arg form", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) send(view.pid, {:load_historical_batch, 0}) Process.sleep(30) assert Process.alive?(view.pid) end test "handles historical_loading_timeout with stale generation", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) send(view.pid, {:historical_loading_timeout, 99_999}) Process.sleep(30) assert Process.alive?(view.pid) end test "handles process_pending_bounds with no pending", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) send(view.pid, {:process_pending_bounds}) Process.sleep(30) assert Process.alive?(view.pid) end test "tracking a callsign then postgres_packet for the same callsign is processed", %{conn: conn} do {:ok, view, _html} = live(conn, "/TRACKED1", on_error: :warn) render_hook(view, "map_ready", %{}) Process.sleep(30) # Packet matching the tracked callsign — should process. packet = %{ id: "pk-track", sender: "TRACKED1", base_callsign: "TRACKED1", 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, {:postgres_packet, packet}) Process.sleep(30) assert Process.alive?(view.pid) end test "tracking a callsign then postgres_packet for DIFFERENT callsign is dropped", %{ conn: conn } do {:ok, view, _html} = live(conn, "/TRACKED2", on_error: :warn) render_hook(view, "map_ready", %{}) Process.sleep(30) # Packet from a different sender — dropped. packet = %{ id: "pk-other", sender: "NOTTRACKED", base_callsign: "NOT", ssid: "1", lat: 34.0, lon: -95.0, has_position: true, symbol_table_id: "/", symbol_code: ">", received_at: DateTime.utc_now(), path: "" } send(view.pid, {:postgres_packet, packet}) Process.sleep(30) assert Process.alive?(view.pid) end test "update_trail_duration triggers cleanup_old_packets", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) render_hook(view, "update_trail_duration", %{"trail_duration" => "6"}) Process.sleep(30) assert Process.alive?(view.pid) end test "invalid bounds (north <= south) is rejected silently", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) # north <= south → invalid; the update should be silently skipped. bounds_params = %{ "bounds" => %{ "north" => 30.0, "south" => 40.0, "east" => -95.0, "west" => -97.0 } } assert render_hook(view, "bounds_changed", bounds_params) assert Process.alive?(view.pid) end test "handles load_rf_path_station_packets with empty list", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) send(view.pid, {:load_rf_path_station_packets, []}) Process.sleep(30) assert Process.alive?(view.pid) end test "historical_loading_timeout with matching generation forces completion", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) render_hook(view, "map_ready", %{}) Process.sleep(30) # Grab the current loading_generation from socket state via :sys.get_state # on the LiveView process is not safe; instead we send the timeout with # the generation the LiveView is likely at right now. Because the LV bumps # generation on reload triggers, sending 1 may match or not — the handler # branches on `generation == socket.assigns.loading_generation and historical_loading`. # If both aren't met, the else branch runs (already tested). Either path # exercises the same top-level handler; we just confirm no crash. state = :sys.get_state(view.pid) # LiveView sock is at state.socket.assigns gen = state.socket.assigns[:loading_generation] || 0 send(view.pid, {:historical_loading_timeout, gen}) Process.sleep(30) assert Process.alive?(view.pid) end test "show_error message sets flash and stays alive", %{conn: conn} do {:ok, view, _html} = live(conn, "/", on_error: :warn) send(view.pid, {:show_error, "Something broke"}) Process.sleep(30) assert Process.alive?(view.pid) end test "handles postgres_packet broadcast via batcher", %{conn: conn} do {:ok, view, _html} = live(conn, "/?lat=33.0&lng=-96.0&z=12", on_error: :warn) render_hook(view, "map_ready", %{}) Process.sleep(50) 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: "" } send(view.pid, {:postgres_packet, packet}) Process.sleep(50) assert render(view) =~ "aprs-map" end end end