Two fixes cover the blank "analysis breakdown" panel the user reported:
1. Bound the point_detail fallback lookback to 24h. Previously
factors_from_fallback_profile would happily use a week-old analysis
profile when no current one existed — now anything older than 24h
returns an empty factor map so the UI can surface "breakdown
unavailable" instead of silently misattributing.
2. Move the Plausible analytics snippet from Layouts.app into
root.html.heex. Full-bleed LiveViews (/map, /contacts/map,
/weather, home) bypass Layouts.app, so the snippet only loaded on
the navbar-wrapped pages. root.html.heex is loaded once per HTTP
document so coverage is now universal.
Added ~30 tests locking both down:
• point_detail fallback: exact-time wins, 24h boundary accepted,
30h-stale rejected, multi-band coverage, missing-cell degrades to
empty factors, default-valid_time path
• analytics_test.exs: Plausible script present on 12 representative
pages, exactly once, loaded async, surviving a login round-trip
Also fixed pre-existing credo issues per standing rule: shortened the
map_live_test ETS match_delete call, extracted a function from the
deeply-nested hrrr_point_enqueuer.enqueue_for_contacts/1 cond, and
swapped Mix.Tasks.Rust.Golden's IO.inspect for Mix.shell().info.
543 lines
19 KiB
Elixir
543 lines
19 KiB
Elixir
defmodule MicrowavepropWeb.MapLiveTest do
|
|
use MicrowavepropWeb.ConnCase, async: false
|
|
|
|
import Phoenix.ConnTest, only: [get: 2]
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Microwaveprop.Propagation
|
|
alias Microwaveprop.Propagation.ScoreCache
|
|
alias Phoenix.LiveView.AsyncResult
|
|
alias Phoenix.LiveView.Utils
|
|
|
|
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 "async initial scores" do
|
|
setup do
|
|
ScoreCache.clear()
|
|
on_exit(fn -> ScoreCache.clear() end)
|
|
:ok
|
|
end
|
|
|
|
test "disconnected HTTP render returns 200 with a data-scores payload", %{conn: conn} do
|
|
valid_time =
|
|
DateTime.utc_now()
|
|
|> DateTime.truncate(:second)
|
|
|> Map.put(:minute, 0)
|
|
|> Map.put(:second, 0)
|
|
|
|
Propagation.replace_scores(
|
|
[%{lat: 33.0, lon: -97.0, valid_time: valid_time, band_mhz: 10_000, score: 42, factors: nil}],
|
|
valid_time
|
|
)
|
|
|
|
conn = get(conn, ~p"/map")
|
|
|
|
assert conn.status == 200
|
|
body = Phoenix.ConnTest.html_response(conn, 200)
|
|
# data-scores is baked into the static HTTP render so first paint
|
|
# ships real data even without a websocket connection.
|
|
assert body =~ ~s(data-scores=)
|
|
refute body =~ ~s(data-scores="[]")
|
|
end
|
|
|
|
test "connected mount resolves @initial_scores to an :ok AsyncResult", %{conn: conn} do
|
|
valid_time =
|
|
DateTime.utc_now()
|
|
|> DateTime.truncate(:second)
|
|
|> Map.put(:minute, 0)
|
|
|> Map.put(:second, 0)
|
|
|
|
Propagation.replace_scores(
|
|
[%{lat: 33.0, lon: -97.0, valid_time: valid_time, band_mhz: 10_000, score: 77, factors: nil}],
|
|
valid_time
|
|
)
|
|
|
|
{:ok, lv, _html} = live(conn, ~p"/map")
|
|
|
|
# The websocket mount kicks off start_async(:initial_scores, ...) and
|
|
# pushes `update_scores` once the fetch resolves.
|
|
assert_push_event(lv, "update_scores", %{scores: scores})
|
|
assert is_list(scores)
|
|
|
|
# Force a render so the AsyncResult assignment is flushed, then
|
|
# inspect the socket's assigns directly.
|
|
_ = render(lv)
|
|
async_result = :sys.get_state(lv.pid).socket.assigns.initial_scores
|
|
|
|
assert %AsyncResult{ok?: true} = async_result
|
|
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
|
|
setup do
|
|
# Earlier tests in the suite may have written score files into the
|
|
# shared `:propagation_scores_dir` and warmed the 5 s
|
|
# `list_valid_times` cache. Reset both so this test starts from the
|
|
# "no data" state its assertion describes.
|
|
scores_dir = Application.fetch_env!(:microwaveprop, :propagation_scores_dir)
|
|
_ = File.rm_rf(scores_dir)
|
|
|
|
:ets.match_delete(
|
|
:microwaveprop_cache,
|
|
{{Microwaveprop.Propagation.ScoresFile, :list_valid_times, :_, :_}, :_, :_}
|
|
)
|
|
|
|
:ok
|
|
end
|
|
|
|
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 "URL params" do
|
|
test "band URL param selects the requested band on mount", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map?band=24000")
|
|
assert html =~ ~s(data-selected-band="24000")
|
|
end
|
|
|
|
test "lat/lon/zoom URL params override the default map center/zoom", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map?lat=40.0&lon=-100.0&zoom=5")
|
|
assert html =~ ~s(data-center-lat="40.0")
|
|
assert html =~ ~s(data-center-lon="-100.0")
|
|
assert html =~ ~s(data-zoom="5")
|
|
end
|
|
|
|
test "invalid band falls back to the default", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map?band=notanumber")
|
|
assert html =~ ~s(data-selected-band="10000")
|
|
end
|
|
|
|
test "unknown band MHz falls back to the default", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map?band=99999")
|
|
assert html =~ ~s(data-selected-band="10000")
|
|
end
|
|
|
|
test "malformed lat/lon/zoom are ignored", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map?lat=banana&lon=apple&zoom=over9000")
|
|
assert html =~ ~s(data-zoom="7")
|
|
refute html =~ ~s(data-center-lat="banana")
|
|
end
|
|
|
|
test "out-of-range lat/lon are ignored", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map?lat=500&lon=-999")
|
|
refute html =~ ~s(data-center-lat="500)
|
|
end
|
|
|
|
test "out-of-range zoom falls back to default", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/map?zoom=42")
|
|
assert html =~ ~s(data-zoom="7")
|
|
end
|
|
|
|
test "unparseable time is ignored", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/map?t=not-a-date")
|
|
# Page renders normally without crashing.
|
|
assert render(lv) =~ "data-selected-time"
|
|
end
|
|
|
|
test "ISO8601 time outside the current forecast window falls back", %{conn: conn} do
|
|
# Year 2000 is well before any valid HRRR data; page should still
|
|
# mount and fall back to the normal 'now' cursor rather than try
|
|
# to fetch scores for an empty time.
|
|
{:ok, _lv, _html} = live(conn, ~p"/map?t=2000-01-01T00:00:00Z")
|
|
end
|
|
|
|
test "every param garbage at once still mounts cleanly", %{conn: conn} do
|
|
{:ok, _lv, html} =
|
|
live(conn, ~p"/map?band=xyz&t=nope&lat=abc&lon=def&zoom=foo")
|
|
|
|
assert html =~ ~s(data-selected-band="10000")
|
|
assert html =~ ~s(data-zoom="7")
|
|
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 "tracking_now?/2" do
|
|
alias MicrowavepropWeb.MapLive
|
|
|
|
test "true when selected time equals the latest at-or-before-now slot" do
|
|
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]
|
|
# closest_to_now at 19:35 with these slots is 19:00.
|
|
assert MapLive.tracking_now?(~U[2026-04-18 19:00:00Z], times, now) == true
|
|
end
|
|
|
|
test "false when selected time is an earlier historical slot" do
|
|
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.tracking_now?(~U[2026-04-18 18:00:00Z], times, now) == false
|
|
end
|
|
|
|
test "false when selected time is a future forecast slot" do
|
|
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.tracking_now?(~U[2026-04-18 20:00:00Z], times, now) == false
|
|
end
|
|
|
|
test "false when valid_times is empty" do
|
|
assert MapLive.tracking_now?(~U[2026-04-18 19:00:00Z], [], ~U[2026-04-18 19:00:00Z]) == false
|
|
end
|
|
|
|
test "false when selected_time is nil" do
|
|
times = [~U[2026-04-18 19:00:00Z]]
|
|
assert MapLive.tracking_now?(nil, times, ~U[2026-04-18 19:00:00Z]) == false
|
|
end
|
|
end
|
|
|
|
describe "advance_now_cursor handler" do
|
|
setup do
|
|
ScoreCache.clear()
|
|
on_exit(fn -> ScoreCache.clear() end)
|
|
:ok
|
|
end
|
|
|
|
test "advances selected_time to the newly-past hour when tracking now", %{conn: conn} do
|
|
# Seed two adjacent hours so there's somewhere to advance to. The
|
|
# later of the two is the one we want the cursor to snap to once
|
|
# the clock ticks past it.
|
|
previous_hour =
|
|
DateTime.utc_now() |> DateTime.truncate(:second) |> Map.merge(%{minute: 0, second: 0}) |> DateTime.add(-1, :hour)
|
|
|
|
current_hour = DateTime.add(previous_hour, 3600, :second)
|
|
|
|
for t <- [previous_hour, current_hour] do
|
|
Propagation.replace_scores(
|
|
[%{lat: 33.0, lon: -97.0, valid_time: t, band_mhz: 10_000, score: 50, factors: nil}],
|
|
t
|
|
)
|
|
end
|
|
|
|
{:ok, lv, _html} = live(conn, ~p"/map")
|
|
|
|
# Force the LV onto the previous hour and mark it as tracking_now?
|
|
# so the tick handler has a clear advance opportunity.
|
|
:sys.replace_state(lv.pid, fn state ->
|
|
socket =
|
|
state.socket
|
|
|> Utils.assign(:selected_time, previous_hour)
|
|
|> Utils.assign(:tracking_now?, true)
|
|
|
|
%{state | socket: socket}
|
|
end)
|
|
|
|
send(lv.pid, :advance_now_cursor)
|
|
_ = render(lv)
|
|
|
|
assigns = :sys.get_state(lv.pid).socket.assigns
|
|
assert assigns.selected_time == current_hour
|
|
end
|
|
|
|
test "leaves selected_time alone when the user is on a specific hour", %{conn: conn} do
|
|
previous_hour =
|
|
DateTime.utc_now() |> DateTime.truncate(:second) |> Map.merge(%{minute: 0, second: 0}) |> DateTime.add(-1, :hour)
|
|
|
|
current_hour = DateTime.add(previous_hour, 3600, :second)
|
|
|
|
for t <- [previous_hour, current_hour] do
|
|
Propagation.replace_scores(
|
|
[%{lat: 33.0, lon: -97.0, valid_time: t, band_mhz: 10_000, score: 50, factors: nil}],
|
|
t
|
|
)
|
|
end
|
|
|
|
{:ok, lv, _html} = live(conn, ~p"/map")
|
|
|
|
:sys.replace_state(lv.pid, fn state ->
|
|
socket =
|
|
state.socket
|
|
|> Utils.assign(:selected_time, previous_hour)
|
|
|> Utils.assign(:tracking_now?, false)
|
|
|
|
%{state | socket: socket}
|
|
end)
|
|
|
|
send(lv.pid, :advance_now_cursor)
|
|
_ = render(lv)
|
|
|
|
assigns = :sys.get_state(lv.pid).socket.assigns
|
|
assert assigns.selected_time == previous_hour
|
|
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")
|
|
|
|
# The connected mount kicks off an async initial-score fetch that
|
|
# emits its own `update_scores` push_event. Drain it here so the
|
|
# assertion below observes the propagation_updated delivery only.
|
|
assert_push_event(lv, "update_scores", %{scores: _initial_scores})
|
|
|
|
# Simulate a new forecast-hour compute: rewrite the .prop 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
|