import { act, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { vi } from "vitest"; import { createInitialDirectorState, useDirectorStore } from "./editor/store/directorStore"; import { usePlaybackStore } from "./editor/runtime/playbackStore"; vi.mock("./editor/canvas/DirectorCanvas", () => ({ DirectorCanvas: () =>
, })); import App from "./App"; beforeEach(() => { useDirectorStore.setState({ ...useDirectorStore.getState(), ...createInitialDirectorState(), }); }); it("renders the director desk header and view mode switch", () => { const { container } = render(); expect(screen.queryByText("3D导演台")).not.toBeInTheDocument(); expect(screen.getByRole("button", { name: "导演视角" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "机位视角" })).toBeInTheDocument(); expect(container.querySelector(".top-bar-center .mode-toggle")).toBeInTheDocument(); expect(screen.queryByLabelText("帮助")).not.toBeInTheDocument(); // 右上角的「关闭」按钮已去掉 expect(screen.queryByLabelText("关闭")).not.toBeInTheDocument(); expect(container.querySelector(".top-bar-actions")).not.toBeInTheDocument(); }); it("notifies the host canvas when the director desk app is ready", () => { const postMessage = vi.spyOn(window.parent, "postMessage").mockImplementation(() => undefined); render(); expect(postMessage).toHaveBeenCalledWith( { type: "storyai:director-desk-ready" }, window.location.origin ); postMessage.mockRestore(); }); it("uses a full-width director desk frame instead of floating card columns", () => { const { container } = render(); const shell = container.querySelector(".director-shell.director-shell-fullbleed"); expect(shell).toBeInTheDocument(); expect(shell?.firstElementChild).toHaveClass("viewport-column"); expect(screen.getByLabelText("场景")).toHaveClass("left-sidebar"); expect(screen.getByLabelText("3D视口")).toHaveClass("viewport-column"); expect(screen.getByLabelText("属性")).toHaveClass("right-sidebar"); }); it("collapses both side panels from the fullscreen toolbar action", async () => { const { container, rerender } = render(); expect(container.querySelector(".director-shell-fullbleed.is-sidebars-collapsed")).not.toBeInTheDocument(); act(() => { useDirectorStore.setState({ ...useDirectorStore.getState(), viewportPanelsCollapsed: true, } as ReturnType); }); rerender(); expect(container.querySelector(".director-shell-fullbleed.is-sidebars-collapsed")).toBeInTheDocument(); expect(screen.getByLabelText("场景")).toHaveAttribute("aria-hidden", "true"); expect(screen.getByLabelText("属性")).toHaveAttribute("aria-hidden", "true"); }); it("shows the floating timeline by default and hides it from the record action", () => { const { container } = render(); expect(container.querySelector(".timeline-panel")).toBeInTheDocument(); act(() => { useDirectorStore.getState().toggleTimelineVisible(); }); expect(container.querySelector(".timeline-panel")).not.toBeInTheDocument(); act(() => { useDirectorStore.getState().toggleTimelineVisible(); }); expect(container.querySelector(".timeline-panel")).toBeInTheDocument(); }); it("switches from director mode to camera mode", async () => { const user = userEvent.setup(); render(); const directorButton = screen.getByRole("button", { name: "导演视角" }); const cameraButton = screen.getByRole("button", { name: "机位视角" }); expect(directorButton).toHaveAttribute("aria-pressed", "true"); expect(cameraButton).toHaveAttribute("aria-pressed", "false"); await user.click(cameraButton); expect(directorButton).toHaveAttribute("aria-pressed", "false"); expect(cameraButton).toHaveAttribute("aria-pressed", "true"); }); it("adds a keyframe at the playhead when K is pressed while the recording panel is open", async () => { const user = userEvent.setup(); render(); act(() => { useDirectorStore.getState().selectObject("char_default_a"); usePlaybackStore.getState().setPlayhead(1.5); }); await user.keyboard("k"); const track = useDirectorStore .getState() .project.timeline.tracks.find((item) => item.objectId === "char_default_a"); expect(track?.keyframes).toHaveLength(1); expect(track?.keyframes[0]?.time).toBe(1.5); }); it("records a camera keyframe with K when the selected target is a camera object", async () => { const user = userEvent.setup(); render(); act(() => { useDirectorStore.getState().selectObject("cam_object_1"); usePlaybackStore.getState().setPlayhead(2); }); await user.keyboard("k"); const track = useDirectorStore .getState() .project.timeline.cameraTracks.find((item) => item.cameraId === "cam_1"); expect(track?.keyframes).toHaveLength(1); expect(track?.keyframes[0]?.time).toBe(2); }); it("ignores K while the recording panel is hidden and while typing in a field", async () => { const user = userEvent.setup(); render(); act(() => { useDirectorStore.getState().selectObject("char_default_a"); // 录制面板隐藏 → K 不再是快捷键 useDirectorStore.getState().toggleTimelineVisible(); }); await user.keyboard("k"); expect(useDirectorStore.getState().project.timeline.tracks).toHaveLength(0); act(() => { useDirectorStore.getState().toggleTimelineVisible(); }); // 在输入框里敲 k 只应该输入文字,不能打点 await user.click(screen.getByLabelText("搜索场景内容")); await user.keyboard("k"); expect(useDirectorStore.getState().project.timeline.tracks).toHaveLength(0); }); it("supports Cmd/Ctrl+C and Cmd/Ctrl+V to duplicate the selected object", async () => { const user = userEvent.setup(); render(); await user.click(screen.getByRole("button", { name: "角色01" })); await user.keyboard("{Control>}c{/Control}"); await user.keyboard("{Control>}v{/Control}"); const state = useDirectorStore.getState(); const characters = state.project.objects.filter((item) => item.kind === "character"); expect(characters).toHaveLength(2); expect(characters[1]?.id).not.toBe("char_default_a"); expect(state.selectedObjectId).toBe(characters[1]?.id ?? null); }); it("supports Cmd/Ctrl+Z to undo the latest scene edit", async () => { const user = userEvent.setup(); render(); act(() => { useDirectorStore.getState().addPresetCharacter("female"); }); expect(useDirectorStore.getState().project.objects.some((item) => item.name === "角色02")).toBe(true); await user.keyboard("{Control>}z{/Control}"); expect(useDirectorStore.getState().project.objects.some((item) => item.name === "角色02")).toBe(false); });