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 "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 "antenna height" do test "renders antenna height input with default 33 ft", %{conn: conn} do {:ok, _lv, html} = live(conn, ~p"/map") assert html =~ ~s(name="height_ft") assert html =~ ~s(value="33") assert html =~ "ft" end test "updates antenna height on change", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/map") html = render_change(lv, "set_antenna_height", %{"height_ft" => "20"}) assert html =~ ~s(value="20") 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 "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 "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