docs/tests: moduledocs on 18 LiveViews, MapLive event coverage, Rust pipeline roundtrip
- Add one-line moduledocs to 18 LiveViews that were carrying @moduledoc false. Each line names the route and summarizes what the page does so a future reader knows where to look before opening the file. - Add MapLive handle_event tests for toggle_radar, select_time / set_selected_time, point_detail, map_bounds, retry_initial_scores. Assertions focus on socket-state transitions and "didn't crash" rather than raw HTML — map_bounds in particular has no visible render-side effect (it push_events the new scores to the JS hook). - Rust pipeline: integration-shaped test that hand-builds a 2-cell surface + pressure grid, runs merge → cell_to_conditions → precompute_band_invariants → composite_score_with → write_atomic → decode, asserting header + body length round-trip. Closes the 'pipeline's happy path is only covered by unit tests' gap.
This commit is contained in:
parent
944c2dc1d4
commit
ff950f9e40
20 changed files with 190 additions and 18 deletions
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.AboutLive do
|
||||
@moduledoc false
|
||||
@moduledoc "Static `/about` page — project overview, credits, contact info."
|
||||
use MicrowavepropWeb, :live_view
|
||||
|
||||
alias Microwaveprop.Repo
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.Admin.ContactEditLive do
|
||||
@moduledoc false
|
||||
@moduledoc "Admin-only contact editor at `/admin/contacts/:id/edit` for manual QSO fixes."
|
||||
use MicrowavepropWeb, :live_view
|
||||
use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Radio.ContactEdit
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.AlgoLive do
|
||||
@moduledoc false
|
||||
@moduledoc "Renders `algo.md` as HTML at `/algo` — documents the scoring algorithm."
|
||||
use MicrowavepropWeb, :live_view
|
||||
|
||||
@external_resource "algo.md"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.BeaconLive.Form do
|
||||
@moduledoc false
|
||||
@moduledoc "Shared new/edit form for beacon records, used by `BeaconLive.Index`."
|
||||
use MicrowavepropWeb, :live_view
|
||||
|
||||
alias Microwaveprop.Beacons
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.BeaconLive.Index do
|
||||
@moduledoc false
|
||||
@moduledoc "`/beacons` list + admin controls for the beacon catalog."
|
||||
use MicrowavepropWeb, :live_view
|
||||
use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Beacons.Beacon
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.BeaconLive.Show do
|
||||
@moduledoc false
|
||||
@moduledoc "Single-beacon detail page with reception-log history at `/beacons/:id`."
|
||||
use MicrowavepropWeb, :live_view
|
||||
|
||||
alias Microwaveprop.Beacons
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.ContactLive.Index do
|
||||
@moduledoc false
|
||||
@moduledoc "`/contacts` table with filter/sort + pagination over QSO records."
|
||||
use MicrowavepropWeb, :live_view
|
||||
use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Radio.Contact
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.ContactLive.Show do
|
||||
@moduledoc false
|
||||
@moduledoc "Per-QSO detail at `/contacts/:id`: weather, HRRR, radar, terrain, charts."
|
||||
use MicrowavepropWeb, :live_view
|
||||
|
||||
import MicrowavepropWeb.Components.SkewTChart
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.ContactMapLive do
|
||||
@moduledoc false
|
||||
@moduledoc "`/contacts/map` plots QSO endpoints + great-circle paths on a Leaflet map."
|
||||
use MicrowavepropWeb, :live_view
|
||||
|
||||
alias Microwaveprop.Radio
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.MapLive do
|
||||
@moduledoc false
|
||||
@moduledoc "`/map` — main propagation heatmap with forecast timeline + point detail drawer."
|
||||
use MicrowavepropWeb, :live_view
|
||||
use LiveStash
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.PathLive do
|
||||
@moduledoc false
|
||||
@moduledoc "`/path` terrain-profile + propagation analysis for a pair of grids/coords."
|
||||
use MicrowavepropWeb, :live_view
|
||||
|
||||
import MicrowavepropWeb.Components.SkewTChart
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.PrivacyLive do
|
||||
@moduledoc false
|
||||
@moduledoc "Static `/privacy` page."
|
||||
use MicrowavepropWeb, :live_view
|
||||
|
||||
@impl true
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.StatusLive do
|
||||
@moduledoc false
|
||||
@moduledoc "`/status` ops dashboard — enrichment progress, queue depths, DB size."
|
||||
use MicrowavepropWeb, :live_view
|
||||
|
||||
import Ecto.Query
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.SubmitLive do
|
||||
@moduledoc false
|
||||
@moduledoc "`/submit` QSO submission form; enqueues enrichment jobs on save."
|
||||
use MicrowavepropWeb, :live_view
|
||||
use LiveStash
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.UserManagementLive.Edit do
|
||||
@moduledoc false
|
||||
@moduledoc "Admin edit page for a single user (roles, suspension)."
|
||||
use MicrowavepropWeb, :live_view
|
||||
|
||||
alias Microwaveprop.Accounts
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.UserManagementLive.Index do
|
||||
@moduledoc false
|
||||
@moduledoc "Admin user list at `/admin/users`."
|
||||
use MicrowavepropWeb, :live_view
|
||||
use MicrowavepropWeb.LiveTableResource, schema: Microwaveprop.Accounts.User
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.UserProfileLive do
|
||||
@moduledoc false
|
||||
@moduledoc "Signed-in user's own profile + callsign linkage at `/account`."
|
||||
use MicrowavepropWeb, :live_view
|
||||
|
||||
alias Microwaveprop.Accounts
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
defmodule MicrowavepropWeb.WeatherMapLive do
|
||||
@moduledoc false
|
||||
@moduledoc "`/weather` — HRRR-derived forecast fields (temp, Td, wind, refractivity) on a map."
|
||||
use MicrowavepropWeb, :live_view
|
||||
|
||||
alias Microwaveprop.Weather
|
||||
|
|
|
|||
|
|
@ -358,6 +358,101 @@ mod tests {
|
|||
.unwrap_err();
|
||||
assert!(matches!(err, PipelineError::F00Reserved));
|
||||
}
|
||||
|
||||
/// Integration-ish: hand-build a 2-cell surface + pressure grid,
|
||||
/// merge → cell_to_conditions → scorer → scores_file::write_atomic
|
||||
/// → scores_file::read, asserting the full chain agrees with
|
||||
/// itself. Covers the post-fetch hot path (the one that scales
|
||||
/// with grid size) without standing up wgrib2.
|
||||
#[test]
|
||||
fn merge_score_write_read_roundtrip() {
|
||||
use chrono::TimeZone;
|
||||
|
||||
// decoder encodes lat/lon at millidegree precision in the key.
|
||||
fn latlon_to_key(lat: f64, lon: f64) -> (i32, i32) {
|
||||
((lat * 1000.0).round() as i32, (lon * 1000.0).round() as i32)
|
||||
}
|
||||
|
||||
let mut sfc = PointGrid::new();
|
||||
let mut prs = PointGrid::new();
|
||||
|
||||
let cells = [(32.0_f64, -97.0_f64), (32.03_f64, -97.03_f64)];
|
||||
for &(lat, lon) in &cells {
|
||||
let mut s = CellValues::new();
|
||||
s.insert("TMP:2 m above ground".into(), 295.0);
|
||||
s.insert("DPT:2 m above ground".into(), 285.0);
|
||||
s.insert("UGRD:10 m above ground".into(), 2.0);
|
||||
s.insert("VGRD:10 m above ground".into(), 1.0);
|
||||
s.insert("TCDC:entire atmosphere".into(), 20.0);
|
||||
s.insert("PRES:surface".into(), 101_000.0);
|
||||
s.insert("HPBL:surface".into(), 500.0);
|
||||
s.insert(
|
||||
"PWAT:entire atmosphere (considered as a single layer)".into(),
|
||||
20.0,
|
||||
);
|
||||
sfc.insert(latlon_to_key(lat, lon), s);
|
||||
|
||||
let mut p = CellValues::new();
|
||||
p.insert("TMP:1000 mb".into(), 295.0);
|
||||
p.insert("DPT:1000 mb".into(), 285.0);
|
||||
p.insert("HGT:1000 mb".into(), 100.0);
|
||||
p.insert("TMP:975 mb".into(), 293.0);
|
||||
p.insert("DPT:975 mb".into(), 280.0);
|
||||
p.insert("HGT:975 mb".into(), 300.0);
|
||||
prs.insert(latlon_to_key(lat, lon), p);
|
||||
}
|
||||
|
||||
let merged = merge_grids(sfc, prs);
|
||||
assert_eq!(merged.len(), 2);
|
||||
|
||||
let valid_time = Utc.with_ymd_and_hms(2026, 6, 15, 18, 0, 0).unwrap();
|
||||
|
||||
let prepared: Vec<_> = merged
|
||||
.into_iter()
|
||||
.filter_map(|(key, cell)| {
|
||||
let (lat, lon) = crate::decoder::key_to_latlon(key);
|
||||
let c = cell_to_conditions(&cell, lat, lon, &valid_time)?;
|
||||
let inv = scorer::precompute_band_invariants(&c);
|
||||
Some((lat, lon, c, inv))
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(prepared.len(), 2);
|
||||
|
||||
let band = &band_config::all_bands()[0];
|
||||
let scores: Vec<ScorePoint> = prepared
|
||||
.iter()
|
||||
.map(|(lat, lon, c, inv)| {
|
||||
let r = scorer::composite_score_with(c, band, Some(*inv));
|
||||
ScorePoint {
|
||||
lat: *lat,
|
||||
lon: *lon,
|
||||
score: r.score,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(scores.len(), 2);
|
||||
for s in &scores {
|
||||
assert!(s.score <= 100);
|
||||
}
|
||||
|
||||
// Write to an atomic .prop file and read it back.
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
scores_file::write_atomic(dir.path(), band.freq_mhz, valid_time, &scores).unwrap();
|
||||
|
||||
let path = scores_file::path_for(dir.path(), band.freq_mhz, valid_time);
|
||||
let bytes = std::fs::read(&path).expect("read back score file");
|
||||
let decoded = scores_file::decode(&bytes).expect("decode");
|
||||
// Decoded grid covers the full CONUS bounding box as a dense
|
||||
// byte array; our 2 score points land at their respective
|
||||
// cells and the rest are sentinel no-data. Spot-check: header
|
||||
// fields survived the round trip and the body is the expected
|
||||
// size for a row_major n_rows × n_cols grid.
|
||||
assert_eq!(decoded.band_mhz, band.freq_mhz);
|
||||
assert_eq!(
|
||||
decoded.body.len(),
|
||||
decoded.n_rows as usize * decoded.n_cols as usize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
|
|
|
|||
|
|
@ -169,6 +169,83 @@ defmodule MicrowavepropWeb.MapLiveTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "toggle_radar" do
|
||||
test "flips the radar layer visibility assign", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/map")
|
||||
|
||||
refute :sys.get_state(lv.pid).socket.assigns.radar_visible
|
||||
render_click(lv, "toggle_radar")
|
||||
assert :sys.get_state(lv.pid).socket.assigns.radar_visible
|
||||
render_click(lv, "toggle_radar")
|
||||
refute :sys.get_state(lv.pid).socket.assigns.radar_visible
|
||||
end
|
||||
end
|
||||
|
||||
describe "select_time / set_selected_time" do
|
||||
test "set_selected_time accepts an iso8601 string and updates the assign",
|
||||
%{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/map")
|
||||
|
||||
now_iso = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
|
||||
render_hook(lv, "set_selected_time", %{"time" => now_iso})
|
||||
|
||||
# The handler accepts the event without crashing. Whether the new
|
||||
# time is actually adopted depends on valid_times, which is empty
|
||||
# in test (no ScoresFiles); that's fine — we're testing the
|
||||
# handler path exists and doesn't raise on malformed/new input.
|
||||
assert Process.alive?(lv.pid)
|
||||
end
|
||||
|
||||
test "select_time event is accepted and doesn't crash the LV",
|
||||
%{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/map")
|
||||
render_click(lv, "select_time", %{"time" => "2026-04-21T22:00:00Z"})
|
||||
assert Process.alive?(lv.pid)
|
||||
end
|
||||
end
|
||||
|
||||
describe "point_detail" do
|
||||
test "handler pushes a `point_detail` event to the client without crashing",
|
||||
%{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/map")
|
||||
|
||||
# point_detail does no server-rendered DOM update — it calls
|
||||
# push_event/3 with the detail payload and a JS hook consumes it.
|
||||
# The assertion is that the handler returns cleanly even in the
|
||||
# "no HRRR profile" path (dev/test has empty .prop files).
|
||||
render_click(lv, "point_detail", %{"lat" => 32.9, "lon" => -97.04})
|
||||
assert Process.alive?(lv.pid)
|
||||
assert render(lv) =~ "propagation-map"
|
||||
end
|
||||
end
|
||||
|
||||
describe "map_bounds" do
|
||||
test "stores the viewport and debounces a forecast preload", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/map")
|
||||
|
||||
render_hook(lv, "map_bounds", %{
|
||||
"south" => 30.0,
|
||||
"north" => 35.0,
|
||||
"west" => -100.0,
|
||||
"east" => -95.0
|
||||
})
|
||||
|
||||
bounds = :sys.get_state(lv.pid).socket.assigns.bounds
|
||||
assert bounds["south"] == 30.0
|
||||
assert bounds["north"] == 35.0
|
||||
assert bounds["west"] == -100.0
|
||||
assert bounds["east"] == -95.0
|
||||
end
|
||||
end
|
||||
|
||||
describe "retry_initial_scores" do
|
||||
test "event is accepted without crashing the LV", %{conn: conn} do
|
||||
{:ok, lv, _html} = live(conn, ~p"/map")
|
||||
render_click(lv, "retry_initial_scores")
|
||||
assert Process.alive?(lv.pid)
|
||||
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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue