Browse Source

test(17-05): add FloatingActionBar and MultiSelectToolbar tests

FloatingActionBar tests:
- Basic rendering of node type buttons
- Node button click adds node with correct type
- Node button drag sets dataTransfer
- Generate combo button dropdown menu
- Provider icon buttons visibility and click
- Edge style toggle
- Run/Stop button states and actions
- Run menu dropdown options

MultiSelectToolbar tests:
- Visibility based on selection count
- Stack horizontally button
- Stack vertically button
- Arrange as grid button
- Create group / ungroup buttons
- Download images button
- Toolbar position calculation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
6d05a9f158
  1. 777
      src/components/__tests__/FloatingActionBar.test.tsx
  2. 538
      src/components/__tests__/MultiSelectToolbar.test.tsx

777
src/components/__tests__/FloatingActionBar.test.tsx

@ -0,0 +1,777 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { FloatingActionBar } from "@/components/FloatingActionBar";
import { ReactFlowProvider } from "@xyflow/react";
import { ProviderSettings } from "@/types";
// Mock the workflow store
const mockAddNode = vi.fn();
const mockExecuteWorkflow = vi.fn();
const mockRegenerateNode = vi.fn();
const mockStopWorkflow = vi.fn();
const mockValidateWorkflow = vi.fn();
const mockSetEdgeStyle = vi.fn();
const mockSetModelSearchOpen = vi.fn();
const mockUseWorkflowStore = vi.fn();
vi.mock("@/store/workflowStore", () => ({
useWorkflowStore: (selector?: (state: unknown) => unknown) => {
if (selector) {
return mockUseWorkflowStore(selector);
}
return mockUseWorkflowStore((s: unknown) => s);
},
}));
// Mock useReactFlow
const mockScreenToFlowPosition = vi.fn((pos) => pos);
const mockGetNodes = vi.fn(() => []);
vi.mock("@xyflow/react", async () => {
const actual = await vi.importActual("@xyflow/react");
return {
...actual,
useReactFlow: () => ({
screenToFlowPosition: mockScreenToFlowPosition,
getNodes: mockGetNodes,
}),
};
});
// Mock ModelSearchDialog
vi.mock("@/components/modals/ModelSearchDialog", () => ({
ModelSearchDialog: ({ isOpen, onClose, initialProvider }: { isOpen: boolean; onClose: () => void; initialProvider?: string }) => (
isOpen ? (
<div data-testid="model-search-dialog" data-provider={initialProvider}>
Model Search Dialog
<button onClick={onClose}>Close</button>
</div>
) : null
),
}));
// Mock fetch for env-status
const mockFetch = vi.fn();
global.fetch = mockFetch;
// Wrapper component for React Flow context
function TestWrapper({ children }: { children: React.ReactNode }) {
return <ReactFlowProvider>{children}</ReactFlowProvider>;
}
// Default provider settings
const defaultProviderSettings: ProviderSettings = {
providers: {
gemini: { id: "gemini", name: "Gemini", enabled: true, apiKey: null, apiKeyEnvVar: "GEMINI_API_KEY" },
openai: { id: "openai", name: "OpenAI", enabled: false, apiKey: null },
replicate: { id: "replicate", name: "Replicate", enabled: false, apiKey: null },
fal: { id: "fal", name: "fal.ai", enabled: true, apiKey: null },
},
};
// Default store state factory
const createDefaultState = (overrides = {}) => ({
nodes: [],
isRunning: false,
executeWorkflow: mockExecuteWorkflow,
regenerateNode: mockRegenerateNode,
stopWorkflow: mockStopWorkflow,
validateWorkflow: mockValidateWorkflow,
edgeStyle: "angular" as const,
setEdgeStyle: mockSetEdgeStyle,
providerSettings: defaultProviderSettings,
setModelSearchOpen: mockSetModelSearchOpen,
modelSearchOpen: false,
modelSearchProvider: null,
addNode: mockAddNode,
...overrides,
});
describe("FloatingActionBar", () => {
beforeEach(() => {
vi.clearAllMocks();
mockValidateWorkflow.mockReturnValue({ valid: true, errors: [] });
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ gemini: true, openai: false, replicate: false }),
});
// Default mock implementation
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState());
});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("Basic Rendering", () => {
it("should render node type buttons", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Image")).toBeInTheDocument();
expect(screen.getByText("Annotate")).toBeInTheDocument();
expect(screen.getByText("Prompt")).toBeInTheDocument();
expect(screen.getByText("Output")).toBeInTheDocument();
});
});
it("should render Generate combo button", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Generate")).toBeInTheDocument();
});
});
it("should render Run button", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Run")).toBeInTheDocument();
});
});
it("should render edge style toggle button", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByTitle("Switch to curved connectors")).toBeInTheDocument();
});
});
});
describe("Node Button Click", () => {
it("should call addNode when Image button is clicked", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Image")).toBeInTheDocument();
});
const imageButton = screen.getByText("Image");
fireEvent.click(imageButton);
expect(mockAddNode).toHaveBeenCalledWith("imageInput", expect.any(Object));
});
it("should call addNode when Annotate button is clicked", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Annotate")).toBeInTheDocument();
});
const annotateButton = screen.getByText("Annotate");
fireEvent.click(annotateButton);
expect(mockAddNode).toHaveBeenCalledWith("annotation", expect.any(Object));
});
it("should call addNode when Prompt button is clicked", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Prompt")).toBeInTheDocument();
});
const promptButton = screen.getByText("Prompt");
fireEvent.click(promptButton);
expect(mockAddNode).toHaveBeenCalledWith("prompt", expect.any(Object));
});
it("should call addNode when Output button is clicked", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Output")).toBeInTheDocument();
});
const outputButton = screen.getByText("Output");
fireEvent.click(outputButton);
expect(mockAddNode).toHaveBeenCalledWith("output", expect.any(Object));
});
});
describe("Node Button Drag", () => {
it("should set dataTransfer with node type on drag start", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Image")).toBeInTheDocument();
});
const imageButton = screen.getByText("Image");
const mockDataTransfer = {
setData: vi.fn(),
effectAllowed: "",
};
fireEvent.dragStart(imageButton, {
dataTransfer: mockDataTransfer,
});
expect(mockDataTransfer.setData).toHaveBeenCalledWith("application/node-type", "imageInput");
expect(mockDataTransfer.effectAllowed).toBe("copy");
});
it("should set dataTransfer with prompt type on drag", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Prompt")).toBeInTheDocument();
});
const promptButton = screen.getByText("Prompt");
const mockDataTransfer = {
setData: vi.fn(),
effectAllowed: "",
};
fireEvent.dragStart(promptButton, {
dataTransfer: mockDataTransfer,
});
expect(mockDataTransfer.setData).toHaveBeenCalledWith("application/node-type", "prompt");
});
});
describe("Generate Combo Button", () => {
it("should open dropdown menu when Generate button is clicked", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Generate")).toBeInTheDocument();
});
const generateButton = screen.getByText("Generate");
fireEvent.click(generateButton);
// Dropdown menu items should appear
expect(screen.getByText("Image", { selector: "button.w-full" })).toBeInTheDocument();
expect(screen.getByText("Video")).toBeInTheDocument();
expect(screen.getByText("Text (LLM)")).toBeInTheDocument();
});
it("should add nanoBanana node when Image option is clicked", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Generate")).toBeInTheDocument();
});
// Open dropdown
fireEvent.click(screen.getByText("Generate"));
// Click Image option in dropdown
const imageOption = screen.getByText("Image", { selector: "button.w-full" });
fireEvent.click(imageOption);
expect(mockAddNode).toHaveBeenCalledWith("nanoBanana", expect.any(Object));
});
it("should add generateVideo node when Video option is clicked", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Generate")).toBeInTheDocument();
});
fireEvent.click(screen.getByText("Generate"));
fireEvent.click(screen.getByText("Video"));
expect(mockAddNode).toHaveBeenCalledWith("generateVideo", expect.any(Object));
});
it("should add llmGenerate node when Text (LLM) option is clicked", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Generate")).toBeInTheDocument();
});
fireEvent.click(screen.getByText("Generate"));
fireEvent.click(screen.getByText("Text (LLM)"));
expect(mockAddNode).toHaveBeenCalledWith("llmGenerate", expect.any(Object));
});
it("should close dropdown after selecting an option", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Generate")).toBeInTheDocument();
});
fireEvent.click(screen.getByText("Generate"));
// Verify dropdown is open
expect(screen.getByText("Video")).toBeInTheDocument();
// Click an option
fireEvent.click(screen.getByText("Video"));
// Dropdown should close
expect(screen.queryByText("Video")).not.toBeInTheDocument();
});
});
describe("Provider Icon Buttons", () => {
it("should render fal.ai icon button (always visible)", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByTitle("Browse fal.ai models")).toBeInTheDocument();
});
});
it("should render Replicate icon when API key is configured", async () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
providerSettings: {
providers: {
...defaultProviderSettings.providers,
replicate: { id: "replicate", name: "Replicate", enabled: true, apiKey: "test-key" },
},
},
}));
});
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ gemini: true, openai: false, replicate: true }),
});
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByTitle("Browse Replicate models")).toBeInTheDocument();
});
});
it("should open ModelSearchDialog with fal provider when fal.ai icon is clicked", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByTitle("Browse fal.ai models")).toBeInTheDocument();
});
const falButton = screen.getByTitle("Browse fal.ai models");
fireEvent.click(falButton);
expect(mockSetModelSearchOpen).toHaveBeenCalledWith(true, "fal");
});
it("should open ModelSearchDialog with replicate provider when Replicate icon is clicked", async () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
providerSettings: {
providers: {
...defaultProviderSettings.providers,
replicate: { id: "replicate", name: "Replicate", enabled: true, apiKey: "test-key" },
},
},
}));
});
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ gemini: true, openai: false, replicate: true }),
});
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByTitle("Browse Replicate models")).toBeInTheDocument();
});
const replicateButton = screen.getByTitle("Browse Replicate models");
fireEvent.click(replicateButton);
expect(mockSetModelSearchOpen).toHaveBeenCalledWith(true, "replicate");
});
});
describe("Edge Style Toggle", () => {
it("should call setEdgeStyle with curved when currently angular", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByTitle("Switch to curved connectors")).toBeInTheDocument();
});
const toggleButton = screen.getByTitle("Switch to curved connectors");
fireEvent.click(toggleButton);
expect(mockSetEdgeStyle).toHaveBeenCalledWith("curved");
});
it("should call setEdgeStyle with angular when currently curved", async () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
edgeStyle: "curved",
}));
});
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByTitle("Switch to angular connectors")).toBeInTheDocument();
});
const toggleButton = screen.getByTitle("Switch to angular connectors");
fireEvent.click(toggleButton);
expect(mockSetEdgeStyle).toHaveBeenCalledWith("angular");
});
});
describe("Run Button", () => {
it("should call executeWorkflow when Run button is clicked", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Run")).toBeInTheDocument();
});
const runButton = screen.getByText("Run");
fireEvent.click(runButton);
expect(mockExecuteWorkflow).toHaveBeenCalled();
});
it("should show Stop button when isRunning is true", async () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
isRunning: true,
}));
});
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Stop")).toBeInTheDocument();
});
});
it("should call stopWorkflow when Stop button is clicked", async () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
isRunning: true,
}));
});
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Stop")).toBeInTheDocument();
});
const stopButton = screen.getByText("Stop");
fireEvent.click(stopButton);
expect(mockStopWorkflow).toHaveBeenCalled();
});
it("should disable Run button when workflow is invalid", async () => {
mockValidateWorkflow.mockReturnValue({ valid: false, errors: ["No nodes"] });
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByText("Run")).toBeInTheDocument();
});
const runButton = screen.getByText("Run").closest("button");
expect(runButton).toBeDisabled();
});
it("should show error message in title when workflow is invalid", async () => {
mockValidateWorkflow.mockReturnValue({ valid: false, errors: ["Missing required nodes"] });
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
const runButton = screen.getByText("Run").closest("button");
expect(runButton).toHaveAttribute("title", "Missing required nodes");
});
});
});
describe("Run Menu Dropdown", () => {
it("should show dropdown chevron when workflow is valid and not running", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByTitle("Run options")).toBeInTheDocument();
});
});
it("should not show dropdown chevron when workflow is invalid", async () => {
mockValidateWorkflow.mockReturnValue({ valid: false, errors: ["No nodes"] });
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.queryByTitle("Run options")).not.toBeInTheDocument();
});
});
it("should not show dropdown chevron when running", async () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
isRunning: true,
}));
});
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.queryByTitle("Run options")).not.toBeInTheDocument();
});
});
it("should open run menu when dropdown chevron is clicked", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByTitle("Run options")).toBeInTheDocument();
});
fireEvent.click(screen.getByTitle("Run options"));
expect(screen.getByText("Run entire workflow")).toBeInTheDocument();
expect(screen.getByText("Run from selected node")).toBeInTheDocument();
expect(screen.getByText("Run selected node only")).toBeInTheDocument();
});
it("should call executeWorkflow when 'Run entire workflow' is clicked", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByTitle("Run options")).toBeInTheDocument();
});
fireEvent.click(screen.getByTitle("Run options"));
fireEvent.click(screen.getByText("Run entire workflow"));
expect(mockExecuteWorkflow).toHaveBeenCalled();
});
it("should disable 'Run from selected node' when no node is selected", async () => {
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByTitle("Run options")).toBeInTheDocument();
});
fireEvent.click(screen.getByTitle("Run options"));
const runFromSelectedButton = screen.getByText("Run from selected node").closest("button");
expect(runFromSelectedButton).toHaveClass("cursor-not-allowed");
});
it("should enable 'Run from selected node' when a single node is selected", async () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [{ id: "node-1", selected: true, type: "prompt" }],
}));
});
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByTitle("Run options")).toBeInTheDocument();
});
fireEvent.click(screen.getByTitle("Run options"));
const runFromSelectedButton = screen.getByText("Run from selected node").closest("button");
expect(runFromSelectedButton).not.toHaveClass("cursor-not-allowed");
});
it("should call executeWorkflow with node id when 'Run from selected node' is clicked", async () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [{ id: "node-1", selected: true, type: "prompt" }],
}));
});
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByTitle("Run options")).toBeInTheDocument();
});
fireEvent.click(screen.getByTitle("Run options"));
fireEvent.click(screen.getByText("Run from selected node"));
expect(mockExecuteWorkflow).toHaveBeenCalledWith("node-1");
});
it("should call regenerateNode when 'Run selected node only' is clicked", async () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [{ id: "node-1", selected: true, type: "prompt" }],
}));
});
render(
<TestWrapper>
<FloatingActionBar />
</TestWrapper>
);
await waitFor(() => {
expect(screen.getByTitle("Run options")).toBeInTheDocument();
});
fireEvent.click(screen.getByTitle("Run options"));
fireEvent.click(screen.getByText("Run selected node only"));
expect(mockRegenerateNode).toHaveBeenCalledWith("node-1");
});
});
});

