You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

150 lines
6.4 KiB

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<string, string> = {
"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<MultiAngleSettings>(DEFAULT_MULTI_ANGLE_SETTINGS);
return (
<MultiAngleEditor
previewImage="data:image/png;base64,input"
settings={settings}
onChange={(patch) => 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(<MultiAngleHarness />);
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(<MultiAngleHarness />);
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(<MultiAngleHarness />);
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(<MultiAngleHarness />);
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);
});
});