import { act, fireEvent, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { DEFAULT_TOOLCRAFT_ORIENTATION_POSE, projectToolcraftOrientationAxes, readToolcraftOrientationPose, } from "./orientation-gizmo-math"; import { mockModelSurfaceBounds, mockOrientationGizmoBounds, renderOrientationGizmoRuntime, } from "./orientation-gizmo-test-harness"; function clickPositiveX(gizmo: HTMLElement, pointerId: number): void { fireEvent.pointerDown(gizmo, { button: 0, clientX: 59.5, clientY: 35, pointerId, }); fireEvent.pointerUp(gizmo, { pointerId }); } describe("Toolcraft orientation gizmo snap integration", () => { it("snaps an axis and lets the section reset restore the hidden pose target", async () => { const store = renderOrientationGizmoRuntime(); const gizmo = await screen.findByTestId("toolcraft-orientation-gizmo"); mockOrientationGizmoBounds(gizmo); vi.useFakeTimers(); clickPositiveX(gizmo, 5); act(() => { vi.advanceTimersByTime(220); }); const snapped = readToolcraftOrientationPose( store.getState().values["view.orbit"], ); expect(snapped.position[0]).toBeCloseTo(5, 8); expect(snapped.position[1]).toBeCloseTo(0, 8); expect(snapped.position[2]).toBeCloseTo(0, 8); fireEvent.click(screen.getByRole("button", { name: "Reset Model section" })); expect(store.getState().values["view.orbit"]).toEqual( DEFAULT_TOOLCRAFT_ORIENTATION_POSE, ); }); it("records one completed axis snap as one undoable history step", async () => { const store = renderOrientationGizmoRuntime(); const gizmo = await screen.findByTestId("toolcraft-orientation-gizmo"); mockOrientationGizmoBounds(gizmo); vi.useFakeTimers(); clickPositiveX(gizmo, 51); act(() => { vi.advanceTimersByTime(120); }); expect(store.getState().history.undo).toHaveLength(1); act(() => { store.dispatch({ type: "history.undo" }); }); expect(store.getState().values["view.orbit"]).toEqual( DEFAULT_TOOLCRAFT_ORIENTATION_POSE, ); }); it("cancels an older snap when another endpoint is clicked", async () => { const store = renderOrientationGizmoRuntime(); const gizmo = await screen.findByTestId("toolcraft-orientation-gizmo"); mockOrientationGizmoBounds(gizmo); vi.useFakeTimers(); clickPositiveX(gizmo, 52); act(() => { vi.advanceTimersByTime(32); }); const currentPose = readToolcraftOrientationPose( store.getState().values["view.orbit"], ); const negativeX = projectToolcraftOrientationAxes(currentPose).find( (projection) => projection.axis === "-x", ); expect(negativeX).toBeDefined(); fireEvent.pointerDown(gizmo, { button: 0, clientX: negativeX?.x, clientY: negativeX?.y, pointerId: 53, }); expect(gizmo.getAttribute("data-hovered-axis")).toBe("-x"); fireEvent.pointerUp(gizmo, { pointerId: 53 }); act(() => { vi.advanceTimersByTime(220); }); const finalPose = readToolcraftOrientationPose( store.getState().values["view.orbit"], ); expect(finalPose.position[0]).toBeCloseTo(-5, 8); expect(finalPose.position[1]).toBeCloseTo(0, 8); expect(finalPose.position[2]).toBeCloseTo(0, 8); const historyLength = store.getState().history.undo.length; act(() => { vi.advanceTimersByTime(220); }); expect(store.getState().values["view.orbit"]).toEqual(finalPose); expect(store.getState().history.undo).toHaveLength(historyLength); }); it("does not let an in-flight axis snap overwrite a later section reset", async () => { const store = renderOrientationGizmoRuntime(); const gizmo = await screen.findByTestId("toolcraft-orientation-gizmo"); mockOrientationGizmoBounds(gizmo); vi.useFakeTimers(); clickPositiveX(gizmo, 6); act(() => { vi.advanceTimersByTime(48); }); expect(store.getState().values["view.orbit"]).not.toEqual( DEFAULT_TOOLCRAFT_ORIENTATION_POSE, ); fireEvent.click(screen.getByRole("button", { name: "Reset Model section" })); expect(store.getState().values["view.orbit"]).toEqual( DEFAULT_TOOLCRAFT_ORIENTATION_POSE, ); act(() => { vi.advanceTimersByTime(220); }); expect(store.getState().values["view.orbit"]).toEqual( DEFAULT_TOOLCRAFT_ORIENTATION_POSE, ); }); it("does not let an old snap resume after direct model orbit takes ownership", async () => { const store = renderOrientationGizmoRuntime(); const gizmo = await screen.findByTestId("toolcraft-orientation-gizmo"); const surface = screen.getByTestId("model-surface"); mockOrientationGizmoBounds(gizmo); mockModelSurfaceBounds(surface); vi.useFakeTimers(); clickPositiveX(gizmo, 7); act(() => { vi.advanceTimersByTime(48); }); fireEvent.pointerDown(surface, { button: 0, clientX: 100, clientY: 100, pointerId: 8, }); fireEvent.pointerMove(surface, { clientX: 128, clientY: 118, pointerId: 8, }); act(() => { vi.advanceTimersByTime(20); }); fireEvent.pointerUp(surface, { pointerId: 8 }); const directOrbitPose = store.getState().values["view.orbit"]; const historyLength = store.getState().history.undo.length; act(() => { vi.advanceTimersByTime(220); }); expect(store.getState().values["view.orbit"]).toEqual(directOrbitPose); expect(store.getState().history.undo).toHaveLength(historyLength); }); });