defmodule ToweropsWeb.LiveHelpersTest do @moduledoc """ Consolidated tests for pure helpers promoted from various LiveView modules. """ use ExUnit.Case, async: true describe "OnboardingLive.next_step/1" do test "advances through the onboarding sequence" do assert :site == ToweropsWeb.OnboardingLive.next_step(:snmp) assert :billing == ToweropsWeb.OnboardingLive.next_step(:site) assert :agent == ToweropsWeb.OnboardingLive.next_step(:billing) assert :done == ToweropsWeb.OnboardingLive.next_step(:agent) end end describe "OnboardingLive.step_index/1" do test "returns 0-indexed position" do assert 0 == ToweropsWeb.OnboardingLive.step_index(:snmp) assert 1 == ToweropsWeb.OnboardingLive.step_index(:site) assert 2 == ToweropsWeb.OnboardingLive.step_index(:billing) assert 3 == ToweropsWeb.OnboardingLive.step_index(:agent) end end describe "InsightsLive.Index.urgency_classes/1" do alias ToweropsWeb.InsightsLive.Index test "critical is red" do assert String.contains?(Index.urgency_classes("critical"), "red") end test "warning is yellow" do assert String.contains?(Index.urgency_classes("warning"), "yellow") end test "info is blue" do assert String.contains?(Index.urgency_classes("info"), "blue") end test "unknown is gray" do assert String.contains?(Index.urgency_classes("mystery"), "gray") assert String.contains?(Index.urgency_classes(nil), "gray") end end describe "InsightsLive.Index.source_classes/1" do alias ToweropsWeb.InsightsLive.Index test "preseem is indigo" do assert String.contains?(Index.source_classes("preseem"), "indigo") end test "gaiia is emerald" do assert String.contains?(Index.source_classes("gaiia"), "emerald") end test "snmp is orange" do assert String.contains?(Index.source_classes("snmp"), "orange") end test "system is gray" do assert String.contains?(Index.source_classes("system"), "gray") end test "unknown is gray" do assert String.contains?(Index.source_classes("x"), "gray") end end describe "InsightsLive.Index.build_filter_params/2" do alias ToweropsWeb.InsightsLive.Index test "merges overrides into base" do base = %{"status" => "active"} result = Index.build_filter_params(base, %{"urgency" => "critical"}) assert result == %{"status" => "active", "urgency" => "critical"} end test "drops nil values" do base = %{"status" => "active"} result = Index.build_filter_params(base, %{"urgency" => nil}) refute Map.has_key?(result, "urgency") end test "override wins over base" do base = %{"status" => "active"} result = Index.build_filter_params(base, %{"status" => "resolved"}) assert result == %{"status" => "resolved"} end end describe "InsightsLive.Index selection helpers" do alias ToweropsWeb.InsightsLive.Index test "selected?/2 returns true when id is in set" do ids = MapSet.new(["a", "b"]) assert Index.selected?(ids, "a") refute Index.selected?(ids, "c") end test "any_selected?/1 returns true for non-empty set" do assert Index.any_selected?(MapSet.new(["a"])) refute Index.any_selected?(MapSet.new()) end end describe "ActivityFeedLive.severity_dot_color/2" do alias ToweropsWeb.ActivityFeedLive test "critical is red regardless of type" do assert "bg-red-500" == ActivityFeedLive.severity_dot_color(:critical, :anything) end test "warning is yellow" do assert "bg-yellow-500" == ActivityFeedLive.severity_dot_color(:warning, :anything) end test "info + alert_resolved is green" do assert "bg-green-500" == ActivityFeedLive.severity_dot_color(:info, :alert_resolved) end test "info + sync is blue" do assert "bg-blue-500" == ActivityFeedLive.severity_dot_color(:info, :sync) end test "info + device_added is cyan" do assert "bg-cyan-500" == ActivityFeedLive.severity_dot_color(:info, :device_added) end test "info + unknown defaults to gray" do assert "bg-gray-400" == ActivityFeedLive.severity_dot_color(:info, :other) end test "catch-all is gray" do assert "bg-gray-400" == ActivityFeedLive.severity_dot_color(:unknown, :other) end end describe "ActivityFeedLive.severity_text_color/2" do alias ToweropsWeb.ActivityFeedLive test "critical is red text" do result = ActivityFeedLive.severity_text_color(:critical, :alert_fired) assert String.contains?(result, "red") end test "alert_resolved is green text" do result = ActivityFeedLive.severity_text_color(:info, :alert_resolved) assert String.contains?(result, "green") end test "fallback is default gray/white" do result = ActivityFeedLive.severity_text_color(:info, :sync) assert String.contains?(result, "gray") or String.contains?(result, "white") end end describe "ActivityFeedLive.type_badge_class/1" do alias ToweropsWeb.ActivityFeedLive test "known types map to their colors" do for {type, color} <- [ {:config_change, "orange"}, {:alert_fired, "red"}, {:alert_resolved, "green"}, {:device_event, "purple"}, {:sync, "blue"}, {:device_added, "cyan"} ] do assert String.contains?(ActivityFeedLive.type_badge_class(type), color), "expected #{type} badge to contain '#{color}'" end end test "unknown defaults to gray" do assert String.contains?(ActivityFeedLive.type_badge_class(:other), "gray") end end describe "ActivityFeedLive.type_label/1" do alias ToweropsWeb.ActivityFeedLive test "returns human labels" do assert "Config" == ActivityFeedLive.type_label(:config_change) assert "Alert" == ActivityFeedLive.type_label(:alert_fired) assert "Resolved" == ActivityFeedLive.type_label(:alert_resolved) assert "Event" == ActivityFeedLive.type_label(:device_event) assert "Sync" == ActivityFeedLive.type_label(:sync) assert "Device" == ActivityFeedLive.type_label(:device_added) assert "Other" == ActivityFeedLive.type_label(:unknown) end end describe "ActivityFeedLive.relative_time/2" do alias ToweropsWeb.ActivityFeedLive setup do now = ~U[2026-04-23 20:00:00Z] %{now: now} end test "very recent = 'just now'", %{now: now} do ts = DateTime.add(now, -2, :second) assert "just now" == ActivityFeedLive.relative_time(ts, now) end test "seconds ago", %{now: now} do ts = DateTime.add(now, -30, :second) assert "30s ago" == ActivityFeedLive.relative_time(ts, now) end test "minutes ago", %{now: now} do ts = DateTime.add(now, -600, :second) assert "10m ago" == ActivityFeedLive.relative_time(ts, now) end test "hours ago", %{now: now} do ts = DateTime.add(now, -3 * 3600, :second) assert "3h ago" == ActivityFeedLive.relative_time(ts, now) end test "yesterday", %{now: now} do ts = DateTime.add(now, -30 * 3600, :second) assert "yesterday" == ActivityFeedLive.relative_time(ts, now) end test "days ago", %{now: now} do ts = DateTime.add(now, -5 * 86_400, :second) assert "5d ago" == ActivityFeedLive.relative_time(ts, now) end test "over a week shows absolute date", %{now: now} do ts = DateTime.add(now, -30 * 86_400, :second) result = ActivityFeedLive.relative_time(ts, now) # Absolute format like "Mar 24, 2026" assert result =~ ~r/\w+ \d+, \d{4}/ end end describe "MaintenanceLive.Form.maybe_clear_scope/2" do alias ToweropsWeb.MaintenanceLive.Form test "org scope nils both site_id and device_id" do result = Form.maybe_clear_scope(%{"site_id" => "s1", "device_id" => "d1"}, "org") assert result["site_id"] == nil assert result["device_id"] == nil end test "site scope nils only device_id" do result = Form.maybe_clear_scope(%{"site_id" => "s1", "device_id" => "d1"}, "site") assert result["site_id"] == "s1" assert result["device_id"] == nil end test "device scope preserves both" do result = Form.maybe_clear_scope(%{"site_id" => "s1", "device_id" => "d1"}, "device") assert result["site_id"] == "s1" assert result["device_id"] == "d1" end end describe "MaintenanceLive.Form.format_datetime_local/1" do alias ToweropsWeb.MaintenanceLive.Form test "nil returns empty string" do assert "" == Form.format_datetime_local(nil) end test "non-DateTime returns empty string" do assert "" == Form.format_datetime_local("not a datetime") assert "" == Form.format_datetime_local(42) end test "DateTime formatted as HTML datetime-local" do dt = ~U[2026-04-23 14:30:00Z] assert "2026-04-23T14:30" == Form.format_datetime_local(dt) end end describe "ReportsLive helpers" do alias ToweropsWeb.ReportsLive test "humanize_type/1 known types" do assert "Uptime Summary" == ReportsLive.humanize_type("uptime_summary") assert "Alert History" == ReportsLive.humanize_type("alert_history") assert "Capacity Trends" == ReportsLive.humanize_type("capacity_trends") assert "RF Link Health" == ReportsLive.humanize_type("rf_link_health") end test "humanize_type/1 passes through unknown" do assert "custom" == ReportsLive.humanize_type("custom") end test "humanize_schedule/1 capitalizes type" do assert "Weekly" == ReportsLive.humanize_schedule(%{"type" => "weekly"}) assert "Daily" == ReportsLive.humanize_schedule(%{"type" => "daily"}) end test "humanize_schedule/1 returns dash for missing type" do assert "-" == ReportsLive.humanize_schedule(%{}) assert "-" == ReportsLive.humanize_schedule(nil) end test "format_recipients/1 joins list with commas" do assert "a@b.com, c@d.com" == ReportsLive.format_recipients(["a@b.com", "c@d.com"]) end test "format_recipients/1 returns dash for non-list" do assert "-" == ReportsLive.format_recipients(nil) assert "-" == ReportsLive.format_recipients("string") end test "status_badge/1 known statuses" do assert {"Success", class} = ReportsLive.status_badge("success") assert String.contains?(class, "green") assert {"Failed", class} = ReportsLive.status_badge("failed") assert String.contains?(class, "red") assert {"Partial", class} = ReportsLive.status_badge("partial_failure") assert String.contains?(class, "yellow") end test "status_badge/1 unknown maps to 'Never Run'" do assert {"Never Run", class} = ReportsLive.status_badge(nil) assert String.contains?(class, "gray") end end describe "UserRegistrationLive.normalize_consent_params/1" do alias ToweropsWeb.UserRegistrationLive test "'on' becomes true for both fields" do result = UserRegistrationLive.normalize_consent_params(%{ "privacy_policy_consent" => "on", "terms_of_service_consent" => "on" }) assert result["privacy_policy_consent"] == true assert result["terms_of_service_consent"] == true end test "preserves existing boolean values" do result = UserRegistrationLive.normalize_consent_params(%{ "privacy_policy_consent" => true, "terms_of_service_consent" => false }) assert result["privacy_policy_consent"] == true assert result["terms_of_service_consent"] == false end test "missing fields remain nil" do result = UserRegistrationLive.normalize_consent_params(%{}) assert result["privacy_policy_consent"] == nil assert result["terms_of_service_consent"] == nil end test "other values pass through unchanged" do result = UserRegistrationLive.normalize_consent_params(%{"privacy_policy_consent" => "nope"}) assert result["privacy_policy_consent"] == "nope" end end end