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 =~ "TRACKME" or 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, :update_time_display, :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, :update_time_display) 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", %{}) # Use the loading_generation that the LV currently has (default 0). send(view.pid, {:load_historical_batch, 0, 0}) 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 end end