From 4f6e19095deed47dae891f4ca30517666298197c Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Wed, 15 Apr 2026 06:06:21 +0200 Subject: [PATCH] Refactor and enhance ProjectSequencerPane tests --- tests/unit/project-sequencer-pane.test.tsx | 261 ++++++++++++++++++--- 1 file changed, 231 insertions(+), 30 deletions(-) diff --git a/tests/unit/project-sequencer-pane.test.tsx b/tests/unit/project-sequencer-pane.test.tsx index 71909643..49bd45ff 100644 --- a/tests/unit/project-sequencer-pane.test.tsx +++ b/tests/unit/project-sequencer-pane.test.tsx @@ -1,7 +1,8 @@ -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { + createActorControlTargetRef, createProjectGlobalControlTargetRef, getControlTargetRefKey } from "../../src/controls/control-surface"; @@ -14,34 +15,35 @@ import { import { createEmptyProjectSequenceLibrary } from "../../src/sequencer/project-sequences"; describe("ProjectSequencerPane", () => { - 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; - - render( + function renderPane({ + scheduler = createEmptyProjectScheduler(), + targetOptions, + selectedRoutineId = null, + onSetRoutineTarget = vi.fn(), + onSetRoutineStartHour = vi.fn(), + onSetRoutineEndHour = vi.fn() + }: { + scheduler?: ReturnType; + targetOptions: Array<{ + key: string; + target: ReturnType< + typeof createProjectGlobalControlTargetRef | typeof createActorControlTargetRef + >; + label: string; + subtitle: string; + groupLabel: string; + defaults: Record; + }>; + selectedRoutineId?: string | null; + onSetRoutineTarget?: ReturnType; + onSetRoutineStartHour?: ReturnType; + onSetRoutineEndHour?: ReturnType; + }) { + return render( { scheduler={scheduler} sequences={createEmptyProjectSequenceLibrary()} dialogues={createEmptyProjectDialogueLibrary()} - selectedRoutineId={routine.id} + selectedRoutineId={selectedRoutineId} selectedSequenceId={null} onSelectRoutine={vi.fn()} onSelectSequence={vi.fn()} @@ -59,11 +61,11 @@ describe("ProjectSequencerPane", () => { onDeleteRoutine={vi.fn()} onDeleteSequence={vi.fn()} onClose={vi.fn()} - onSetRoutineTarget={vi.fn()} + onSetRoutineTarget={onSetRoutineTarget} onSetRoutineTitle={vi.fn()} onSetRoutineEnabled={vi.fn()} - onSetRoutineStartHour={vi.fn()} - onSetRoutineEndHour={vi.fn()} + onSetRoutineStartHour={onSetRoutineStartHour} + onSetRoutineEndHour={onSetRoutineEndHour} onSetRoutinePriority={vi.fn()} onSetRoutineSequenceId={vi.fn()} onSetRoutineEffectOption={vi.fn()} @@ -102,6 +104,36 @@ describe("ProjectSequencerPane", () => { onSetVisibilityStepMode={vi.fn()} /> ); + } + + 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) @@ -109,4 +141,173 @@ describe("ProjectSequencerPane", () => { expect(screen.queryByText("Target")).toBeNull(); expect(screen.queryByText("Legacy Day Filter")).toBeNull(); }); + + it("moves sequence placements horizontally and between target rows", () => { + 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.pointerDown(block, { button: 0, clientX: 540, clientY: 24 }); + fireEvent.pointerMove(window, { clientX: 600, clientY: 120 }); + fireEvent.pointerUp(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", () => { + 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.pointerDown(screen.getByLabelText("Resize start of Morning Patrol"), { + button: 0, + clientX: 540, + clientY: 24 + }); + fireEvent.pointerMove(window, { clientX: 525, clientY: 24 }); + fireEvent.pointerUp(window, { clientX: 525, clientY: 24 }); + + expect(onSetRoutineStartHour).toHaveBeenLastCalledWith(routine.id, 8.75); + + onSetRoutineStartHour.mockClear(); + onSetRoutineEndHour.mockClear(); + + fireEvent.pointerDown(screen.getByLabelText("Resize end of Morning Patrol"), { + button: 0, + clientX: 600, + clientY: 24 + }); + fireEvent.pointerMove(window, { clientX: 630, clientY: 24 }); + fireEvent.pointerUp(window, { clientX: 630, clientY: 24 }); + + expect(onSetRoutineEndHour).toHaveBeenLastCalledWith(routine.id, 10.5); + }); });