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