prop/test/microwaveprop_web/live/rover_locations_live/show_test.exs
Graham McIntire 08ac76a4b8
refactor(rover): pattern-match-first event handlers + missing test branches
show.ex (rover-planning) and show.ex (rover-locations) auth + delete
flows go from nested case/case/case to a `with` ladder fed by a single
`authenticated/1` clause that returns `{:ok, user}` or
`{:error, :unauthenticated}`. The else block enumerates the small set
of failure tuples instead of rebuilding nested error returns.

Other idiomatic-Elixir tightening:
- `progress_summary/1` reduces with a `tally_path/2` multi-clause
  helper (status as a head pattern, not three Enum.count passes).
- `paths_by_rover_location/1` extracts `group_to_pair/1`,
  `station_position/1`, and `group_lat/1` so each transformation is a
  pattern match instead of an anonymous fn with `&& fallbacks`.
- `error_summary/1` is two clauses (empty vs populated errors)
  instead of a pipe-into-case.
- Drag-to-edit show.ex consolidates the working-coords assigns into
  `assign_working_coords/3`.

Bug fix uncovered while writing tests: `add_station` on a fresh
/rover-planning/new form was a visual no-op on the first click —
cast_assoc replaced the unseeded default Station struct with the new
params row instead of appending. Now the initial changeset is built
from `%{"stations" => %{"0" => %{"position" => "0"}}}` so add_station
appends from the get-go. Regression test added at
test/microwaveprop_web/live/rover_planning_live_test.exs.

New test branches:
- form: add_station appends on first click + remove_station drops
  the targeted row.
- rover-planning show: add_rover_site whitespace-input rejection,
  delete_rover_site permission denial.
- rover-locations show: save_edit rejects unauthenticated drivers.

Suite: 3228 tests, 0 failures. Credo strict: 0 issues.
2026-05-03 13:01:36 -05:00

179 lines
6.8 KiB
Elixir

