Dependency updates:
- phoenix_live_view: ~> 1.2.0 (was ~> 1.1.0)
- Transitive bumps: bandit 1.11.1→1.12.0, credo 1.7.18→1.7.19,
phoenix 1.8.7→1.8.8, req 0.5.18→0.6.1, swoosh 1.26.0→1.26.1
Fixes:
- core_components.ex: add 'type' to button :global allowlist
(Phoenix 1.8+ attribute validation)
- status_live_test.exs: invalidate {:all_stats} cache key to
prevent stale ETS cache from leaking between tests
- path_live_test.exs: accept all valid async states in assertion
(race between progress frame and compute completion)
- skewt_live_test.exs: accept loading or completed state in assertion
HEEx auto-formatting from mix format (self-closing tag normalization)
233 lines
7.6 KiB
Elixir
233 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 "loading indicator visible while async is in flight", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/skewt?q=EM12kp")
|
|
|
|
# The async may have already completed by the time we render.
|
|
# Assert that EITHER the loading indicator is still showing
|
|
# OR the resolved content appeared.
|
|
html = render(view)
|
|
assert has_element?(view, ".loading") or 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
|