diff --git a/packages/serverless-workflow-diagram-editor/src/core/autoLayout.ts b/packages/serverless-workflow-diagram-editor/src/core/autoLayout.ts index 11163be..b30de77 100644 --- a/packages/serverless-workflow-diagram-editor/src/core/autoLayout.ts +++ b/packages/serverless-workflow-diagram-editor/src/core/autoLayout.ts @@ -18,8 +18,8 @@ import { ExtendedGraph, Position, Size } from "./graph"; // Defaults export const DEFAULT_NODE_SIZE = { - height: 50, - width: 90, + height: 60, + width: 180, }; export function applyAutoLayout(graph: ExtendedGraph): ExtendedGraph { diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css index a9d8873..b9deba5 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css @@ -81,7 +81,7 @@ } .dec-root.dark .custom-node-container { - @apply dec:bg-[rgb(85,83,83)] + @apply dec:bg-[#2d3748] dec:border-[#e5e4e2]; } @@ -161,4 +161,75 @@ dec:border-gray-600 dec:text-gray-200; } + + /* task leaf nodes */ + .dec-root .dec-task-node-container { + @apply dec:rounded-lg + dec:bg-white + dec:shadow-sm + dec:transition-[border,box-shadow] + dec:h-full + dec:w-full; + border-top: 4px solid var(--task-node-color); + } + + .dec-root.dark .dec-task-node-container { + @apply dec:bg-[#2d3748]; + } + + .dec-root .dec-task-node-container:hover { + @apply dec:shadow-[0_0_10px_rgba(0,65,208,0.3)]; + } + + .dec-root.dark .dec-task-node-container:hover { + @apply dec:shadow-[0_0_10px_rgba(255,255,255,0.3)]; + } + + .dec-root .dec-task-node-container.selected { + @apply dec:shadow-[0_0_10px_rgba(59,130,246,0.8)]; + } + + .dec-root.dark .dec-task-node-container.selected { + @apply dec:bg-[#3d4a5c] + dec:shadow-[0_0_10px_rgba(255,255,255,0.3)]; + } + + .dec-root .dec-task-node-content { + @apply dec:flex + dec:items-center + dec:gap-3 + dec:px-4 + dec:py-3; + } + + .dec-root .dec-task-node-icon { + color: var(--task-node-color); + } + + .dec-root .dec-task-node-label { + @apply dec:flex + dec:flex-col + dec:gap-0.5; + } + + .dec-root .dec-task-node-name { + @apply dec:text-sm + dec:text-black + dec:leading-tight; + } + + .dec-root.dark .dec-task-node-name { + @apply dec:text-white; + } + + .dec-root .dec-task-node-type { + @apply dec:text-[9px] + dec:uppercase + dec:text-gray-500 + dec:leading-tight; + } + + .dec-root.dark .dec-task-node-type { + @apply dec:text-gray-400; + } } diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx index 558cad2..64a24ce 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.tsx @@ -39,7 +39,7 @@ const initialNodes: RF.Node[] = [ position: { x: 100, y: 0 }, height: DEFAULT_NODE_SIZE.height, width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 1" }, + data: { label: "CallNode" }, }, { id: "n2", @@ -55,15 +55,15 @@ const initialNodes: RF.Node[] = [ position: { x: 100, y: 200 }, height: DEFAULT_NODE_SIZE.height, width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 3" }, + data: { label: "SwitchNode" }, }, { id: "n4", type: GraphNodeType.Emit, - position: { x: 0, y: 300 }, + position: { x: -100, y: 300 }, height: DEFAULT_NODE_SIZE.height, width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 4" }, + data: { label: "EmitNode" }, }, { id: "n5", @@ -76,7 +76,7 @@ const initialNodes: RF.Node[] = [ { id: "n6", type: GraphNodeType.Fork, - position: { x: 200, y: 300 }, + position: { x: 300, y: 300 }, height: DEFAULT_NODE_SIZE.height, width: DEFAULT_NODE_SIZE.width, data: { label: "Node 6" }, @@ -87,7 +87,7 @@ const initialNodes: RF.Node[] = [ position: { x: 100, y: 400 }, height: DEFAULT_NODE_SIZE.height, width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 7" }, + data: { label: "ListenNode" }, }, { id: "n8", @@ -95,7 +95,7 @@ const initialNodes: RF.Node[] = [ position: { x: 100, y: 500 }, height: DEFAULT_NODE_SIZE.height, width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 8" }, + data: { label: "RaiseNode" }, }, { id: "n9", @@ -103,7 +103,7 @@ const initialNodes: RF.Node[] = [ position: { x: 100, y: 600 }, height: DEFAULT_NODE_SIZE.height, width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 9" }, + data: { label: "RunNode" }, }, { id: "n10", @@ -111,7 +111,7 @@ const initialNodes: RF.Node[] = [ position: { x: 100, y: 700 }, height: DEFAULT_NODE_SIZE.height, width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 10" }, + data: { label: "SetNode" }, }, { id: "n11", @@ -127,7 +127,7 @@ const initialNodes: RF.Node[] = [ position: { x: 100, y: 900 }, height: DEFAULT_NODE_SIZE.height, width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 12" }, + data: { label: "WaitNode" }, }, ]; @@ -139,10 +139,12 @@ const initialEdges: RF.Edge[] = [ type: GraphEdgeType.Default, data: { wayPoints: [ - { x: 145, y: 60 }, - { x: 170, y: 60 }, - { x: 170, y: 85 }, - { x: 145, y: 85 }, + { x: 190, y: 60 }, + { x: 190, y: 70 }, + { x: 140, y: 70 }, + { x: 140, y: 85 }, + { x: 190, y: 85 }, + { x: 190, y: 95 }, ], }, }, diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/nodes/Nodes.tsx b/packages/serverless-workflow-diagram-editor/src/react-flow/nodes/Nodes.tsx index 17f1820..d5f7bb0 100644 --- a/packages/serverless-workflow-diagram-editor/src/react-flow/nodes/Nodes.tsx +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/nodes/Nodes.tsx @@ -14,8 +14,10 @@ * limitations under the License. */ +import type React from "react"; import { GraphNodeType } from "@serverlessworkflow/sdk"; import * as RF from "@xyflow/react"; +import { type LeafNodeType, taskNodeConfigMap } from "./taskNodeConfig"; // Node types must match sdk GraphNodeType enum export const NodeTypes: RF.NodeTypes = { @@ -38,6 +40,35 @@ export type BaseNodeData = { label: string; }; +interface NodeContentProps { + id: string; + data: BaseNodeData; + selected: boolean; + type: string; +} + +function TaskNodeContent({ id, data, selected, type }: NodeContentProps) { + const config = taskNodeConfigMap[type as LeafNodeType]; + const Icon = config.icon; + return ( +
+ +
+ +
+ {data.label} + {config.typeLabel} +
+
+ +
+ ); +} + // TODO: These props are just a placeholder interface PlaceholderProps { id: string; @@ -65,8 +96,7 @@ function PlaceholderContent({ id, data, selected, type }: PlaceholderProps) { /* call node */ export type CallNodeType = RF.Node; export function CallNode({ id, data, selected, type }: RF.NodeProps) { - // TODO: This component is just a placeholder - return ; + return ; } /* do node */ @@ -79,8 +109,7 @@ export function DoNode({ id, data, selected, type }: RF.NodeProps) { /* emit node */ export type EmitNodeType = RF.Node; export function EmitNode({ id, data, selected, type }: RF.NodeProps) { - // TODO: This component is just a placeholder - return ; + return ; } /* for node */ @@ -100,36 +129,31 @@ export function ForkNode({ id, data, selected, type }: RF.NodeProps; export function ListenNode({ id, data, selected, type }: RF.NodeProps) { - // TODO: This component is just a placeholder - return ; + return ; } /* raise node */ export type RaiseNodeType = RF.Node; export function RaiseNode({ id, data, selected, type }: RF.NodeProps) { - // TODO: This component is just a placeholder - return ; + return ; } /* run node */ export type RunNodeType = RF.Node; export function RunNode({ id, data, selected, type }: RF.NodeProps) { - // TODO: This component is just a placeholder - return ; + return ; } /* set node */ export type SetNodeType = RF.Node; export function SetNode({ id, data, selected, type }: RF.NodeProps) { - // TODO: This component is just a placeholder - return ; + return ; } /* switch node */ export type SwitchNodeType = RF.Node; export function SwitchNode({ id, data, selected, type }: RF.NodeProps) { - // TODO: This component is just a placeholder - return ; + return ; } /* try node */ @@ -142,6 +166,5 @@ export function TryNode({ id, data, selected, type }: RF.NodeProps) /* wait node */ export type WaitNodeType = RF.Node; export function WaitNode({ id, data, selected, type }: RF.NodeProps) { - // TODO: This component is just a placeholder - return ; + return ; } diff --git a/packages/serverless-workflow-diagram-editor/src/react-flow/nodes/taskNodeConfig.ts b/packages/serverless-workflow-diagram-editor/src/react-flow/nodes/taskNodeConfig.ts new file mode 100644 index 0000000..b71a715 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/react-flow/nodes/taskNodeConfig.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GraphNodeType } from "@serverlessworkflow/sdk"; +import { + AlertTriangle, + ArrowRightLeft, + AudioLines, + Clock, + Megaphone, + PenLine, + Phone, + Terminal, +} from "lucide-react"; +import type { ComponentType } from "react"; + +export type LeafNodeType = + | typeof GraphNodeType.Call + | typeof GraphNodeType.Emit + | typeof GraphNodeType.Listen + | typeof GraphNodeType.Raise + | typeof GraphNodeType.Run + | typeof GraphNodeType.Set + | typeof GraphNodeType.Switch + | typeof GraphNodeType.Wait; + +export interface TaskNodeConfig { + color: string; + icon: ComponentType<{ size?: number; className?: string }>; + typeLabel: string; +} + +export const taskNodeConfigMap: Record = { + [GraphNodeType.Call]: { + color: "#2563EB", + icon: Phone, + typeLabel: "CALL", + }, + [GraphNodeType.Emit]: { + color: "#8B5CF6", + icon: Megaphone, + typeLabel: "EMIT", + }, + [GraphNodeType.Listen]: { + color: "#CA8A04", + icon: AudioLines, + typeLabel: "LISTEN", + }, + [GraphNodeType.Raise]: { + color: "#DC2626", + icon: AlertTriangle, + typeLabel: "RAISE", + }, + [GraphNodeType.Run]: { + color: "#10B981", + icon: Terminal, + typeLabel: "RUN", + }, + [GraphNodeType.Set]: { + color: "#EA580C", + icon: PenLine, + typeLabel: "SET", + }, + [GraphNodeType.Switch]: { + color: "#BE185D", + icon: ArrowRightLeft, + typeLabel: "SWITCH", + }, + [GraphNodeType.Wait]: { + color: "#84CC16", + icon: Clock, + typeLabel: "WAIT", + }, +}; diff --git a/packages/serverless-workflow-diagram-editor/tests-e2e/diagram-editor.spec.ts b/packages/serverless-workflow-diagram-editor/tests-e2e/diagram-editor.spec.ts index d5ce377..98cd46b 100644 --- a/packages/serverless-workflow-diagram-editor/tests-e2e/diagram-editor.spec.ts +++ b/packages/serverless-workflow-diagram-editor/tests-e2e/diagram-editor.spec.ts @@ -23,7 +23,7 @@ test("diagram editor renders correctly", async ({ page }) => { await expect(page.getByTestId("diagram-container")).toBeVisible(); // Check at least one specific node - await expect(page.getByTestId("rf__node-n1")).toContainText("Node 1"); + await expect(page.getByTestId("rf__node-n1")).toContainText("CallNodeCALL"); // Check total nodes const nodes = page.locator('[data-testid^="rf__node-"]'); diff --git a/packages/serverless-workflow-diagram-editor/tests/react-flow/nodes/Nodes.test.tsx b/packages/serverless-workflow-diagram-editor/tests/react-flow/nodes/Nodes.test.tsx index 10847f0..b5a1896 100644 --- a/packages/serverless-workflow-diagram-editor/tests/react-flow/nodes/Nodes.test.tsx +++ b/packages/serverless-workflow-diagram-editor/tests/react-flow/nodes/Nodes.test.tsx @@ -20,6 +20,49 @@ import * as RF from "@xyflow/react"; import { GraphNodeType } from "@serverlessworkflow/sdk"; import { NodeTypes } from "../../../src/react-flow/nodes/Nodes"; import { DEFAULT_NODE_SIZE } from "../../../src/core"; +import { taskNodeConfigMap, type LeafNodeType } from "../../../src/react-flow/nodes/taskNodeConfig"; + +function testNode(id: string, type: string, y: number, label: string): RF.Node { + return { + id, + type, + position: { x: 100, y }, + height: DEFAULT_NODE_SIZE.height, + width: DEFAULT_NODE_SIZE.width, + data: { label }, + }; +} + +const allNodes: RF.Node[] = [ + testNode("n1", GraphNodeType.Call, 0, "Node 1"), + testNode("n2", GraphNodeType.Do, 100, "Node 2"), + testNode("n3", GraphNodeType.Switch, 200, "Node 3"), + testNode("n4", GraphNodeType.Emit, 300, "Node 4"), + testNode("n5", GraphNodeType.For, 400, "Node 5"), + testNode("n6", GraphNodeType.Fork, 500, "Node 6"), + testNode("n7", GraphNodeType.Listen, 600, "Node 7"), + testNode("n8", GraphNodeType.Raise, 700, "Node 8"), + testNode("n9", GraphNodeType.Run, 800, "Node 9"), + testNode("n10", GraphNodeType.Set, 900, "Node 10"), + testNode("n11", GraphNodeType.Try, 1000, "Node 11"), + testNode("n12", GraphNodeType.Wait, 1100, "Node 12"), +]; + +const allEdges: RF.Edge[] = [ + { id: "n1-n2", source: "n1", target: "n2" }, + { id: "n2-n3", source: "n2", target: "n3" }, + { id: "n3-n4", source: "n3", target: "n4" }, + { id: "n3-n5", source: "n3", target: "n5" }, + { id: "n3-n6", source: "n3", target: "n6" }, + { id: "n4-n7", source: "n4", target: "n7" }, + { id: "n5-n7", source: "n5", target: "n7" }, + { id: "n6-n7", source: "n6", target: "n7" }, + { id: "n7-n8", source: "n7", target: "n8" }, + { id: "n8-n9", source: "n8", target: "n9" }, + { id: "n9-n10", source: "n9", target: "n10" }, + { id: "n10-n11", source: "n10", target: "n11" }, + { id: "n11-n12", source: "n11", target: "n12" }, +]; describe("React Flow custom node types", () => { afterEach(() => { @@ -27,151 +70,53 @@ describe("React Flow custom node types", () => { }); it("render react flow custom node types", () => { - const nodes: RF.Node[] = [ - { - id: "n1", - type: GraphNodeType.Call, - position: { x: 100, y: 0 }, - height: DEFAULT_NODE_SIZE.height, - width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 1" }, - }, - { - id: "n2", - type: GraphNodeType.Do, - position: { x: 100, y: 100 }, - height: DEFAULT_NODE_SIZE.height, - width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 2" }, - }, - { - id: "n3", - type: GraphNodeType.Switch, - position: { x: 100, y: 200 }, - height: DEFAULT_NODE_SIZE.height, - width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 3" }, - }, - { - id: "n4", - type: GraphNodeType.Emit, - position: { x: 0, y: 300 }, - height: DEFAULT_NODE_SIZE.height, - width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 4" }, - }, - { - id: "n5", - type: GraphNodeType.For, - position: { x: 100, y: 300 }, - height: DEFAULT_NODE_SIZE.height, - width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 5" }, - }, - { - id: "n6", - type: GraphNodeType.Fork, - position: { x: 200, y: 300 }, - height: DEFAULT_NODE_SIZE.height, - width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 6" }, - }, - { - id: "n7", - type: GraphNodeType.Listen, - position: { x: 100, y: 400 }, - height: DEFAULT_NODE_SIZE.height, - width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 7" }, - }, - { - id: "n8", - type: GraphNodeType.Raise, - position: { x: 100, y: 500 }, - height: DEFAULT_NODE_SIZE.height, - width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 8" }, - }, - { - id: "n9", - type: GraphNodeType.Run, - position: { x: 100, y: 600 }, - height: DEFAULT_NODE_SIZE.height, - width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 9" }, - }, - { - id: "n10", - type: GraphNodeType.Set, - position: { x: 100, y: 700 }, - height: DEFAULT_NODE_SIZE.height, - width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 10" }, - }, - { - id: "n11", - type: GraphNodeType.Try, - position: { x: 100, y: 800 }, - height: DEFAULT_NODE_SIZE.height, - width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 11" }, - }, - { - id: "n12", - type: GraphNodeType.Wait, - position: { x: 100, y: 900 }, - height: DEFAULT_NODE_SIZE.height, - width: DEFAULT_NODE_SIZE.width, - data: { label: "Node 12" }, - }, - ]; - - const edges: RF.Edge[] = [ - { id: "n1-n2", source: "n1", target: "n2" }, - { id: "n2-n3", source: "n2", target: "n3" }, - { id: "n3-n4", source: "n3", target: "n4" }, - { id: "n3-n5", source: "n3", target: "n5" }, - { id: "n3-n6", source: "n3", target: "n6" }, - { id: "n4-n7", source: "n4", target: "n7" }, - { id: "n5-n7", source: "n5", target: "n7" }, - { id: "n6-n7", source: "n6", target: "n7" }, - { id: "n7-n8", source: "n7", target: "n8" }, - { id: "n8-n9", source: "n8", target: "n9" }, - { id: "n9-n10", source: "n9", target: "n10" }, - { id: "n10-n11", source: "n10", target: "n11" }, - { id: "n11-n12", source: "n11", target: "n12" }, - ]; - render(
- +
, ); - const callNode = screen.getByTestId("call-node-n1"); - const doNode = screen.getByTestId("do-node-n2"); - const switchNode = screen.getByTestId("switch-node-n3"); - const emitNode = screen.getByTestId("emit-node-n4"); - const forNode = screen.getByTestId("for-node-n5"); - const forkNode = screen.getByTestId("fork-node-n6"); - const listenNode = screen.getByTestId("listen-node-n7"); - const raiseNode = screen.getByTestId("raise-node-n8"); - const runNode = screen.getByTestId("run-node-n9"); - const setNode = screen.getByTestId("set-node-n10"); - const tryNode = screen.getByTestId("try-node-n11"); - const waitNode = screen.getByTestId("wait-node-n12"); + expect(screen.getByTestId("call-node-n1")).toBeInTheDocument(); + expect(screen.getByTestId("do-node-n2")).toBeInTheDocument(); + expect(screen.getByTestId("switch-node-n3")).toBeInTheDocument(); + expect(screen.getByTestId("emit-node-n4")).toBeInTheDocument(); + expect(screen.getByTestId("for-node-n5")).toBeInTheDocument(); + expect(screen.getByTestId("fork-node-n6")).toBeInTheDocument(); + expect(screen.getByTestId("listen-node-n7")).toBeInTheDocument(); + expect(screen.getByTestId("raise-node-n8")).toBeInTheDocument(); + expect(screen.getByTestId("run-node-n9")).toBeInTheDocument(); + expect(screen.getByTestId("set-node-n10")).toBeInTheDocument(); + expect(screen.getByTestId("try-node-n11")).toBeInTheDocument(); + expect(screen.getByTestId("wait-node-n12")).toBeInTheDocument(); + }); + + describe("should render leaf nodes with TaskNodeContent", () => { + const leafNodes: { id: string; type: LeafNodeType; testId: string }[] = [ + { id: "n1", type: GraphNodeType.Call, testId: "call" }, + { id: "n3", type: GraphNodeType.Switch, testId: "switch" }, + { id: "n4", type: GraphNodeType.Emit, testId: "emit" }, + { id: "n7", type: GraphNodeType.Listen, testId: "listen" }, + { id: "n8", type: GraphNodeType.Raise, testId: "raise" }, + { id: "n9", type: GraphNodeType.Run, testId: "run" }, + { id: "n10", type: GraphNodeType.Set, testId: "set" }, + { id: "n12", type: GraphNodeType.Wait, testId: "wait" }, + ]; + + it.each(leafNodes)("should render %s node with correct config", ({ id, type, testId }) => { + render( +
+ +
, + ); + + const nodeData = allNodes.find((n) => n.id === id); + const node = screen.getByTestId(`${testId}-node-${id}`); + const config = taskNodeConfigMap[type]; - expect(callNode).toBeInTheDocument(); - expect(doNode).toBeInTheDocument(); - expect(switchNode).toBeInTheDocument(); - expect(emitNode).toBeInTheDocument(); - expect(forNode).toBeInTheDocument(); - expect(forkNode).toBeInTheDocument(); - expect(listenNode).toBeInTheDocument(); - expect(raiseNode).toBeInTheDocument(); - expect(runNode).toBeInTheDocument(); - expect(setNode).toBeInTheDocument(); - expect(tryNode).toBeInTheDocument(); - expect(waitNode).toBeInTheDocument(); + expect(node).toHaveClass("dec-task-node-container"); + expect(node.querySelector(".dec-task-node-type")?.textContent).toBe(config.typeLabel); + expect(node.querySelector(".dec-task-node-name")?.textContent).toBe(nodeData?.data.label); + expect(node.style.getPropertyValue("--task-node-color")).toBe(config.color); + }); }); }); diff --git a/packages/serverless-workflow-diagram-editor/tests/react-flow/nodes/taskNodeConfig.test.ts b/packages/serverless-workflow-diagram-editor/tests/react-flow/nodes/taskNodeConfig.test.ts new file mode 100644 index 0000000..19ff1e4 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/tests/react-flow/nodes/taskNodeConfig.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { it, expect, describe } from "vitest"; +import { GraphNodeType } from "@serverlessworkflow/sdk"; +import { LeafNodeType, taskNodeConfigMap } from "../../../src/react-flow/nodes/taskNodeConfig"; + +const leafNodeTypes: LeafNodeType[] = [ + GraphNodeType.Call, + GraphNodeType.Emit, + GraphNodeType.Listen, + GraphNodeType.Raise, + GraphNodeType.Run, + GraphNodeType.Set, + GraphNodeType.Switch, + GraphNodeType.Wait, +]; + +describe("taskNodeConfig", () => { + it("should have config for all leaf nodes", () => { + for (const leaf of leafNodeTypes) { + expect(taskNodeConfigMap[leaf]).toBeDefined(); + } + }); + + it.each(leafNodeTypes)("should have config color, icon and typeLabel for %s", (leaf) => { + const config = taskNodeConfigMap[leaf]; + + expect(config.color).toMatch(/^#([0-9A-Fa-f]{6})$/); + expect(config.icon).toBeDefined(); + expect(config.typeLabel).toBe(config.typeLabel.toUpperCase()); + }); +});