Skip to content

Commit fcd4e90

Browse files
committed
Render node and fix tests to support react flow
Signed-off-by: handreyrc <handrey.cunha@gmail.com>
1 parent d9cc425 commit fcd4e90

File tree

9 files changed

+241
-49
lines changed

9 files changed

+241
-49
lines changed

packages/serverless-workflow-diagram-editor/src/diagram-editor/DiagramEditor.tsx

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,45 @@
1414
* limitations under the License.
1515
*/
1616

17-
import type { CSSProperties } from "react";
17+
import * as React from "react";
18+
import { Diagram, DiagramRef } from "../react-flow/diagram/Diagram";
1819

19-
const clickmeBtnStyle: CSSProperties = {
20-
border: "2px solid blue",
21-
borderRadius: "10px",
22-
fontSize: "large",
23-
fontWeight: "500",
24-
background: "blue",
25-
color: "white",
20+
/**
21+
* DiagramEditor component API
22+
*/
23+
export type DiagramEditorRef = {
24+
doSomething: () => void; // TODO: to be implemented, it is just a placeholder
2625
};
2726

2827
export type DiagramEditorProps = {
29-
content: string;
3028
isReadOnly: boolean;
29+
locale: string;
30+
diagramEditorRef?: React.Ref<DiagramEditorRef>;
3131
};
3232

33-
export const DiagramEditor = (props: DiagramEditorProps) => {
34-
//TODO: Implement the actual component this is just a placeholder
33+
export const DiagramEditor = ({ isReadOnly, locale, diagramEditorRef }: DiagramEditorProps) => {
34+
// TODO: i18n
35+
// TODO: store, context
36+
// TODO: ErrorBounduary / fallback
37+
38+
// Refs
39+
const diagramDivRef = React.useRef<HTMLDivElement>(null);
40+
const diagramRef = React.useRef<DiagramRef>(null);
41+
42+
// Allow imperativelly controlling the Editor
43+
React.useImperativeHandle(
44+
diagramEditorRef,
45+
() => ({
46+
doSomething: () => {
47+
// TODO: to be implemented, it is just a placeholder
48+
},
49+
}),
50+
[],
51+
);
3552

3653
return (
3754
<>
38-
<h1>Hello from DiagramEditor component!</h1>
39-
<p>Read-only: {props.isReadOnly ? "true" : "false"}</p>
40-
<p>Content: {props.content}</p>
41-
<button style={clickmeBtnStyle} onClick={() => alert("Hello from Diagram!")}>
42-
Click me!
43-
</button>
55+
<Diagram ref={diagramRef} divRef={diagramDivRef} />
4456
</>
4557
);
4658
};

packages/serverless-workflow-diagram-editor/tests/react-flow/Sample.test.tsx renamed to packages/serverless-workflow-diagram-editor/src/react-flow/diagram/Diagram.css

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { expect, describe, it } from "vitest";
17+
.diagram-container {
18+
height: 100%;
19+
position: relative;
20+
}
1821

19-
describe("MyComponent", () => {
20-
it("Just a sample test", () => {
21-
expect(true).toBeTruthy();
22-
});
23-
});
22+
.diagram-background {
23+
--xy-background-pattern-color: #ccc;
24+
background-color: #E5E4E2;
25+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2021-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as React from "react";
18+
import * as RF from "@xyflow/react";
19+
import "@xyflow/react/dist/style.css";
20+
import "./Diagram.css";
21+
22+
const FIT_VIEW_OPTIONS: RF.FitViewOptions = { maxZoom: 1, minZoom: 0.1, duration: 400 };
23+
24+
// TODO: Nodes and Edges are hardcoded for now to generate a renderable basic workflow
25+
// It shall be replaced by the actual implementation based on graph structure
26+
const initialNodes: RF.Node[] = [
27+
{ id: "n1", position: { x: 100, y: 0 }, data: { label: "Node 1" } },
28+
{ id: "n2", position: { x: 100, y: 100 }, data: { label: "Node 2" } },
29+
{ id: "n3", position: { x: 0, y: 200 }, data: { label: "Node 3" } },
30+
{ id: "n4", position: { x: 200, y: 200 }, data: { label: "Node 4" } },
31+
{ id: "n5", position: { x: 100, y: 300 }, data: { label: "Node 5" } },
32+
];
33+
const initialEdges: RF.Edge[] = [
34+
{ id: "n1-n2", source: "n1", target: "n2" },
35+
{ id: "n2-n3", source: "n2", target: "n3" },
36+
{ id: "n2-n4", source: "n2", target: "n4" },
37+
{ id: "n3-n5", source: "n3", target: "n5" },
38+
{ id: "n4-n5", source: "n4", target: "n5" },
39+
];
40+
41+
/**
42+
* Diagram component API
43+
*/
44+
export type DiagramRef = {
45+
doSomething: () => void; // TODO: to be implemented, it is just a placeholder
46+
};
47+
48+
export type DiagramProps = {
49+
divRef?: React.RefObject<HTMLDivElement | null>;
50+
ref?: React.Ref<DiagramRef>;
51+
};
52+
53+
export const Diagram = ({ divRef, ref }: DiagramProps) => {
54+
const [minimapVisible, setMinimapVisible] = React.useState(false);
55+
56+
const [nodes, setNodes] = React.useState<RF.Node[]>(initialNodes);
57+
const [edges, setEdges] = React.useState<RF.Edge[]>(initialEdges);
58+
59+
const onNodesChange = React.useCallback<RF.OnNodesChange>(
60+
(changes) => setNodes((nodesSnapshot) => RF.applyNodeChanges(changes, nodesSnapshot)),
61+
[],
62+
);
63+
const onEdgesChange = React.useCallback<RF.OnEdgesChange>(
64+
(changes) => setEdges((edgesSnapshot) => RF.applyEdgeChanges(changes, edgesSnapshot)),
65+
[],
66+
);
67+
const onConnect = React.useCallback<RF.OnConnect>(
68+
(params) => setEdges((edgesSnapshot) => RF.addEdge(params, edgesSnapshot)),
69+
[],
70+
);
71+
72+
React.useImperativeHandle(
73+
ref,
74+
() => ({
75+
doSomething: () => {
76+
// TODO: to be implemented, it is just a placeholder
77+
},
78+
}),
79+
[],
80+
);
81+
82+
return (
83+
<div ref={divRef} className={"diagram-container"} data-testid={"diagram-container"}>
84+
<RF.ReactFlow
85+
nodes={nodes}
86+
edges={edges}
87+
onNodesChange={onNodesChange}
88+
onEdgesChange={onEdgesChange}
89+
onConnect={onConnect}
90+
onlyRenderVisibleElements={true}
91+
zoomOnDoubleClick={false}
92+
elementsSelectable={true}
93+
panOnScroll={true}
94+
zoomOnScroll={false}
95+
preventScrolling={true}
96+
selectionOnDrag={true}
97+
proOptions={{ hideAttribution: true }}
98+
fitView
99+
>
100+
{minimapVisible && <RF.MiniMap pannable zoomable position={"top-right"} />}
101+
102+
<RF.Controls
103+
fitViewOptions={FIT_VIEW_OPTIONS}
104+
position={"bottom-right"}
105+
showInteractive={false} // Remove lock screen from zoombar
106+
>
107+
{/* 3. Add custom button to Controls */}
108+
<RF.ControlButton onClick={() => setMinimapVisible(!minimapVisible)}>M</RF.ControlButton>
109+
</RF.Controls>
110+
<RF.Background className="diagram-background" variant={RF.BackgroundVariant.Cross} />
111+
</RF.ReactFlow>
112+
</div>
113+
);
114+
};

packages/serverless-workflow-diagram-editor/stories/DiagramEditor.stories.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,6 @@ type Story = StoryObj<typeof meta>;
3636
export const Component: Story = {
3737
args: {
3838
isReadOnly: true,
39-
content: "Sample Content",
39+
locale: "EN",
4040
},
4141
};

packages/serverless-workflow-diagram-editor/stories/DiagramEditor.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import {
2222
/** Primary UI component for user interaction */
2323
export const DiagramEditor = ({ ...props }: DiagramEditorProps) => {
2424
return (
25-
<>
26-
<SWDiagramEditor content={props.content} isReadOnly={props.isReadOnly} />
27-
</>
25+
<div style={{ height: "100vh" }}>
26+
<SWDiagramEditor isReadOnly={props.isReadOnly} locale={props.locale} />
27+
</div>
2828
);
2929
};

packages/serverless-workflow-diagram-editor/tests/diagram-editor/DiagramEditor.story.test.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,24 @@
1717
import { render, screen } from "@testing-library/react";
1818
import { composeStories } from "@storybook/react-vite";
1919
import * as stories from "../../stories/DiagramEditor.stories";
20-
import userEvent from "@testing-library/user-event";
2120
import { vi, test, expect, afterEach, describe } from "vitest";
2221

2322
// Composes all stories in the file
2423
const { Component } = composeStories(stories);
2524

26-
describe("DiagramEditor component story", () => {
25+
describe("Story - DiagramEditor component", () => {
2726
afterEach(() => {
2827
vi.restoreAllMocks();
2928
});
3029

31-
test("Render DiagramEditor Component from story", async () => {
32-
const content = "Sample Content";
30+
test("Renders react flow Diagram component", async () => {
31+
const locale = "EN";
3332
const isReadOnly = true;
34-
const alertMock = vi.spyOn(window, "alert").mockImplementation(() => {});
3533

36-
render(<Component content={content} isReadOnly={isReadOnly} />);
34+
render(<Component locale={locale} isReadOnly={isReadOnly} />);
3735

38-
const user = userEvent.setup();
39-
const button = screen.getByRole("button", { name: /Click me!/i });
36+
const reactFlowContainer = screen.getByTestId("diagram-container");
4037

41-
await user.click(button);
42-
43-
expect(alertMock).toHaveBeenCalledWith("Hello from Diagram!");
38+
expect(reactFlowContainer).toBeInTheDocument();
4439
});
4540
});

packages/serverless-workflow-diagram-editor/tests/diagram-editor/DiagramEditor.test.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,21 @@
1616

1717
import { render, screen } from "@testing-library/react";
1818
import { DiagramEditor } from "../../src/diagram-editor";
19-
import userEvent from "@testing-library/user-event";
2019
import { vi, test, expect, afterEach, describe } from "vitest";
2120

2221
describe("DiagramEditor Component", () => {
2322
afterEach(() => {
2423
vi.restoreAllMocks();
2524
});
2625

27-
test("Render DiagramEditor Component", async () => {
28-
const content = "Sample Content";
26+
test("Renders react flow Diagram component", async () => {
27+
const locale = "EN";
2928
const isReadOnly = true;
30-
const alertMock = vi.spyOn(window, "alert").mockImplementation(() => {});
3129

32-
render(<DiagramEditor content={content} isReadOnly={isReadOnly} />);
30+
render(<DiagramEditor locale={locale} isReadOnly={isReadOnly} />);
3331

34-
const user = userEvent.setup();
35-
const button = screen.getByRole("button", { name: /Click me!/i });
32+
const reactFlowContainer = screen.getByTestId("diagram-container");
3633

37-
await user.click(button);
38-
39-
expect(alertMock).toHaveBeenCalledWith("Hello from Diagram!");
34+
expect(reactFlowContainer).toBeInTheDocument();
4035
});
4136
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2021-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { render, screen } from "@testing-library/react";
18+
import { Diagram } from "../../../src/react-flow/diagram/Diagram";
19+
import { vi, test, expect, afterEach, describe } from "vitest";
20+
21+
describe("Diagram Component", () => {
22+
afterEach(() => {
23+
vi.restoreAllMocks();
24+
});
25+
26+
test("Renders react flow nodes", async () => {
27+
render(<Diagram />);
28+
29+
const node1 = screen.getByText("Node 1");
30+
const node2 = screen.getByText("Node 2");
31+
const node3 = screen.getByText("Node 3");
32+
const node4 = screen.getByText("Node 4");
33+
const node5 = screen.getByText("Node 5");
34+
35+
expect(node1).toBeInTheDocument();
36+
expect(node2).toBeInTheDocument();
37+
expect(node3).toBeInTheDocument();
38+
expect(node4).toBeInTheDocument();
39+
expect(node5).toBeInTheDocument();
40+
});
41+
});

packages/serverless-workflow-diagram-editor/tests/setupTests.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,44 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { afterEach } from "vitest";
17+
import { afterEach, vi } from "vitest";
1818
import { cleanup } from "@testing-library/react";
1919
import "@testing-library/jest-dom/vitest"; // This extends vitest's expect with jest-dom matchers
2020

2121
// Run cleanup after each test to unmount React components and clean up the DOM
2222
afterEach(() => {
2323
cleanup();
2424
});
25+
26+
// Mock ResizeObserver
27+
vi.stubGlobal(
28+
"ResizeObserver",
29+
class {
30+
observe() {}
31+
unobserve() {}
32+
disconnect() {}
33+
},
34+
);
35+
36+
// Mock DOMMatrix (required for coordinate calculations)
37+
vi.stubGlobal(
38+
"DOMMatrixReadOnly",
39+
class {
40+
m22 = 1;
41+
constructor(transform: string) {
42+
/* logic to parse transform if needed */
43+
}
44+
},
45+
);
46+
47+
// Mock PointerEvent (required for drag-and-drop actions)
48+
if (!global.PointerEvent) {
49+
vi.stubGlobal(
50+
"PointerEvent",
51+
class extends Event {
52+
constructor(type: string, params: PointerEventInit = {}) {
53+
super(type, params);
54+
}
55+
},
56+
);
57+
}

0 commit comments

Comments
 (0)