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