import { describe, expect, it } from "vitest"; import { createEvent, defineToolcraft, fireEvent, renderControlsPanelWithSchema, screen, vi, } from "../testing/controls-panel-test-utils"; describe("ControlsPanel vector pad interactions", () => { it("maps default spatial vector pad movement to screen coordinates", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-1", controls: { position: { applicability: { mode: "always" as const }, defaultValue: { x: "0.00", y: "0.00" }, label: "Position", target: "geometry.position", type: "vector", }, }, title: "Geometry", }, ], title: "Controls", }, }, }, modules: [], }); renderControlsPanelWithSchema(schema); const pad = screen.getByRole("button", { name: "Position X/Y pad" }); expect(pad.getAttribute("data-vector-pad-coordinate-mode")).toBe("screen"); vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({ bottom: 100, height: 100, left: 0, right: 100, toJSON: () => undefined, top: 0, width: 100, x: 0, y: 0, } as DOMRect); Object.defineProperty(pad, "setPointerCapture", { configurable: true, value: vi.fn(), }); fireEvent.pointerDown(pad, { buttons: 1, clientX: 0, clientY: 0, pointerId: 1, }); const values = JSON.parse( screen.getByTestId("values-json").textContent ?? "{}", ); expect(values["geometry.position"]).toEqual({ x: -1, y: -1 }); }); it("resets a vector pad to its default value on double click", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-2", controls: { position: { applicability: { mode: "always" as const }, defaultValue: { x: "0.48", y: "-0.34" }, label: "Position", target: "geometry.position", type: "vector", }, }, title: "Geometry", }, ], title: "Controls", }, }, }, modules: [], }); renderControlsPanelWithSchema(schema); const pad = screen.getByRole("button", { name: "Position X/Y pad" }); vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({ bottom: 100, height: 100, left: 0, right: 100, toJSON: () => undefined, top: 0, width: 100, x: 0, y: 0, } as DOMRect); Object.defineProperty(pad, "setPointerCapture", { configurable: true, value: vi.fn(), }); fireEvent.pointerDown(pad, { buttons: 1, clientX: 0, clientY: 0, pointerId: 1, }); expect( JSON.parse(screen.getByTestId("values-json").textContent ?? "{}")[ "geometry.position" ], ).toEqual({ x: -1, y: -1 }); fireEvent.doubleClick(pad); const values = JSON.parse( screen.getByTestId("values-json").textContent ?? "{}", ); expect(values["geometry.position"]).toEqual({ x: 0.48, y: -0.34 }); expect(screen.getByRole("button", { name: "Edit Position X value" }).textContent).toBe("0.48"); expect(screen.getByRole("button", { name: "Edit Position Y value" }).textContent).toBe("-0.34"); }); it("locks vector pad movement to the dominant axis while shift is held", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-3", controls: { position: { applicability: { mode: "always" as const }, defaultValue: { x: "0.00", y: "0.00" }, label: "Position", target: "geometry.position", type: "vector", }, }, title: "Geometry", }, ], title: "Controls", }, }, }, modules: [], }); renderControlsPanelWithSchema(schema); const pad = screen.getByRole("button", { name: "Position X/Y pad" }); vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({ bottom: 100, height: 100, left: 0, right: 100, toJSON: () => undefined, top: 0, width: 100, x: 0, y: 0, } as DOMRect); Object.defineProperty(pad, "setPointerCapture", { configurable: true, value: vi.fn(), }); fireEvent.pointerDown(pad, { buttons: 1, clientX: 50, clientY: 50, pointerId: 1, }); fireEvent.pointerMove(pad, { buttons: 1, clientX: 90, clientY: 70, pointerId: 1, shiftKey: true, }); const values = JSON.parse( screen.getByTestId("values-json").textContent ?? "{}", ); expect(values["geometry.position"]).toEqual({ x: 0.8, y: 0 }); }); it("locks vector pad movement vertically when shift drag is y-dominant", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-4", controls: { position: { applicability: { mode: "always" as const }, defaultValue: { x: "0.00", y: "0.00" }, label: "Position", target: "geometry.position", type: "vector", }, }, title: "Geometry", }, ], title: "Controls", }, }, }, modules: [], }); renderControlsPanelWithSchema(schema); const pad = screen.getByRole("button", { name: "Position X/Y pad" }); vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({ bottom: 100, height: 100, left: 0, right: 100, toJSON: () => undefined, top: 0, width: 100, x: 0, y: 0, } as DOMRect); Object.defineProperty(pad, "setPointerCapture", { configurable: true, value: vi.fn(), }); fireEvent.pointerDown(pad, { buttons: 1, clientX: 50, clientY: 50, pointerId: 1, }); fireEvent.pointerMove(pad, { buttons: 1, clientX: 70, clientY: 90, pointerId: 1, shiftKey: true, }); const values = JSON.parse( screen.getByTestId("values-json").textContent ?? "{}", ); expect(values["geometry.position"]).toEqual({ x: 0, y: 0.8 }); }); it("renders vector pad values as compact rounded coordinates", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-5", controls: { center: { applicability: { mode: "always" as const }, defaultValue: { x: "-0.07070312499999998", y: "-0.00392795138", }, label: "Center", target: "geometry.center", type: "vector", }, }, title: "Geometry", }, ], title: "Controls", }, }, }, modules: [], }); renderControlsPanelWithSchema(schema); expect(screen.getByRole("button", { name: "Edit Center X value" }).textContent).toBe("-0.07"); expect(screen.getByRole("button", { name: "Edit Center Y value" }).textContent).toBe("0.00"); expect( screen.queryByText("-0.07070312499999998, -0.00392795138"), ).toBeNull(); }); it("prevents native text selection while dragging a vector pad", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-6", controls: { center: { applicability: { mode: "always" as const }, defaultValue: { x: "0.00", y: "0.00" }, label: "Center", target: "geometry.center", type: "vector", }, }, title: "Geometry", }, ], title: "Controls", }, }, }, modules: [], }); renderControlsPanelWithSchema(schema); const pad = screen.getByRole("button", { name: "Center X/Y pad" }); vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({ bottom: 100, height: 100, left: 0, right: 100, toJSON: () => undefined, top: 0, width: 100, x: 0, y: 0, } as DOMRect); Object.defineProperty(pad, "setPointerCapture", { configurable: true, value: vi.fn(), }); const pointerDown = createEvent.pointerDown(pad, { buttons: 1, clientX: 50, clientY: 50, pointerId: 1, }); fireEvent(pad, pointerDown); expect(pointerDown.defaultPrevented).toBe(true); expect(pad.className).toContain("select-none"); }); it("allows cartesian vector pads when a product intentionally uses y-up math", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-7", controls: { vector: { applicability: { mode: "always" as const }, coordinateMode: "cartesian", defaultValue: { x: "0.00", y: "0.00" }, label: "Vector", target: "math.vector", type: "vector", }, }, title: "Math", }, ], title: "Controls", }, }, }, modules: [], }); renderControlsPanelWithSchema(schema); const pad = screen.getByRole("button", { name: "Vector X/Y pad" }); expect(pad.getAttribute("data-vector-pad-coordinate-mode")).toBe( "cartesian", ); vi.spyOn(pad, "getBoundingClientRect").mockReturnValue({ bottom: 100, height: 100, left: 0, right: 100, toJSON: () => undefined, top: 0, width: 100, x: 0, y: 0, } as DOMRect); Object.defineProperty(pad, "setPointerCapture", { configurable: true, value: vi.fn(), }); fireEvent.pointerDown(pad, { buttons: 1, clientX: 0, clientY: 0, pointerId: 1, }); const values = JSON.parse( screen.getByTestId("values-json").textContent ?? "{}", ); expect(values["math.vector"]).toEqual({ x: -1, y: 1 }); }); });