Contact detail page now renders an atmospheric profile section backed by HRRR when available and ERA5 (reanalysis) otherwise, labeled with the data source. When both sources are missing and HRRR is known-unavailable (pre-2014 contacts), the Era5FetchWorker is enqueued so the month-tile backfill gets triggered from a user visit. Status page ERA5 progress previously computed "complete" as (total_contacts - hrrr_unavailable_contacts), which treated every contact with any HRRR status as ERA5-complete — inflating the bar to 76% while era5_profiles had 0 rows. Replaced with a per-candidate EXISTS query against era5_profiles using the same 0.15°/30-min tolerance Weather.find_nearest_era5/3 uses at lookup time.
195 lines
5.8 KiB
Elixir
195 lines
5.8 KiB
Elixir
defmodule MicrowavepropWeb.ContactLiveTest do
|
|
use MicrowavepropWeb.ConnCase, async: true
|
|
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Microwaveprop.Radio.Contact
|
|
alias Microwaveprop.Repo
|
|
alias Microwaveprop.Weather.Era5Profile
|
|
|
|
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_params[station1]=asc")
|
|
|
|
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")
|
|
# W5LUA appears in the matching row; K5TR/N5AC should not render in
|
|
# the table body because the search filtered that contact out.
|
|
assert html =~ "W5LUA"
|
|
refute html =~ "N5AC"
|
|
end
|
|
|
|
test "pagination reaches later pages", %{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, station1: "W#{i}TEST"})
|
|
end
|
|
|
|
{:ok, _lv, html} = live(conn, ~p"/contacts")
|
|
assert html =~ "Page"
|
|
|
|
{:ok, _lv, html2} = live(conn, ~p"/contacts?page=2")
|
|
assert html2 =~ "Page"
|
|
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
|
|
|
|
test "shows atmospheric profile section with ERA5 source for pre-HRRR contact", %{conn: conn} do
|
|
# Pre-2014 contact — HRRR is unavailable, ERA5 is the fallback source.
|
|
contact =
|
|
create_contact(%{
|
|
qso_timestamp: ~U[2010-06-15 18:00:00Z],
|
|
pos1: %{"lat" => 32.9, "lon" => -97.0},
|
|
pos2: %{"lat" => 30.3, "lon" => -97.7}
|
|
})
|
|
|
|
{:ok, _} =
|
|
Repo.insert(%Era5Profile{
|
|
valid_time: ~U[2010-06-15 18:00:00Z],
|
|
lat: 33.0,
|
|
lon: -97.0,
|
|
profile: [
|
|
%{"pres" => 850.0, "tmpc" => 15.0, "dwpc" => 10.0, "hght" => 1500.0}
|
|
],
|
|
surface_temp_c: 25.0,
|
|
surface_dewpoint_c: 18.0,
|
|
surface_pressure_mb: 1012.0,
|
|
hpbl_m: 1200.0,
|
|
pwat_mm: 30.0,
|
|
surface_refractivity: 320.0,
|
|
min_refractivity_gradient: -50.0,
|
|
ducting_detected: false
|
|
})
|
|
|
|
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
|
|
|
|
html = render_async(lv)
|
|
assert html =~ "Atmospheric Profile"
|
|
assert html =~ "ERA5"
|
|
# Collapsed summary shows HPBL / PWAT / lat,lon from the injected ERA5 row.
|
|
assert html =~ "1200"
|
|
assert html =~ "33.0"
|
|
end
|
|
end
|
|
end
|