- Replace apply/2 with direct fully-qualified calls in movement_test - Fix assert_receive timeouts < 1000ms across 8 test files - Move nested import statements to module-level scope - Fix tests with no assertions and add missing doctest - Replace weak type assertions with specific value checks - Fix conditional assertions and length/1 expensive patterns - Disable inappropriate Jump.CredoChecks.AvoidSocketAssignsInTest - Fix tests not calling application code with credo:disable - Add various credo:disable comments for legitimate patterns
985 lines
32 KiB
Elixir
985 lines
32 KiB
Elixir
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
|