Historical contacts showed a skew-T log-P diagram that stopped at 700 mb
because HrrrClient and Era5Client only fetched pressure-level data down
to the top of the boundary layer. The chart canvas already ran up to
100 mb, so the trace clipped mid-atmosphere.
Two complementary fixes:
1. Extend @pressure_levels in HrrrClient and Era5Client with
650/600/550/500/450/400/350/300/250/200/150/100 mb so new fetches
cover the full troposphere + lower stratosphere.
2. Prefer the native hybrid-sigma profile for the contact-detail
skew-T when one has been backfilled for the contact's hour. The
native profile already stores all 50 hybrid levels up to ~19 km,
so historical contacts covered by the native backfill get a full
trace without re-hitting S3. A new HrrrNativeProfile.to_skew_t_profile/1
converts the parallel arrays into the %{"pres","tmpc","dwpc","hght"}
list shape the renderer expects, deriving dewpoint from SPFH via the
Magnus inverse. Weather.find_nearest_native_profile/3 mirrors
find_nearest_hrrr/3 for the lookup.
253 lines
7.9 KiB
Elixir
253 lines
7.9 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
|
|
alias Microwaveprop.Weather.HrrrNativeProfile
|
|
|
|
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
|
|
|
|
test "skew-T uses native HRRR profile when backfilled", %{conn: conn} do
|
|
# Native backfill has reached this hour — use its full-atmosphere
|
|
# profile for the skew-T instead of the 700 mb-capped pressure-level one.
|
|
contact =
|
|
create_contact(%{
|
|
qso_timestamp: ~U[2024-06-15 18:00:00Z],
|
|
pos1: %{"lat" => 32.9, "lon" => -97.0},
|
|
pos2: %{"lat" => 30.3, "lon" => -97.7}
|
|
})
|
|
|
|
# ERA5 row keeps the atmospheric profile section rendering with its
|
|
# aggregate metadata (HPBL / PWAT / surface scalars).
|
|
{:ok, _} =
|
|
Repo.insert(%Era5Profile{
|
|
valid_time: ~U[2024-06-15 18:00:00Z],
|
|
lat: 32.9,
|
|
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
|
|
})
|
|
|
|
# Native profile at the same point/time. `pressure_pa: [..., 30_000.0]`
|
|
# corresponds to 300 mb — a level the pressure-level profile cannot
|
|
# cover because HRRR/ERA5 stopped at 700 mb historically.
|
|
{:ok, _} =
|
|
%HrrrNativeProfile{}
|
|
|> HrrrNativeProfile.changeset(%{
|
|
valid_time: ~U[2024-06-15 18:00:00Z],
|
|
run_time: ~U[2024-06-15 18:00:00Z],
|
|
lat: 32.9,
|
|
lon: -97.0,
|
|
level_count: 3,
|
|
heights_m: [10.0, 1500.0, 9500.0],
|
|
temp_k: [298.0, 288.0, 228.0],
|
|
spfh: [0.012, 0.006, 0.0001],
|
|
pressure_pa: [101_000.0, 85_000.0, 30_000.0],
|
|
u_wind_ms: [2.0, 5.0, 20.0],
|
|
v_wind_ms: [1.0, 2.0, 5.0],
|
|
tke_m2s2: [0.5, 0.2, 0.05]
|
|
})
|
|
|> Repo.insert()
|
|
|
|
{:ok, lv, _html} = live(conn, ~p"/contacts/#{contact.id}")
|
|
html = render_async(lv)
|
|
|
|
assert html =~ "HRRR native"
|
|
# 300 mb comes from the native profile — pressure-level data capped at 700.
|
|
assert html =~ "300.0"
|
|
end
|
|
end
|
|
end
|