defmodule MicrowavepropWeb.SubmitLiveTest do use MicrowavepropWeb.ConnCase, async: true import Phoenix.LiveViewTest alias Microwaveprop.Radio.Contact alias Microwaveprop.Repo alias Microwaveprop.Terrain.ElevationClient alias Microwaveprop.Weather.HrrrClient alias Microwaveprop.Weather.IemClient setup do Req.Test.stub(IemClient, fn conn -> case conn.request_path do "/cgi-bin/request/asos.py" -> Req.Test.text(conn, "#DEBUG,\nstation,valid,tmpf\n") _ -> Req.Test.json(conn, %{"profiles" => []}) end end) Req.Test.stub(HrrrClient, fn conn -> Plug.Conn.send_resp(conn, 404, "not found") end) 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 describe "mount" do test "renders the submission form", %{conn: conn} do {:ok, _lv, html} = live(conn, ~p"/submit") assert html =~ "Submit Contact" assert html =~ "Station 1" assert html =~ "Station 2" assert html =~ "Grid 1" assert html =~ "Grid 2" assert html =~ "Band" assert html =~ "Mode" end end describe "switch_tab" do test "ignores an unknown tab string instead of crashing the LiveView", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/submit") # An unknown tab name used to flow into String.to_existing_atom/1 # and bring the LiveView down. We expect the event to be a no-op # and the process to stay alive. render_hook(lv, "switch_tab", %{"tab" => "definitely-not-a-tab-#{System.unique_integer()}"}) assert Process.alive?(lv.pid) end test "honors a known tab name", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/submit") html = render_hook(lv, "switch_tab", %{"tab" => "csv"}) # Active tab moved to CSV — the CSV section's heading is now visible. assert html =~ "CSV" end end describe "validate" do test "shows validation errors on change", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/submit") html = lv |> form("#contact-form", contact: %{station1: ""}) |> render_change() assert html =~ "can't be blank" end end describe "csv upload" do test "renders CSV upload tab and sample download link", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/submit") html = lv |> element("[phx-value-tab=csv]") |> render_click() assert html =~ "Upload CSV" assert html =~ "/downloads/sample_contacts.csv" assert html =~ "Download sample CSV" end test "uploads valid CSV and shows preview with confirm button", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/submit") lv |> element("[phx-value-tab=csv]") |> render_click() csv_content = "station1,station2,grid1,grid2,band,mode,qso_timestamp\nW5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:00:00Z\n" csv_input = file_input(lv, "#csv-upload-form", :csv, [ %{name: "contacts.csv", content: csv_content, type: "text/csv"} ]) render_upload(csv_input, "contacts.csv") html = lv |> form("#csv-upload-form", %{submitter_email: "test@example.com"}) |> render_submit() assert html =~ "Review before submitting" assert html =~ "Looks good" # Nothing was inserted yet assert Repo.aggregate(Contact, :count) == 0 lv |> element("button[phx-click=confirm_csv]") |> render_click() {path, _flash} = assert_redirect(lv) assert path =~ ~r"^/imports/[0-9a-f-]{36}$" # Oban runs inline, so by the time the redirect fires the chunk # worker has already inserted the contact. assert Repo.aggregate(Contact, :count) == 1 end test "preview flags duplicates before confirmation", %{conn: conn} do # Seed an existing contact so the next upload is a duplicate of it. {:ok, _} = Microwaveprop.Radio.create_contact(%{ "station1" => "W5XD", "station2" => "K5TR", "grid1" => "EM12", "grid2" => "EM00", "band" => "10000", "mode" => "CW", "qso_timestamp" => "2026-03-28T18:00:00Z", "submitter_email" => "seed@example.com" }) {:ok, lv, _html} = live(conn, ~p"/submit") lv |> element("[phx-value-tab=csv]") |> render_click() csv_content = "station1,station2,grid1,grid2,band,mode,qso_timestamp\n" <> "W5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:15:00Z\n" csv_input = file_input(lv, "#csv-upload-form", :csv, [ %{name: "contacts.csv", content: csv_content, type: "text/csv"} ]) render_upload(csv_input, "contacts.csv") html = lv |> form("#csv-upload-form", %{submitter_email: "test@example.com"}) |> render_submit() assert html =~ "Duplicates" assert html =~ "Already in database" refute html =~ "Looks good" end test "preview shows refinements when upload extends an existing grid", %{conn: conn} do {:ok, existing} = Microwaveprop.Radio.create_contact(%{ "station1" => "W5XD", "station2" => "K5TR", "grid1" => "EM12", "grid2" => "EM00", "band" => "10000", "mode" => "CW", "qso_timestamp" => "2026-03-28T18:00:00Z", "submitter_email" => "seed@example.com" }) {:ok, lv, _html} = live(conn, ~p"/submit") lv |> element("[phx-value-tab=csv]") |> render_click() csv_content = "station1,station2,grid1,grid2,band,mode,qso_timestamp\n" <> "W5XD,K5TR,EM12KP37,EM00CD22,10000,CW,2026-03-28T18:10:00Z\n" csv_input = file_input(lv, "#csv-upload-form", :csv, [ %{name: "contacts.csv", content: csv_content, type: "text/csv"} ]) render_upload(csv_input, "contacts.csv") html = lv |> form("#csv-upload-form", %{submitter_email: "test@example.com"}) |> render_submit() assert html =~ "Refinements" assert html =~ "EM12KP37" assert html =~ "refine 1 existing" lv |> element("button[phx-click=confirm_csv]") |> render_click() {path, _flash} = assert_redirect(lv) assert path =~ ~r"^/imports/[0-9a-f-]{36}$" assert Repo.aggregate(Contact, :count) == 1 refreshed = Repo.reload(existing) assert refreshed.grid1 == "EM12KP37" assert refreshed.grid2 == "EM00CD22" end test "shows errors for invalid rows and still offers the confirm button", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/submit") lv |> element("[phx-value-tab=csv]") |> render_click() csv_content = "station1,station2,grid1,grid2,band,mode,qso_timestamp\n" <> "W5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:00:00Z\n" <> ",,,,,,\n" csv_input = file_input(lv, "#csv-upload-form", :csv, [ %{name: "contacts.csv", content: csv_content, type: "text/csv"} ]) render_upload(csv_input, "contacts.csv") html = lv |> form("#csv-upload-form", %{submitter_email: "test@example.com"}) |> render_submit() assert html =~ "Invalid rows" assert html =~ "Row 3" assert html =~ "Looks good" end test "cancel_csv clears the preview", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/submit") lv |> element("[phx-value-tab=csv]") |> render_click() csv_content = "station1,station2,grid1,grid2,band,mode,qso_timestamp\nW5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:00:00Z\n" csv_input = file_input(lv, "#csv-upload-form", :csv, [ %{name: "contacts.csv", content: csv_content, type: "text/csv"} ]) render_upload(csv_input, "contacts.csv") lv |> form("#csv-upload-form", %{submitter_email: "test@example.com"}) |> render_submit() html = lv |> element("button[phx-click=cancel_csv]") |> render_click() assert html =~ "Upload CSV" assert Repo.aggregate(Contact, :count) == 0 end test "requires email for CSV upload", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/submit") lv |> element("[phx-value-tab=csv]") |> render_click() csv_content = "station1,station2,grid1,grid2,band,mode,qso_timestamp\nW5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:00:00Z\n" csv_input = file_input(lv, "#csv-upload-form", :csv, [ %{name: "contacts.csv", content: csv_content, type: "text/csv"} ]) render_upload(csv_input, "contacts.csv") html = lv |> form("#csv-upload-form", %{submitter_email: ""}) |> render_submit() assert html =~ "Email is required for CSV upload" end test "ADIF confirm redirects to /imports/:id", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/submit") lv |> element("[phx-value-tab=adif]") |> render_click() adif_content = "K5TR" <> "W5XD" <> "EM00" <> "EM12" <> "10368" <> "CW" <> "20260328" <> "180000" <> "" adif_input = file_input(lv, "#adif-upload-form", :adif, [ %{name: "contacts.adi", content: adif_content, type: "application/octet-stream"} ]) render_upload(adif_input, "contacts.adi") html = lv |> form("#adif-upload-form", %{submitter_email: "test@example.com"}) |> render_submit() assert html =~ "Review before submitting" lv |> element("button[phx-click=confirm_csv]") |> render_click() {path, _flash} = assert_redirect(lv) assert path =~ ~r"^/imports/[0-9a-f-]{36}$" assert Repo.aggregate(Contact, :count) == 1 end test "handles CSV with no data rows", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/submit") lv |> element("[phx-value-tab=csv]") |> render_click() csv_content = "station1,station2,grid1,grid2,band,mode,qso_timestamp\n" csv_input = file_input(lv, "#csv-upload-form", :csv, [ %{name: "header_only.csv", content: csv_content, type: "text/csv"} ]) render_upload(csv_input, "header_only.csv") html = lv |> form("#csv-upload-form", %{submitter_email: "test@example.com"}) |> render_submit() assert html =~ "CSV file has no data rows" end end describe "save" do test "creates QSO and redirects on valid submit", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/submit") lv |> form("#contact-form", contact: %{ station1: "W5XD", station2: "K5TR", qso_timestamp: "2026-03-28T18:00", mode: "CW", band: "1296", grid1: "EM12", grid2: "EM00", submitter_email: "test@example.com" } ) |> render_submit() contact = Repo.one!(Contact) assert contact.station1 == "W5XD" assert contact.user_submitted == true assert contact.pos1["lat"] assert_redirect(lv, ~p"/contacts/#{contact.id}") end test "shows errors on invalid submit", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/submit") html = lv |> form("#contact-form", contact: %{station1: ""}) |> render_submit() assert html =~ "can't be blank" end test "persists notes from the form on save", %{conn: conn} do {:ok, lv, _html} = live(conn, ~p"/submit") lv |> form("#contact-form", contact: %{ station1: "W5XD", station2: "K5TR", qso_timestamp: "2026-03-28T18:00", mode: "CW", band: "1296", grid1: "EM12", grid2: "EM00", submitter_email: "test@example.com", notes: "Tropo duct along the coast" } ) |> render_submit() contact = Repo.one!(Contact) assert contact.notes == "Tropo duct along the coast" end test "renders a notes textarea on the single-contact tab", %{conn: conn} do {:ok, _lv, html} = live(conn, ~p"/submit") assert html =~ ~s(name="contact[notes]") assert html =~ "textarea" end end end