Browse Source
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
2 changed files with 1315 additions and 0 deletions
@ -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"); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -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…
Reference in new issue