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 Earth–Moon geometry WebGL hook with initial station geometry", %{conn: conn} do {:ok, _view, html} = live(conn, ~p"/eme?source=EM13&band=1296") assert html =~ "Earth–Moon 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