diff --git a/test/aprsme/packets/prepared_queries_test.exs b/test/aprsme/packets/prepared_queries_test.exs index fca3d7e..6ed4c38 100644 --- a/test/aprsme/packets/prepared_queries_test.exs +++ b/test/aprsme/packets/prepared_queries_test.exs @@ -472,4 +472,99 @@ defmodule Aprsme.Packets.PreparedQueriesTest do assert length(result) == 2 end end + + describe "get_recent_packets_in_bounds/2" do + test "returns packets within the bounding box" do + {:ok, _} = + create_positioned_packet(%{ + sender: "INBOUNDS-1", + base_callsign: "INBOUNDS", + lat: Decimal.new("33.5"), + lon: Decimal.new("-96.5") + }) + + {:ok, _} = + create_positioned_packet(%{ + sender: "OUTOFBOUNDS-1", + base_callsign: "OUTOFBOUNDS", + lat: Decimal.new("50.0"), + lon: Decimal.new("-120.0") + }) + + # bounds = [north, south, east, west] + result = PreparedQueries.get_recent_packets_in_bounds([34.0, 33.0, -96.0, -97.0], 24) + + senders = Enum.map(result, & &1.sender) + assert "INBOUNDS-1" in senders + refute "OUTOFBOUNDS-1" in senders + end + end + + describe "get_packet_count_in_area/2" do + test "counts only packets inside the bounding box" do + {:ok, _} = + create_positioned_packet(%{ + sender: "COUNT-IN-1", + base_callsign: "COUNT", + lat: Decimal.new("33.25"), + lon: Decimal.new("-96.25") + }) + + {:ok, _} = + create_positioned_packet(%{ + sender: "COUNT-OUT", + base_callsign: "COUNT", + lat: Decimal.new("55.0"), + lon: Decimal.new("-30.0") + }) + + # bounds = [north, south, east, west] + count = PreparedQueries.get_packet_count_in_area([34.0, 33.0, -96.0, -97.0], 24) + assert is_integer(count) + assert count >= 1 + end + + test "returns 0 when area has no packets" do + assert PreparedQueries.get_packet_count_in_area([0.0, -1.0, 180.0, 179.0], 1) == 0 + end + end + + describe "has_weather_packets?/1" do + test "returns true when callsign has weather packets" do + {:ok, _} = + create_positioned_packet(%{ + sender: "HASWX-1", + base_callsign: "HASWX", + temperature: 72.0, + humidity: 50.0 + }) + + assert PreparedQueries.has_weather_packets?("HASWX-1") + end + + test "returns false when callsign has no weather packets" do + refute PreparedQueries.has_weather_packets?("NOWX-NEVER") + end + end + + describe "weather_callsigns/1" do + test "returns a MapSet containing callsigns with weather data" do + {:ok, _} = + create_positioned_packet(%{ + sender: "WXSET-1", + base_callsign: "WXSET", + has_weather: true, + temperature: 65.0 + }) + + result = PreparedQueries.weather_callsigns(["WXSET-1", "NOWX-1"]) + assert %MapSet{} = result + assert MapSet.member?(result, "WXSET-1") + refute MapSet.member?(result, "NOWX-1") + end + + test "returns an empty MapSet for empty input" do + assert PreparedQueries.weather_callsigns([]) == MapSet.new() + end + end end diff --git a/test/aprsme_web/live/info_live/show_helpers_test.exs b/test/aprsme_web/live/info_live/show_helpers_test.exs index 9c0568c..8b193f2 100644 --- a/test/aprsme_web/live/info_live/show_helpers_test.exs +++ b/test/aprsme_web/live/info_live/show_helpers_test.exs @@ -165,4 +165,33 @@ defmodule AprsmeWeb.InfoLive.ShowHelpersTest do assert html =~ " "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 "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 diff --git a/test/aprsme_web/live/weather_live/callsign_view_test.exs b/test/aprsme_web/live/weather_live/callsign_view_test.exs index f03188d..88df5dc 100644 --- a/test/aprsme_web/live/weather_live/callsign_view_test.exs +++ b/test/aprsme_web/live/weather_live/callsign_view_test.exs @@ -77,5 +77,68 @@ defmodule AprsmeWeb.WeatherLive.CallsignViewTest do # returned as-is. assert CallsignView.format_weather_value(%{humidity: "65"}, :humidity, "en") == "65" end + + test "returns nil for unparseable string values on tracked keys" do + assert CallsignView.format_weather_value(%{temperature: "not-a-number"}, :temperature, "en") == + nil + end + + test "returns stringified value for untracked numeric keys" do + # humidity is numeric but not in the formatter map — passes through as string. + assert CallsignView.format_weather_value(%{humidity: 65}, :humidity, "en") == "65" + end + + test "formats rain fields with units" do + result = CallsignView.format_weather_value(%{rain_1h: 0.25}, :rain_1h, "en") + assert is_binary(result) + assert String.contains?(result, " ") + end + end + + describe "live packet broadcast handling" do + test "ignores non-matching postgres_packet broadcasts", %{conn: conn} do + {:ok, lv, _html} = live(conn, ~p"/weather/OWNCALL", on_error: :warn) + + # Broadcast an unrelated packet with no weather data + unrelated = %{ + sender: "DIFFERENT", + temperature: nil, + humidity: nil, + pressure: nil, + wind_speed: nil, + wind_direction: nil, + rain_1h: nil + } + + send(lv.pid, {:postgres_packet, unrelated}) + Process.sleep(20) + assert Process.alive?(lv.pid) + end + + test "ignores postgres_packet with wrong callsign", %{conn: conn} do + {:ok, lv, _html} = live(conn, ~p"/weather/MYCALL", on_error: :warn) + + wrong_call = %{ + sender: "OTHER", + temperature: 72.5, + humidity: 50, + pressure: nil, + wind_speed: nil, + wind_direction: nil, + rain_1h: nil + } + + send(lv.pid, {:postgres_packet, wrong_call}) + Process.sleep(20) + assert Process.alive?(lv.pid) + end + + test "ignores unknown messages without crashing", %{conn: conn} do + {:ok, lv, _html} = live(conn, ~p"/weather/UNKCALL", on_error: :warn) + + send(lv.pid, :some_unknown_message) + Process.sleep(20) + assert Process.alive?(lv.pid) + end end end