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