2026-04-15 06:06:21 +02:00
|
|
|
import { fireEvent, render, screen } from "@testing-library/react";
|
2026-04-15 05:26:37 +02:00
|
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
|
|
|
|
|
|
import {
|
2026-04-15 06:06:21 +02:00
|
|
|
createActorControlTargetRef,
|
2026-04-15 05:26:37 +02:00
|
|
|
createProjectGlobalControlTargetRef,
|
|
|
|
|
getControlTargetRefKey
|
|
|
|
|
} from "../../src/controls/control-surface";
|
|
|
|
|
import { ProjectSequencerPane } from "../../src/app/ProjectSequencerPane";
|
|
|
|
|
import {
|
|
|
|
|
createEmptyProjectScheduler,
|
|
|
|
|
createProjectScheduleRoutine
|
|
|
|
|
} from "../../src/scheduler/project-scheduler";
|
2026-04-15 06:06:42 +02:00
|
|
|
import type { ProjectScheduleTargetOption } from "../../src/scheduler/project-schedule-control-options";
|
2026-04-15 05:26:37 +02:00
|
|
|
import { createEmptyProjectSequenceLibrary } from "../../src/sequencer/project-sequences";
|
|
|
|
|
|
|
|
|
|
describe("ProjectSequencerPane", () => {
|
2026-04-15 06:06:21 +02:00
|
|
|
function renderPane({
|
|
|
|
|
scheduler = createEmptyProjectScheduler(),
|
|
|
|
|
targetOptions,
|
|
|
|
|
selectedRoutineId = null,
|
|
|
|
|
onSetRoutineTarget = vi.fn(),
|
|
|
|
|
onSetRoutineStartHour = vi.fn(),
|
|
|
|
|
onSetRoutineEndHour = vi.fn()
|
|
|
|
|
}: {
|
|
|
|
|
scheduler?: ReturnType<typeof createEmptyProjectScheduler>;
|
2026-04-15 06:06:42 +02:00
|
|
|
targetOptions: ProjectScheduleTargetOption[];
|
2026-04-15 06:06:21 +02:00
|
|
|
selectedRoutineId?: string | null;
|
|
|
|
|
onSetRoutineTarget?: ReturnType<typeof vi.fn>;
|
|
|
|
|
onSetRoutineStartHour?: ReturnType<typeof vi.fn>;
|
|
|
|
|
onSetRoutineEndHour?: ReturnType<typeof vi.fn>;
|
|
|
|
|
}) {
|
|
|
|
|
return render(
|
2026-04-15 05:26:37 +02:00
|
|
|
<ProjectSequencerPane
|
|
|
|
|
mode="timeline"
|
|
|
|
|
onSetMode={vi.fn()}
|
2026-04-15 06:06:21 +02:00
|
|
|
targetOptions={targetOptions}
|
2026-04-15 05:26:37 +02:00
|
|
|
teleportTargetOptions={[]}
|
|
|
|
|
sceneTransitionTargetOptions={[]}
|
|
|
|
|
visibilityTargetOptions={[]}
|
|
|
|
|
scheduler={scheduler}
|
|
|
|
|
sequences={createEmptyProjectSequenceLibrary()}
|
2026-04-15 09:26:28 +02:00
|
|
|
npcTalkTargetOptions={[]}
|
2026-04-15 06:06:21 +02:00
|
|
|
selectedRoutineId={selectedRoutineId}
|
2026-04-15 05:26:37 +02:00
|
|
|
selectedSequenceId={null}
|
|
|
|
|
onSelectRoutine={vi.fn()}
|
|
|
|
|
onSelectSequence={vi.fn()}
|
|
|
|
|
onAddRoutine={vi.fn()}
|
|
|
|
|
onAddSequence={vi.fn()}
|
|
|
|
|
onDeleteRoutine={vi.fn()}
|
|
|
|
|
onDeleteSequence={vi.fn()}
|
|
|
|
|
onClose={vi.fn()}
|
2026-04-15 06:25:11 +02:00
|
|
|
onCreateRoutineSequence={vi.fn()}
|
2026-04-15 06:06:21 +02:00
|
|
|
onSetRoutineTarget={onSetRoutineTarget}
|
2026-04-15 05:26:37 +02:00
|
|
|
onSetRoutineTitle={vi.fn()}
|
|
|
|
|
onSetRoutineEnabled={vi.fn()}
|
2026-04-15 06:06:21 +02:00
|
|
|
onSetRoutineStartHour={onSetRoutineStartHour}
|
|
|
|
|
onSetRoutineEndHour={onSetRoutineEndHour}
|
2026-04-15 05:26:37 +02:00
|
|
|
onSetRoutinePriority={vi.fn()}
|
|
|
|
|
onSetRoutineSequenceId={vi.fn()}
|
|
|
|
|
onSetSequenceTitle={vi.fn()}
|
2026-04-15 07:40:33 +02:00
|
|
|
onAddControlEffect={vi.fn()}
|
2026-04-15 09:26:28 +02:00
|
|
|
onAddNpcTalkEffect={vi.fn()}
|
2026-04-15 05:26:37 +02:00
|
|
|
onAddTeleportStep={vi.fn()}
|
|
|
|
|
onAddSceneTransitionStep={vi.fn()}
|
|
|
|
|
onAddVisibilityStep={vi.fn()}
|
|
|
|
|
onDeleteStep={vi.fn()}
|
|
|
|
|
onSetControlStepTarget={vi.fn()}
|
|
|
|
|
onSetControlStepEffectOption={vi.fn()}
|
|
|
|
|
onSetControlStepNumericValue={vi.fn()}
|
|
|
|
|
onSetControlStepColorValue={vi.fn()}
|
|
|
|
|
onSetControlStepAnimationClip={vi.fn()}
|
|
|
|
|
onSetControlStepAnimationLoop={vi.fn()}
|
2026-04-15 06:25:11 +02:00
|
|
|
onSetControlStepPathId={vi.fn()}
|
|
|
|
|
onSetControlStepPathSpeed={vi.fn()}
|
|
|
|
|
onSetControlStepPathLoop={vi.fn()}
|
2026-04-15 11:18:37 +02:00
|
|
|
onSetControlStepPathSmooth={vi.fn()}
|
2026-04-15 09:26:28 +02:00
|
|
|
onSetNpcTalkStepNpcEntityId={vi.fn()}
|
|
|
|
|
onSetNpcTalkStepDialogueId={vi.fn()}
|
2026-04-15 05:26:37 +02:00
|
|
|
onSetTeleportStepTarget={vi.fn()}
|
|
|
|
|
onSetSceneTransitionStepTarget={vi.fn()}
|
|
|
|
|
onSetVisibilityStepTarget={vi.fn()}
|
|
|
|
|
onSetVisibilityStepMode={vi.fn()}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2026-04-15 06:06:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
});
|
2026-04-15 05:26:37 +02:00
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
screen.getByText(/Project event placements run attached sequences only/i)
|
|
|
|
|
).toBeVisible();
|
2026-04-15 05:56:41 +02:00
|
|
|
expect(screen.queryByText("Target")).toBeNull();
|
|
|
|
|
expect(screen.queryByText("Legacy Day Filter")).toBeNull();
|
2026-04-15 05:26:37 +02:00
|
|
|
});
|
2026-04-15 06:06:21 +02:00
|
|
|
|
2026-04-15 06:07:29 +02:00
|
|
|
it("moves sequence placements horizontally and between target rows", async () => {
|
2026-04-15 06:06:21 +02:00
|
|
|
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<HTMLElement>(
|
|
|
|
|
"[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;
|
2026-04-15 06:08:00 +02:00
|
|
|
document.elementFromPoint = vi.fn((_x: number, y: number) =>
|
2026-04-15 06:06:21 +02:00
|
|
|
y >= 100 ? tracks[1] : tracks[0]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const block = screen.getByRole("button", { name: /morning patrol/i });
|
|
|
|
|
|
2026-04-15 06:10:59 +02:00
|
|
|
fireEvent.mouseDown(block, { button: 0, clientX: 540, clientY: 24 });
|
|
|
|
|
fireEvent.mouseMove(window, { clientX: 600, clientY: 120 });
|
|
|
|
|
fireEvent.mouseUp(window, { clientX: 600, clientY: 120 });
|
2026-04-15 06:06:21 +02:00
|
|
|
|
|
|
|
|
document.elementFromPoint = originalElementFromPoint;
|
|
|
|
|
|
|
|
|
|
expect(onSetRoutineTarget).toHaveBeenCalledWith(
|
|
|
|
|
routine.id,
|
|
|
|
|
getControlTargetRefKey(actorB)
|
|
|
|
|
);
|
|
|
|
|
expect(onSetRoutineStartHour).toHaveBeenCalledWith(routine.id, 10);
|
|
|
|
|
expect(onSetRoutineEndHour).toHaveBeenCalledWith(routine.id, 11);
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-15 06:07:29 +02:00
|
|
|
it("resizes sequence placements from both edges with minute precision", async () => {
|
2026-04-15 06:06:21 +02:00
|
|
|
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<HTMLElement>("[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: () => ({})
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-15 06:10:59 +02:00
|
|
|
fireEvent.mouseDown(screen.getByLabelText("Resize start of Morning Patrol"), {
|
2026-04-15 06:06:21 +02:00
|
|
|
button: 0,
|
|
|
|
|
clientX: 540,
|
|
|
|
|
clientY: 24
|
|
|
|
|
});
|
2026-04-15 06:10:59 +02:00
|
|
|
fireEvent.mouseMove(window, { clientX: 525, clientY: 24 });
|
|
|
|
|
fireEvent.mouseUp(window, { clientX: 525, clientY: 24 });
|
2026-04-15 06:06:21 +02:00
|
|
|
|
|
|
|
|
expect(onSetRoutineStartHour).toHaveBeenLastCalledWith(routine.id, 8.75);
|
|
|
|
|
|
|
|
|
|
onSetRoutineStartHour.mockClear();
|
|
|
|
|
onSetRoutineEndHour.mockClear();
|
|
|
|
|
|
2026-04-15 06:10:59 +02:00
|
|
|
fireEvent.mouseDown(screen.getByLabelText("Resize end of Morning Patrol"), {
|
2026-04-15 06:06:21 +02:00
|
|
|
button: 0,
|
|
|
|
|
clientX: 600,
|
|
|
|
|
clientY: 24
|
|
|
|
|
});
|
2026-04-15 06:10:59 +02:00
|
|
|
fireEvent.mouseMove(window, { clientX: 630, clientY: 24 });
|
|
|
|
|
fireEvent.mouseUp(window, { clientX: 630, clientY: 24 });
|
2026-04-15 06:06:21 +02:00
|
|
|
|
|
|
|
|
expect(onSetRoutineEndHour).toHaveBeenLastCalledWith(routine.id, 10.5);
|
|
|
|
|
});
|
2026-04-15 05:26:37 +02:00
|
|
|
});
|