349 lines
12 KiB
Elixir
349 lines
12 KiB
Elixir
defmodule ToweropsWeb.LiveHelpersTest do
|
|
@moduledoc """
|
|
Consolidated tests for pure helpers promoted from various LiveView modules.
|
|
"""
|
|
use ExUnit.Case, async: true
|
|
|
|
alias ToweropsWeb.ActivityFeedLive
|
|
alias ToweropsWeb.InsightsLive.Index
|
|
alias ToweropsWeb.MaintenanceLive.Form
|
|
alias ToweropsWeb.ReportsLive
|
|
alias ToweropsWeb.UserRegistrationLive
|
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 result =~ ~r/gray|white/
|
|
end
|
|
end
|
|
|
|
describe "ActivityFeedLive.type_badge_class/1" do
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|