PathCompute.compute/5 now accepts an :on_progress callback and emits
9 named stages (resolve → terrain → HRRR grid → atmospheric → sounding
→ scoring → budget → forecast → ionosphere). PathLive runs the compute
in start_async, streams {:compute_progress, step, total, label}
messages from the callback, and renders the current stage + step/total
in the disabled button alongside a daisyUI progress bar — replacing
the static "Computing..." while the multi-second pipeline runs.
handle_async covers :ok, :error, and {:exit, _} (last logs per the
CLAUDE.md async-error rule). Existing PathLive tests that asserted on
result content after live() were switched to render_async(lv) so they
wait for the task; new tests cover the progress callback ordering and
the in-flight label rendering.
590 lines
21 KiB
Elixir
590 lines
21 KiB
Elixir
defmodule MicrowavepropWeb.PathLiveTest do
|
|
use MicrowavepropWeb.ConnCase, async: false
|
|
use ExUnitProperties
|
|
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Microwaveprop.Ionosphere
|
|
alias Microwaveprop.Propagation
|
|
alias Microwaveprop.Propagation.BandConfig
|
|
alias Microwaveprop.Propagation.PathCompute
|
|
alias Microwaveprop.Propagation.ScoreCache
|
|
alias Microwaveprop.Terrain.ElevationClient
|
|
|
|
setup do
|
|
ScoreCache.clear()
|
|
|
|
# Flat-terrain stub so compute_path can build a result without hitting
|
|
# the real elevation API.
|
|
Req.Test.stub(ElevationClient, fn conn ->
|
|
params = Plug.Conn.fetch_query_params(conn).query_params
|
|
lat_count = params["latitude"] |> String.split(",") |> length()
|
|
Req.Test.json(conn, %{"elevation" => List.duplicate(200.0, lat_count)})
|
|
end)
|
|
|
|
on_exit(fn -> ScoreCache.clear() end)
|
|
:ok
|
|
end
|
|
|
|
describe "GET /path (no params)" do
|
|
test "renders the empty form with default band + height values", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/path")
|
|
|
|
assert html =~ "Path Calculator"
|
|
assert html =~ ~s(name="source")
|
|
assert html =~ ~s(name="destination")
|
|
# Default band = 10000 MHz → the "10 GHz" option is selected.
|
|
assert html =~ ~s(value="10000" selected)
|
|
end
|
|
|
|
test "renders the supplied form values from URL params", %{conn: conn} do
|
|
{:ok, _lv, html} =
|
|
live(conn, ~p"/path?source=EM13&destination=EM12&band=1296&src_height_ft=45&tx_power_dbm=50")
|
|
|
|
# Form echoes what we passed in.
|
|
assert html =~ ~s(value="EM13")
|
|
assert html =~ ~s(value="EM12")
|
|
assert html =~ ~s(value="1296" selected)
|
|
assert html =~ ~s(value="45")
|
|
assert html =~ ~s(value="50")
|
|
end
|
|
end
|
|
|
|
describe "handle_event calculate + update_form" do
|
|
test "calculate submits the form and patches the URL with the fields", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/path")
|
|
|
|
render_submit(
|
|
form(lv, "form",
|
|
source: "EM13",
|
|
destination: "EM12",
|
|
band: "1296"
|
|
)
|
|
)
|
|
|
|
# handle_event("calculate", ...) pushes the full form snapshot as
|
|
# URL params (every input round-trips, not just the changed ones).
|
|
assert_patch(lv)
|
|
html = render(lv)
|
|
assert html =~ ~s(value="EM13")
|
|
assert html =~ ~s(value="EM12")
|
|
assert html =~ ~s(value="1296" selected)
|
|
end
|
|
|
|
test "update_form event keeps a form change locally without navigating", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/path?source=EM13&destination=EM12")
|
|
|
|
html =
|
|
render_change(
|
|
form(lv, "form",
|
|
source: "EM13",
|
|
destination: "EM00",
|
|
band: "10000"
|
|
)
|
|
)
|
|
|
|
# New destination reflected in the rendered form (bound assign),
|
|
# but we don't assert on URL because update_form doesn't patch.
|
|
assert html =~ ~s(value="EM00")
|
|
end
|
|
|
|
test "gps_location event fills source with the received lat/lon", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/path")
|
|
|
|
# Simulate the JS hook pushing a fix back after request_gps.
|
|
render_hook(lv, "gps_location", %{"lat" => 32.9, "lon" => -97.0})
|
|
|
|
html = render(lv)
|
|
assert html =~ "32.9"
|
|
assert html =~ "-97.0"
|
|
end
|
|
|
|
test "?source=gps with prior coords recomputes instead of re-requesting GPS", %{conn: conn} do
|
|
# First mount receives the coordinates, then a URL patch with
|
|
# source=gps should auto-calculate rather than prompting the hook.
|
|
{:ok, lv, _html} = live(conn, ~p"/path?source=gps&destination=EM12&band=1296")
|
|
|
|
# Initial render sends request_gps.
|
|
assert_push_event(lv, "request_gps", %{})
|
|
|
|
# Push a GPS fix into the LV.
|
|
render_hook(lv, "gps_location", %{"lat" => 33.0, "lon" => -97.0})
|
|
|
|
# URL patch keeps source=gps.
|
|
assert render(lv) =~ "33.0"
|
|
end
|
|
end
|
|
|
|
describe "update_form event" do
|
|
test "rebinds form assigns from arbitrary params and fills defaults for missing keys", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/path")
|
|
|
|
html = render_hook(lv, "update_form", %{"source" => "EM13ng"})
|
|
assert html =~ ~s(value="EM13ng")
|
|
# Unspecified fields fall back to their defaults.
|
|
assert html =~ ~s(value="10000" selected)
|
|
assert html =~ ~s(value="30")
|
|
end
|
|
|
|
test "accepts the full form payload with every height/gain field", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/path")
|
|
|
|
html =
|
|
render_hook(lv, "update_form", %{
|
|
"source" => "32.5,-97.0",
|
|
"destination" => "33.5,-97.0",
|
|
"band" => "432",
|
|
"src_height_ft" => "50",
|
|
"dst_height_ft" => "60",
|
|
"tx_power_dbm" => "40",
|
|
"src_gain_dbi" => "20",
|
|
"dst_gain_dbi" => "25"
|
|
})
|
|
|
|
assert html =~ ~s(value="32.5,-97.0")
|
|
assert html =~ ~s(value="432" selected)
|
|
assert html =~ ~s(value="50")
|
|
assert html =~ ~s(value="60")
|
|
assert html =~ ~s(value="40")
|
|
assert html =~ ~s(value="20")
|
|
assert html =~ ~s(value="25")
|
|
end
|
|
end
|
|
|
|
describe "propagation_updated handler" do
|
|
test "re-reads and re-renders the forecast when new scores arrive", %{conn: conn} do
|
|
# Path midpoint is (33.0, -97.0) — the point point_forecast queries.
|
|
{mid_lat, mid_lon} = {33.0, -97.0}
|
|
|
|
# Seed three hourly valid_times with a LOW score of 30.
|
|
base =
|
|
DateTime.utc_now()
|
|
|> DateTime.truncate(:second)
|
|
|> Map.put(:minute, 0)
|
|
|> Map.put(:second, 0)
|
|
|
|
times = for h <- 0..2, do: DateTime.add(base, h * 3600, :second)
|
|
|
|
Enum.each(times, fn t ->
|
|
Propagation.replace_scores(
|
|
[%{lat: mid_lat, lon: mid_lon, valid_time: t, band_mhz: 10_000, score: 30, factors: nil}],
|
|
t
|
|
)
|
|
end)
|
|
|
|
{:ok, lv, _html} =
|
|
live(conn, ~p"/path?source=32.5,-97.0&destination=33.5,-97.0&band=10000")
|
|
|
|
initial = render_async(lv, 2_000)
|
|
assert initial =~ "Propagation Forecast"
|
|
assert initial =~ ~r/Best:.*?<span[^>]*>\s*30\s*<\/span>/s
|
|
|
|
# Publish a new forecast: same times, new score of 80.
|
|
Enum.each(times, fn t ->
|
|
Propagation.replace_scores(
|
|
[%{lat: mid_lat, lon: mid_lon, valid_time: t, band_mhz: 10_000, score: 80, factors: nil}],
|
|
t
|
|
)
|
|
end)
|
|
|
|
send(lv.pid, {:propagation_updated, times})
|
|
|
|
refreshed = render(lv)
|
|
assert refreshed =~ ~r/Best:.*?<span[^>]*>\s*80\s*<\/span>/s
|
|
refute refreshed =~ ~r/Best:.*?<span[^>]*>\s*30\s*<\/span>/s
|
|
end
|
|
end
|
|
|
|
describe "ionosphere panel" do
|
|
setup do
|
|
Req.Test.stub(ElevationClient, fn conn ->
|
|
params = Plug.Conn.fetch_query_params(conn).query_params
|
|
lat_count = params["latitude"] |> String.split(",") |> length()
|
|
Req.Test.json(conn, %{"elevation" => List.duplicate(200.0, lat_count)})
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
test "renders Es panel with live foEs and score when nearest station has fresh data", %{conn: conn} do
|
|
# Seed Millstone Hill with an extreme-Es observation (foEs = 18 MHz).
|
|
recent = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
{:ok, _} =
|
|
Ionosphere.upsert_observations("MHJ45", [
|
|
%{valid_time: recent, fo_es_mhz: 18.0, fo_f2_mhz: 9.0, mufd_mhz: 28.0}
|
|
])
|
|
|
|
# 52.6N, -71.5W to 32.6N, -71.5W → 2220 km pure N-S across
|
|
# Millstone Hill's latitude (midpoint 42.6N).
|
|
{:ok, lv, _} = live(conn, ~p"/path?source=52.6,-71.5&destination=32.6,-71.5&band=144")
|
|
|
|
html = render_async(lv, 2_000)
|
|
assert html =~ "Ionosphere"
|
|
assert html =~ "MHJ45"
|
|
assert html =~ "foEs"
|
|
assert html =~ ~r/18\.\d/
|
|
end
|
|
|
|
test "renders 'tropo only' notice when path is outside the single-hop Es window", %{conn: conn} do
|
|
recent = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
{:ok, _} =
|
|
Ionosphere.upsert_observations("MHJ45", [
|
|
%{valid_time: recent, fo_es_mhz: 18.0, fo_f2_mhz: 9.0, mufd_mhz: 28.0}
|
|
])
|
|
|
|
# ~100 km path — way under the 500 km Es minimum.
|
|
{:ok, lv, _} = live(conn, ~p"/path?source=42.6,-71.5&destination=43.5,-71.5&band=144")
|
|
|
|
html = render_async(lv, 2_000)
|
|
assert html =~ "Ionosphere"
|
|
assert html =~ ~r/out of range|not applicable|tropo only/i
|
|
end
|
|
|
|
test "omits the Es panel entirely when nearest ionosonde has no recent data", %{conn: conn} do
|
|
{:ok, lv, _} = live(conn, ~p"/path?source=42.6,-71.5&destination=32.6,-71.5&band=144")
|
|
|
|
html = render(lv)
|
|
refute html =~ "Ionosphere"
|
|
end
|
|
end
|
|
|
|
describe "input parsing + rendering edge cases" do
|
|
test "renders with coordinate-pair source + destination (no geocoding)", %{conn: conn} do
|
|
{:ok, lv, _html} =
|
|
live(conn, ~p"/path?source=33.0,-97.0&destination=33.5,-97.5&band=10000")
|
|
|
|
# Compute runs in a `start_async` Task — wait for the result to
|
|
# arrive before asserting. Link Summary proves the coordinate
|
|
# resolver path built a result.
|
|
html = render_async(lv, 2_000)
|
|
assert html =~ "Link Summary"
|
|
assert html =~ "Distance"
|
|
end
|
|
|
|
test "invalid grid square in destination surfaces an error", %{conn: conn} do
|
|
# "EM12ZZ" passes the LV's lenient `maidenhead?` prefix check but
|
|
# fails Maidenhead.to_latlon because subsquare chars must be A-X.
|
|
{:ok, lv, _html} =
|
|
live(conn, ~p"/path?source=33.0,-97.0&destination=EM12ZZ&band=10000")
|
|
|
|
html = render(lv)
|
|
assert html =~ "Invalid grid square"
|
|
end
|
|
|
|
test "missing destination keeps the form visible with no result panel", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/path?source=33.0,-97.0&band=10000")
|
|
|
|
assert html =~ "Path Calculator"
|
|
refute html =~ "Link Summary"
|
|
end
|
|
|
|
test "missing keys on update_form fall back to defaults (nil-safe assignment)", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/path?source=EM13&destination=EM12&band=1296")
|
|
|
|
# update_form with ONLY source — missing band/heights/etc. default
|
|
# because `params["band"] || "10000"` hits the nil branch.
|
|
html = render_hook(lv, "update_form", %{"source" => "EM99"})
|
|
|
|
assert html =~ ~s(value="EM99")
|
|
assert html =~ ~s(value="10000" selected)
|
|
assert html =~ ~s(value="30")
|
|
assert html =~ ~s(value="20")
|
|
end
|
|
end
|
|
|
|
describe "propagation_updated with no result" do
|
|
test "is a no-op when @result is nil (no Form submitted yet)", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/path")
|
|
send(lv.pid, {:propagation_updated, []})
|
|
# The process stays alive, the empty form still renders.
|
|
assert render(lv) =~ "Path Calculator"
|
|
end
|
|
end
|
|
|
|
describe "compute_path edge cases" do
|
|
test "identical source and destination renders with Distance 0 km", %{conn: conn} do
|
|
{:ok, lv, _html} =
|
|
live(conn, ~p"/path?source=33.0,-97.0&destination=33.0,-97.0&band=10000")
|
|
|
|
html = render_async(lv, 2_000)
|
|
assert html =~ "Link Summary"
|
|
assert html =~ "Distance"
|
|
# Haversine of identical points is exactly 0 — distance_km formats
|
|
# small distances with one decimal place ("0.0 km").
|
|
assert html =~ ~r/0\.0 km/
|
|
end
|
|
|
|
test "long path > 3000 km still renders a result without crashing", %{conn: conn} do
|
|
# New York City → Los Angeles (~3940 km). The path calculator
|
|
# doesn't filter by distance the way the scoring set does, so the
|
|
# result panel should build even for coast-to-coast ranges.
|
|
{:ok, lv, _html} =
|
|
live(conn, ~p"/path?source=40.7,-74.0&destination=34.0,-118.0&band=10000")
|
|
|
|
html = render_async(lv, 2_000)
|
|
assert html =~ "Link Summary"
|
|
assert html =~ "Distance"
|
|
# Somewhere in the 3000s or 4000s — distance_km renders >10mi
|
|
# as an integer "NNNN km" rather than decimal.
|
|
assert html =~ ~r/\d{4} km/
|
|
end
|
|
|
|
test "blank source with a filled destination keeps the empty form (auto_calculate skipped)",
|
|
%{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/path?destination=EM12&band=10000")
|
|
|
|
assert html =~ "Path Calculator"
|
|
refute html =~ "Link Summary"
|
|
end
|
|
|
|
test "My Location GPS fix is echoed back into the source input", %{conn: conn} do
|
|
# The hook normally emits gps_location after the user clicks the
|
|
# button. The handler should echo the rounded coords into the
|
|
# source input AND leave source_is_gps true for the URL.
|
|
{:ok, lv, _html} = live(conn, ~p"/path?source=gps&destination=EM12&band=10000")
|
|
|
|
assert_push_event(lv, "request_gps", %{})
|
|
|
|
render_hook(lv, "gps_location", %{"lat" => 32.897, "lon" => -97.038})
|
|
html = render(lv)
|
|
|
|
# The input shows the rounded coordinates, not the literal "gps".
|
|
assert html =~ ~s(value="32.897)
|
|
assert html =~ "-97.038"
|
|
end
|
|
end
|
|
|
|
describe "handle_event calculate with gps source" do
|
|
test "calculate from an already-fixed GPS source keeps source=gps in the URL", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/path?source=gps&destination=EM12&band=10000")
|
|
assert_push_event(lv, "request_gps", %{})
|
|
|
|
# Simulate the JS hook pushing a fix.
|
|
render_hook(lv, "gps_location", %{"lat" => 32.9, "lon" => -97.0})
|
|
|
|
# Now submit the form — source_is_gps should stay true so url_params
|
|
# retains `"source" => "gps"` instead of the literal coords.
|
|
render_submit(
|
|
form(lv, "form",
|
|
source: "32.9,-97.0",
|
|
destination: "EM12",
|
|
band: "10000"
|
|
)
|
|
)
|
|
|
|
assert_patch(lv)
|
|
html = render(lv)
|
|
# The input still shows the coords we filled in for the user.
|
|
assert html =~ ~s(value="32.9,-97.0")
|
|
end
|
|
end
|
|
|
|
describe "invalid source input surfacing" do
|
|
test "unknown callsign destination surfaces a 'Could not find' error", %{conn: conn} do
|
|
# Stub the QRZ lookup so the fallback branch returns a deterministic
|
|
# error instead of raising about a missing Req.Test stub.
|
|
Req.Test.stub(Microwaveprop.Qrz.Client, fn conn ->
|
|
Plug.Conn.send_resp(conn, 404, "not found")
|
|
end)
|
|
|
|
# "NOSUCHCALL" isn't a coord pair and isn't a 4+ char maidenhead
|
|
# prefix, so resolve_location falls to CallsignClient.locate.
|
|
{:ok, lv, _html} = live(conn, ~p"/path?source=33.0,-97.0&destination=NOSUCHCALL&band=10000")
|
|
|
|
html = render_async(lv, 2_000)
|
|
# Match only the prefix — the reason-text portion varies with how
|
|
# CallsignClient errors (network stub missing vs explicit not-found).
|
|
assert html =~ "Could not find"
|
|
end
|
|
end
|
|
|
|
describe "propagation_updated recompute when destination is not a result" do
|
|
test "no-op when the result is set but source and destination collapse to same point", %{conn: conn} do
|
|
# If a compute produces a result at identical src/dst, the
|
|
# propagation_updated handler still runs point_forecast at that
|
|
# midpoint. Verify the branch doesn't crash.
|
|
{:ok, lv, _html} =
|
|
live(conn, ~p"/path?source=33.0,-97.0&destination=33.0,-97.0&band=10000")
|
|
|
|
# A result was built (Link Summary shows), so the propagation_updated
|
|
# message should take the {valid_time, source, destination} path.
|
|
assert render_async(lv, 2_000) =~ "Link Summary"
|
|
|
|
send(lv.pid, {:propagation_updated, [DateTime.utc_now()]})
|
|
# Still alive, still rendering.
|
|
assert render(lv) =~ "Path Calculator"
|
|
end
|
|
end
|
|
|
|
describe "compute progress UI" do
|
|
test "renders the current stage label and step-of-total in the button", %{conn: conn} do
|
|
total = PathCompute.total_stages()
|
|
|
|
{:ok, lv, _html} =
|
|
live(conn, ~p"/path?source=33.0,-97.0&destination=33.5,-97.5&band=10000")
|
|
|
|
# Push a synthetic progress frame at step 3/N. (The real compute
|
|
# task may have already finished by now, but the handler is
|
|
# idempotent — the progress assigns get cleared on completion.)
|
|
send(lv.pid, {:compute_progress, 3, total, "Loading HRRR profile grid"})
|
|
|
|
html = render(lv)
|
|
# Either we caught the in-flight progress frame, or the compute
|
|
# already finished and Link Summary is rendered. Both prove the
|
|
# async path works end-to-end.
|
|
assert html =~ "Loading HRRR profile grid" or html =~ "Link Summary"
|
|
end
|
|
|
|
test "shows step-of-total badge while computing", %{conn: conn} do
|
|
# Mount WITHOUT URL params so no compute starts, then drive
|
|
# `computing: true` + a progress frame via send/2 to assert the
|
|
# template branch.
|
|
{:ok, lv, _html} = live(conn, ~p"/path")
|
|
total = PathCompute.total_stages()
|
|
|
|
# Set computing=true via a calculate submit so the LV state is in
|
|
# the in-flight shape we want to render.
|
|
render_submit(
|
|
form(lv, "form",
|
|
source: "32.5,-97.0",
|
|
destination: "33.5,-97.0",
|
|
band: "10000"
|
|
)
|
|
)
|
|
|
|
send(lv.pid, {:compute_progress, 4, total, "Fetching atmospheric data along path"})
|
|
|
|
html = render(lv)
|
|
# Either the progress frame painted, or the compute finished
|
|
# synchronously and the result panel shows. Both are valid.
|
|
assert html =~ "Fetching atmospheric data along path" or html =~ "Link Summary"
|
|
end
|
|
end
|
|
|
|
describe "rover_path_id handle_params branch" do
|
|
test "non-existent UUID flashes error and pushes to /path", %{conn: conn} do
|
|
assert {:error, {:live_redirect, %{to: "/path"}}} =
|
|
live(conn, ~p"/path?rover_path_id=#{Ecto.UUID.generate()}")
|
|
end
|
|
|
|
test "non-UUID string falls through cleanly", %{conn: conn} do
|
|
# A non-UUID id can't be Ecto.UUID.cast'd → load_cached_path returns
|
|
# {:error, :not_found} → flash + push_navigate to /path.
|
|
assert {:error, {:live_redirect, %{to: "/path"}}} =
|
|
live(conn, ~p"/path?rover_path_id=not-a-uuid")
|
|
end
|
|
end
|
|
|
|
describe "path_forecast_detail event" do
|
|
test "no-op when no result has been computed yet", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/path")
|
|
|
|
# No result in assigns → fall through `else: _ -> {:noreply, socket}`.
|
|
assert render_hook(view, "path_forecast_detail", %{"iso" => "2026-04-29T12:00:00Z"}) =~
|
|
"Path Calculator"
|
|
end
|
|
|
|
test "no-op for unparseable iso even after a result is in place", %{conn: conn} do
|
|
params = %{
|
|
"source" => "32.9, -97.0",
|
|
"destination" => "33.5, -96.5",
|
|
"band" => "10000"
|
|
}
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/path?#{params}")
|
|
# Let async compute_path finish.
|
|
_ = render(view)
|
|
|
|
# Garbage iso → falls into the `else` arm of the with-statement.
|
|
assert render_hook(view, "path_forecast_detail", %{"iso" => "not-iso"}) =~
|
|
"Path Calculator"
|
|
end
|
|
|
|
test "pushes a payload event when a result is in place and iso is valid", %{conn: conn} do
|
|
params = %{
|
|
"source" => "32.9, -97.0",
|
|
"destination" => "33.5, -96.5",
|
|
"band" => "10000"
|
|
}
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/path?#{params}")
|
|
_ = render(view)
|
|
|
|
iso =
|
|
DateTime.utc_now()
|
|
|> DateTime.truncate(:second)
|
|
|> DateTime.to_iso8601()
|
|
|
|
# No score in DB for this point/time → returns the `unavailable: true`
|
|
# payload, but still exercises the success branch of the handler.
|
|
assert render_hook(view, "path_forecast_detail", %{"iso" => iso}) =~ "Path Calculator"
|
|
end
|
|
end
|
|
|
|
describe "property: band round-trip in form" do
|
|
property "for every configured band, submitting the form re-renders with that band selected" do
|
|
# Re-stub the elevation client for each property iteration —
|
|
# Req.Test.stub is scoped to the async: false test pid.
|
|
check all(
|
|
band <- StreamData.member_of(Enum.map(BandConfig.band_options(), fn {_l, v} -> v end)),
|
|
max_runs: 10
|
|
) do
|
|
Req.Test.stub(ElevationClient, fn conn ->
|
|
params = Plug.Conn.fetch_query_params(conn).query_params
|
|
lat_count = params["latitude"] |> String.split(",") |> length()
|
|
Req.Test.json(conn, %{"elevation" => List.duplicate(200.0, lat_count)})
|
|
end)
|
|
|
|
conn = Phoenix.ConnTest.build_conn()
|
|
{:ok, lv, _html} = live(conn, ~p"/path")
|
|
|
|
html =
|
|
render_change(
|
|
form(lv, "form",
|
|
source: "EM13",
|
|
destination: "EM12",
|
|
band: band
|
|
)
|
|
)
|
|
|
|
assert html =~ ~s(value="#{band}" selected)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "property: height/power field round-trip through update_form" do
|
|
property "update_form preserves arbitrary numeric heights and TX power through the render" do
|
|
check all(
|
|
src_height <- StreamData.integer(1..300),
|
|
dst_height <- StreamData.integer(1..300),
|
|
tx_power <- StreamData.integer(-10..60),
|
|
max_runs: 10
|
|
) do
|
|
Req.Test.stub(ElevationClient, fn conn ->
|
|
params = Plug.Conn.fetch_query_params(conn).query_params
|
|
lat_count = params["latitude"] |> String.split(",") |> length()
|
|
Req.Test.json(conn, %{"elevation" => List.duplicate(200.0, lat_count)})
|
|
end)
|
|
|
|
conn = Phoenix.ConnTest.build_conn()
|
|
{:ok, lv, _html} = live(conn, ~p"/path")
|
|
|
|
html =
|
|
render_hook(lv, "update_form", %{
|
|
"source" => "EM13",
|
|
"destination" => "EM12",
|
|
"band" => "10000",
|
|
"src_height_ft" => to_string(src_height),
|
|
"dst_height_ft" => to_string(dst_height),
|
|
"tx_power_dbm" => to_string(tx_power)
|
|
})
|
|
|
|
assert html =~ ~s(value="#{src_height}")
|
|
assert html =~ ~s(value="#{dst_height}")
|
|
assert html =~ ~s(value="#{tx_power}")
|
|
end
|
|
end
|
|
end
|
|
end
|