prop/test/microwaveprop_web/live/skewt_live_test.exs
Graham McIntire fd976b0cd5
fix: resolve 391 Credo issues across codebase
- Add jump_credo_checks ~> 0.4 with all 20 checks enabled
- Fix all standard Credo issues: 139 @spec (113 done, 26 remain),
  4 refactoring, 3 alias usage, 9 System.cmd env, 5 unsafe_to_atom,
  2 max line length, 9 assert_receive timeout
- Fix 170+ jump_credo_checks warnings:
  - 117 TopLevelAliasImportRequire: move nested alias/import to module top
  - 32 UseObanProWorker: switch to Oban.Pro.Worker
  - 4 DoctestIExExamples: add doctests / create test file
  - ~20 WeakAssertion: strengthen type-check assertions
  - Various ConditionalAssertion, AssertReceiveTimeout fixes
- Exclude vendor/ from Credo analysis
- Remaining: 175 warnings (mostly opinionated WeakAssertion,
  AvoidSocketAssignsInTest), 26 @spec annotations
2026-06-12 13:51:32 -05:00

232 lines
7.6 KiB
Elixir

defmodule MicrowavepropWeb.SkewtLiveTest do
use MicrowavepropWeb.ConnCase, async: false
import Phoenix.LiveViewTest
alias Microwaveprop.Weather.HrrrProfile
alias Microwaveprop.Weather.Sounding
alias Microwaveprop.Weather.Station
describe "with HRRR profile data near the queried location" do
setup do
profile_data = [
%{"pres" => 1000.0, "hght" => 110, "tmpc" => 25.0, "dwpc" => 18.0},
%{"pres" => 925.0, "hght" => 770, "tmpc" => 18.0, "dwpc" => 12.0},
%{"pres" => 850.0, "hght" => 1500, "tmpc" => 14.0, "dwpc" => 8.0},
%{"pres" => 700.0, "hght" => 3000, "tmpc" => 0.0, "dwpc" => -10.0},
%{"pres" => 500.0, "hght" => 5500, "tmpc" => -20.0, "dwpc" => -30.0},
%{"pres" => 400.0, "hght" => 7000, "tmpc" => -30.0, "dwpc" => -40.0},
%{"pres" => 300.0, "hght" => 9000, "tmpc" => -45.0, "dwpc" => -55.0},
%{"pres" => 250.0, "hght" => 10_500, "tmpc" => -55.0, "dwpc" => -65.0},
%{"pres" => 200.0, "hght" => 12_000, "tmpc" => -55.0, "dwpc" => -65.0},
%{"pres" => 150.0, "hght" => 13_500, "tmpc" => -55.0, "dwpc" => -65.0},
%{"pres" => 100.0, "hght" => 16_000, "tmpc" => -55.0, "dwpc" => -65.0}
]
# Insert near EM12kp (32.646, -97.125)
now = DateTime.truncate(DateTime.utc_now(), :second)
%HrrrProfile{}
|> HrrrProfile.changeset(%{
valid_time: now,
run_time: now,
lat: 32.65,
lon: -97.12,
profile: profile_data,
hpbl_m: 1500.0,
pwat_mm: 25.0,
surface_temp_c: 25.0,
surface_dewpoint_c: 18.0,
surface_pressure_mb: 1013.0,
surface_refractivity: 320.5,
min_refractivity_gradient: -45.0,
ducting_detected: false
})
|> Microwaveprop.Repo.insert!()
%{valid_time: now}
end
test "renders the time selector when valid_times list is non-empty", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/skewt?q=EM12kp")
_ = render_async(view)
# We can't reliably assert HRRR data renders since fetch_rich_cell
# tries an HTTP call that fails — but the page should not crash.
assert render(view) =~ "Skew-T"
end
test "select_time event triggers a re-load with the chosen valid_time", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/skewt?q=EM12kp")
_ = render_async(view)
# Trigger select_time with a valid future ISO. Even if no profile is
# found for this time, the handler exercises the full re-load path.
future = DateTime.utc_now() |> DateTime.add(3600, :second) |> DateTime.to_iso8601()
_ = render_hook(view, "select_time", %{"valid_time" => future})
_ = render_async(view)
assert render(view) =~ "Skew-T"
end
end
describe "with a sounding readout near the queried point" do
setup do
# Insert a station + sounding so build_sounding_readout returns
# non-nil and renders the side panel.
now = DateTime.truncate(DateTime.utc_now(), :second)
{:ok, station} =
%Station{}
|> Station.changeset(%{
station_code: "FWD",
station_type: "sounding",
name: "Fort Worth",
lat: 32.835,
lon: -97.298
})
|> Microwaveprop.Repo.insert()
sounding_attrs = %{
station_id: station.id,
observed_at: now,
ducting_detected: false,
min_refractivity_gradient: -45.0,
boundary_layer_depth_m: 1500.0,
precipitable_water_mm: 25.0,
surface_refractivity: 320.0,
level_count: 3,
profile: [
%{"pres" => 1000.0, "hght" => 110, "tmpc" => 25.0, "dwpc" => 18.0},
%{"pres" => 925.0, "hght" => 770, "tmpc" => 18.0, "dwpc" => 12.0},
%{"pres" => 850.0, "hght" => 1500, "tmpc" => 14.0, "dwpc" => 8.0}
]
}
%Sounding{}
|> Sounding.changeset(sounding_attrs)
|> Microwaveprop.Repo.insert!()
:ok
end
test "renders without crashing when sounding data is in the DB", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/skewt?q=EM12kp")
_ = render_async(view)
assert render(view) =~ "Skew-T"
end
end
describe "select_time event" do
test "ignored when no location is set yet", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/skewt")
# No location resolved → select_time falls through to the no-op.
result = render_hook(view, "select_time", %{"valid_time" => "2026-04-29T12:00:00Z"})
# Render still works.
assert result =~ "Skew-T"
end
test "ignored when valid_time is unparseable", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/skewt?q=EM12kp")
render_async(view)
result = render_hook(view, "select_time", %{"valid_time" => "not-a-timestamp"})
assert result =~ "Skew-T"
end
end
describe "search event" do
test "patches URL when user submits the form", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/skewt")
render_hook(view, "search", %{"q" => "EM13"})
assert render_async(view) =~ "EM13"
end
end
describe "GET /skewt" do
test "renders search bar with no results when no query is supplied", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/skewt")
assert html =~ "Skew-T-Log-P"
assert html =~ ~s|placeholder="EM12kp|
refute html =~ "viewBox=\"0 0 720"
end
test "initial render with a URL query shows the page chrome + a loading indicator", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/skewt?q=EM12kp")
assert html =~ "Skew-T-Log-P"
assert html =~ ~s|placeholder="EM12kp|
assert html =~ "Resolving location"
refute html =~ "EM12KP"
end
test "after the async completes the resolved location label appears", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/skewt?q=EM12kp")
html = render_async(view)
assert html =~ "EM12KP"
assert html =~ "32.646"
assert html =~ "-97.125"
refute html =~ "Resolving location"
end
test "submitting the form patches the URL with the query", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/skewt")
view |> form("form", q: "EM12") |> render_submit()
assert render_async(view) =~ "EM12"
end
test "shows info alert when no HRRR profiles are stored", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/skewt?q=EM12kp")
html = render_async(view)
assert html =~ "No HRRR profiles are currently stored"
assert has_element?(view, ".alert.alert-info")
end
test "time selector is not shown when there are no valid times", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/skewt?q=EM12kp")
render_async(view)
refute has_element?(view, "button[phx-click=select_time]")
end
test "page renders with loading indicator or resolved content after mount", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/skewt?q=EM12kp")
# The async may have already completed by the time we render.
# Await it and assert the resolved state.
html = render_async(view)
assert html =~ "EM12KP"
end
test "on successful async the loading state is cleared and location shown", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/skewt?q=EM12kp")
html = render_async(view)
refute html =~ "Resolving location"
assert html =~ "EM12KP"
assert html =~ "32.646"
end
test "coordinates are formatted with 3 decimal places", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/skewt?q=EM12kp")
html = render_async(view)
assert html =~ "32.646"
assert html =~ "-97.125"
end
end
end