import { useState } from "react"; import { describe, it, expect, vi } from "vitest"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { buildMultiAnglePrompt, DEFAULT_MULTI_ANGLE_SETTINGS, MULTI_ANGLE_PRESET_SETTINGS, MultiAngleEditor, type MultiAnglePresetId, type MultiAngleSettings, } from "@/components/MultiAngleEditor"; vi.mock("@/i18n", () => { const messages: Record = { "multiAngle.editorTitle": "Multi-Angle Editor", "multiAngle.sightControl": "Multi-angle sight control", "multiAngle.noImage": "No image", "multiAngle.horizontal": "Horizontal", "multiAngle.vertical": "Vertical", "multiAngle.zoom": "Zoom", "multiAngle.prompt": "Prompt", "multiAngle.enableCustomPrompt": "Enable custom prompt", "multiAngle.promptDialog": "Multi-angle prompt", "multiAngle.promptInput": "Multi-angle prompt input", "multiAngle.promptPlaceholder": "Optional prompt", "multiAngle.resetAria": "Reset multi-angle parameters", "multiAngle.reset": "Reset", "multiAngle.adjustVerticalUp": "Adjust vertical up", "multiAngle.adjustVerticalDown": "Adjust vertical down", "multiAngle.adjustHorizontalLeft": "Adjust horizontal left", "multiAngle.adjustHorizontalRight": "Adjust horizontal right", "multiAngle.preset.custom": "Custom", "multiAngle.preset.fisheye": "Fisheye", "multiAngle.preset.tilt": "Tilt", "multiAngle.preset.frontOverhead": "Front overhead", "multiAngle.preset.frontLow": "Front low angle", "multiAngle.preset.panoramicOverhead": "Panoramic overhead", "multiAngle.preset.back": "Back view", }; return { useI18n: () => ({ t: (key: string) => messages[key] ?? key, }), }; }); function MultiAngleHarness() { const [settings, setSettings] = useState(DEFAULT_MULTI_ANGLE_SETTINGS); return ( setSettings((current) => ({ ...current, ...patch, preset: patch.preset ?? "custom" }))} onSelectPreset={(preset: MultiAnglePresetId) => setSettings({ ...MULTI_ANGLE_PRESET_SETTINGS[preset] })} onTogglePrompt={(enabled) => setSettings((current) => ({ ...current, includePrompt: enabled }))} onReset={() => setSettings((current) => ({ ...DEFAULT_MULTI_ANGLE_SETTINGS, includePrompt: current.includePrompt, promptText: current.promptText, })) } /> ); } describe("MultiAngleEditor", () => { it("renders default custom multi-angle controls", () => { render(); expect(screen.getByText("Multi-Angle Editor")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Custom" })).toBeEnabled(); expect(screen.getByRole("button", { name: "Fisheye" })).toBeEnabled(); expect(screen.getByRole("button", { name: "Tilt" })).toBeEnabled(); expect(screen.getByLabelText("Horizontal")).toHaveValue("0"); expect(screen.getByLabelText("Vertical")).toHaveValue("-30"); expect(screen.getByLabelText("Zoom")).toHaveValue("5"); expect(screen.getByRole("switch", { name: "Prompt" })).toHaveAttribute("aria-checked", "false"); expect(screen.queryByRole("dialog", { name: "Multi-angle prompt" })).not.toBeInTheDocument(); }); it("applies the fisheye preset parameters and prompt", () => { render(); fireEvent.click(screen.getByRole("button", { name: "Fisheye" })); expect(screen.getByRole("button", { name: "Fisheye" })).toHaveClass("border-[var(--border-strong)]"); expect(screen.getByLabelText("Horizontal")).toHaveValue("0"); expect(screen.getByLabelText("Vertical")).toHaveValue("30"); expect(screen.getByLabelText("Zoom")).toHaveValue("10"); expect(screen.getByRole("switch", { name: "Prompt" })).toHaveAttribute("aria-checked", "true"); expect(screen.getByDisplayValue(/Ultra-close fisheye lens/)).toBeInTheDocument(); expect(screen.getByDisplayValue(/barrel distortion/)).toBeInTheDocument(); }); it("syncs cube drag movement with custom parameters", async () => { render(); const globe = screen.getByRole("application", { name: "Multi-angle sight control" }); fireEvent.change(screen.getByLabelText("Vertical"), { target: { value: "0" } }); fireEvent.pointerDown(globe, { pointerId: 1, pointerType: "mouse", clientX: 100, clientY: 100, buttons: 1 }); fireEvent.pointerMove(globe, { pointerId: 1, pointerType: "mouse", clientX: 150, clientY: 80, buttons: 1 }); await waitFor(() => expect(screen.getByLabelText("Horizontal")).toHaveValue("40")); expect(screen.getByLabelText("Vertical")).toHaveValue("-16"); fireEvent.pointerUp(globe, { pointerId: 1, pointerType: "mouse" }); fireEvent.change(screen.getByLabelText("Horizontal"), { target: { value: "360" } }); fireEvent.change(screen.getByLabelText("Vertical"), { target: { value: "57" } }); fireEvent.change(screen.getByLabelText("Zoom"), { target: { value: "8.6" } }); expect(globe).toHaveAttribute("data-horizontal", "360"); expect(globe).toHaveAttribute("data-vertical", "57"); expect(globe).toHaveAttribute("data-zoom", "8.6"); }); it("wraps horizontal cube drag across zero degrees", async () => { render(); const globe = screen.getByRole("application", { name: "Multi-angle sight control" }); fireEvent.pointerDown(globe, { pointerId: 1, pointerType: "mouse", clientX: 100, clientY: 100, buttons: 1 }); fireEvent.pointerMove(globe, { pointerId: 1, pointerType: "mouse", clientX: 80, clientY: 100, buttons: 1 }); await waitFor(() => expect(screen.getByLabelText("Horizontal")).toHaveValue("344")); }); it("builds a platform-neutral multi-angle prompt", () => { const prompt = buildMultiAnglePrompt({ ...DEFAULT_MULTI_ANGLE_SETTINGS, horizontal: 316, vertical: 57, zoom: 8.6, includePrompt: true, promptText: "Keep background layers clear", }); expect(prompt).toMatch(/horizontal orbit 316 degrees counterclockwise/); expect(prompt).toMatch(/front overhead view/); expect(prompt).toMatch(/shot scale zoom 8.6/); expect(prompt).toMatch(/normal 2D image/); expect(prompt).toMatch(/Do not create a 3D model/); expect(prompt).toMatch(/prompt: Keep background layers clear/); expect(prompt).not.toMatch(/libTV/i); }); });