import * as React from "react"; import { cleanup, render } from "@testing-library/react"; import { afterEach, beforeAll, beforeEach, expect, vi } from "vitest"; import { defineToolcraft } from "../../schema/define-toolcraft"; import type { ToolcraftExternalStore } from "../../state/toolcraft-external-store"; import { ToolcraftRoot } from "../app-shell/toolcraft-root"; import { useToolcraftStore } from "../app-shell/toolcraft-store-context"; import { CanvasShell } from "../canvas/canvas-shell"; import { ControlsPanel } from "../controls-panel/controls-panel"; import { DEFAULT_TOOLCRAFT_ORIENTATION_POSE, type ToolcraftOrientationPose, } from "./orientation-gizmo-math"; import { useToolcraftModelOrbitInteraction } from "./use-toolcraft-model-orbit-interaction"; const schema = defineToolcraft({ canvas: { enabled: true, size: { height: 200, unit: "px", width: 300 }, }, panels: { controls: { sections: [ { controls: { scale: { defaultValue: 1, label: "Scale", max: 2, min: 0.5, target: "model.scale", type: "slider", }, orientation: { defaultValue: DEFAULT_TOOLCRAFT_ORIENTATION_POSE, keyframeable: false, label: false, target: "view.orbit", type: "orientationGizmo", }, }, title: "Model", }, ], title: "Master Controls", }, }, }); function CaptureStore({ capture, }: { capture: (store: ToolcraftExternalStore) => void; }): null { capture(useToolcraftStore()); return null; } function OrbitSurface({ hit }: { hit: boolean }): React.JSX.Element { const handlers = useToolcraftModelOrbitInteraction({ hitTest: () => hit, target: "view.orbit", }); return (
); } function createCanvasContextMock(): CanvasRenderingContext2D { return { arc: vi.fn(), beginPath: vi.fn(), clearRect: vi.fn(), fill: vi.fn(), lineCap: "butt", lineTo: vi.fn(), moveTo: vi.fn(), setTransform: vi.fn(), stroke: vi.fn(), } as unknown as CanvasRenderingContext2D; } beforeAll(() => { Object.defineProperty(window, "matchMedia", { configurable: true, value: () => ({ addEventListener: () => undefined, matches: false, removeEventListener: () => undefined, }), }); Object.defineProperty(window, "requestAnimationFrame", { configurable: true, value: (callback: FrameRequestCallback) => window.setTimeout(() => callback(performance.now()), 16), writable: true, }); Object.defineProperty(window, "cancelAnimationFrame", { configurable: true, value: (handle: number) => window.clearTimeout(handle), writable: true, }); }); beforeEach(() => { vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue( createCanvasContextMock(), ); Object.defineProperty(window, "requestAnimationFrame", { configurable: true, value: vi.fn((callback: FrameRequestCallback) => window.setTimeout(() => callback(performance.now()), 16), ), writable: true, }); Object.defineProperty(window, "cancelAnimationFrame", { configurable: true, value: vi.fn((handle: number) => window.clearTimeout(handle)), writable: true, }); }); afterEach(() => { cleanup(); window.localStorage.clear(); vi.restoreAllMocks(); vi.useRealTimers(); }); const gizmoBounds: DOMRect = { bottom: 70, height: 70, left: 0, right: 70, toJSON: () => ({}), top: 0, width: 70, x: 0, y: 0, }; const modelSurfaceBounds: DOMRect = { bottom: 200, height: 200, left: 0, right: 300, toJSON: () => ({}), top: 0, width: 300, x: 0, y: 0, }; export function expectOrientationPoseClose( received: ToolcraftOrientationPose, expected: ToolcraftOrientationPose, ): void { for (const key of ["position", "up"] as const) { expected[key].forEach((value, index) => { expect(received[key][index]).toBeCloseTo(value, 12); }); } } export function mockOrientationGizmoBounds(element: Element): void { vi.spyOn(element, "getBoundingClientRect").mockReturnValue(gizmoBounds); } export function mockModelSurfaceBounds(element: Element): void { vi.spyOn(element, "getBoundingClientRect").mockReturnValue( modelSurfaceBounds, ); } export function renderOrientationGizmoRuntime({ hit = true, }: { hit?: boolean; } = {}): ToolcraftExternalStore { let store: ToolcraftExternalStore | undefined; render(
{ store = value; }} />
, ); if (!store) { throw new Error("Orientation gizmo test runtime did not expose its store."); } return store; }