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());
+ });
+});