prop/test/microwaveprop_web/live/eme_live_test.exs
Graham McIntire c514626a62
test: coverage round 3 (83.96% → 84.39%) + 10 new property tests
64 unit tests + 10 property tests across three parallel agents.

- ContactLive.Show round 3 (72% → ~80%): enrichment failure
  surfaces (terrain/iemre/weather :failed, hrrr :unavailable NARR
  fallback), radar-only mechanism, load_solar + enqueue_missing_solar
  paths, blocked_message nil obs_dist, band_summary for 144M/1296M/
  2304M/47G/75G, admin edit no-op, mode changes, fetch_queue_counts
  with seeded Oban jobs. Properties: propagation_mechanism summary is
  always a binary, obs_sort_key totality over arbitrary field keys.

- Weather clients: iem_client transport timeouts + CSV edge cases,
  rtma_client idx url property, swpc_client HTTP 503/404 + empty-
  array parses + JSON/CSV totality properties, ncei_metar_client
  parse edge cases, snmp_client OID-roundtrip property + parse
  edge cases.

- Mid-coverage LiveViews: UserProfileLive avatar/render branches,
  EmeLive invalid-grid/unknown-callsign + form-validation property,
  PathLive zero-distance + 3000km + height/power round-trip property,
  BeaconLive numeric bearing + non-admin approve denied, MapLive
  select_band/toggle_radar/toggle_grid events + band-URL property.

Fix: propagation_mechanism property test uses System.unique_integer
for lat + valid_time nonces instead of StreamData-shrinkable jitter;
the shrink toward 0 was tripping the hrrr_profiles unique index.

205 → 215 properties, 2743 → 2812 tests, 0 failures.
2026-04-24 10:15:37 -05:00

184 lines
6.4 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

