prop/test/microwaveprop_web/live/pskr_spots_live_test.exs
Graham McInitre cdf4c75dd5 feat: clickable band filters on PSK Reporter tab, fix sync blocking in refresh
- Fix handle_info(:refresh_spots) to use start_async instead of blocking
  LiveView process on DB fetches
- Add require Logger to fix pre-existing compiler warning
- Make band counts clickable: clicking a band filters table to that band's
  last 100 spots; clicking again clears the filter
- Highlight active filter badge, show Clear filter link when filtered
- Update subtitle and empty state dynamically based on filter
- Filter persists across auto-refresh (60s)
- Add 5 test cases for band filtering behavior
2026-07-20 12:41:18 -05:00

294 lines
8.3 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

defmodule MicrowavepropWeb.PskrSpotsLiveTest do
use MicrowavepropWeb.ConnCase, async: true
import Phoenix.LiveViewTest
alias Microwaveprop.Repo
defp now, do: DateTime.utc_now()
defp dump_uuid, do: Ecto.UUID.dump!(Ecto.UUID.generate())
defp insert_spot(attrs) do
defaults = %{
hour_utc: now(),
band: "2m",
sender_grid: "EM12KL",
receiver_grid: "DM43ST",
spot_count: 1
}
fields = Map.merge(defaults, attrs)
Repo.query!(
"""
INSERT INTO pskr_spots_hourly
(id, hour_utc, band, sender_grid, receiver_grid,
sender_lat, sender_lon, receiver_lat, receiver_lon,
distance_km, spot_count, max_snr_db, min_snr_db,
modes, sender_callsigns, receiver_callsigns,
first_spot_at, last_spot_at,
inserted_at, updated_at)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $19)
""",
[
dump_uuid(),
fields.hour_utc,
fields.band,
fields.sender_grid,
fields.receiver_grid,
Map.get(fields, :sender_lat, 32.5),
Map.get(fields, :sender_lon, -96.5),
Map.get(fields, :receiver_lat, 33.5),
Map.get(fields, :receiver_lon, -110.5),
Map.get(fields, :distance_km, 1_200.0),
fields.spot_count,
Map.get(fields, :max_snr_db, 5),
Map.get(fields, :min_snr_db, 0),
Map.get(fields, :modes, ["FT8"]),
Map.get(fields, :sender_callsigns, []),
Map.get(fields, :receiver_callsigns, []),
Map.get(fields, :first_spot_at, fields.hour_utc),
Map.get(fields, :last_spot_at, fields.hour_utc)
]
)
end
describe "page rendering" do
test "renders the pskr spots page", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/pskreporter")
assert html =~ "PSK Reporter Spots"
end
test "sets page title", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/pskreporter")
assert html =~ "PSK Reporter Spots"
end
test "shows empty state when no spots exist", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/pskreporter")
assert html =~ "No PSK Reporter spots received yet"
end
test "shows total spots count of zero when empty", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/pskreporter")
assert html =~ "Total spots stored:"
assert html =~ "0"
end
end
describe "with spots in the database" do
test "displays total spots count and per-band counts", %{conn: conn} do
insert_spot(%{band: "2m", spot_count: 42})
insert_spot(%{band: "70cm", spot_count: 7})
{:ok, _lv, html} = live(conn, ~p"/pskreporter")
assert html =~ "Total spots stored:"
# 42 + 7 = 49 total
assert html =~ "49"
# Band badges show per-band counts
assert html =~ "2m"
assert html =~ "70cm"
end
test "displays band as badge", %{conn: conn} do
insert_spot(%{band: "70cm"})
{:ok, _lv, html} = live(conn, ~p"/pskreporter")
assert html =~ "70cm"
end
test "displays SNR range", %{conn: conn} do
insert_spot(%{min_snr_db: -5, max_snr_db: 12})
{:ok, _lv, html} = live(conn, ~p"/pskreporter")
assert html =~ "-512 dB"
end
test "displays single SNR when min equals max", %{conn: conn} do
insert_spot(%{min_snr_db: 3, max_snr_db: 3})
{:ok, _lv, html} = live(conn, ~p"/pskreporter")
assert html =~ "3 dB"
refute html =~ "33 dB"
end
test "displays sender and receiver callsigns", %{conn: conn} do
insert_spot(%{sender_callsigns: ["K5ABC"], receiver_callsigns: ["K7XYZ"]})
{:ok, _lv, html} = live(conn, ~p"/pskreporter")
assert html =~ "K5ABC"
assert html =~ "K7XYZ"
end
test "displays multiple callsigns comma-separated", %{conn: conn} do
insert_spot(%{sender_callsigns: ["K5ABC", "W1AW"], receiver_callsigns: ["K7XYZ"]})
{:ok, _lv, html} = live(conn, ~p"/pskreporter")
assert html =~ "K5ABC, W1AW"
end
test "shows dash for empty callsigns", %{conn: conn} do
insert_spot(%{sender_callsigns: [], receiver_callsigns: []})
{:ok, _lv, html} = live(conn, ~p"/pskreporter")
assert html =~ ""
end
test "displays modes", %{conn: conn} do
insert_spot(%{modes: ["FT8", "FT4"]})
{:ok, _lv, html} = live(conn, ~p"/pskreporter")
assert html =~ "FT8"
assert html =~ "FT4"
end
test "shows most recent spots first", %{conn: conn} do
t1 = ~U[2026-07-01 12:00:00Z]
t2 = ~U[2026-07-01 13:00:00Z]
insert_spot(%{sender_grid: "EARLIER", last_spot_at: t1, hour_utc: t1})
insert_spot(%{sender_grid: "LATER", last_spot_at: t2, hour_utc: t2})
{:ok, _lv, html} = live(conn, ~p"/pskreporter")
# LATER should appear before EARLIER in the HTML
assert String.contains?(html, "LATER")
assert String.contains?(html, "EARLIER")
assert String.index(html, "LATER") < String.index(html, "EARLIER")
end
test "limits to 100 most recent spots", %{conn: conn} do
# Insert 105 spots — only the 100 most recent should render
for i <- 1..105 do
t = DateTime.add!(now(), i - 105, :second)
insert_spot(%{sender_grid: "GRID#{i}", last_spot_at: t, hour_utc: t})
end
{:ok, _lv, html} = live(conn, ~p"/pskreporter")
# The oldest 5 should be excluded (GRID1GRID5 have the earliest timestamps)
refute html =~ "GRID1"
refute html =~ "GRID2"
refute html =~ "GRID3"
refute html =~ "GRID4"
refute html =~ "GRID5"
# The most recent 100 should be present
assert html =~ "GRID105"
assert html =~ "GRID6"
end
end
describe "band filtering" do
test "filtering by band shows only that band's spots", %{conn: conn} do
insert_spot(%{band: "2m", sender_grid: "GRID2M", last_spot_at: now()})
insert_spot(%{band: "70cm", sender_grid: "GRID70", last_spot_at: now()})
{:ok, lv, _html} = live(conn, ~p"/pskreporter")
# Both bands visible initially
html = render(lv)
assert html =~ "GRID2M"
assert html =~ "GRID70"
# Click the 2m badge
html =
lv
|> element("button[phx-value-band=\"2m\"]")
|> render_click()
assert html =~ "GRID2M"
refute html =~ "GRID70"
# Filter badge should be highlighted
assert html =~ "bg-primary/15"
end
test "clicking the active band again clears the filter", %{conn: conn} do
insert_spot(%{band: "2m", sender_grid: "GRID2M", last_spot_at: now()})
insert_spot(%{band: "70cm", sender_grid: "GRID70", last_spot_at: now()})
{:ok, lv, _html} = live(conn, ~p"/pskreporter")
# Filter to 2m
html =
lv
|> element("button[phx-value-band=\"2m\"]")
|> render_click()
refute html =~ "GRID70"
# Click 2m again to clear
html =
lv
|> element("button[phx-value-band=\"2m\"]")
|> render_click()
assert html =~ "GRID2M"
assert html =~ "GRID70"
end
test "clear filter button appears when filter is active", %{conn: conn} do
insert_spot(%{band: "2m", last_spot_at: now()})
{:ok, lv, _html} = live(conn, ~p"/pskreporter")
# No clear filter button initially
refute render(lv) =~ "Clear filter"
# Filter by band
html =
lv
|> element("button[phx-value-band=\"2m\"]")
|> render_click()
assert html =~ "Clear filter"
# Click clear filter
html =
lv
|> element("button", "Clear filter")
|> render_click()
refute html =~ "Clear filter"
end
test "subtitle reflects active filter", %{conn: conn} do
insert_spot(%{band: "70cm", last_spot_at: now()})
{:ok, lv, html} = live(conn, ~p"/pskreporter")
assert html =~ "from the MQTT firehose"
html =
lv
|> element("button[phx-value-band=\"70cm\"]")
|> render_click()
assert html =~ "70cm"
refute html =~ "from the MQTT firehose"
end
test "empty state shows band name when filtered", %{conn: conn} do
insert_spot(%{band: "2m", last_spot_at: now()})
{:ok, lv, _html} = live(conn, ~p"/pskreporter")
html =
lv
|> element("button[phx-value-band=\"70cm\"]")
|> render_click()
assert html =~ "No spots found for band 70cm"
end
end
end