prop/test/microwaveprop_web/live/eme_live_test.exs
Graham McIntire 2defa3db7a
refactor: deduplicate helpers and simplify Scorer
- Unify haversine_km / deg_to_rad: Geo is the canonical home (atan2
  form for antipodal stability); Radio, common_volume, rain_scatter,
  ionosphere, terrain/srtm, terrain/elevation_client now delegate.
- Drop safe_min/safe_max wrappers in favor of Enum.min/max defaults.
- Move parse_float / parse_int to MicrowavepropWeb.LiveHelpers; eme,
  path, and rover LiveViews import from there.
- Extract MicrowavepropWeb.LocationResolver from the eme/path
  resolve_source / resolve_location duplicate. Standardize the kind
  key and "Invalid grid square" error message.
- Convert Scorer cond chains (humidity, td_depression, sky, wind,
  rain, pwat, pressure) to multi-clause guarded defs, matching the
  existing classify_time_period style.
- extract_profile_fields uses a named map accumulator instead of a
  6-tuple; build_path_conditions guards on %{temps: []} / %{dewpoints: []}.
- Collapse scores_file clamp/3 to max |> min.
- humidity_effect_label lives in BandConfig; map_live and path_live
  both use it.
2026-04-28 14:14:34 -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 grid square"
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 grid square" 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