defmodule MicrowavepropWeb.EmeLiveTest do
use MicrowavepropWeb.ConnCase, async: true
use ExUnitProperties
import Phoenix.LiveViewTest
alias Microwaveprop.Propagation.BandConfig
setup do
# CallsignClient.locate/1 falls through to Qrz.Client for any input
# that isn't a grid or coord pair. Stub the QRZ HTTP plug so the
# "unknown callsign" path returns a deterministic error instead of
# raising about a missing stub.
Req.Test.stub(Microwaveprop.Qrz.Client, fn conn ->
Plug.Conn.send_resp(conn, 404, "not found")
end)
:ok
end
describe "GET /eme" do
test "renders the empty form when no source is supplied", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/eme")
assert html =~ "EME Calculator"
assert html =~ "Station (callsign, grid, or"
assert html =~ "Calculate"
# No aim readout yet.
refute html =~ "Antenna aim"
end
test "renders the live Moon aim + link budget when a grid source is provided", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/eme?source=EM13&band=1296&tx_power_dbm=50&tx_gain_dbi=30")
assert html =~ "Antenna aim"
assert html =~ "Link budget"
assert html =~ "EM13"
assert html =~ "Azimuth"
assert html =~ "Elevation"
assert html =~ "Slant range"
# Path loss should be in the high 200s (dB) for 1296 MHz.
assert html =~ "dB"
end
test "submitting the form patches the URL with the supplied params", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/eme")
view
|> form("form",
source: "EM13",
band: "432",
tx_power_dbm: "60",
tx_gain_dbi: "25",
bandwidth_hz: "100",
t_sys_k: "150"
)
|> render_submit()
# Flush any pending assign updates to catch redirects/patches.
html = render(view)
assert html =~ "EM13"
assert html =~ "Antenna aim"
end
test "coordinate pair 'lat,lon' resolves without touching the callsign API", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/eme?source=32.9,-97.0&band=1296")
assert html =~ "Antenna aim"
assert html =~ "32.9"
assert html =~ "-97.0"
end
test "invalid Maidenhead grid surfaces a validation error", %{conn: conn} do
# A 4-char prefix that passes the loose regex but trips
# Maidenhead.to_latlon's strict validator (subsquare "ZZ" isn't A-X).
{:ok, _view, html} = live(conn, ~p"/eme?source=EM12ZZ")
assert html =~ "Invalid Maidenhead grid"
refute html =~ "Antenna aim"
end
test "unknown callsign input surfaces a 'Could not find' error", %{conn: conn} do
# A random string that's neither a coord nor a maidenhead grid falls
# into the CallsignClient.locate path. The test DB has no cached
# entry and the real QRZ API is unreachable in tests → error branch.
{:ok, _view, html} = live(conn, ~p"/eme?source=NOSUCHCALL")
assert html =~ "Could not find"
refute html =~ "Antenna aim"
end
test "empty source renders the 'enter callsign or grid' empty-state message",
%{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/eme?source=&band=1296")
assert html =~ "Enter your callsign or grid"
refute html =~ "Antenna aim"
end
test "typing into form emits the WebGL globe hook state via push_event",
%{conn: conn} do
# Initial render: empty source, no eme:update. Triggering a form
# change with a valid grid should both patch the URL and push the
# eme:update payload to the hook.
{:ok, view, _html} = live(conn, ~p"/eme")
render_change(
form(view, "form",
source: "EM13",
band: "1296",
tx_power_dbm: "50",
tx_gain_dbi: "30",
bandwidth_hz: "100",
t_sys_k: "150"
)
)
assert_push_event(view, "eme:update", %{lat: _, lon: _, az: _, el: _})
end
test "renders the EarthMoon geometry WebGL hook with initial station geometry", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/eme?source=EM13&band=1296")
assert html =~ "EarthMoon geometry"
# The WebGL globe is rendered by a LiveView hook — assert the
# hook element + the initial station/moon data attributes that
# feed the Three.js scene on mount.
assert html =~ ~s(id="eme-globe")
assert html =~ ~s(phx-hook="EmeGlobe")
assert html =~ ~s(phx-update="ignore")
assert html =~ ~s(data-station-lat=")
assert html =~ ~s(data-station-lon=")
assert html =~ ~s(data-moon-az=")
assert html =~ ~s(data-moon-el=")
end
end
describe "property: form validation across valid/invalid source inputs" do
property "valid inputs render an Antenna aim readout, invalid inputs render a validation error" do
bands = Enum.map(BandConfig.band_options(), fn {_label, value} -> value end)
# A valid-input sample is a Maidenhead grid + any configured band;
# an invalid-input sample is a non-grid/non-callsign string that
# neither resolves as coords nor passes the maidenhead prefix
# regex. Both categories are exercised in the same property.
valid_source_gen =
StreamData.member_of(["EM13", "FM19", "EN00", "DM04", "FN31", "CM87", "EL09"])
invalid_source_gen =
StreamData.member_of(["ZZ99", "ZZ00", "Z1", "@@@@", "ZZZZZZ"])
check all(
band <- StreamData.member_of(bands),
valid_source <- valid_source_gen,
invalid_source <- invalid_source_gen,
max_runs: 10
) do
conn = Phoenix.ConnTest.build_conn()
{:ok, _valid_view, valid_html} =
live(conn, ~p"/eme?source=#{valid_source}&band=#{band}")
assert valid_html =~ "Antenna aim", """
Expected a valid input to produce an Antenna aim readout.
source=#{inspect(valid_source)} band=#{band}
"""
{:ok, _invalid_view, invalid_html} =
live(conn, ~p"/eme?source=#{invalid_source}&band=#{band}")
# Either the loose maidenhead regex catches it (invalid grid
# error) or it falls through to callsign lookup (not found).
assert invalid_html =~ "Invalid Maidenhead grid" or
invalid_html =~ "Could not find",
"""
Expected an invalid input to produce a validation error.
source=#{inspect(invalid_source)} band=#{band}
"""
refute invalid_html =~ "Antenna aim"
end
end
end
end