538
src/components/__tests__/MultiSelectToolbar.test.tsx

@ -0,0 +1,538 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { MultiSelectToolbar } from "@/components/MultiSelectToolbar";
import { ReactFlowProvider } from "@xyflow/react";
// Mock JSZip
vi.mock("jszip", () => ({
default: vi.fn().mockImplementation(() => ({
file: vi.fn(),
generateAsync: vi.fn().mockResolvedValue(new Blob()),
})),
}));
// Mock the workflow store
const mockOnNodesChange = vi.fn();
const mockCreateGroup = vi.fn();
const mockRemoveNodesFromGroup = vi.fn();
const mockUseWorkflowStore = vi.fn();
vi.mock("@/store/workflowStore", () => ({
useWorkflowStore: (selector?: (state: unknown) => unknown) => {
if (selector) {
return mockUseWorkflowStore(selector);
}
return mockUseWorkflowStore((s: unknown) => s);
},
}));
// Mock useReactFlow
const mockGetViewport = vi.fn(() => ({ x: 0, y: 0, zoom: 1 }));
vi.mock("@xyflow/react", async () => {
const actual = await vi.importActual("@xyflow/react");
return {
...actual,
useReactFlow: () => ({
getViewport: mockGetViewport,
}),
};
});
// Wrapper component for React Flow context
function TestWrapper({ children }: { children: React.ReactNode }) {
return <ReactFlowProvider>{children}</ReactFlowProvider>;
}
// Create mock nodes for testing
const createMockNode = (id: string, overrides = {}) => ({
id,
type: "prompt",
position: { x: 100, y: 100 },
data: {},
selected: true,
measured: { width: 220, height: 200 },
...overrides,
});
// Default store state factory
const createDefaultState = (overrides = {}) => ({
nodes: [],
onNodesChange: mockOnNodesChange,
createGroup: mockCreateGroup,
removeNodesFromGroup: mockRemoveNodesFromGroup,
...overrides,
});
describe("MultiSelectToolbar", () => {
beforeEach(() => {
vi.clearAllMocks();
// Default mock implementation - no nodes selected
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState());
});
});
describe("Visibility", () => {
it("should not render when no nodes are selected", () => {
const { container } = render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
expect(container.firstChild).toBeNull();
});
it("should not render when only one node is selected", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [createMockNode("node-1")],
}));
});
const { container } = render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
expect(container.firstChild).toBeNull();
});
it("should render when two or more nodes are selected", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { position: { x: 0, y: 0 } }),
createMockNode("node-2", { position: { x: 300, y: 0 } }),
],
}));
});
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
expect(screen.getByTitle("Stack horizontally (H)")).toBeInTheDocument();
});
it("should not render when nodes are selected but less than 2", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1"),
createMockNode("node-2", { selected: false }),
],
}));
});
const { container } = render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
expect(container.firstChild).toBeNull();
});
});
describe("Basic Rendering", () => {
beforeEach(() => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { position: { x: 0, y: 0 } }),
createMockNode("node-2", { position: { x: 300, y: 0 } }),
],
}));
});
});
it("should render stack horizontally button", () => {
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
expect(screen.getByTitle("Stack horizontally (H)")).toBeInTheDocument();
});
it("should render stack vertically button", () => {
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
expect(screen.getByTitle("Stack vertically (V)")).toBeInTheDocument();
});
it("should render arrange as grid button", () => {
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
expect(screen.getByTitle("Arrange as grid (G)")).toBeInTheDocument();
});
it("should render create group button when nodes are not in a group", () => {
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
expect(screen.getByTitle("Create group")).toBeInTheDocument();
});
it("should render download images button", () => {
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
expect(screen.getByTitle("Download images as ZIP")).toBeInTheDocument();
});
});
describe("Stack Horizontally", () => {
it("should call onNodesChange when stack horizontally button is clicked", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { position: { x: 0, y: 50 } }),
createMockNode("node-2", { position: { x: 300, y: 0 } }),
],
}));
});
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
const stackHorizontalButton = screen.getByTitle("Stack horizontally (H)");
fireEvent.click(stackHorizontalButton);
// Should be called for each node
expect(mockOnNodesChange).toHaveBeenCalled();
});
it("should position nodes from left to right based on their original x position", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { position: { x: 500, y: 100 } }),
createMockNode("node-2", { position: { x: 100, y: 50 } }),
],
}));
});
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
fireEvent.click(screen.getByTitle("Stack horizontally (H)"));
// First call should position node-2 (at x=100, so it comes first)
expect(mockOnNodesChange).toHaveBeenCalledWith([
expect.objectContaining({
type: "position",
id: "node-2",
position: expect.objectContaining({ y: 50 }), // Aligned to topmost y
}),
]);
});
});
describe("Stack Vertically", () => {
it("should call onNodesChange when stack vertically button is clicked", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { position: { x: 50, y: 0 } }),
createMockNode("node-2", { position: { x: 0, y: 300 } }),
],
}));
});
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
const stackVerticalButton = screen.getByTitle("Stack vertically (V)");
fireEvent.click(stackVerticalButton);
expect(mockOnNodesChange).toHaveBeenCalled();
});
it("should position nodes from top to bottom based on their original y position", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { position: { x: 100, y: 500 } }),
createMockNode("node-2", { position: { x: 50, y: 100 } }),
],
}));
});
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
fireEvent.click(screen.getByTitle("Stack vertically (V)"));
// First call should position node-2 (at y=100, so it comes first)
expect(mockOnNodesChange).toHaveBeenCalledWith([
expect.objectContaining({
type: "position",
id: "node-2",
position: expect.objectContaining({ x: 50 }), // Aligned to leftmost x
}),
]);
});
});
describe("Arrange as Grid", () => {
it("should call onNodesChange when arrange as grid button is clicked", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { position: { x: 0, y: 0 } }),
createMockNode("node-2", { position: { x: 100, y: 100 } }),
createMockNode("node-3", { position: { x: 200, y: 200 } }),
createMockNode("node-4", { position: { x: 300, y: 300 } }),
],
}));
});
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
const gridButton = screen.getByTitle("Arrange as grid (G)");
fireEvent.click(gridButton);
expect(mockOnNodesChange).toHaveBeenCalled();
});
it("should arrange nodes in a grid pattern", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { position: { x: 0, y: 0 } }),
createMockNode("node-2", { position: { x: 500, y: 0 } }),
createMockNode("node-3", { position: { x: 0, y: 500 } }),
createMockNode("node-4", { position: { x: 500, y: 500 } }),
],
}));
});
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
fireEvent.click(screen.getByTitle("Arrange as grid (G)"));
// With 4 nodes, should create a 2x2 grid
expect(mockOnNodesChange).toHaveBeenCalled();
});
});
describe("Create Group", () => {
it("should call createGroup with selected node IDs when create group button is clicked", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { position: { x: 0, y: 0 } }),
createMockNode("node-2", { position: { x: 300, y: 0 } }),
],
}));
});
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
const createGroupButton = screen.getByTitle("Create group");
fireEvent.click(createGroupButton);
expect(mockCreateGroup).toHaveBeenCalledWith(["node-1", "node-2"]);
});
it("should show ungroup button when selected nodes are in a group", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { position: { x: 0, y: 0 }, groupId: "group-1" }),
createMockNode("node-2", { position: { x: 300, y: 0 }, groupId: "group-1" }),
],
}));
});
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
expect(screen.getByTitle("Remove from group")).toBeInTheDocument();
expect(screen.queryByTitle("Create group")).not.toBeInTheDocument();
});
it("should call removeNodesFromGroup when ungroup button is clicked", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { position: { x: 0, y: 0 }, groupId: "group-1" }),
createMockNode("node-2", { position: { x: 300, y: 0 }, groupId: "group-1" }),
],
}));
});
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
const ungroupButton = screen.getByTitle("Remove from group");
fireEvent.click(ungroupButton);
expect(mockRemoveNodesFromGroup).toHaveBeenCalledWith(["node-1", "node-2"]);
});
it("should show ungroup button when at least one selected node is in a group", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { position: { x: 0, y: 0 }, groupId: "group-1" }),
createMockNode("node-2", { position: { x: 300, y: 0 } }), // Not in a group
],
}));
});
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
expect(screen.getByTitle("Remove from group")).toBeInTheDocument();
});
});
describe("Download Images", () => {
it("should render download images button", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { position: { x: 0, y: 0 } }),
createMockNode("node-2", { position: { x: 300, y: 0 } }),
],
}));
});
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
expect(screen.getByTitle("Download images as ZIP")).toBeInTheDocument();
});
it("should not download when no images are available", async () => {
// Mock URL.createObjectURL and URL.revokeObjectURL
const createObjectURLSpy = vi.fn();
const revokeObjectURLSpy = vi.fn();
global.URL.createObjectURL = createObjectURLSpy;
global.URL.revokeObjectURL = revokeObjectURLSpy;
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { type: "prompt", position: { x: 0, y: 0 }, data: { prompt: "test" } }),
createMockNode("node-2", { type: "prompt", position: { x: 300, y: 0 }, data: { prompt: "test2" } }),
],
}));
});
render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
const downloadButton = screen.getByTitle("Download images as ZIP");
fireEvent.click(downloadButton);
// Should not create object URL when no images
expect(createObjectURLSpy).not.toHaveBeenCalled();
});
});
describe("Toolbar Position", () => {
it("should position toolbar based on selected nodes bounding box", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { position: { x: 100, y: 200 } }),
createMockNode("node-2", { position: { x: 400, y: 200 } }),
],
}));
});
const { container } = render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
const toolbar = container.firstChild as HTMLElement;
expect(toolbar).toHaveStyle({ transform: "translateX(-50%)" });
});
it("should account for viewport zoom in positioning", () => {
mockGetViewport.mockReturnValue({ x: 100, y: 50, zoom: 2 });
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
createMockNode("node-1", { position: { x: 0, y: 0 } }),
createMockNode("node-2", { position: { x: 200, y: 0 } }),
],
}));
});
const { container } = render(
<TestWrapper>
<MultiSelectToolbar />
</TestWrapper>
);
const toolbar = container.firstChild as HTMLElement;
expect(toolbar).toBeInTheDocument();
});
});
});
Loading…
Cancel
Save