import { fireEvent, render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { createActorControlTargetRef, createProjectGlobalControlTargetRef, getControlTargetRefKey } from "../../src/controls/control-surface"; import { ProjectSequencerPane } from "../../src/app/ProjectSequencerPane"; import { createEmptyProjectScheduler, createProjectScheduleRoutine } from "../../src/scheduler/project-scheduler"; import type { ProjectScheduleTargetOption } from "../../src/scheduler/project-schedule-control-options"; import { createEmptyProjectSequenceLibrary } from "../../src/sequencer/project-sequences"; describe("ProjectSequencerPane", () => { function renderPane({ scheduler = createEmptyProjectScheduler(), targetOptions, selectedRoutineId = null, onSetRoutineTarget = vi.fn(), onSetRoutineStartHour = vi.fn(), onSetRoutineEndHour = vi.fn() }: { scheduler?: ReturnType; targetOptions: ProjectScheduleTargetOption[]; selectedRoutineId?: string | null; onSetRoutineTarget?: ReturnType; onSetRoutineStartHour?: ReturnType; onSetRoutineEndHour?: ReturnType; }) { return render( ); } it("renders project event placements without crashing when they have no inline effects", () => { const target = createProjectGlobalControlTargetRef(); const routine = createProjectScheduleRoutine({ id: "routine-project-events", title: "Project Events", target, startHour: 9, endHour: 17, priority: 0, effects: [] }); const scheduler = createEmptyProjectScheduler(); scheduler.routines[routine.id] = routine; renderPane({ scheduler, targetOptions: [ { key: getControlTargetRefKey(target), target, label: "Project Events", subtitle: "One-shot project events", groupLabel: "Project", defaults: {} } ], selectedRoutineId: routine.id }); expect( screen.getByText(/Project event placements run attached sequences only/i) ).toBeVisible(); expect(screen.queryByText("Target")).toBeNull(); expect(screen.queryByText("Legacy Day Filter")).toBeNull(); }); it("moves sequence placements horizontally and between target rows", async () => { const actorA = createActorControlTargetRef("actor-a"); const actorB = createActorControlTargetRef("actor-b"); const routine = createProjectScheduleRoutine({ id: "routine-a", title: "Morning Patrol", target: actorA, startHour: 9, endHour: 10, priority: 0, effects: [] }); const scheduler = createEmptyProjectScheduler(); scheduler.routines[routine.id] = routine; const onSetRoutineTarget = vi.fn(); const onSetRoutineStartHour = vi.fn(); const onSetRoutineEndHour = vi.fn(); const { container } = renderPane({ scheduler, targetOptions: [ { key: getControlTargetRefKey(actorA), target: actorA, label: "Guard A", subtitle: "NPC", groupLabel: "Actors", defaults: { actorAnimationClipNames: [], actorPathOptions: [] } }, { key: getControlTargetRefKey(actorB), target: actorB, label: "Guard B", subtitle: "NPC", groupLabel: "Actors", defaults: { actorAnimationClipNames: [], actorPathOptions: [] } } ], selectedRoutineId: routine.id, onSetRoutineTarget, onSetRoutineStartHour, onSetRoutineEndHour }); const tracks = container.querySelectorAll( "[data-sequencer-track='true']" ); expect(tracks).toHaveLength(2); Object.defineProperty(tracks[0], "getBoundingClientRect", { configurable: true, value: () => ({ x: 0, y: 0, width: 1440, height: 64, top: 0, left: 0, right: 1440, bottom: 64, toJSON: () => ({}) }) }); const originalElementFromPoint = document.elementFromPoint; document.elementFromPoint = vi.fn((_x: number, y: number) => y >= 100 ? tracks[1] : tracks[0] ); const block = screen.getByRole("button", { name: /morning patrol/i }); fireEvent.mouseDown(block, { button: 0, clientX: 540, clientY: 24 }); fireEvent.mouseMove(window, { clientX: 600, clientY: 120 }); fireEvent.mouseUp(window, { clientX: 600, clientY: 120 }); document.elementFromPoint = originalElementFromPoint; expect(onSetRoutineTarget).toHaveBeenCalledWith( routine.id, getControlTargetRefKey(actorB) ); expect(onSetRoutineStartHour).toHaveBeenCalledWith(routine.id, 10); expect(onSetRoutineEndHour).toHaveBeenCalledWith(routine.id, 11); }); it("resizes sequence placements from both edges with minute precision", async () => { const actor = createActorControlTargetRef("actor-a"); const routine = createProjectScheduleRoutine({ id: "routine-a", title: "Morning Patrol", target: actor, startHour: 9, endHour: 10, priority: 0, effects: [] }); const scheduler = createEmptyProjectScheduler(); scheduler.routines[routine.id] = routine; const onSetRoutineStartHour = vi.fn(); const onSetRoutineEndHour = vi.fn(); const { container } = renderPane({ scheduler, targetOptions: [ { key: getControlTargetRefKey(actor), target: actor, label: "Guard A", subtitle: "NPC", groupLabel: "Actors", defaults: { actorAnimationClipNames: [], actorPathOptions: [] } } ], selectedRoutineId: routine.id, onSetRoutineStartHour, onSetRoutineEndHour }); const track = container.querySelector("[data-sequencer-track='true']"); expect(track).not.toBeNull(); Object.defineProperty(track, "getBoundingClientRect", { configurable: true, value: () => ({ x: 0, y: 0, width: 1440, height: 64, top: 0, left: 0, right: 1440, bottom: 64, toJSON: () => ({}) }) }); fireEvent.mouseDown(screen.getByLabelText("Resize start of Morning Patrol"), { button: 0, clientX: 540, clientY: 24 }); fireEvent.mouseMove(window, { clientX: 525, clientY: 24 }); fireEvent.mouseUp(window, { clientX: 525, clientY: 24 }); expect(onSetRoutineStartHour).toHaveBeenLastCalledWith(routine.id, 8.75); onSetRoutineStartHour.mockClear(); onSetRoutineEndHour.mockClear(); fireEvent.mouseDown(screen.getByLabelText("Resize end of Morning Patrol"), { button: 0, clientX: 600, clientY: 24 }); fireEvent.mouseMove(window, { clientX: 630, clientY: 24 }); fireEvent.mouseUp(window, { clientX: 630, clientY: 24 }); expect(onSetRoutineEndHour).toHaveBeenLastCalledWith(routine.id, 10.5); }); });