towerops/test/towerops_web/live/coverage_live/map_test.exs
Graham McIntire 0e2cc6b7ce chore(coverage,types): clean dialyzer + new tests + ETH canopy provider
Dialyzer:
* Add Lidar.Tile @type, narrow several @specs to match success-typing,
  prefix discarded function returns with `_ =`, fix the broken
  File.stream! call in the ms-buildings worker (it was causing dialyzer
  to mark every private fun unreachable), broaden Antenna.spec key
  types from `float()` to `number()` to match catalog int literals.
* Add :geo, :geo_postgis, :db_connection to plt_add_apps so the dep
  PLT actually contains the modules we use.
* Result: 0 unsuppressed dialyzer warnings (was 50+ in project code).

Tree canopy:
* Replace the dead landfire.gov URL with the ETH Zurich Global
  Canopy Height 10 m product. ETH publishes proper COGs with
  overviews on libdrive.ethz.ch — confirmed working with
  /vsicurl/ byte-range reads. The module now picks the 3°×3° tile
  containing the bbox centroid, with both a `:metres` decoder
  (ETH Byte raster, NoData=255) and a `:landfire_evh` decoder for
  operators self-hosting LANDFIRE EVH COGs.
* Re-enable canopy by default now that there's a working source.

Tests (+105 tests, 0 failures):
* CoverageLive Index/Show/Form/Map — empty/loaded/auth states,
  LiveView event handlers, formatters.
* Coverages.Building changeset + helpers.
* Coverages.TreeCanopy provider tile-URL builder and decoders.
* Workers.MsBuildingsImportWorker (full import flow incl. blank
  lines, MultiPolygon, idempotency, hash fallback) — also fixed
  the partial-index ON CONFLICT bug that prevented imports from
  ever working: the unique index has WHERE ms_footprint_id IS
  NOT NULL, so the worker now uses an :unsafe_fragment conflict
  target that includes the predicate.
* GraphQLDocsController smoke test (was 0%).

Coverage rose from 73.19% → 74% with the major project modules I
touched all moved from 0% / <50% to >75%.
2026-05-07 09:11:17 -05:00

115 lines
3.6 KiB
Elixir

defmodule ToweropsWeb.CoverageLive.MapTest do
use ToweropsWeb.ConnCase
import Phoenix.LiveViewTest
import Towerops.CoveragesFixtures
import Towerops.OrganizationsFixtures
alias Towerops.Coverages
setup :register_and_log_in_user
setup %{conn: conn, user: user} do
org = organization_fixture(user.id)
site = site_fixture(org.id, %{latitude: 30.27, longitude: -97.74})
conn = Plug.Conn.put_session(conn, :current_organization_id, org.id)
%{conn: conn, org: org, site: site}
end
describe "mount" do
test "shows empty state when no ready coverages exist", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/coverage")
assert html =~ "No ready coverages"
end
test "shows ready coverages in tower toggle list", %{conn: conn, org: org, site: site} do
cov = coverage_fixture(org.id, site.id, %{name: "Mt Wilson"})
{:ok, ready} =
Coverages.mark_status(cov, "ready", %{
png_path: "/coverage/#{org.id}/#{cov.id}/rssi.png",
raster_path: "/coverage/#{org.id}/#{cov.id}/rssi.tif",
bbox_min_lat: 30.0,
bbox_min_lon: -98.0,
bbox_max_lat: 30.5,
bbox_max_lon: -97.5
})
{:ok, _view, html} = live(conn, ~p"/coverage")
assert html =~ ready.name
end
test "requires authentication" do
conn = build_conn()
assert {:error, {:redirect, %{to: path}}} = live(conn, ~p"/coverage")
assert path =~ "/users/log-in"
end
end
describe "tower toggle events" do
setup %{org: org, site: site} do
cov = coverage_fixture(org.id, site.id, %{name: "TowerA"})
{:ok, ready} =
Coverages.mark_status(cov, "ready", %{
png_path: "/coverage/#{org.id}/#{cov.id}/rssi.png",
raster_path: "/coverage/#{org.id}/#{cov.id}/rssi.tif",
bbox_min_lat: 30.0,
bbox_min_lon: -98.0,
bbox_max_lat: 30.5,
bbox_max_lon: -97.5
})
%{coverage: ready}
end
test "toggle_coverage flips one tower on/off", %{conn: conn, coverage: cov} do
{:ok, view, _html} = live(conn, ~p"/coverage")
# First click: disable
render_click(view, "toggle_coverage", %{"id" => cov.id})
# Second click: re-enable
assert render_click(view, "toggle_coverage", %{"id" => cov.id})
end
test "show_all and hide_all bulk toggle", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/coverage")
assert render_click(view, "hide_all", %{})
assert render_click(view, "show_all", %{})
end
test "set_probe_units switches between ft and m", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/coverage")
assert render_click(view, "set_probe_units", %{"units" => "m"})
assert render_click(view, "set_probe_units", %{"units" => "ft"})
end
test "close_probe is a no-op when no probe is open", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/coverage")
assert render_click(view, "close_probe", %{})
end
end
describe "format helpers" do
alias ToweropsWeb.CoverageLive.Map, as: M
test "fmt_distance converts to mi or km" do
assert M.fmt_distance(1609.344, "ft") == "1.00 mi"
assert M.fmt_distance(1000, "m") == "1.00 km"
assert M.fmt_distance(nil, "ft") == ""
end
test "fmt_rssi formats values" do
assert M.fmt_rssi(:no_coverage) == "N/A"
assert M.fmt_rssi(nil) == ""
assert M.fmt_rssi(-65.0) =~ "dBm"
end
test "coverages_payload shapes records for the JS hook" do
payload = M.coverages_payload([])
assert payload == []
end
end
end