defmodule MicrowavepropWeb.RoverLocationsLive.ShowTest do
use MicrowavepropWeb.ConnCase, async: true
import Phoenix.LiveViewTest
alias Microwaveprop.Radio.Maidenhead
alias Microwaveprop.Repo
alias Microwaveprop.Rover
describe "show" do
test "renders the location and a map container", %{conn: conn} do
user = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, loc} =
Rover.create_location(user, %{
lat: 32.5,
lon: -97.5,
status: :good,
notes: "great spot"
})
{:ok, lv, html} = live(conn, ~p"/rover-locations/#{loc.id}")
assert html =~ "Rover Location"
assert html =~ "great spot"
assert html =~ "EM12gm"
assert has_element?(lv, "#location-map-#{loc.id}[phx-hook=LocationMap]")
end
test "redirects when the location does not exist", %{conn: conn} do
missing_id = Ecto.UUID.generate()
assert {:error, {:live_redirect, %{to: "/rover-locations"}}} =
live(conn, ~p"/rover-locations/#{missing_id}")
end
test "redirects when the id is not a valid UUID", %{conn: conn} do
assert {:error, {:live_redirect, %{to: "/rover-locations"}}} =
live(conn, ~p"/rover-locations/not-a-uuid")
end
test "owner sees a Delete button; visitors do not", %{conn: conn} do
user = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, loc} = Rover.create_location(user, %{lat: 32.5, lon: -97.5, status: :good})
# Anonymous: no Delete
{:ok, _lv, html} = live(conn, ~p"/rover-locations/#{loc.id}")
refute html =~ "Delete"
# Owner: Delete present
conn = log_in_user(conn, user)
{:ok, _lv, html} = live(conn, ~p"/rover-locations/#{loc.id}")
assert html =~ "Delete"
end
test "admin sees Delete on someone else's location", %{conn: conn} do
owner = Microwaveprop.AccountsFixtures.user_fixture()
admin = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, admin} = Microwaveprop.Accounts.admin_update_user(admin, %{is_admin: true})
{:ok, loc} = Rover.create_location(owner, %{lat: 32.5, lon: -97.5, status: :good})
conn = log_in_user(conn, admin)
{:ok, _lv, html} = live(conn, ~p"/rover-locations/#{loc.id}")
assert html =~ "Delete"
end
test "owner can enter edit mode and the marker is told to become draggable",
%{conn: conn} do
user = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, loc} = Rover.create_location(user, %{lat: 32.5, lon: -97.5, status: :good})
conn = log_in_user(conn, user)
{:ok, lv, _html} = live(conn, ~p"/rover-locations/#{loc.id}")
html = lv |> element("button", "Edit") |> render_click()
assert html =~ "Editing — drag the marker"
# Save + Cancel show up; Edit button hides.
assert has_element?(lv, "button[phx-click='save_edit']")
assert has_element?(lv, "button[phx-click='cancel_edit']")
refute has_element?(lv, "button[phx-click='toggle_edit']")
end
test "drag updates the working coords + grid (without persisting)", %{conn: conn} do
user = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, loc} = Rover.create_location(user, %{lat: 32.5, lon: -97.5, status: :good})
conn = log_in_user(conn, user)
{:ok, lv, _html} = live(conn, ~p"/rover-locations/#{loc.id}")
_ = lv |> element("button", "Edit") |> render_click()
# Simulate the JS hook firing the dragend event.
html = render_hook(lv, "location_dragged", %{"lat" => 33.25, "lon" => -97.75})
assert html =~ "33.25"
assert html =~ "-97.75"
# Grid recomputed for the new coords.
assert html =~ Maidenhead.from_latlon(33.25, -97.75, 10)
# DB still has the original coords until Save is clicked.
assert %{lat: 32.5, lon: -97.5} = Repo.reload(loc)
end
test "save persists the dragged coords to the DB", %{conn: conn} do
user = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, loc} = Rover.create_location(user, %{lat: 32.5, lon: -97.5, status: :good})
conn = log_in_user(conn, user)
{:ok, lv, _html} = live(conn, ~p"/rover-locations/#{loc.id}")
_ = lv |> element("button", "Edit") |> render_click()
_ = render_hook(lv, "location_dragged", %{"lat" => 33.25, "lon" => -97.75})
html = lv |> element("button[phx-click='save_edit']") |> render_click()
assert html =~ "Location updated"
reloaded = Repo.reload(loc)
assert_in_delta reloaded.lat, 33.25, 1.0e-6
assert_in_delta reloaded.lon, -97.75, 1.0e-6
end
test "cancel reverts the working coords without writing to the DB",
%{conn: conn} do
user = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, loc} = Rover.create_location(user, %{lat: 32.5, lon: -97.5, status: :good})
conn = log_in_user(conn, user)
{:ok, lv, _html} = live(conn, ~p"/rover-locations/#{loc.id}")
_ = lv |> element("button", "Edit") |> render_click()
_ = render_hook(lv, "location_dragged", %{"lat" => 99.0, "lon" => 99.0})
html = lv |> element("button[phx-click='cancel_edit']") |> render_click()
# Working coords reset to the stored values.
assert html =~ "32.5"
refute html =~ "99.0, 99.0"
reloaded = Repo.reload(loc)
assert reloaded.lat == 32.5
assert reloaded.lon == -97.5
end
test "save_edit fails closed when the user is signed out mid-session", %{conn: conn} do
user = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, loc} = Rover.create_location(user, %{lat: 32.5, lon: -97.5, status: :good})
# Anonymous conn — driving the event directly through render_hook
# exercises the unauthenticated branch of save_edit.
{:ok, lv, _html} = live(conn, ~p"/rover-locations/#{loc.id}")
html = render_hook(lv, "save_edit", %{})
assert html =~ "Sign in required"
assert Repo.reload(loc).lat == 32.5
end
test "non-owner does not see the Edit button", %{conn: conn} do
owner = Microwaveprop.AccountsFixtures.user_fixture()
visitor = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, loc} = Rover.create_location(owner, %{lat: 32.5, lon: -97.5, status: :good})
conn = log_in_user(conn, visitor)
{:ok, lv, _html} = live(conn, ~p"/rover-locations/#{loc.id}")
refute has_element?(lv, "button[phx-click='toggle_edit']")
end
test "owner can delete from the show page and is redirected", %{conn: conn} do
user = Microwaveprop.AccountsFixtures.user_fixture()
{:ok, loc} = Rover.create_location(user, %{lat: 32.5, lon: -97.5, status: :good})
conn = log_in_user(conn, user)
{:ok, lv, _html} = live(conn, ~p"/rover-locations/#{loc.id}")
assert {:ok, _index_lv, _html} =
lv
|> element("button", "Delete")
|> render_click()
|> follow_redirect(conn, ~p"/rover-locations")
end
end
end