import { describe, expect, it } from "vitest"; import { timelineModule } from "../../../modules/built-ins/timeline/timeline-module"; import { defineToolcraft, fireEvent, renderControlsPanel, renderControlsPanelWithSchema, screen, waitFor, } from "../testing/controls-panel-test-utils"; describe("ControlsPanel keyframe controls", () => { it("renders keyframe actions for timeline-expanded keyframe-capable controls", () => { renderControlsPanel(undefined, { timeline: { expanded: true, keyframeGroups: [] }, }); expect( screen.getByRole("button", { name: "Add Anchor keyframe" }), ).toBeTruthy(); expect( screen.getByRole("button", { name: "Add Opacity keyframe" }), ).toBeTruthy(); expect( screen.getByRole("button", { name: "Add Static opacity keyframe" }), ).toBeTruthy(); expect( screen.getByRole("button", { name: "Add Output Mix keyframe" }), ).toBeTruthy(); expect( screen.getByRole("button", { name: "Add Curves keyframe" }), ).toBeTruthy(); expect( screen.queryByRole("button", { name: "Add Prompt keyframe" }), ).toBeNull(); expect( screen.queryByRole("button", { name: "Add Blend keyframe" }), ).toBeNull(); expect( screen.queryByRole("button", { name: "Add Channels keyframe" }), ).toBeNull(); expect( screen.queryByRole("button", { name: "Add Enabled keyframe" }), ).toBeNull(); expect( screen.queryByRole("button", { name: "Add Canvas width keyframe" }), ).toBeNull(); expect( screen .getByRole("button", { name: "Add Opacity keyframe" }) .closest("[data-control-field-label]"), ).toBeTruthy(); expect( screen .getByRole("button", { name: "Add Output Mix keyframe" }) .closest("[data-control-field-label]"), ).toBeTruthy(); expect( screen .getByRole("button", { name: "Add Curves keyframe" }) .closest("[data-control-field-label]"), ).toBeTruthy(); fireEvent.click( screen.getByRole("button", { name: "Add Opacity keyframe" }), ); expect(screen.getByTestId("timeline-expanded").textContent).toBe("true"); const opacityKeyframeButton = screen.getByRole("button", { name: "Disable Opacity keyframes", }); expect(opacityKeyframeButton.className).toContain( "!text-[color:var(--link)]", ); expect(opacityKeyframeButton.className).toContain( "data-popup-open:!text-[color:var(--link)]", ); expect(opacityKeyframeButton.className).toContain( "[&_svg]:!fill-[color:var(--link)]", ); expect(opacityKeyframeButton.style.color).toBe("var(--link)"); expect(screen.getByTestId("timeline-keyframes").textContent).toContain( '"controlId":"selectedLayer.opacity"', ); expect(screen.getByTestId("timeline-keyframes").textContent).toContain( '"valueLabel":"75%"', ); expect(screen.getByTestId("timeline-keyframes").textContent).toContain( '"value":75', ); expect( screen.queryByRole("button", { name: "Add Prompt keyframe" }), ).toBeNull(); }); it("updates the selected control keyframe instead of the current playhead time", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-1", controls: { range: { applicability: { mode: "always" as const }, defaultValue: { end: "100%", start: "0%" }, label: "Range", target: "shape.range", type: "rangeInput", }, }, }, ], title: "Controls", }, }, }, modules: [timelineModule({ mode: "keyframes" })], }); renderControlsPanelWithSchema( schema, {}, { timeline: { currentTimeSeconds: 4, expanded: true, keyframeGroups: [ { controlId: "shape.range", keyframes: [ { controlId: "shape.range", controlLabel: "Range", id: "shape.range::2", timeSeconds: 2, value: { end: "100%", start: "0%" }, valueLabel: "0% - 100%", }, ], label: "Range", }, ], selectedKeyframeId: "shape.range::2", }, }, ); fireEvent.change(screen.getByLabelText("Range start"), { target: { value: "25%" }, }); fireEvent.blur(screen.getByLabelText("Range start")); const keyframeGroups = JSON.parse( screen.getByTestId("timeline-keyframes").textContent ?? "[]", ) as Array<{ keyframes: Array<{ id: string; timeSeconds: number; value: { end: string; start: string }; }>; }>; expect(keyframeGroups[0]?.keyframes).toHaveLength(1); expect(keyframeGroups[0]?.keyframes[0]).toMatchObject({ id: "shape.range::2", timeSeconds: 2, value: { end: "100%", start: "25%" }, }); }); it("keeps keyframe actions hidden while the timeline is collapsed", () => { renderControlsPanel(); expect(screen.getByTestId("timeline-expanded").textContent).toBe("false"); expect( screen.queryByRole("button", { name: "Add Prompt keyframe" }), ).toBeNull(); expect( screen.queryByRole("button", { name: "Add Opacity keyframe" }), ).toBeNull(); }); it("keeps keyframe actions hidden for playback-only timeline apps", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-2", controls: { opacity: { applicability: { mode: "always" as const }, defaultValue: 75, label: "Opacity", target: "selectedLayer.opacity", type: "slider", }, prompt: { applicability: { mode: "always" as const }, defaultValue: "Initial prompt", label: "Prompt", target: "generation.prompt", type: "text", }, }, title: "Basic", }, ], title: "Controls", }, }, }, modules: [ timelineModule({ mode: "playback", }), ], }); renderControlsPanelWithSchema(schema, undefined, { timeline: { expanded: true, keyframeGroups: [] }, }); expect(screen.getByTestId("timeline-expanded").textContent).toBe("true"); expect( screen.queryByRole("button", { name: "Add Prompt keyframe" }), ).toBeNull(); expect( screen.queryByRole("button", { name: "Add Opacity keyframe" }), ).toBeNull(); }); it("suppresses control transitions when keyframe controls appear", async () => { const { container } = renderControlsPanel(); const content = () => container.querySelector('[data-slot="toolcraft-panel-content"]'); await waitFor(() => { expect( content()?.getAttribute("data-toolcraft-controls-mounting"), ).toBeNull(); }); fireEvent.click(screen.getByRole("button", { name: "Expand timeline" })); expect(content()?.getAttribute("data-toolcraft-controls-mounting")).toBe( "true", ); }); });