prop/test/microwaveprop_web/live/contact_live_test.exs
Graham McIntire fc245367e3
User profiles at /u/:callsign, flexible band input, assorted UX cleanup
Profile page:
  * New MicrowavepropWeb.UserProfileLive at /u/:callsign is a public
    page showing a user's contacts and beacons. Resolves case-
    insensitively so /u/w5isp and /u/W5ISP are the same thing; unknown
    callsigns redirect to /. Uses daisyUI card / stats / table
    components with an avatar-placeholder initial and hero icons.
  * Accounts.get_user_by_callsign/1 (case-insensitive) plus
    Radio.list_contacts_for_user/1 and Beacons.list_beacons_for_user/1
    back the page. Beacons list includes both approved and pending so
    owners see their drafts.
  * The top nav bar and the three LiveView sidebars (MapLive,
    WeatherMapLive, ContactMapLive) now render the logged-in callsign
    as a navigate link to /u/:callsign instead of a static label.
  * Nine new tests cover the lookup, the LiveView render, and the
    ownership-scoped queries.

Flexible band input:
  * New Microwaveprop.Radio.BandResolver module converts any of:
    ADIF wavelength labels ("33cm", "1.25cm", "6mm", case/whitespace
    insensitive), numeric frequency strings ("903.100", "10368.000"),
    and canonical MHz integers into the one of the site's known bands.
    Returns the nearest allowed band for numeric inputs >= 900 MHz,
    nil otherwise.
  * 902 MHz is added to Contact.@allowed_bands, ContactEdit.@allowed_bands,
    AdifImport.@allowed_bands, and the BandResolver list so "33cm"
    round-trips end-to-end.
  * AdifImport and CsvImport now delegate band resolution to
    BandResolver, and Radio.create_contact/2 normalizes the :band attr
    on the way in so the manual form and any API callers benefit too.
    CsvImport's "invalid band" tests previously used 99999 MHz which
    the new resolver snaps to the nearest allowed band; swapped to
    "notaband" which is truly unresolvable.

Contacts and beacons list UX:
  * Remove the "Submitted" column from /contacts — it duplicated info
    already visible on the detail page and was pushing the real
    columns off narrow viewports. submitted_cell/1 and its three
    column-specific tests go with it.
  * Hide the Lat / Lon columns from /beacons — six decimal places of
    coordinates weren't useful next to the grid square and took a
    disproportionate amount of row width.
2026-04-12 16:13:08 -05:00

168 lines
4.9 KiB
Elixir

defmodule MicrowavepropWeb.ContactLiveTest do
use MicrowavepropWeb.ConnCase, async: true
import Phoenix.LiveViewTest
alias Microwaveprop.Radio.Contact
alias Microwaveprop.Repo
setup do
Req.Test.stub(Microwaveprop.Terrain.ElevationClient, fn conn ->
Req.Test.json(conn, [])
end)
Req.Test.stub(Microwaveprop.Weather.HrrrClient, fn conn ->
Plug.Conn.send_resp(conn, 404, "not found")
end)
Req.Test.stub(Microwaveprop.Weather.SolarClient, fn conn ->
Req.Test.text(conn, "")
end)
:ok
end
defp create_contact(attrs \\ %{}) do
default = %{
station1: "W5XD",
station2: "K5TR",
qso_timestamp: ~U[2026-03-28 18:00:00Z],
mode: "CW",
band: Decimal.new("1296"),
grid1: "EM12",
grid2: "EM00",
pos1: %{"lat" => 32.9, "lon" => -97.0},
pos2: %{"lat" => 30.3, "lon" => -97.7},
distance_km: Decimal.new("295")
}
{changeset_attrs, direct_attrs} = Map.split(Map.merge(default, attrs), [:user_submitted])
{:ok, contact} =
%Contact{}
|> Contact.changeset(direct_attrs)
|> Ecto.Changeset.change(changeset_attrs)
|> Repo.insert()
contact
end
describe "Index" do
test "renders page with Contacts heading", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/contacts")
assert html =~ "Contacts"
end
test "table shows contact data", %{conn: conn} do
create_contact()
{:ok, _lv, html} = live(conn, ~p"/contacts")
assert html =~ "W5XD"
assert html =~ "K5TR"
assert html =~ "CW"
assert html =~ "1296"
end
test "row links to detail page", %{conn: conn} do
contact = create_contact()
{:ok, _lv, html} = live(conn, ~p"/contacts")
assert html =~ ~p"/contacts/#{contact.id}"
end
test "sort params order the table", %{conn: conn} do
create_contact(%{station1: "ZZ9ZZ"})
create_contact(%{station1: "AA1AA"})
{:ok, _lv, html} = live(conn, ~p"/contacts?sort_by=station1&sort_order=asc")
# AA1AA should appear before ZZ9ZZ
aa_pos = html |> :binary.match("AA1AA") |> elem(0)
zz_pos = html |> :binary.match("ZZ9ZZ") |> elem(0)
assert aa_pos < zz_pos
end
test "search by single callsign matches either station", %{conn: conn} do
create_contact(%{station1: "W5LUA", station2: "W5HN"})
create_contact(%{station1: "K5TR", station2: "N5AC"})
{:ok, _lv, html} = live(conn, ~p"/contacts?search=W5LUA")
assert html =~ "W5LUA"
refute html =~ "K5TR"
end
test "search by two callsigns finds contacts between that pair", %{conn: conn} do
create_contact(%{station1: "W5LUA", station2: "W5HN"})
create_contact(%{station1: "W5HN", station2: "W5LUA"})
create_contact(%{station1: "W5LUA", station2: "K5TR"})
{:ok, _lv, html} = live(conn, ~p"/contacts?search=W5LUA+W5HN")
assert html =~ "W5LUA"
assert html =~ "W5HN"
# The W5LUA-K5TR contact should not appear
refute html =~ "K5TR"
end
test "pagination works", %{conn: conn} do
for i <- 1..25 do
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
create_contact(%{qso_timestamp: ts})
end
{:ok, _lv, html} = live(conn, ~p"/contacts")
assert html =~ "Page 1 of 2"
{:ok, _lv, html2} = live(conn, ~p"/contacts?page=2")
assert html2 =~ "Page 2 of 2"
end
end
describe "Show" do
test "renders contact detail fields", %{conn: conn} do
contact = create_contact()
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "W5XD"
assert html =~ "K5TR"
# Mode, band, grids are shown in the elevation profile section (requires SRTM)
# or in the header subtitle
assert html =~ "2026-03-28"
end
test "shows surface observations section", %{conn: conn} do
contact = create_contact()
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "Surface Observations"
end
test "shows sounding section", %{conn: conn} do
contact = create_contact()
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "Soundings"
end
test "shows solar index section", %{conn: conn} do
contact = create_contact()
{:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "Solar Conditions"
end
test "renders with sort assigns", %{conn: conn} do
contact = create_contact()
{:ok, lv, html} = live(conn, ~p"/contacts/#{contact.id}")
# Page renders without error
assert html =~ "Surface Observations"
assert html =~ "Soundings"
# Sort assigns are initialized (no crash on render)
assert render(lv) =~ contact.station1
end
test "raises for bad UUID", %{conn: conn} do
assert_raise Ecto.NoResultsError, fn ->
live(conn, ~p"/contacts/#{Ecto.UUID.generate()}")
end
end
end
end