- Add jump_credo_checks ~> 0.4 with all 20 checks enabled - Fix all standard Credo issues: 139 @spec (113 done, 26 remain), 4 refactoring, 3 alias usage, 9 System.cmd env, 5 unsafe_to_atom, 2 max line length, 9 assert_receive timeout - Fix 170+ jump_credo_checks warnings: - 117 TopLevelAliasImportRequire: move nested alias/import to module top - 32 UseObanProWorker: switch to Oban.Pro.Worker - 4 DoctestIExExamples: add doctests / create test file - ~20 WeakAssertion: strengthen type-check assertions - Various ConditionalAssertion, AssertReceiveTimeout fixes - Exclude vendor/ from Credo analysis - Remaining: 175 warnings (mostly opinionated WeakAssertion, AvoidSocketAssignsInTest), 26 @spec annotations
183 lines
6.4 KiB
Elixir
183 lines
6.4 KiB
Elixir
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 =~ ~r/Invalid grid square|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
|