Browse Source
SplitGridNode tests (23): - Basic rendering with title, input/output handles - Grid configuration display (rows x cols) - Empty state and unconfigured warning - Source image display with grid overlay - Settings modal open/close and auto-open behavior - Split button functionality and disabled states - Child node count display - Loading and error states - Custom title editing GroupNode tests (28): - Basic rendering with group name and color - Color picker button and delete button - Group name editing (Enter, Escape, blur) - Color picker open/close and color selection - Delete group functionality - Node resizer visibility based on selection - Header drag functionality - All 6 group colors (neutral, blue, green, purple, orange, red) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>handoff-20260429-1057
2 changed files with 865 additions and 0 deletions
@ -0,0 +1,468 @@ |
|||||
|
import { describe, it, expect, vi, beforeEach } from "vitest"; |
||||
|
import { render, screen, fireEvent } from "@testing-library/react"; |
||||
|
import { GroupNode } from "@/components/nodes/GroupNode"; |
||||
|
import { ReactFlowProvider } from "@xyflow/react"; |
||||
|
|
||||
|
// Mock the workflow store
|
||||
|
const mockUpdateGroup = vi.fn(); |
||||
|
const mockDeleteGroup = vi.fn(); |
||||
|
const mockMoveGroupNodes = vi.fn(); |
||||
|
const mockUseWorkflowStore = vi.fn(); |
||||
|
|
||||
|
// Mock GROUP_COLORS
|
||||
|
const mockGroupColors: Record<string, string> = { |
||||
|
neutral: "#262626", |
||||
|
blue: "#1e3a5f", |
||||
|
green: "#1a3d2e", |
||||
|
purple: "#2d2458", |
||||
|
orange: "#3d2a1a", |
||||
|
red: "#3d1a1a", |
||||
|
}; |
||||
|
|
||||
|
vi.mock("@/store/workflowStore", () => ({ |
||||
|
useWorkflowStore: () => mockUseWorkflowStore(), |
||||
|
GROUP_COLORS: { |
||||
|
neutral: "#262626", |
||||
|
blue: "#1e3a5f", |
||||
|
green: "#1a3d2e", |
||||
|
purple: "#2d2458", |
||||
|
orange: "#3d2a1a", |
||||
|
red: "#3d1a1a", |
||||
|
}, |
||||
|
})); |
||||
|
|
||||
|
// Mock useReactFlow
|
||||
|
vi.mock("@xyflow/react", async () => { |
||||
|
const actual = await vi.importActual("@xyflow/react"); |
||||
|
return { |
||||
|
...actual, |
||||
|
useReactFlow: () => ({ |
||||
|
getNodes: vi.fn(() => []), |
||||
|
setNodes: vi.fn(), |
||||
|
}), |
||||
|
NodeResizer: ({ isVisible, children }: { isVisible: boolean; children?: React.ReactNode }) => ( |
||||
|
<div data-testid="node-resizer" data-visible={isVisible}> |
||||
|
{children} |
||||
|
</div> |
||||
|
), |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
// Wrapper component for React Flow context
|
||||
|
function TestWrapper({ children }: { children: React.ReactNode }) { |
||||
|
return <ReactFlowProvider>{children}</ReactFlowProvider>; |
||||
|
} |
||||
|
|
||||
|
describe("GroupNode", () => { |
||||
|
const defaultGroup = { |
||||
|
id: "group-1", |
||||
|
name: "Test Group", |
||||
|
color: "blue" as const, |
||||
|
position: { x: 0, y: 0 }, |
||||
|
size: { width: 300, height: 200 }, |
||||
|
locked: false, |
||||
|
}; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
vi.clearAllMocks(); |
||||
|
// Default mock implementation
|
||||
|
mockUseWorkflowStore.mockReturnValue({ |
||||
|
groups: { "group-1": defaultGroup }, |
||||
|
updateGroup: mockUpdateGroup, |
||||
|
deleteGroup: mockDeleteGroup, |
||||
|
moveGroupNodes: mockMoveGroupNodes, |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
const createNodeProps = (data: { groupId: string } = { groupId: "group-1" }) => ({ |
||||
|
id: "group-node-1", |
||||
|
type: "group" as const, |
||||
|
data, |
||||
|
selected: false, |
||||
|
}); |
||||
|
|
||||
|
describe("Basic Rendering", () => { |
||||
|
it("should render the group name", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Test Group")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should apply group color to background", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const groupContainer = container.querySelector(".rounded-xl"); |
||||
|
expect(groupContainer).toBeInTheDocument(); |
||||
|
// Browser converts hex to rgb, so check for the RGB values instead
|
||||
|
// #1e3a5f = rgb(30, 58, 95)
|
||||
|
expect(groupContainer?.getAttribute("style")).toContain("rgb(30, 58, 95)"); |
||||
|
}); |
||||
|
|
||||
|
it("should render color picker button", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const colorButton = container.querySelector('button[title="Change color"]'); |
||||
|
expect(colorButton).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render delete button", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const deleteButton = container.querySelector('button[title="Delete group"]'); |
||||
|
expect(deleteButton).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should return null when group not found", () => { |
||||
|
mockUseWorkflowStore.mockReturnValue({ |
||||
|
groups: {}, |
||||
|
updateGroup: mockUpdateGroup, |
||||
|
deleteGroup: mockDeleteGroup, |
||||
|
moveGroupNodes: mockMoveGroupNodes, |
||||
|
}); |
||||
|
|
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps({ groupId: "non-existent" })} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Should render nothing
|
||||
|
expect(container.querySelector(".rounded-xl")).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Group Name Editing", () => { |
||||
|
it("should enter edit mode when name is clicked", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const nameSpan = screen.getByText("Test Group"); |
||||
|
fireEvent.click(nameSpan); |
||||
|
|
||||
|
// Input should be visible
|
||||
|
const input = screen.getByDisplayValue("Test Group"); |
||||
|
expect(input).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should update group name on Enter key", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Click to edit
|
||||
|
const nameSpan = screen.getByText("Test Group"); |
||||
|
fireEvent.click(nameSpan); |
||||
|
|
||||
|
// Change value and press Enter
|
||||
|
const input = screen.getByDisplayValue("Test Group"); |
||||
|
fireEvent.change(input, { target: { value: "New Name" } }); |
||||
|
fireEvent.keyDown(input, { key: "Enter" }); |
||||
|
|
||||
|
expect(mockUpdateGroup).toHaveBeenCalledWith("group-1", { name: "New Name" }); |
||||
|
}); |
||||
|
|
||||
|
it("should cancel editing on Escape key", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Click to edit
|
||||
|
const nameSpan = screen.getByText("Test Group"); |
||||
|
fireEvent.click(nameSpan); |
||||
|
|
||||
|
// Change value and press Escape
|
||||
|
const input = screen.getByDisplayValue("Test Group"); |
||||
|
fireEvent.change(input, { target: { value: "Changed" } }); |
||||
|
fireEvent.keyDown(input, { key: "Escape" }); |
||||
|
|
||||
|
// Should revert and exit edit mode
|
||||
|
expect(mockUpdateGroup).not.toHaveBeenCalled(); |
||||
|
expect(screen.getByText("Test Group")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should update group name on blur", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Click to edit
|
||||
|
const nameSpan = screen.getByText("Test Group"); |
||||
|
fireEvent.click(nameSpan); |
||||
|
|
||||
|
// Change value and blur
|
||||
|
const input = screen.getByDisplayValue("Test Group"); |
||||
|
fireEvent.change(input, { target: { value: "Blurred Name" } }); |
||||
|
fireEvent.blur(input); |
||||
|
|
||||
|
expect(mockUpdateGroup).toHaveBeenCalledWith("group-1", { name: "Blurred Name" }); |
||||
|
}); |
||||
|
|
||||
|
it("should not update if name is unchanged", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Click to edit
|
||||
|
const nameSpan = screen.getByText("Test Group"); |
||||
|
fireEvent.click(nameSpan); |
||||
|
|
||||
|
// Press Enter without changing
|
||||
|
const input = screen.getByDisplayValue("Test Group"); |
||||
|
fireEvent.keyDown(input, { key: "Enter" }); |
||||
|
|
||||
|
expect(mockUpdateGroup).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("should not update if name is empty", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Click to edit
|
||||
|
const nameSpan = screen.getByText("Test Group"); |
||||
|
fireEvent.click(nameSpan); |
||||
|
|
||||
|
// Clear value and blur
|
||||
|
const input = screen.getByDisplayValue("Test Group"); |
||||
|
fireEvent.change(input, { target: { value: " " } }); |
||||
|
fireEvent.blur(input); |
||||
|
|
||||
|
expect(mockUpdateGroup).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Color Picker", () => { |
||||
|
it("should open color picker when color button is clicked", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const colorButton = container.querySelector('button[title="Change color"]'); |
||||
|
fireEvent.click(colorButton!); |
||||
|
|
||||
|
// Color options should be visible (6 colors)
|
||||
|
const colorOptions = container.querySelectorAll('.grid.grid-cols-4 button'); |
||||
|
expect(colorOptions.length).toBe(6); |
||||
|
}); |
||||
|
|
||||
|
it("should change group color when color option is clicked", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Open color picker
|
||||
|
const colorButton = container.querySelector('button[title="Change color"]'); |
||||
|
fireEvent.click(colorButton!); |
||||
|
|
||||
|
// Click on green color option
|
||||
|
const greenOption = container.querySelector('button[title="Green"]'); |
||||
|
fireEvent.click(greenOption!); |
||||
|
|
||||
|
expect(mockUpdateGroup).toHaveBeenCalledWith("group-1", { color: "green" }); |
||||
|
}); |
||||
|
|
||||
|
it("should close color picker after selecting a color", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Open color picker
|
||||
|
const colorButton = container.querySelector('button[title="Change color"]'); |
||||
|
fireEvent.click(colorButton!); |
||||
|
|
||||
|
// Select a color
|
||||
|
const greenOption = container.querySelector('button[title="Green"]'); |
||||
|
fireEvent.click(greenOption!); |
||||
|
|
||||
|
// Color picker should be closed
|
||||
|
const colorOptions = container.querySelectorAll('.grid.grid-cols-4 button'); |
||||
|
expect(colorOptions.length).toBe(0); |
||||
|
}); |
||||
|
|
||||
|
it("should highlight current color in picker", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Open color picker
|
||||
|
const colorButton = container.querySelector('button[title="Change color"]'); |
||||
|
fireEvent.click(colorButton!); |
||||
|
|
||||
|
// Blue option should have highlight styles (current color)
|
||||
|
const blueOption = container.querySelector('button[title="Blue"]'); |
||||
|
expect(blueOption).toHaveClass("border-white"); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Delete Functionality", () => { |
||||
|
it("should call deleteGroup when delete button is clicked", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const deleteButton = container.querySelector('button[title="Delete group"]'); |
||||
|
fireEvent.click(deleteButton!); |
||||
|
|
||||
|
expect(mockDeleteGroup).toHaveBeenCalledWith("group-1"); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Node Resizer", () => { |
||||
|
it("should show resizer when selected", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} selected={true} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const resizer = screen.getByTestId("node-resizer"); |
||||
|
expect(resizer).toHaveAttribute("data-visible", "true"); |
||||
|
}); |
||||
|
|
||||
|
it("should hide resizer when not selected", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} selected={false} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const resizer = screen.getByTestId("node-resizer"); |
||||
|
expect(resizer).toHaveAttribute("data-visible", "false"); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Header Drag", () => { |
||||
|
it("should have cursor-grab class on header", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const header = container.querySelector(".cursor-grab"); |
||||
|
expect(header).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should not start drag when clicking on buttons", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Click on delete button
|
||||
|
const deleteButton = container.querySelector('button[title="Delete group"]'); |
||||
|
fireEvent.mouseDown(deleteButton!); |
||||
|
|
||||
|
// moveGroupNodes should not be called
|
||||
|
expect(mockMoveGroupNodes).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
|
||||
|
it("should start drag when clicking on header background", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const header = container.querySelector(".cursor-grab"); |
||||
|
fireEvent.mouseDown(header!, { clientX: 100, clientY: 100 }); |
||||
|
|
||||
|
// Move mouse
|
||||
|
fireEvent.mouseMove(window, { clientX: 120, clientY: 120 }); |
||||
|
|
||||
|
// moveGroupNodes should be called with delta
|
||||
|
expect(mockMoveGroupNodes).toHaveBeenCalledWith("group-1", { x: 20, y: 20 }); |
||||
|
}); |
||||
|
|
||||
|
it("should stop drag on mouseup", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const header = container.querySelector(".cursor-grab"); |
||||
|
fireEvent.mouseDown(header!, { clientX: 100, clientY: 100 }); |
||||
|
fireEvent.mouseMove(window, { clientX: 120, clientY: 120 }); |
||||
|
fireEvent.mouseUp(window); |
||||
|
|
||||
|
// Clear the mock
|
||||
|
mockMoveGroupNodes.mockClear(); |
||||
|
|
||||
|
// Move mouse again - should not trigger moveGroupNodes
|
||||
|
fireEvent.mouseMove(window, { clientX: 140, clientY: 140 }); |
||||
|
expect(mockMoveGroupNodes).not.toHaveBeenCalled(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Different Group Colors", () => { |
||||
|
// Map color names to RGB values (browser converts hex to rgb)
|
||||
|
it.each([ |
||||
|
["neutral", "rgb(38, 38, 38)"], |
||||
|
["blue", "rgb(30, 58, 95)"], |
||||
|
["green", "rgb(26, 61, 46)"], |
||||
|
["purple", "rgb(45, 36, 88)"], |
||||
|
["orange", "rgb(61, 42, 26)"], |
||||
|
["red", "rgb(61, 26, 26)"], |
||||
|
])("should render with %s color", (colorName, colorRgb) => { |
||||
|
mockUseWorkflowStore.mockReturnValue({ |
||||
|
groups: { |
||||
|
"group-1": { ...defaultGroup, color: colorName } |
||||
|
}, |
||||
|
updateGroup: mockUpdateGroup, |
||||
|
deleteGroup: mockDeleteGroup, |
||||
|
moveGroupNodes: mockMoveGroupNodes, |
||||
|
}); |
||||
|
|
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<GroupNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const groupContainer = container.querySelector(".rounded-xl"); |
||||
|
// Check that container has inline style with the color (browser converts hex to rgb)
|
||||
|
expect(groupContainer?.getAttribute("style")).toContain(colorRgb); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,397 @@ |
|||||
|
import { describe, it, expect, vi, beforeEach } from "vitest"; |
||||
|
import { render, screen, fireEvent } from "@testing-library/react"; |
||||
|
import { SplitGridNode } from "@/components/nodes/SplitGridNode"; |
||||
|
import { ReactFlowProvider } from "@xyflow/react"; |
||||
|
import { SplitGridNodeData } from "@/types"; |
||||
|
|
||||
|
// Mock the workflow store
|
||||
|
const mockUpdateNodeData = vi.fn(); |
||||
|
const mockRegenerateNode = vi.fn(); |
||||
|
const mockUseWorkflowStore = vi.fn(); |
||||
|
|
||||
|
vi.mock("@/store/workflowStore", () => ({ |
||||
|
useWorkflowStore: (selector: (state: unknown) => unknown) => mockUseWorkflowStore(selector), |
||||
|
})); |
||||
|
|
||||
|
// Mock useReactFlow
|
||||
|
vi.mock("@xyflow/react", async () => { |
||||
|
const actual = await vi.importActual("@xyflow/react"); |
||||
|
return { |
||||
|
...actual, |
||||
|
useReactFlow: () => ({ |
||||
|
getNodes: vi.fn(() => []), |
||||
|
setNodes: vi.fn(), |
||||
|
}), |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
// Mock the SplitGridSettingsModal
|
||||
|
vi.mock("@/components/SplitGridSettingsModal", () => ({ |
||||
|
SplitGridSettingsModal: ({ onClose }: { onClose: () => void }) => ( |
||||
|
<div data-testid="split-grid-settings-modal"> |
||||
|
<button onClick={onClose}>Close Modal</button> |
||||
|
</div> |
||||
|
), |
||||
|
})); |
||||
|
|
||||
|
// Wrapper component for React Flow context
|
||||
|
function TestWrapper({ children }: { children: React.ReactNode }) { |
||||
|
return <ReactFlowProvider>{children}</ReactFlowProvider>; |
||||
|
} |
||||
|
|
||||
|
describe("SplitGridNode", () => { |
||||
|
beforeEach(() => { |
||||
|
vi.clearAllMocks(); |
||||
|
// Default mock implementation
|
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
const state = { |
||||
|
updateNodeData: mockUpdateNodeData, |
||||
|
regenerateNode: mockRegenerateNode, |
||||
|
isRunning: false, |
||||
|
currentNodeId: null, |
||||
|
groups: {}, |
||||
|
nodes: [], |
||||
|
}; |
||||
|
return selector(state); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
const createDefaultNodeData = (overrides: Partial<SplitGridNodeData> = {}): SplitGridNodeData => ({ |
||||
|
sourceImage: null, |
||||
|
targetCount: 4, |
||||
|
defaultPrompt: "", |
||||
|
generateSettings: { |
||||
|
aspectRatio: "1:1", |
||||
|
resolution: "1K", |
||||
|
model: "nano-banana", |
||||
|
useGoogleSearch: false, |
||||
|
}, |
||||
|
childNodeIds: [], |
||||
|
gridRows: 2, |
||||
|
gridCols: 2, |
||||
|
isConfigured: false, |
||||
|
status: "idle", |
||||
|
error: null, |
||||
|
...overrides, |
||||
|
}); |
||||
|
|
||||
|
const createNodeProps = (data: Partial<SplitGridNodeData> = {}) => ({ |
||||
|
id: "split-grid-node-1", |
||||
|
type: "splitGrid" as const, |
||||
|
data: createDefaultNodeData(data), |
||||
|
selected: false, |
||||
|
}); |
||||
|
|
||||
|
describe("Basic Rendering", () => { |
||||
|
it("should render the title 'Split Grid'", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Split Grid")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render input handle for image", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const imageHandle = container.querySelector('[data-handletype="image"]'); |
||||
|
expect(imageHandle).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render output handle for reference", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const referenceHandle = container.querySelector('[data-handletype="reference"]'); |
||||
|
expect(referenceHandle).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should render grid configuration summary", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ gridRows: 2, gridCols: 3, targetCount: 6 })} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("2x3 grid (6 images)")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Empty State", () => { |
||||
|
it("should show 'Connect image' message when no source image", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ sourceImage: null })} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Connect image")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should show unconfigured warning when not configured", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ isConfigured: false })} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Not configured - click Settings")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Source Image Display", () => { |
||||
|
it("should display source image when provided", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ sourceImage: "data:image/png;base64,abc123" })} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const img = screen.getByAltText("Source grid"); |
||||
|
expect(img).toBeInTheDocument(); |
||||
|
expect(img).toHaveAttribute("src", "data:image/png;base64,abc123"); |
||||
|
}); |
||||
|
|
||||
|
it("should show grid overlay on source image", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ |
||||
|
sourceImage: "data:image/png;base64,abc123", |
||||
|
gridRows: 2, |
||||
|
gridCols: 2, |
||||
|
targetCount: 4 |
||||
|
})} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Check for grid overlay cells
|
||||
|
const gridCells = container.querySelectorAll(".border.border-blue-400\\/50"); |
||||
|
expect(gridCells.length).toBe(4); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Settings Modal", () => { |
||||
|
it("should open settings modal when Settings button is clicked", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const settingsButton = screen.getByText("Settings"); |
||||
|
fireEvent.click(settingsButton); |
||||
|
|
||||
|
expect(screen.getByTestId("split-grid-settings-modal")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should close settings modal when onClose is called", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Open modal
|
||||
|
const settingsButton = screen.getByText("Settings"); |
||||
|
fireEvent.click(settingsButton); |
||||
|
|
||||
|
expect(screen.getByTestId("split-grid-settings-modal")).toBeInTheDocument(); |
||||
|
|
||||
|
// Close modal
|
||||
|
const closeButton = screen.getByText("Close Modal"); |
||||
|
fireEvent.click(closeButton); |
||||
|
|
||||
|
expect(screen.queryByTestId("split-grid-settings-modal")).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should auto-open settings when not configured and no child nodes", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ isConfigured: false, childNodeIds: [] })} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Modal should be open automatically
|
||||
|
expect(screen.getByTestId("split-grid-settings-modal")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should not auto-open settings when already configured", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ isConfigured: true })} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Modal should not be open
|
||||
|
expect(screen.queryByTestId("split-grid-settings-modal")).not.toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Split Button", () => { |
||||
|
it("should render Split button", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ isConfigured: true })} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Split")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should call regenerateNode when Split button is clicked", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ isConfigured: true })} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const splitButton = screen.getByText("Split"); |
||||
|
fireEvent.click(splitButton); |
||||
|
|
||||
|
expect(mockRegenerateNode).toHaveBeenCalledWith("split-grid-node-1"); |
||||
|
}); |
||||
|
|
||||
|
it("should disable Split button when not configured", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ isConfigured: false })} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const splitButton = screen.getByText("Split"); |
||||
|
expect(splitButton).toBeDisabled(); |
||||
|
}); |
||||
|
|
||||
|
it("should disable Split button when workflow is running", () => { |
||||
|
mockUseWorkflowStore.mockImplementation((selector) => { |
||||
|
const state = { |
||||
|
updateNodeData: mockUpdateNodeData, |
||||
|
regenerateNode: mockRegenerateNode, |
||||
|
isRunning: true, |
||||
|
currentNodeId: null, |
||||
|
groups: {}, |
||||
|
nodes: [], |
||||
|
}; |
||||
|
return selector(state); |
||||
|
}); |
||||
|
|
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ isConfigured: true })} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const splitButton = screen.getByText("Split"); |
||||
|
expect(splitButton).toBeDisabled(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Child Node Count", () => { |
||||
|
it("should display child node count when configured", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ |
||||
|
isConfigured: true, |
||||
|
childNodeIds: [ |
||||
|
{ imageInput: "1", prompt: "2", nanoBanana: "3" }, |
||||
|
{ imageInput: "4", prompt: "5", nanoBanana: "6" }, |
||||
|
{ imageInput: "7", prompt: "8", nanoBanana: "9" }, |
||||
|
] |
||||
|
})} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("3 generate sets created")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Loading State", () => { |
||||
|
it("should show loading spinner when status is loading", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ status: "loading" })} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
const spinner = container.querySelector(".animate-spin"); |
||||
|
expect(spinner).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should show loading overlay on source image when loading", () => { |
||||
|
const { container } = render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ |
||||
|
sourceImage: "data:image/png;base64,abc", |
||||
|
status: "loading" |
||||
|
})} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Check for loading overlay
|
||||
|
const overlay = container.querySelector(".bg-neutral-900\\/70"); |
||||
|
expect(overlay).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Error State", () => { |
||||
|
it("should show error message when status is error", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ status: "error", error: "Something went wrong" })} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Something went wrong")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should show default error message when error is null", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ status: "error", error: null })} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("Error")).toBeInTheDocument(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe("Custom Title", () => { |
||||
|
it("should display custom title when provided", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps({ customTitle: "My Split" })} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
expect(screen.getByText("My Split - Split Grid")).toBeInTheDocument(); |
||||
|
}); |
||||
|
|
||||
|
it("should call updateNodeData when custom title is changed", () => { |
||||
|
render( |
||||
|
<TestWrapper> |
||||
|
<SplitGridNode {...createNodeProps()} /> |
||||
|
</TestWrapper> |
||||
|
); |
||||
|
|
||||
|
// Click on title to edit
|
||||
|
const title = screen.getByText("Split Grid"); |
||||
|
fireEvent.click(title); |
||||
|
|
||||
|
// Type new title
|
||||
|
const input = screen.getByPlaceholderText("Custom title..."); |
||||
|
fireEvent.change(input, { target: { value: "New Title" } }); |
||||
|
fireEvent.keyDown(input, { key: "Enter" }); |
||||
|
|
||||
|
expect(mockUpdateNodeData).toHaveBeenCalledWith("split-grid-node-1", { customTitle: "New Title" }); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
Loading…
Reference in new issue