- Remove the 7-day outlook strip and daily_outlook_at/3. It was derived from point_forecast, which is capped at HRRR's 18-hour horizon, so it never had 7 days of data to show. - factors_for: fall back to the nearest persisted analysis profile at or before the requested time. Only f00 hours persist profiles, so clicking any other hour used to yield an empty Analysis and factor table. After the recent cursor fix lands 'Now' on a forecast hour more often, this regressed further. - available_valid_times/1: cap the forward horizon to 18h from now. Leftover valid_times from prior cycles used to pile on the timeline without adding information.
294 lines
9.9 KiB
Elixir
294 lines
9.9 KiB
Elixir
defmodule MicrowavepropWeb.MapLiveTest do
|
|
use MicrowavepropWeb.ConnCase, async: false
|
|
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Microwaveprop.Propagation
|
|
alias Microwaveprop.Propagation.ScoreCache
|
|
|
|
describe "mount" do
|
|
test "renders full-page map", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map")
|
|
assert html =~ ~s(id="propagation-map")
|
|
assert html =~ ~s(phx-hook="PropagationMap")
|
|
end
|
|
|
|
test "renders band dropdown with all bands", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map")
|
|
assert html =~ "10 GHz"
|
|
assert html =~ "24 GHz"
|
|
assert html =~ "47 GHz"
|
|
assert html =~ "75 GHz"
|
|
assert html =~ "241 GHz"
|
|
end
|
|
|
|
test "defaults to 10 GHz selected", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map")
|
|
assert html =~ ~s(class="active")
|
|
end
|
|
|
|
test "includes data-scores attribute", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map")
|
|
assert html =~ "data-scores"
|
|
end
|
|
|
|
test "renders grid toggle", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map")
|
|
assert html =~ "Grid squares"
|
|
assert html =~ ~s(phx-click="toggle_grid")
|
|
end
|
|
|
|
test "renders scoring algorithm link", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map")
|
|
assert html =~ "Scoring Algorithm"
|
|
assert html =~ ~s(/algo)
|
|
end
|
|
|
|
test "renders submit QSO link", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map")
|
|
assert html =~ "Submit a Contact"
|
|
assert html =~ ~s(/submit)
|
|
end
|
|
end
|
|
|
|
describe "CONUS-only banner" do
|
|
@banner_copy "This data set currently covers the continental United States only."
|
|
|
|
test "is hidden when no CF lat/lon in session", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map")
|
|
refute html =~ ~s(id="conus-banner")
|
|
refute html =~ @banner_copy
|
|
end
|
|
|
|
test "is hidden when CF lat/lon is inside CONUS (DFW)", %{conn: conn} do
|
|
conn = Phoenix.ConnTest.init_test_session(conn, %{cf_lat: 32.9, cf_lon: -97.0})
|
|
{:ok, _lv, html} = live(conn, ~p"/map")
|
|
refute html =~ ~s(id="conus-banner")
|
|
refute html =~ @banner_copy
|
|
end
|
|
|
|
test "is visible when CF lat/lon is outside CONUS (London)", %{conn: conn} do
|
|
conn = Phoenix.ConnTest.init_test_session(conn, %{cf_lat: 51.5, cf_lon: -0.1})
|
|
{:ok, _lv, html} = live(conn, ~p"/map")
|
|
assert html =~ ~s(id="conus-banner")
|
|
assert html =~ @banner_copy
|
|
end
|
|
end
|
|
|
|
describe "select_band" do
|
|
test "updates selected band on event", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/map")
|
|
|
|
html = render_click(lv, "select_band", %{"value" => 24_000})
|
|
|
|
assert html =~ "24 GHz"
|
|
end
|
|
end
|
|
|
|
describe "compute_viewshed" do
|
|
test "accepts compute_viewshed event without crashing", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/map")
|
|
# Event should be accepted; async computation may fail (no SRTM in test)
|
|
# but the LiveView should not crash
|
|
render_click(lv, "compute_viewshed", %{"lat" => 32.9, "lon" => -97.0})
|
|
# If we get here without crash, the event handler exists
|
|
assert render(lv) =~ "propagation-map"
|
|
end
|
|
end
|
|
|
|
describe "toggle_grid" do
|
|
test "toggles grid visibility", %{conn: conn} do
|
|
{:ok, lv, html} = live(conn, ~p"/map")
|
|
refute html =~ "checked"
|
|
|
|
html = render_click(lv, "toggle_grid")
|
|
assert html =~ "checked"
|
|
|
|
html = render_click(lv, "toggle_grid")
|
|
refute html =~ "checked"
|
|
end
|
|
end
|
|
|
|
describe "daily outlook strip" do
|
|
test "no longer renders on the main map", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map")
|
|
refute html =~ "daily-outlook"
|
|
refute html =~ "7-day outlook"
|
|
refute html =~ "Extended outlook"
|
|
end
|
|
end
|
|
|
|
describe "data timestamp indicator" do
|
|
test "renders a 'Data from …' line on both mobile and desktop sidebars", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map")
|
|
assert html =~ ~s(id="data-timestamp-mobile")
|
|
assert html =~ ~s(id="data-timestamp-desktop")
|
|
# Clean test DB has no scores; the indicator falls back to "No data".
|
|
assert html =~ "No data"
|
|
end
|
|
end
|
|
|
|
describe "cursor_for_now/2" do
|
|
alias MicrowavepropWeb.MapLive
|
|
|
|
test "returns nil for an empty list" do
|
|
assert MapLive.cursor_for_now([], DateTime.utc_now()) == nil
|
|
end
|
|
|
|
test "picks the latest time at or before now — never future" do
|
|
# Valid times at 18:00, 19:00, 20:00 UTC. Wall clock 19:35.
|
|
# Nearest-by-abs would round up to 20:00 (25 min future);
|
|
# floor picks 19:00 (35 min past).
|
|
times = [~U[2026-04-18 18:00:00Z], ~U[2026-04-18 19:00:00Z], ~U[2026-04-18 20:00:00Z]]
|
|
now = ~U[2026-04-18 19:35:00Z]
|
|
|
|
assert MapLive.cursor_for_now(times, now) == ~U[2026-04-18 19:00:00Z]
|
|
end
|
|
|
|
test "matches exact hour when now is on the hour" do
|
|
times = [~U[2026-04-18 18:00:00Z], ~U[2026-04-18 19:00:00Z]]
|
|
assert MapLive.cursor_for_now(times, ~U[2026-04-18 19:00:00Z]) == ~U[2026-04-18 19:00:00Z]
|
|
end
|
|
|
|
test "falls back to the earliest time when every slot is in the future" do
|
|
times = [~U[2026-04-18 20:00:00Z], ~U[2026-04-18 21:00:00Z]]
|
|
now = ~U[2026-04-18 19:00:00Z]
|
|
assert MapLive.cursor_for_now(times, now) == ~U[2026-04-18 20:00:00Z]
|
|
end
|
|
|
|
test "ignores list order" do
|
|
times = [~U[2026-04-18 20:00:00Z], ~U[2026-04-18 18:00:00Z], ~U[2026-04-18 19:00:00Z]]
|
|
now = ~U[2026-04-18 19:35:00Z]
|
|
assert MapLive.cursor_for_now(times, now) == ~U[2026-04-18 19:00:00Z]
|
|
end
|
|
end
|
|
|
|
describe "format_data_timestamp/1" do
|
|
alias MicrowavepropWeb.MapLive
|
|
|
|
test "returns 'No data' for nil" do
|
|
assert MapLive.format_data_timestamp(nil) == "No data"
|
|
end
|
|
|
|
test "renders the valid_time and a 'now' suffix when within ±30 minutes" do
|
|
now = DateTime.utc_now()
|
|
label = MapLive.format_data_timestamp(now)
|
|
assert label =~ "UTC"
|
|
assert label =~ "now"
|
|
end
|
|
|
|
test "renders a '+Nh' suffix when the valid_time is in the future" do
|
|
future = DateTime.add(DateTime.utc_now(), 3 * 3600, :second)
|
|
assert MapLive.format_data_timestamp(future) =~ "+3h"
|
|
end
|
|
|
|
test "renders a 'Nh ago' suffix when the valid_time is in the past" do
|
|
past = DateTime.add(DateTime.utc_now(), -4 * 3600, :second)
|
|
assert MapLive.format_data_timestamp(past) =~ "4h ago"
|
|
end
|
|
end
|
|
|
|
describe "propagation_updated handler" do
|
|
setup do
|
|
ScoreCache.clear()
|
|
|
|
on_exit(fn ->
|
|
ScoreCache.clear()
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
test "pushes freshly-written scores when the cache still holds stale ones", %{conn: conn} do
|
|
# Use a valid_time close enough to now that available_valid_times
|
|
# keeps it in the timeline.
|
|
valid_time = DateTime.utc_now() |> DateTime.truncate(:second) |> Map.put(:minute, 0) |> Map.put(:second, 0)
|
|
|
|
# Seed the on-disk store and warm the cache with the same content
|
|
# so the map has a baseline to render on mount.
|
|
Propagation.replace_scores(
|
|
[%{lat: 33.0, lon: -97.0, valid_time: valid_time, band_mhz: 10_000, score: 40, factors: nil}],
|
|
valid_time
|
|
)
|
|
|
|
Propagation.warm_cache_and_broadcast(10_000, valid_time)
|
|
ScoreCache.sync()
|
|
|
|
{:ok, lv, _html} = live(conn, ~p"/map")
|
|
|
|
# Simulate a new forecast-hour compute: rewrite the .ntms file on
|
|
# disk to the updated score, but do *not* touch the cache. This
|
|
# reproduces the race where the cache_refresh message has not yet
|
|
# been applied by the time MapLive handles propagation_updated.
|
|
Propagation.replace_scores(
|
|
[%{lat: 33.0, lon: -97.0, valid_time: valid_time, band_mhz: 10_000, score: 90, factors: nil}],
|
|
valid_time
|
|
)
|
|
|
|
# Deliver the propagation_updated PubSub message directly.
|
|
send(lv.pid, {:propagation_updated, [valid_time]})
|
|
|
|
# The map must emit the new score (90), not the stale cached one (40).
|
|
assert_push_event(lv, "update_scores", %{scores: scores})
|
|
assert Enum.any?(scores, fn s -> s.score == 90 end)
|
|
refute Enum.any?(scores, fn s -> s.score == 40 end)
|
|
end
|
|
end
|
|
|
|
describe "pipeline status chip" do
|
|
test "renders the aggregated pipeline status chip on mount", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map")
|
|
# Both the mobile and desktop sidebars carry their own chip.
|
|
assert html =~ ~s(id="pipeline-status-mobile")
|
|
assert html =~ ~s(id="pipeline-status-desktop")
|
|
# With a clean test DB no pipeline workers have ever run.
|
|
assert html =~ ~s(data-pipeline-state="unknown")
|
|
end
|
|
|
|
test "shows the current forecast hour while PropagationGridWorker runs", %{conn: conn} do
|
|
# Put a grid-worker row in executing state so PipelineStatus.current/0
|
|
# reports :running. Bypassing Oban's normal insert keeps the SQL
|
|
# sandbox from running the worker inline.
|
|
now = DateTime.utc_now()
|
|
|
|
Microwaveprop.Repo.insert!(
|
|
struct!(Oban.Job, %{
|
|
state: "executing",
|
|
queue: "propagation",
|
|
worker: "Microwaveprop.Workers.PropagationGridWorker",
|
|
args: %{},
|
|
attempt: 1,
|
|
max_attempts: 3,
|
|
inserted_at: now,
|
|
scheduled_at: now,
|
|
attempted_at: now
|
|
})
|
|
)
|
|
|
|
{:ok, lv, html} = live(conn, ~p"/map")
|
|
# Before any progress message arrives the chip uses the base label.
|
|
assert html =~ "Updating propagation"
|
|
assert html =~ ~s(data-pipeline-state="running")
|
|
|
|
future = DateTime.add(DateTime.utc_now(), 3 * 3600, :second)
|
|
|
|
Phoenix.PubSub.broadcast(
|
|
Microwaveprop.PubSub,
|
|
"propagation:pipeline",
|
|
{:propagation_pipeline_progress, %{forecast_hour: 5, valid_time: future}}
|
|
)
|
|
|
|
html = render(lv)
|
|
assert html =~ "through +3h"
|
|
|
|
Phoenix.PubSub.broadcast(
|
|
Microwaveprop.PubSub,
|
|
"propagation:pipeline",
|
|
{:propagation_pipeline_progress, %{forecast_hour: 2, valid_time: DateTime.utc_now()}}
|
|
)
|
|
|
|
html = render(lv)
|
|
assert html =~ "through now"
|
|
end
|
|
end
|
|
end
|