towerops/test/towerops_web/live/schedule_live_test.exs
Graham McIntire 3bba1a6289
fix: display on-call user names instead of emails on schedule index
Update add_member test to use phx-change select instead of form submit.
2026-03-11 13:35:43 -05:00

496 lines
15 KiB
Elixir

defmodule ToweropsWeb.ScheduleLiveTest do
use ToweropsWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Towerops.OnCallFixtures
alias Towerops.OnCall
setup :register_and_log_in_user
setup %{conn: conn, user: user} do
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
conn = Plug.Conn.put_session(conn, :current_organization_id, organization.id)
%{conn: conn, organization: organization}
end
describe "Index" do
test "mounts and lists schedules", %{conn: conn, organization: organization} do
schedule = schedule_fixture(organization.id, %{name: "Primary Rotation"})
{:ok, _view, html} = live(conn, ~p"/schedules")
assert html =~ "Schedules"
assert html =~ "Primary Rotation"
assert html =~ schedule.timezone
end
test "shows empty state when no schedules", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/schedules")
assert html =~ "No schedules"
assert html =~ "Create an on-call schedule"
end
test "shows who is currently on-call for each schedule", %{
conn: conn,
organization: organization,
user: user
} do
schedule = schedule_fixture(organization.id)
layer =
layer_fixture(schedule.id, %{
start_date: ~U[2025-01-01 09:00:00Z],
handoff_time: ~T[09:00:00],
rotation_type: "weekly"
})
layer_member_fixture(layer.id, user.id)
{:ok, _view, html} = live(conn, ~p"/schedules")
assert html =~ user.email
end
test "shows 'No one' when nobody is on-call", %{conn: conn, organization: organization} do
schedule_fixture(organization.id)
{:ok, _view, html} = live(conn, ~p"/schedules")
assert html =~ "No one"
end
test "has link to create new schedule", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/schedules")
assert has_element?(view, "a", "New Schedule")
end
test "has tabs for Schedules and Escalation Policies", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/schedules")
assert html =~ "Schedules"
assert html =~ "Escalation Policies"
end
test "switching to escalation policies tab shows policies", %{
conn: conn,
organization: organization
} do
escalation_policy_fixture(organization.id, %{name: "Critical Alerts Policy"})
{:ok, view, _html} = live(conn, ~p"/schedules")
html = view |> element("a", "Escalation Policies") |> render_click()
assert html =~ "Critical Alerts Policy"
end
test "escalation policies tab shows empty state when none exist", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/schedules?tab=escalation-policies")
assert html =~ "No escalation policies"
end
test "requires authentication" do
conn = build_conn()
{:error, redirect} = live(conn, ~p"/schedules")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "Show" do
test "mounts and displays schedule details", %{conn: conn, organization: organization} do
schedule =
schedule_fixture(organization.id, %{
name: "Weekend Rotation",
timezone: "America/New_York"
})
{:ok, _view, html} = live(conn, ~p"/schedules/#{schedule.id}")
assert html =~ "Weekend Rotation"
assert html =~ "America/New_York"
end
test "shows description if present", %{conn: conn, organization: organization} do
schedule =
schedule_fixture(organization.id, %{
name: "Described Schedule",
description: "Covers weekday shifts"
})
{:ok, _view, html} = live(conn, ~p"/schedules/#{schedule.id}")
assert html =~ "Covers weekday shifts"
end
test "shows Currently On-Call section with no one on-call", %{
conn: conn,
organization: organization
} do
schedule = schedule_fixture(organization.id)
{:ok, _view, html} = live(conn, ~p"/schedules/#{schedule.id}")
assert html =~ "Currently On-Call"
assert html =~ "No one is currently on-call"
end
test "shows currently on-call user", %{conn: conn, organization: organization, user: user} do
schedule = schedule_fixture(organization.id)
layer =
layer_fixture(schedule.id, %{
start_date: ~U[2025-01-01 09:00:00Z],
handoff_time: ~T[09:00:00],
rotation_type: "weekly"
})
layer_member_fixture(layer.id, user.id)
{:ok, _view, html} = live(conn, ~p"/schedules/#{schedule.id}")
assert html =~ "Currently On-Call"
assert html =~ user.email
end
test "shows layers with rotation details", %{conn: conn, organization: organization} do
schedule = schedule_fixture(organization.id)
layer_fixture(schedule.id, %{
name: "Primary Layer",
rotation_type: "weekly",
rotation_interval: 2,
handoff_time: ~T[09:00:00]
})
{:ok, _view, html} = live(conn, ~p"/schedules/#{schedule.id}")
assert html =~ "Primary Layer"
assert html =~ "Weekly"
assert html =~ "09:00"
end
test "shows layer members", %{conn: conn, organization: organization, user: user} do
schedule = schedule_fixture(organization.id)
layer = layer_fixture(schedule.id, %{name: "Layer Alpha"})
layer_member_fixture(layer.id, user.id)
{:ok, _view, html} = live(conn, ~p"/schedules/#{schedule.id}")
assert html =~ user.email
end
test "shows empty state for layers", %{conn: conn, organization: organization} do
schedule = schedule_fixture(organization.id)
{:ok, _view, html} = live(conn, ~p"/schedules/#{schedule.id}")
assert html =~ "No layers configured"
end
test "shows empty state for overrides", %{conn: conn, organization: organization} do
schedule = schedule_fixture(organization.id)
{:ok, _view, html} = live(conn, ~p"/schedules/#{schedule.id}")
assert html =~ "No overrides configured"
end
test "can toggle add layer form", %{conn: conn, organization: organization} do
schedule = schedule_fixture(organization.id)
{:ok, view, _html} = live(conn, ~p"/schedules/#{schedule.id}")
refute has_element?(view, "form[phx-submit='save_layer']")
view |> element("button", "Add Layer") |> render_click()
assert has_element?(view, "form[phx-submit='save_layer']")
end
test "can add a layer", %{conn: conn, organization: organization} do
schedule = schedule_fixture(organization.id)
{:ok, view, _html} = live(conn, ~p"/schedules/#{schedule.id}")
view |> element("button", "Add Layer") |> render_click()
view
|> form("form[phx-submit='save_layer']",
layer: %{
name: "New Layer",
rotation_type: "daily",
rotation_interval: 1,
handoff_time: "09:00",
start_date: "2026-03-01T09:00"
}
)
|> render_submit()
html = render(view)
assert html =~ "New Layer"
refute has_element?(view, "form[phx-submit='save_layer']")
end
test "can add a member to a layer", %{conn: conn, organization: organization, user: user} do
schedule = schedule_fixture(organization.id)
layer = layer_fixture(schedule.id, %{name: "Staffing Layer"})
{:ok, view, _html} = live(conn, ~p"/schedules/#{schedule.id}")
view
|> element("select[phx-change='add_member'][phx-value-layer_id='#{layer.id}']")
|> render_change(%{"user_id" => user.id, "layer_id" => layer.id})
html = render(view)
assert html =~ user.email
end
test "can delete a layer", %{conn: conn, organization: organization} do
schedule = schedule_fixture(organization.id)
layer = layer_fixture(schedule.id, %{name: "Temporary Layer"})
{:ok, view, html} = live(conn, ~p"/schedules/#{schedule.id}")
assert html =~ "Temporary Layer"
view
|> element("button[phx-click='delete_layer'][phx-value-id='#{layer.id}']")
|> render_click()
html = render(view)
refute html =~ "Temporary Layer"
end
test "can remove a member", %{conn: conn, organization: organization, user: user} do
schedule = schedule_fixture(organization.id)
layer = layer_fixture(schedule.id)
member = layer_member_fixture(layer.id, user.id)
{:ok, view, html} = live(conn, ~p"/schedules/#{schedule.id}")
assert html =~ user.email
view
|> element("button[phx-click='remove_member'][phx-value-id='#{member.id}']")
|> render_click()
# The user email should no longer appear as a member badge
refute has_element?(view, "span", user.email)
end
test "can toggle add override form", %{conn: conn, organization: organization} do
schedule = schedule_fixture(organization.id)
{:ok, view, _html} = live(conn, ~p"/schedules/#{schedule.id}")
refute has_element?(view, "form[phx-submit='save_override']")
view |> element("button", "Add Override") |> render_click()
assert has_element?(view, "form[phx-submit='save_override']")
end
test "can add an override", %{conn: conn, organization: organization, user: user} do
schedule = schedule_fixture(organization.id)
{:ok, view, _html} = live(conn, ~p"/schedules/#{schedule.id}")
view |> element("button", "Add Override") |> render_click()
view
|> form("form[phx-submit='save_override']",
override: %{
user_id: user.id,
start_time: "2026-04-01T09:00",
end_time: "2026-04-02T09:00"
}
)
|> render_submit()
html = render(view)
assert html =~ user.email
assert html =~ "2026-04-01"
refute has_element?(view, "form[phx-submit='save_override']")
end
test "can delete an override", %{conn: conn, organization: organization, user: user} do
schedule = schedule_fixture(organization.id)
override = override_fixture(schedule.id, user.id)
{:ok, view, html} = live(conn, ~p"/schedules/#{schedule.id}")
assert html =~ user.email
view
|> element("button[phx-click='delete_override'][phx-value-id='#{override.id}']")
|> render_click()
html = render(view)
assert html =~ "No overrides configured"
end
test "has edit and delete buttons", %{conn: conn, organization: organization} do
schedule = schedule_fixture(organization.id)
{:ok, view, _html} = live(conn, ~p"/schedules/#{schedule.id}")
assert has_element?(view, "a", "Edit")
assert has_element?(view, "button", "Delete")
end
test "delete redirects to index", %{conn: conn, organization: organization} do
schedule = schedule_fixture(organization.id)
{:ok, view, _html} = live(conn, ~p"/schedules/#{schedule.id}")
view |> element("button", "Delete") |> render_click()
{path, flash} = assert_redirect(view)
assert path == ~p"/schedules"
assert flash["info"] =~ "Schedule deleted"
end
test "requires authentication", %{organization: organization} do
schedule = schedule_fixture(organization.id)
conn = build_conn()
{:error, redirect} = live(conn, ~p"/schedules/#{schedule.id}")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "Form - New" do
test "renders new schedule form", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/schedules/new")
assert html =~ "New Schedule"
assert html =~ "Name"
assert html =~ "Timezone"
assert html =~ "Save Schedule"
end
test "validates required fields", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/schedules/new")
html =
view
|> form("form", schedule: %{name: "", timezone: ""})
|> render_change()
assert html =~ "can't be blank"
end
test "creates schedule and redirects to show", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/schedules/new")
view
|> form("form",
schedule: %{
name: "New On-Call Schedule",
timezone: "America/Chicago"
}
)
|> render_submit()
{path, flash} = assert_redirect(view)
assert path =~ ~r|^/schedules/.+|
assert flash["info"] =~ "Schedule created"
end
test "creates schedule with optional description", %{conn: conn, organization: organization} do
{:ok, view, _html} = live(conn, ~p"/schedules/new")
view
|> form("form",
schedule: %{
name: "Documented Schedule",
timezone: "America/Chicago",
description: "Handles weekend coverage"
}
)
|> render_submit()
assert_redirect(view)
schedules = OnCall.list_schedules(organization.id)
schedule = Enum.find(schedules, &(&1.name == "Documented Schedule"))
assert schedule
end
test "requires authentication" do
conn = build_conn()
{:error, redirect} = live(conn, ~p"/schedules/new")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "Form - Edit" do
test "renders edit form with existing data", %{conn: conn, organization: organization} do
schedule =
schedule_fixture(organization.id, %{
name: "Existing Schedule",
timezone: "America/New_York"
})
{:ok, _view, html} = live(conn, ~p"/schedules/#{schedule.id}/edit")
assert html =~ "Edit Schedule"
assert html =~ "Existing Schedule"
assert html =~ "America/New_York"
end
test "validates required fields on change", %{conn: conn, organization: organization} do
schedule = schedule_fixture(organization.id)
{:ok, view, _html} = live(conn, ~p"/schedules/#{schedule.id}/edit")
html =
view
|> form("form", schedule: %{name: ""})
|> render_change()
assert html =~ "can't be blank"
end
test "updates schedule and redirects to show", %{conn: conn, organization: organization} do
schedule = schedule_fixture(organization.id, %{name: "Old Name"})
{:ok, view, _html} = live(conn, ~p"/schedules/#{schedule.id}/edit")
view
|> form("form", schedule: %{name: "Updated Name"})
|> render_submit()
{path, flash} = assert_redirect(view)
assert path == ~p"/schedules/#{schedule.id}"
assert flash["info"] =~ "Schedule updated"
updated = OnCall.get_schedule!(schedule.id)
assert updated.name == "Updated Name"
end
test "requires authentication", %{organization: organization} do
schedule = schedule_fixture(organization.id)
conn = build_conn()
{:error, redirect} = live(conn, ~p"/schedules/#{schedule.id}/edit")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
end