import { describe, expect, it } from "vitest"; import type { ResolvedToolcraftAppSchema } from "../../../schema/resolved-app-schema"; import type { ResolvedToolcraftControlSchema } from "../../../schema/types"; import { isToolcraftBuiltInControlSchema } from "../../../schema/control-schema"; type SliderControl = Extract< ResolvedToolcraftControlSchema, { type: "slider" | "rangeSlider"; } >; import { defineToolcraft, fireEvent, renderControlsPanelWithSchema, screen, } from "../testing/controls-panel-test-utils"; function withForgedControl( schema: ResolvedToolcraftAppSchema, controlId: string, forge: (control: SliderControl) => SliderControl, ): ResolvedToolcraftAppSchema { const controlsPanel = schema.panels.controls; if (!controlsPanel) { throw new Error("Expected a controls panel"); } let didForgeControl = false; const sections = controlsPanel.sections.map((section) => { const control = section.controls[controlId]; if (!control) { return section; } if ( !isToolcraftBuiltInControlSchema(control) || (control.type !== "slider" && control.type !== "rangeSlider") ) { throw new Error("Expected a slider control"); } didForgeControl = true; return { ...section, controls: { ...section.controls, [controlId]: forge(control), }, }; }); if (!didForgeControl) { throw new Error(`Expected control "${controlId}"`); } return { ...schema, panels: { ...schema.panels, controls: { ...controlsPanel, sections, }, }, }; } function createDiscreteSliderSchema(type: "rangeSlider" | "slider") { return defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft fixture", }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "fixture-section-1", controls: { points: { applicability: { mode: "always" as const }, ...(type === "slider" ? { type, defaultValue: 16 } : { type, defaultValue: [8, 24] }), label: "Points", max: 31, min: 0, step: 1, target: "shape.points", variant: "discrete", }, }, title: "Shape", }, ], title: "Controls", }, }, }, modules: [], }); } describe("ControlsPanel slider rendering and layout", () => { it("renders schema discrete sliders with Toolcraft variant and markers", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-1", controls: { grain: { applicability: { mode: "always" as const }, defaultValue: 3, label: "Grain", max: 5, min: 0, step: 1, target: "shader.grain", type: "slider", variant: "discrete", }, }, title: "Texture", }, ], title: "Generation Controls", }, }, }, modules: [], }); const { container } = renderControlsPanelWithSchema(schema); const slider = container.querySelector( '[data-slot="slider"][data-variant="discrete"]', ); const markers = Array.from( container.querySelectorAll('[data-slot="slider-marker"]'), ); expect(slider).toBeTruthy(); expect(markers).toHaveLength(4); for (const marker of markers) { expect(marker.className).toContain( "group-hover/slider-control:opacity-100", ); expect(marker.className.split(/\s+/)).not.toContain("opacity-100"); } fireEvent.pointerDown(slider as HTMLElement, { button: 0 }); for (const marker of markers) { expect(marker.className.split(/\s+/)).toContain("opacity-100"); } }); it("renders one discrete slider marker for each fractional step", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-2", controls: { duration: { applicability: { mode: "always" as const }, defaultValue: 0.3, label: "Duration", markerCount: 3, max: 0.5, min: 0, step: 0.1, target: "animation.duration", type: "slider", variant: "discrete", }, }, title: "Timing", }, ], title: "Generation Controls", }, }, }, modules: [], }); const { container } = renderControlsPanelWithSchema(schema); const slider = container.querySelector( '[data-slot="slider"][data-variant="discrete"]', ); const markers = Array.from( container.querySelectorAll('[data-slot="slider-marker"]'), ); expect(slider).toBeTruthy(); expect(markers).toHaveLength(4); }); it("derives 30 internal markers for a valid 32-position domain", () => { const schema = withForgedControl( createDiscreteSliderSchema("slider"), "points", (control) => ({ ...control, markerCount: 65 }), ); const { container } = renderControlsPanelWithSchema(schema); expect( container.querySelector('[data-slot="slider"][data-variant="discrete"]'), ).toBeTruthy(); expect( container.querySelectorAll('[data-slot="slider-marker"]'), ).toHaveLength(30); }); it.each(["slider", "rangeSlider"] as const)( "hides markers for a forged 65-position discrete %s", (type) => { const schema = withForgedControl( createDiscreteSliderSchema(type), "points", (control) => ({ ...control, markerCount: 65, max: 64 }), ); const { container } = renderControlsPanelWithSchema(schema); expect( container.querySelector( '[data-slot="slider"][data-variant="discrete"]', ), ).toBeTruthy(); expect( container.querySelectorAll('[data-slot="slider-marker"]'), ).toHaveLength(0); }, ); it.each(["slider", "rangeSlider"] as const)( "hides markers for a forged domain-less discrete %s", (type) => { const schema = withForgedControl( createDiscreteSliderSchema(type), "points", (control) => ({ ...control, markerCount: undefined, max: undefined, min: undefined, step: undefined, }), ); const { container } = renderControlsPanelWithSchema(schema); expect( container.querySelector( '[data-slot="slider"][data-variant="discrete"]', ), ).toBeTruthy(); expect( container.querySelectorAll('[data-slot="slider-marker"]'), ).toHaveLength(0); }, ); it("keeps stepped continuous sliders free of discrete markers", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-3", controls: { speed: { applicability: { mode: "always" as const }, defaultValue: 118, label: "Reveal speed", max: 150, min: 0, step: 1, target: "animation.speed", type: "slider", unit: " cols/s", }, }, title: "Reveal", }, ], title: "Generation Controls", }, }, }, modules: [], }); const { container } = renderControlsPanelWithSchema(schema); expect( container.querySelector('[data-slot="slider"][data-variant="discrete"]'), ).toBeNull(); expect( container.querySelectorAll('[data-slot="slider-marker"]'), ).toHaveLength(0); expect(container.textContent).toContain("118 cols/s"); }); it("keeps textual slider value labels non-editable", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-4", controls: { letterSpacing: { applicability: { mode: "always" as const }, defaultValue: 2, label: "Letter spacing", max: 4, min: 0, step: 1, target: "text.letterSpacing", type: "slider", valueLabel: "Normal", }, }, title: "Text", }, ], title: "Generation Controls", }, }, }, modules: [], }); renderControlsPanelWithSchema(schema); const valueLabel = screen .getAllByText("Normal") .find((node) => node.getAttribute("aria-hidden") !== "true"); expect( screen.queryByRole("button", { name: "Edit Letter spacing value" }), ).toBeNull(); expect(valueLabel).toBeTruthy(); expect(valueLabel?.className).toContain("cursor-default"); expect(valueLabel?.className).not.toContain("cursor-text"); }); it("keeps schema sliders stacked even when a layout group asks for an inline row", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-5", controls: { fps: { applicability: { mode: "always" as const }, defaultValue: 17, label: "FPS", max: 30, min: 1, step: 1, target: "animation.fps", type: "slider", unit: "fps", variant: "discrete", }, speed: { applicability: { mode: "always" as const }, defaultValue: 3.7, label: "Speed", max: 4, min: 0.1, step: 0.1, target: "animation.speed", type: "slider", }, }, layoutGroups: [ { columns: 2, controls: ["fps", "speed"], layout: "inline", }, ], title: "Pattern Animation", }, ], title: "Generation Controls", }, }, }, modules: [], }); const { container } = renderControlsPanelWithSchema(schema); const fpsSlider = container.querySelector( '[data-slot="slider"][data-variant="discrete"]', ); expect( container.querySelector('[data-control-layout="inline"]'), ).toBeNull(); expect(fpsSlider).toBeTruthy(); expect( container.querySelectorAll('[data-slot="slider-marker"]').length, ).toBeGreaterThan(0); expect(container.textContent).toContain("17 fps"); expect(container.textContent).toContain("3.7"); }); it("passes schema disabled state into slider controls", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-6", controls: { opacity: { applicability: { mode: "always" as const }, defaultValue: 70, disabled: true, label: "Opacity", max: 100, min: 0, step: 1, target: "shader.opacity", type: "slider", }, }, title: "Output", }, ], title: "Generation Controls", }, }, }, modules: [], }); const { container } = renderControlsPanelWithSchema(schema); const field = screen.getByText("Opacity").closest('[data-slot="field"]'); const slider = container.querySelector('[data-slot="slider"]'); expect(field?.getAttribute("data-disabled")).toBe("true"); expect(slider).toBeTruthy(); expect(slider?.hasAttribute("data-disabled")).toBe(true); }); it("does not place any schema slider in inline layout rows", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-7", controls: { range: { applicability: { mode: "always" as const }, defaultValue: [20, 80], label: "Range", max: 100, min: 0, step: 1, target: "shape.range", type: "rangeSlider", unit: "%", }, strength: { applicability: { mode: "always" as const }, defaultValue: 50, label: "Strength", max: 100, min: 0, step: 1, target: "shape.strength", type: "slider", unit: "%", }, }, layoutGroups: [ { columns: 2, controls: ["range", "strength"], layout: "inline", }, ], title: "Shape", }, ], title: "Controls", }, }, }, modules: [], }); const { container } = renderControlsPanelWithSchema(schema); expect( container.querySelector('[data-control-layout="inline"]'), ).toBeNull(); expect(screen.getByText("Range")).toBeTruthy(); expect(screen.getByText("Strength")).toBeTruthy(); }); });