fixes
This commit is contained in:
parent
f6f1ad1e2f
commit
d6ac5cccfc
15 changed files with 451 additions and 195 deletions
|
|
@ -112,9 +112,12 @@ let MapAPRSMap = {
|
|||
typeof lat === "number" &&
|
||||
typeof lng === "number" &&
|
||||
typeof zoom === "number" &&
|
||||
lat >= -90 && lat <= 90 &&
|
||||
lng >= -180 && lng <= 180 &&
|
||||
zoom >= 1 && zoom <= 20
|
||||
lat >= -90 &&
|
||||
lat <= 90 &&
|
||||
lng >= -180 &&
|
||||
lng <= 180 &&
|
||||
zoom >= 1 &&
|
||||
zoom <= 20
|
||||
) {
|
||||
initialCenter = { lat, lng };
|
||||
initialZoom = zoom;
|
||||
|
|
@ -136,8 +139,10 @@ let MapAPRSMap = {
|
|||
!initialCenter ||
|
||||
typeof initialCenter.lat !== "number" ||
|
||||
typeof initialCenter.lng !== "number"
|
||||
) throw new Error("Invalid center data");
|
||||
if (isNaN(initialZoom) || initialZoom < 1 || initialZoom > 20) throw new Error("Invalid zoom data");
|
||||
)
|
||||
throw new Error("Invalid center data");
|
||||
if (isNaN(initialZoom) || initialZoom < 1 || initialZoom > 20)
|
||||
throw new Error("Invalid zoom data");
|
||||
} catch (error) {
|
||||
console.error("Error parsing map data attributes:", error);
|
||||
initialCenter = { lat: 39.8283, lng: -98.5795 };
|
||||
|
|
@ -283,7 +288,7 @@ let MapAPRSMap = {
|
|||
const zoom = self.map.getZoom();
|
||||
localStorage.setItem(
|
||||
"aprs_map_state",
|
||||
JSON.stringify({ lat: center.lat, lng: center.lng, zoom })
|
||||
JSON.stringify({ lat: center.lat, lng: center.lng, zoom }),
|
||||
);
|
||||
}, 300);
|
||||
});
|
||||
|
|
@ -307,7 +312,7 @@ let MapAPRSMap = {
|
|||
const zoom = self.map.getZoom();
|
||||
localStorage.setItem(
|
||||
"aprs_map_state",
|
||||
JSON.stringify({ lat: center.lat, lng: center.lng, zoom })
|
||||
JSON.stringify({ lat: center.lat, lng: center.lng, zoom }),
|
||||
);
|
||||
}, 300);
|
||||
});
|
||||
|
|
@ -528,6 +533,19 @@ let MapAPRSMap = {
|
|||
});
|
||||
});
|
||||
|
||||
// Handle bulk loading of historical packets
|
||||
self.handleEvent("add_historical_packets", (data: { packets: MarkerData[] }) => {
|
||||
if (data.packets && Array.isArray(data.packets)) {
|
||||
data.packets.forEach((packet) => {
|
||||
self.addMarker({
|
||||
...packet,
|
||||
historical: true,
|
||||
popup: self.buildPopupContent(packet),
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle refresh markers event
|
||||
self.handleEvent("refresh_markers", () => {
|
||||
// Remove markers that are outside current bounds
|
||||
|
|
@ -658,7 +676,7 @@ let MapAPRSMap = {
|
|||
id: data.id,
|
||||
callsign: data.callsign,
|
||||
lat: lat,
|
||||
lng: lng
|
||||
lng: lng,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ config :aprs, Aprs.Repo,
|
|||
hostname: "localhost",
|
||||
database: "aprs_test#{System.get_env("MIX_TEST_PARTITION")}",
|
||||
pool: Ecto.Adapters.SQL.Sandbox,
|
||||
pool_size: 10,
|
||||
pool_size: System.schedulers_online() * 2,
|
||||
types: Aprs.PostgresTypes
|
||||
|
||||
# We don't run a server during test. If one is required,
|
||||
|
|
@ -27,6 +27,9 @@ config :aprs, AprsWeb.Endpoint,
|
|||
# Disable Oban during tests to prevent background job execution
|
||||
config :aprs, Oban, testing: :inline
|
||||
|
||||
# Configure the packets module to use the mock in tests
|
||||
config :aprs, :packets_module, Aprs.PacketsMock
|
||||
|
||||
# Disable APRS-IS external connections in test environment
|
||||
config :aprs,
|
||||
aprs_is_server: "mock.aprs.test",
|
||||
|
|
|
|||
13
lib/aprs/packet_replay_behaviour.ex
Normal file
13
lib/aprs/packet_replay_behaviour.ex
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
defmodule Aprs.PacketReplayBehaviour do
|
||||
@moduledoc """
|
||||
Behavior definition for the PacketReplay module.
|
||||
This allows us to mock the PacketReplay module in tests.
|
||||
"""
|
||||
|
||||
@callback start_replay(pid(), list(Aprs.Packet.t()), keyword()) :: {:ok, reference()} | {:error, term()}
|
||||
@callback stop_replay(reference()) :: :ok
|
||||
@callback pause_replay(reference()) :: :ok | {:error, :not_found}
|
||||
@callback resume_replay(reference()) :: :ok | {:error, :not_found}
|
||||
@callback get_replay_status(reference()) :: {:ok, map()} | {:error, :not_found}
|
||||
@callback adjust_replay_speed(reference(), float()) :: :ok | {:error, :not_found}
|
||||
end
|
||||
|
|
@ -3,6 +3,8 @@ defmodule Aprs.Packets do
|
|||
The Packets context.
|
||||
"""
|
||||
|
||||
@behaviour Aprs.PacketsBehaviour
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
|
||||
alias Aprs.BadPacket
|
||||
|
|
|
|||
11
lib/aprs/packets_behaviour.ex
Normal file
11
lib/aprs/packets_behaviour.ex
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
defmodule Aprs.PacketsBehaviour do
|
||||
@moduledoc """
|
||||
Behavior definition for the Packets context.
|
||||
This allows us to mock the Packets module in tests.
|
||||
"""
|
||||
|
||||
@callback get_packets_for_replay(map()) :: [Aprs.Packet.t()]
|
||||
@callback get_recent_packets(map()) :: [Aprs.Packet.t()]
|
||||
@callback get_historical_packet_count(map()) :: non_neg_integer()
|
||||
@callback stream_packets_for_replay(map()) :: Enumerable.t()
|
||||
end
|
||||
|
|
@ -12,7 +12,6 @@ defmodule AprsWeb.MapLive.Index do
|
|||
|
||||
@default_center %{lat: 39.8283, lng: -98.5795}
|
||||
@default_zoom 5
|
||||
@default_replay_speed 25.0
|
||||
@finch_name Aprs.Finch
|
||||
|
||||
@impl true
|
||||
|
|
@ -46,18 +45,10 @@ defmodule AprsWeb.MapLive.Index do
|
|||
},
|
||||
map_center: @default_center,
|
||||
map_zoom: @default_zoom,
|
||||
replay_active: false,
|
||||
replay_speed: @default_replay_speed,
|
||||
replay_paused: false,
|
||||
replay_packets: [],
|
||||
replay_index: 0,
|
||||
replay_timer_ref: nil,
|
||||
replay_start_time: nil,
|
||||
replay_end_time: nil,
|
||||
historical_packets: %{},
|
||||
packet_age_threshold: one_hour_ago,
|
||||
map_ready: false,
|
||||
replay_started: false,
|
||||
historical_loaded: false,
|
||||
pending_geolocation: nil,
|
||||
bounds_update_timer: nil,
|
||||
pending_bounds: nil,
|
||||
|
|
@ -158,59 +149,6 @@ defmodule AprsWeb.MapLive.Index do
|
|||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_replay", _params, socket) do
|
||||
if socket.assigns.replay_active do
|
||||
# Stop replay
|
||||
if socket.assigns.replay_timer_ref,
|
||||
do: Process.cancel_timer(socket.assigns.replay_timer_ref)
|
||||
|
||||
# Clear historical packets from the map
|
||||
socket =
|
||||
socket
|
||||
|> push_event("clear_historical_packets", %{})
|
||||
|> assign(
|
||||
replay_active: false,
|
||||
replay_timer_ref: nil,
|
||||
replay_paused: false,
|
||||
replay_packets: [],
|
||||
replay_index: 0,
|
||||
historical_packets: %{},
|
||||
replay_started: false
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
else
|
||||
# If not active, the user manually requested a replay restart
|
||||
{:noreply, start_historical_replay(socket)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("pause_replay", _params, socket) do
|
||||
if socket.assigns.replay_active do
|
||||
if socket.assigns.replay_paused do
|
||||
# Resume replay
|
||||
timer_ref = Process.send_after(self(), :replay_next_packet, 1000)
|
||||
{:noreply, assign(socket, replay_paused: false, replay_timer_ref: timer_ref)}
|
||||
else
|
||||
# Pause replay
|
||||
if socket.assigns.replay_timer_ref,
|
||||
do: Process.cancel_timer(socket.assigns.replay_timer_ref)
|
||||
|
||||
{:noreply, assign(socket, replay_paused: true, replay_timer_ref: nil)}
|
||||
end
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("adjust_replay_speed", %{"speed" => speed}, socket) do
|
||||
speed_float = to_float(speed)
|
||||
{:noreply, assign(socket, replay_speed: speed_float)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("clear_and_reload_markers", _params, socket) do
|
||||
# Only filter the current visible_packets, do not re-query the database
|
||||
|
|
@ -333,7 +271,7 @@ defmodule AprsWeb.MapLive.Index do
|
|||
def handle_info({:ip_location, %{lat: lat, lng: lng}}, socket), do: handle_info_ip_location(lat, lng, socket)
|
||||
|
||||
def handle_info(:initialize_replay, socket), do: handle_info_initialize_replay(socket)
|
||||
def handle_info(:replay_next_packet, socket), do: handle_replay_next_packet(socket)
|
||||
|
||||
def handle_info(:cleanup_old_packets, socket), do: handle_cleanup_old_packets(socket)
|
||||
|
||||
def handle_info({:postgres_packet, packet}, socket), do: handle_info_postgres_packet(packet, socket)
|
||||
|
|
@ -383,9 +321,9 @@ defmodule AprsWeb.MapLive.Index do
|
|||
end
|
||||
|
||||
defp handle_info_initialize_replay(socket) do
|
||||
if not socket.assigns.replay_started and socket.assigns.map_ready do
|
||||
if not socket.assigns.historical_loaded and socket.assigns.map_ready do
|
||||
socket = start_historical_replay(socket)
|
||||
{:noreply, assign(socket, replay_started: true)}
|
||||
{:noreply, socket}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
|
@ -448,67 +386,6 @@ defmodule AprsWeb.MapLive.Index do
|
|||
end
|
||||
|
||||
# Handle replaying the next historical packet
|
||||
defp handle_replay_next_packet(socket) do
|
||||
%{
|
||||
replay_packets: packets,
|
||||
replay_index: index,
|
||||
replay_speed: speed,
|
||||
historical_packets: historical_packets
|
||||
} = socket.assigns
|
||||
|
||||
if index < length(packets) do
|
||||
handle_next_replay_packet(socket, packets, index, speed, historical_packets)
|
||||
else
|
||||
handle_replay_end(socket)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_next_replay_packet(socket, packets, index, speed, historical_packets) do
|
||||
packet = Enum.at(packets, index)
|
||||
next_index = index + 1
|
||||
packet_data = build_packet_data(packet)
|
||||
|
||||
socket =
|
||||
if packet_data do
|
||||
packet_data =
|
||||
Map.merge(packet_data, %{
|
||||
"is_historical" => true,
|
||||
"timestamp" =>
|
||||
if(Map.has_key?(packet, :received_at),
|
||||
do: DateTime.to_iso8601(packet.received_at)
|
||||
)
|
||||
})
|
||||
|
||||
packet_id =
|
||||
"hist_#{if Map.has_key?(packet, :id), do: packet.id, else: System.unique_integer([:positive])}"
|
||||
|
||||
new_historical_packets = Map.put(historical_packets, packet_id, packet)
|
||||
|
||||
socket
|
||||
|> push_event("historical_packet", packet_data)
|
||||
|> assign(
|
||||
replay_index: next_index,
|
||||
historical_packets: new_historical_packets
|
||||
)
|
||||
else
|
||||
assign(socket, replay_index: next_index)
|
||||
end
|
||||
|
||||
delay_ms = trunc(1000 / speed)
|
||||
timer_ref = Process.send_after(self(), :replay_next_packet, delay_ms)
|
||||
{:noreply, assign(socket, replay_timer_ref: timer_ref)}
|
||||
end
|
||||
|
||||
defp handle_replay_end(socket) do
|
||||
socket =
|
||||
assign(socket,
|
||||
replay_active: false,
|
||||
replay_timer_ref: nil,
|
||||
replay_started: false
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
|
|
@ -693,8 +570,8 @@ defmodule AprsWeb.MapLive.Index do
|
|||
# Helper function to start historical replay
|
||||
@spec start_historical_replay(Socket.t()) :: Socket.t()
|
||||
defp start_historical_replay(socket) do
|
||||
# Only fetch historical packets if map is ready and not already replaying
|
||||
if socket.assigns.map_ready and not socket.assigns.replay_active do
|
||||
# Only fetch historical packets if map is ready
|
||||
if socket.assigns.map_ready do
|
||||
# Get time range for historical data
|
||||
now = DateTime.utc_now()
|
||||
one_hour_ago = DateTime.add(now, -60 * 60, :second)
|
||||
|
|
@ -711,24 +588,56 @@ defmodule AprsWeb.MapLive.Index do
|
|||
historical_packets = fetch_historical_packets(bounds, one_hour_ago, now)
|
||||
|
||||
if Enum.empty?(historical_packets) do
|
||||
# No historical packets found - silently continue without replay
|
||||
# No historical packets found - silently continue
|
||||
socket
|
||||
else
|
||||
# Clear any previous historical packets from the map
|
||||
socket = push_event(socket, "clear_historical_packets", %{})
|
||||
|
||||
# Start replay
|
||||
timer_ref = Process.send_after(self(), :replay_next_packet, 1000)
|
||||
# Group packets by callsign and get only the most recent for each
|
||||
latest_packets_by_callsign =
|
||||
historical_packets
|
||||
|> Enum.group_by(&generate_callsign/1)
|
||||
|> Enum.map(fn {_callsign, packets} ->
|
||||
# Sort by received_at descending and take the most recent
|
||||
Enum.max_by(packets, & &1.received_at, DateTime)
|
||||
end)
|
||||
|
||||
# Build packet data for all latest packets
|
||||
packet_data_list =
|
||||
latest_packets_by_callsign
|
||||
|> Enum.map(fn packet ->
|
||||
case build_packet_data(packet) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
packet_data ->
|
||||
# Generate a unique ID for this historical packet
|
||||
packet_id =
|
||||
"hist_#{if Map.has_key?(packet, :id), do: packet.id, else: System.unique_integer([:positive])}"
|
||||
|
||||
packet_data
|
||||
|> Map.put("id", packet_id)
|
||||
|> Map.put("is_historical", true)
|
||||
end
|
||||
end)
|
||||
# Remove nil values
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
# Send all historical packets at once
|
||||
socket = push_event(socket, "add_historical_packets", %{packets: packet_data_list})
|
||||
|
||||
# Store historical packets in assigns for reference
|
||||
historical_packets_map =
|
||||
packet_data_list
|
||||
|> Enum.zip(latest_packets_by_callsign)
|
||||
|> Enum.reduce(%{}, fn {packet_data, packet}, acc ->
|
||||
Map.put(acc, packet_data["id"], packet)
|
||||
end)
|
||||
|
||||
assign(socket,
|
||||
replay_active: true,
|
||||
replay_packets: historical_packets,
|
||||
replay_index: 0,
|
||||
replay_timer_ref: timer_ref,
|
||||
replay_start_time: one_hour_ago,
|
||||
replay_end_time: now,
|
||||
historical_packets: %{},
|
||||
replay_started: true
|
||||
historical_packets: historical_packets_map,
|
||||
historical_loaded: true
|
||||
)
|
||||
end
|
||||
else
|
||||
|
|
@ -758,7 +667,8 @@ defmodule AprsWeb.MapLive.Index do
|
|||
}
|
||||
|
||||
# Call the database through the Packets context
|
||||
packets = Aprs.Packets.get_packets_for_replay(packets_params)
|
||||
packets_module = Application.get_env(:aprs, :packets_module, Aprs.Packets)
|
||||
packets = packets_module.get_packets_for_replay(packets_params)
|
||||
|
||||
# Sort packets by received_at timestamp to ensure chronological replay
|
||||
Enum.sort_by(packets, fn packet -> packet.received_at end)
|
||||
|
|
@ -1001,19 +911,6 @@ defmodule AprsWeb.MapLive.Index do
|
|||
|
||||
defp send_default_ip_location, do: send(self(), {:ip_location, @default_center})
|
||||
|
||||
# Helper function to convert string or float to float
|
||||
@spec to_float(number() | String.t()) :: float()
|
||||
defp to_float(value) when is_float(value), do: value
|
||||
defp to_float(value) when is_integer(value), do: value * 1.0
|
||||
|
||||
@spec to_float(String.t()) :: float()
|
||||
defp to_float(value) when is_binary(value) do
|
||||
case Float.parse(value) do
|
||||
{float_val, _} -> float_val
|
||||
:error -> 0.0
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def terminate(_reason, socket) do
|
||||
if socket.assigns.buffer_timer, do: Process.cancel_timer(socket.assigns.buffer_timer)
|
||||
|
|
@ -1022,11 +919,6 @@ defmodule AprsWeb.MapLive.Index do
|
|||
Process.cancel_timer(socket.assigns.bounds_update_timer)
|
||||
end
|
||||
|
||||
# Clean up replay timer
|
||||
if socket.assigns[:replay_timer_ref] do
|
||||
Process.cancel_timer(socket.assigns.replay_timer_ref)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
|
|
|
|||
1
mix.exs
1
mix.exs
|
|
@ -76,6 +76,7 @@ defmodule Aprs.MixProject do
|
|||
{:mix_test_watch, "~> 1.1", only: [:dev, :test]},
|
||||
{:sobelow, "~> 0.8", only: :dev},
|
||||
{:stream_data, "~> 1.2.0", only: [:dev, :test]},
|
||||
{:mox, "~> 1.2", only: :test},
|
||||
{:styler, "~> 1.4.2", only: [:dev, :test], runtime: false}
|
||||
]
|
||||
end
|
||||
|
|
|
|||
2
mix.lock
2
mix.lock
|
|
@ -47,7 +47,9 @@
|
|||
"mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
|
||||
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||
"mix_test_watch": {:hex, :mix_test_watch, "1.3.0", "2ffc9f72b0d1f4ecf0ce97b044e0e3c607c3b4dc21d6228365e8bc7c2856dc77", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "f9e5edca976857ffac78632e635750d158df14ee2d6185a15013844af7570ffe"},
|
||||
"mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"oban": {:hex, :oban, "2.19.4", "045adb10db1161dceb75c254782f97cdc6596e7044af456a59decb6d06da73c1", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fcc6219e6464525b808d97add17896e724131f498444a292071bf8991c99f97"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||
|
|
|
|||
|
|
@ -110,9 +110,7 @@ defmodule AprsWeb.Integration.AprsStatusTest do
|
|||
# Test map ready event
|
||||
assert render_hook(view, "map_ready", %{})
|
||||
|
||||
# Test replay speed adjustment if available
|
||||
speed_params = %{"speed" => "1.0"}
|
||||
render_hook(view, "adjust_replay_speed", speed_params)
|
||||
# Replay functionality has been removed - historical packets are now loaded all at once
|
||||
end
|
||||
|
||||
test "real-time updates are disabled without APRS connection", %{conn: conn} do
|
||||
|
|
|
|||
|
|
@ -64,36 +64,24 @@ defmodule AprsWeb.MapLive.IndexTest do
|
|||
assert render_hook(view, "bounds_changed", bounds_params)
|
||||
end
|
||||
|
||||
test "handles adjust_replay_speed event with float values", %{conn: conn} do
|
||||
test "loads historical packets when map is ready", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Test with float speed value
|
||||
speed_params = %{"speed" => 1.5}
|
||||
assert render_hook(view, "adjust_replay_speed", speed_params)
|
||||
# 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(600)
|
||||
|
||||
# The view should still be rendering without errors after loading historical packets
|
||||
assert render(view) =~ "aprs-map"
|
||||
end
|
||||
|
||||
test "handles adjust_replay_speed event with string values", %{conn: conn} do
|
||||
test "handles clear_and_reload_markers event", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Test with string speed value
|
||||
speed_params = %{"speed" => "2.0"}
|
||||
assert render_hook(view, "adjust_replay_speed", speed_params)
|
||||
end
|
||||
|
||||
test "handles adjust_replay_speed event with integer values", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Test with integer speed value
|
||||
speed_params = %{"speed" => 3}
|
||||
assert render_hook(view, "adjust_replay_speed", speed_params)
|
||||
end
|
||||
|
||||
test "handles adjust_replay_speed event with integer string values", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Test with integer string value (should work with improved Float.parse)
|
||||
speed_params = %{"speed" => "1"}
|
||||
assert render_hook(view, "adjust_replay_speed", speed_params)
|
||||
# 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
|
||||
|
|
|
|||
266
test/integration/historical_packets_test.exs
Normal file
266
test/integration/historical_packets_test.exs
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
defmodule Aprs.Integration.HistoricalPacketsTest do
|
||||
use AprsWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
@moduletag :skip_packets_mock
|
||||
|
||||
describe "historical packet loading" do
|
||||
setup do
|
||||
# Create test packets with different timestamps
|
||||
now = DateTime.utc_now()
|
||||
one_hour_ago = DateTime.add(now, -3600, :second)
|
||||
thirty_minutes_ago = DateTime.add(now, -1800, :second)
|
||||
ten_minutes_ago = DateTime.add(now, -600, :second)
|
||||
|
||||
# Mock packet data for the same callsign at different times
|
||||
packet1 = %Aprs.Packet{
|
||||
id: 1,
|
||||
base_callsign: "TEST1",
|
||||
ssid: 0,
|
||||
has_position: true,
|
||||
lat: 39.8,
|
||||
lon: -98.5,
|
||||
received_at: one_hour_ago,
|
||||
data_extended: %{
|
||||
symbol_table_id: "/",
|
||||
symbol_code: ">",
|
||||
comment: "Old position"
|
||||
}
|
||||
}
|
||||
|
||||
packet2 = %Aprs.Packet{
|
||||
id: 2,
|
||||
base_callsign: "TEST1",
|
||||
ssid: 0,
|
||||
has_position: true,
|
||||
lat: 39.9,
|
||||
lon: -98.4,
|
||||
received_at: thirty_minutes_ago,
|
||||
data_extended: %{
|
||||
symbol_table_id: "/",
|
||||
symbol_code: ">",
|
||||
comment: "Middle position"
|
||||
}
|
||||
}
|
||||
|
||||
packet3 = %Aprs.Packet{
|
||||
id: 3,
|
||||
base_callsign: "TEST1",
|
||||
ssid: 0,
|
||||
has_position: true,
|
||||
lat: 40.0,
|
||||
lon: -98.3,
|
||||
received_at: ten_minutes_ago,
|
||||
data_extended: %{
|
||||
symbol_table_id: "/",
|
||||
symbol_code: ">",
|
||||
comment: "Latest position"
|
||||
}
|
||||
}
|
||||
|
||||
# Mock packet for different callsign
|
||||
packet4 = %Aprs.Packet{
|
||||
id: 4,
|
||||
base_callsign: "TEST2",
|
||||
ssid: 0,
|
||||
has_position: true,
|
||||
lat: 38.8,
|
||||
lon: -97.5,
|
||||
received_at: thirty_minutes_ago,
|
||||
data_extended: %{
|
||||
symbol_table_id: "/",
|
||||
symbol_code: "k",
|
||||
comment: "Different station"
|
||||
}
|
||||
}
|
||||
|
||||
{:ok, packets: [packet1, packet2, packet3, packet4]}
|
||||
end
|
||||
|
||||
test "loads all historical packets at once when map is ready", %{conn: conn, packets: mock_packets} do
|
||||
# Mock the Packets.get_packets_for_replay function
|
||||
expect_packets_for_replay_with_bounds(mock_packets)
|
||||
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Set map bounds that include our test packets
|
||||
bounds_params = %{
|
||||
"bounds" => %{
|
||||
"north" => "45.0",
|
||||
"south" => "35.0",
|
||||
"east" => "-90.0",
|
||||
"west" => "-105.0"
|
||||
}
|
||||
}
|
||||
|
||||
# Update bounds first
|
||||
render_hook(view, "bounds_changed", bounds_params)
|
||||
|
||||
# Trigger map ready event
|
||||
render_hook(view, "map_ready", %{})
|
||||
|
||||
# Wait for the historical packet loading to complete
|
||||
Process.sleep(700)
|
||||
|
||||
# The LiveView should have pushed an event with historical packets
|
||||
# Note: In real implementation, we'd need to verify the push_event was called
|
||||
# For now, we verify the mock was called correctly
|
||||
verify!()
|
||||
end
|
||||
|
||||
test "only loads packets within map bounds", %{conn: conn, packets: mock_packets} do
|
||||
# Mock to return only packets within the specified bounds
|
||||
# TEST2 is at lat: 38.8, lon: -97.5, which is outside these bounds
|
||||
test1_packets = Enum.filter(mock_packets, fn packet -> packet.base_callsign == "TEST1" end)
|
||||
expect_packets_for_replay_with_bounds(test1_packets)
|
||||
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Set map bounds that exclude TEST2
|
||||
bounds_params = %{
|
||||
"bounds" => %{
|
||||
"north" => "41.0",
|
||||
"south" => "39.0",
|
||||
"east" => "-98.0",
|
||||
"west" => "-99.0"
|
||||
}
|
||||
}
|
||||
|
||||
# Update bounds first
|
||||
render_hook(view, "bounds_changed", bounds_params)
|
||||
|
||||
# Trigger map ready event
|
||||
render_hook(view, "map_ready", %{})
|
||||
|
||||
# Wait for the historical packet loading to complete
|
||||
Process.sleep(700)
|
||||
|
||||
verify!()
|
||||
end
|
||||
|
||||
test "handles empty historical packets gracefully", %{conn: conn} do
|
||||
# Mock to return empty list
|
||||
expect_packets_for_replay([])
|
||||
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Trigger map ready event
|
||||
render_hook(view, "map_ready", %{})
|
||||
|
||||
# Wait for the historical packet loading attempt
|
||||
Process.sleep(700)
|
||||
|
||||
# Should not crash
|
||||
verify!()
|
||||
end
|
||||
|
||||
test "clears historical packets when requested", %{conn: conn, packets: mock_packets} do
|
||||
expect_packets_for_replay(mock_packets)
|
||||
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# First load historical packets
|
||||
render_hook(view, "map_ready", %{})
|
||||
Process.sleep(700)
|
||||
|
||||
# Clear and reload markers
|
||||
render_hook(view, "clear_and_reload_markers", %{})
|
||||
|
||||
verify!()
|
||||
end
|
||||
|
||||
test "handles locate_me event after historical packets are loaded", %{conn: conn, packets: mock_packets} do
|
||||
expect_packets_for_replay(mock_packets)
|
||||
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Load historical packets first
|
||||
render_hook(view, "map_ready", %{})
|
||||
Process.sleep(700)
|
||||
|
||||
# Request location
|
||||
render_hook(view, "locate_me", %{})
|
||||
|
||||
verify!()
|
||||
end
|
||||
end
|
||||
|
||||
describe "live packet updates with historical data" do
|
||||
setup do
|
||||
# Create a historical packet
|
||||
now = DateTime.utc_now()
|
||||
thirty_minutes_ago = DateTime.add(now, -1800, :second)
|
||||
|
||||
historical_packet = %Aprs.Packet{
|
||||
id: 100,
|
||||
base_callsign: "LIVE1",
|
||||
ssid: 0,
|
||||
has_position: true,
|
||||
lat: 39.8,
|
||||
lon: -98.5,
|
||||
received_at: thirty_minutes_ago,
|
||||
data_extended: %{
|
||||
symbol_table_id: "/",
|
||||
symbol_code: ">",
|
||||
comment: "Old position"
|
||||
}
|
||||
}
|
||||
|
||||
{:ok, historical_packet: historical_packet}
|
||||
end
|
||||
|
||||
test "new live packet updates marker for same callsign", %{conn: conn, historical_packet: historical_packet} do
|
||||
expect_packets_for_replay([historical_packet])
|
||||
|
||||
{:ok, view, _html} = live(conn, "/")
|
||||
|
||||
# Set bounds and load historical packets
|
||||
bounds_params = %{
|
||||
"bounds" => %{
|
||||
"north" => "45.0",
|
||||
"south" => "35.0",
|
||||
"east" => "-90.0",
|
||||
"west" => "-105.0"
|
||||
}
|
||||
}
|
||||
|
||||
render_hook(view, "bounds_changed", bounds_params)
|
||||
render_hook(view, "map_ready", %{})
|
||||
Process.sleep(700)
|
||||
|
||||
# Simulate a new live packet for the same callsign
|
||||
new_packet = %{
|
||||
id: "LIVE1",
|
||||
callsign: "LIVE1",
|
||||
base_callsign: "LIVE1",
|
||||
ssid: 0,
|
||||
lat: 39.9,
|
||||
lng: -98.4,
|
||||
data_type: "position",
|
||||
path: "WIDE1-1,WIDE2-1",
|
||||
comment: "New live position",
|
||||
data_extended: %{
|
||||
symbol_table_id: "/",
|
||||
symbol_code: ">"
|
||||
},
|
||||
symbol_table_id: "/",
|
||||
symbol_code: ">",
|
||||
timestamp: DateTime.to_iso8601(DateTime.utc_now()),
|
||||
popup: "<div>New live position</div>"
|
||||
}
|
||||
|
||||
# Send the new packet event
|
||||
send(view.pid, %Phoenix.Socket.Broadcast{
|
||||
topic: "aprs_messages",
|
||||
event: "packet",
|
||||
payload: new_packet
|
||||
})
|
||||
|
||||
# Give the LiveView time to process the message
|
||||
Process.sleep(100)
|
||||
|
||||
verify!()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -22,17 +22,28 @@ defmodule AprsWeb.ConnCase do
|
|||
# The default endpoint for testing
|
||||
use AprsWeb, :verified_routes
|
||||
|
||||
import Aprs.MockHelpers
|
||||
|
||||
# Import conveniences for testing with connections
|
||||
import AprsWeb.ConnCase
|
||||
import Mox
|
||||
import Phoenix.ConnTest
|
||||
import Plug.Conn
|
||||
|
||||
@endpoint AprsWeb.Endpoint
|
||||
|
||||
setup :verify_on_exit!
|
||||
end
|
||||
end
|
||||
|
||||
setup tags do
|
||||
Aprs.DataCase.setup_sandbox(tags)
|
||||
|
||||
# Stub PacketsMock by default for tests that use MapLive but don't test historical packets
|
||||
if !tags[:skip_packets_mock] do
|
||||
Aprs.MockHelpers.stub_packets_mock()
|
||||
end
|
||||
|
||||
{:ok, conn: Phoenix.ConnTest.build_conn()}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,11 @@ defmodule Aprs.DataCase do
|
|||
import Ecto
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
import Mox
|
||||
|
||||
alias Aprs.Repo
|
||||
|
||||
setup :verify_on_exit!
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
44
test/support/mocks.ex
Normal file
44
test/support/mocks.ex
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Mocks are defined in test_helper.exs
|
||||
# This file can be used for mock implementations or test utilities
|
||||
|
||||
defmodule Aprs.MockHelpers do
|
||||
@moduledoc """
|
||||
Helper functions for setting up mocks in tests
|
||||
"""
|
||||
|
||||
import Mox
|
||||
|
||||
@doc """
|
||||
Stubs the PacketsMock to return an empty list for get_packets_for_replay.
|
||||
Use this in tests that don't care about historical packets but use MapLive.
|
||||
"""
|
||||
def stub_packets_mock do
|
||||
stub(Aprs.PacketsMock, :get_packets_for_replay, fn _opts -> [] end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets up an expectation for PacketsMock with custom return value.
|
||||
"""
|
||||
def expect_packets_for_replay(packets) do
|
||||
expect(Aprs.PacketsMock, :get_packets_for_replay, fn _opts -> packets end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets up an expectation for PacketsMock with filtering based on bounds.
|
||||
"""
|
||||
def expect_packets_for_replay_with_bounds(packets) do
|
||||
expect(Aprs.PacketsMock, :get_packets_for_replay, fn opts ->
|
||||
case opts[:bounds] do
|
||||
[west, south, east, north] ->
|
||||
Enum.filter(packets, fn packet ->
|
||||
packet.has_position &&
|
||||
packet.lat >= south && packet.lat <= north &&
|
||||
packet.lon >= west && packet.lon <= east
|
||||
end)
|
||||
|
||||
_ ->
|
||||
packets
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
ExUnit.start()
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Aprs.Repo, :manual)
|
||||
|
||||
# Configure Mox
|
||||
Mox.defmock(Aprs.PacketsMock, for: Aprs.PacketsBehaviour)
|
||||
Mox.defmock(Aprs.PacketReplayMock, for: Aprs.PacketReplayBehaviour)
|
||||
|
||||
# Ensure no external APRS connections during tests
|
||||
Application.put_env(:aprs, :disable_aprs_connection, true)
|
||||
Application.put_env(:aprs, :aprs_is_server, "mock.aprs.test")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue