diff --git a/src/components/CanvasOutlineDrawer.tsx b/src/components/CanvasOutlineDrawer.tsx index ab5004a0..c4bd7ecf 100644 --- a/src/components/CanvasOutlineDrawer.tsx +++ b/src/components/CanvasOutlineDrawer.tsx @@ -250,11 +250,11 @@ export function CanvasOutlineDrawer({ open, onClose }: CanvasOutlineDrawerProps) open={open} onClose={onClose} placement="left" - width={280} + size={280} title={t("canvasOutline.title")} classNames={{ mask: "!bg-black/20", - content: "!bg-[var(--surface-2)] !text-[var(--text-primary)]", + section: "!bg-[var(--surface-2)] !text-[var(--text-primary)]", header: "!border-[var(--border-subtle)] !px-4 !py-3", body: "!flex !h-full !flex-col !gap-3 !p-3", }} diff --git a/src/components/__tests__/CanvasOutlineDrawer.test.tsx b/src/components/__tests__/CanvasOutlineDrawer.test.tsx new file mode 100644 index 00000000..50c9bc25 --- /dev/null +++ b/src/components/__tests__/CanvasOutlineDrawer.test.tsx @@ -0,0 +1,107 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { fireEvent, render, screen } from "@testing-library/react"; + +import { CanvasOutlineDrawer } from "@/components/CanvasOutlineDrawer"; + +const translations: Record = { + "canvasOutline.title": "Canvas outline", + "canvasOutline.group": "Group", + "canvasOutline.nodeFallback": "Node", + "canvasOutline.searchPlaceholder": "Search nodes", + "canvasOutline.empty": "No nodes on the canvas", + "canvasOutline.noSearchResults": "No matching nodes", + "canvasOutline.totalNodes": "{count} nodes", + "canvasOutline.groupNodeCount": "{count} nodes", + "canvasOutline.expandGroup": "Expand group", + "canvasOutline.collapseGroup": "Collapse group", + "node.imageInput": "Image Input", + "node.prompt": "Prompt", +}; + +vi.mock("@/i18n", () => ({ + useI18n: () => ({ + t: (key: string, values?: Record) => { + let text = translations[key] ?? key; + if (values) { + for (const [name, value] of Object.entries(values)) { + text = text.replace(`{${name}}`, String(value)); + } + } + return text; + }, + }), +})); + +const mockSelectSingleNode = vi.fn(); +const mockSetNavigationTarget = vi.fn(); +const mockUseWorkflowStore = vi.fn(); + +vi.mock("@/store/workflowStore", () => ({ + useWorkflowStore: (selector: (state: unknown) => unknown) => mockUseWorkflowStore(selector), +})); + +const createState = () => ({ + nodes: [ + { + id: "image-1", + type: "imageInput", + position: { x: 0, y: 0 }, + data: { customTitle: "Hero image" }, + }, + { + id: "group-node-group-1", + type: "group", + position: { x: 100, y: 100 }, + data: { groupId: "group-1" }, + }, + { + id: "prompt-1", + type: "prompt", + parentId: "group-node-group-1", + groupId: "group-1", + position: { x: 20, y: 30 }, + data: {}, + }, + ], + groups: { + "group-1": { + id: "group-1", + name: "Storyboard", + color: "neutral", + position: { x: 100, y: 100 }, + size: { width: 400, height: 300 }, + }, + }, + selectSingleNode: mockSelectSingleNode, + setNavigationTarget: mockSetNavigationTarget, +}); + +describe("CanvasOutlineDrawer", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockUseWorkflowStore.mockImplementation((selector) => selector(createState())); + }); + + it("renders root nodes, grouped child nodes, and navigates to clicked nodes", () => { + render(); + + expect(screen.getByText("Hero image")).toBeInTheDocument(); + expect(screen.getByText("Storyboard")).toBeInTheDocument(); + expect(screen.getByText("Prompt")).toBeInTheDocument(); + expect(screen.getByText("2 nodes")).toBeInTheDocument(); + + fireEvent.click(screen.getByTitle("Hero image")); + + expect(mockSelectSingleNode).toHaveBeenCalledWith("image-1"); + expect(mockSetNavigationTarget).toHaveBeenCalledWith("image-1"); + }); + + it("navigates to the backing group node when a group row is clicked", () => { + render(); + + fireEvent.click(screen.getByTitle("Storyboard")); + + expect(mockSelectSingleNode).toHaveBeenCalledWith("group-node-group-1"); + expect(mockSetNavigationTarget).toHaveBeenCalledWith("group-node-group-1"); + }); +});