Skip to content

Commit c2ba309

Browse files
chore: extend unit tests for editing
1 parent babec92 commit c2ba309

24 files changed

Lines changed: 1184 additions & 519 deletions

src/component/EditMenu.tsx

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import * as React from "react";
22

3+
import { EditMenuItem } from "./EditMenuItem";
34
import { FlowElementReference } from "../model/FlowElement";
45
import { addContentElement, addDivergingGateway } from "../model/action/addElement";
56
import { addBranch } from "../model/action/addBranch";
6-
import { removeElement } from "../model/action/removeElement";
7+
import { isChangeNextElementAllowed } from "../model/action/changeNextElement";
8+
import { removeElement, isRemoveElementAllowed } from "../model/action/removeElement";
79

810
import { SelectableElementType, EditActionResult, DraggableType } from "../types/EditAction";
911
import { FlowModelerProps, MenuOptions } from "../types/FlowModelerProps";
1012
import { ElementType } from "../types/GridCellData";
11-
import { EditMenuItem } from "./EditMenuItem";
1213

1314
const onClickStopPropagation = (event: React.MouseEvent): void => event.stopPropagation();
1415

@@ -17,7 +18,7 @@ export class EditMenu extends React.Component<{
1718
referenceElement?: FlowElementReference;
1819
branchIndex?: number;
1920
menuOptions?: FlowModelerProps["options"]["editActions"];
20-
onChange?: (change: (originalFlow: FlowModelerProps["flow"]) => EditActionResult) => void;
21+
onChange: (change: (originalFlow: FlowModelerProps["flow"]) => EditActionResult) => void;
2122
}> {
2223
isNextElementReferencedByOthers = (): boolean => {
2324
const { referenceElement, branchIndex } = this.props;
@@ -43,7 +44,7 @@ export class EditMenu extends React.Component<{
4344
this.isNextElementReferencedByOthers()
4445
? { type: dragType, originType: targetType, originElement: referenceElement, originBranchIndex: branchIndex }
4546
: undefined;
46-
return <EditMenuItem options={options} defaultClassName={defaultClassName} onClick={onClick} dragItem={dragItem} />;
47+
return <EditMenuItem key={defaultClassName} options={options} defaultClassName={defaultClassName} onClick={onClick} dragItem={dragItem} />;
4748
}
4849

4950
onAddContentElementClick = (): void => {
@@ -96,11 +97,11 @@ export class EditMenu extends React.Component<{
9697
}
9798

9899
renderChangeNextElementItem(): React.ReactNode {
99-
const { targetType, menuOptions } = this.props;
100-
if (targetType !== ElementType.Content && targetType !== ElementType.ConnectGatewayToElement) {
101-
return null;
100+
const { targetType, referenceElement, branchIndex, menuOptions } = this.props;
101+
if (isChangeNextElementAllowed(targetType, referenceElement, branchIndex)) {
102+
return this.renderMenuItem(menuOptions ? menuOptions.changeNextElement : undefined, "change-next", undefined, DraggableType.LINK);
102103
}
103-
return this.renderMenuItem(menuOptions ? menuOptions.changeNextElement : undefined, "change-next", undefined, DraggableType.LINK);
104+
return null;
104105
}
105106

106107
onRemoveClick = (): void => {
@@ -111,18 +112,14 @@ export class EditMenu extends React.Component<{
111112
};
112113

113114
renderRemoveItem(): React.ReactNode {
114-
const { targetType, menuOptions } = this.props;
115-
if (targetType !== ElementType.Content && targetType !== ElementType.ConnectGatewayToElement) {
116-
return null;
115+
const { targetType, referenceElement, branchIndex, menuOptions } = this.props;
116+
if (isRemoveElementAllowed(targetType, referenceElement, branchIndex)) {
117+
return this.renderMenuItem(menuOptions ? menuOptions.removeElement : undefined, "remove", this.onRemoveClick);
117118
}
118-
return this.renderMenuItem(menuOptions ? menuOptions.removeElement : undefined, "remove", this.onRemoveClick);
119+
return null;
119120
}
120121

121122
render(): React.ReactNode {
122-
const { onChange } = this.props;
123-
if (!onChange) {
124-
return null;
125-
}
126123
const menuItems = [
127124
this.renderAddContentElementItem(),
128125
this.renderAddDivergingGatewayItem(),

src/component/EditMenuItem.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ import * as React from "react";
22
import { useDrag } from "react-dnd";
33

44
import { DraggedLinkContext } from "../types/EditAction";
5-
import { MenuOptions } from "../types/FlowModelerProps";
65

76
const disabledDragging: [{ isDragging: boolean }, React.LegacyRef<HTMLSpanElement>] = [{ isDragging: false }, undefined];
87

98
export const EditMenuItem: React.FC<{
10-
options: MenuOptions;
9+
options: { className?: string; title?: string };
1110
defaultClassName: string;
1211
onClick?: (event: React.MouseEvent) => void;
1312
dragItem?: DraggedLinkContext;
@@ -24,10 +23,8 @@ export const EditMenuItem: React.FC<{
2423
const baseClassName = (options && options.className) || `menu-item ${defaultClassName}`;
2524
return (
2625
<span
27-
key={defaultClassName}
28-
title={options && options.title}
2926
className={`${baseClassName}${isDragging ? " dragging" : ""}`}
30-
{...{ ref: drag, onClick }}
27+
{...{ title: options ? options.title : undefined, ref: drag, onClick }}
3128
/>
3229
);
3330
};

src/model/action/addBranch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ export const addBranch = (
1616
const followings = nextConvergingGateway.getFollowingElements();
1717
nextConvergingGateway = followings[followings.length - 1];
1818
} while (nextConvergingGateway.getPrecedingElements().length < 2);
19-
gatewayInFlow.nextElements.push({ id: nextConvergingGateway.getId() });
19+
gatewayInFlow.nextElements.push({ id: nextConvergingGateway.getId(), conditionData: data });
2020
return { changedFlow };
2121
};

src/model/action/changeNextElement.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import { EditActionResult } from "../../types/EditAction";
66
import { FlowModelerProps, FlowContent, FlowGatewayDiverging } from "../../types/FlowModelerProps";
77
import { ElementType } from "../../types/GridCellData";
88

9+
export const isChangeNextElementAllowed = (targetType: ElementType, referenceElement: FlowElementReference, branchIndex?: number): boolean =>
10+
(targetType === ElementType.Content || targetType === ElementType.ConnectGatewayToElement) &&
11+
referenceElement.getFollowingElements()[branchIndex || 0].getPrecedingElements().length > 1;
12+
913
export const changeNextElement = (
1014
originalFlow: FlowModelerProps["flow"],
1115
newNextElement: FlowElementReference,

src/model/action/removeElement.ts

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,26 @@ import cloneDeep from "lodash.clonedeep";
22

33
import { replaceAllLinks } from "./editUtils";
44
import { FlowElementReference } from "../FlowElement";
5-
import { isDivergingGateway } from "../modelUtils";
65

76
import { EditActionResult } from "../../types/EditAction";
87
import { FlowModelerProps, FlowContent, FlowGatewayDiverging } from "../../types/FlowModelerProps";
98
import { ElementType } from "../../types/GridCellData";
109

11-
const isElementReferencing = (elementId: string) => (element: FlowContent | FlowGatewayDiverging): boolean => {
12-
if (isDivergingGateway(element)) {
13-
return element.nextElements.some(({ id }) => id === elementId);
14-
}
15-
return element.nextElementId === elementId;
16-
};
17-
18-
const isElementReferenced = (flow: FlowModelerProps["flow"], elementId: string): boolean =>
19-
Object.values(flow.elements).some(isElementReferencing(elementId));
20-
21-
const removeOrphanElement = (flow: FlowModelerProps["flow"], elementId: string): void => {
22-
if (elementId in flow.elements && !isElementReferenced(flow, elementId)) {
23-
const orphan = flow.elements[elementId];
24-
delete flow.elements[elementId];
25-
if (isDivergingGateway(orphan)) {
26-
orphan.nextElements.forEach(({ id }) => removeOrphanElement(flow, id));
27-
} else {
28-
removeOrphanElement(flow, orphan.nextElementId);
29-
}
30-
}
31-
};
10+
/**
11+
* Only content elements may be removed or gateway branches that point to converging gateways (to not remove other elements implicitly).
12+
*
13+
* @param {ElementType} targetType - indication of type of element being target for removal
14+
* @param {FlowElementReference} referenceElement - content or diverging gateway element
15+
* @param {?number} branchIndex - index in reference gateway identifying the targeted ConnectGatewayToElement
16+
* @returns {boolean} whether removeElement() is allowed to be called for the targeted element
17+
*/
18+
export const isRemoveElementAllowed = (targetType: ElementType, referenceElement: FlowElementReference, branchIndex?: number): boolean =>
19+
targetType === ElementType.Content ||
20+
(targetType === ElementType.ConnectGatewayToElement && referenceElement.getFollowingElements()[branchIndex].getPrecedingElements().length > 1);
3221

3322
export const removeElement = (
3423
originalFlow: FlowModelerProps["flow"],
35-
targetType: ElementType.Content | ElementType.GatewayDiverging | ElementType.ConnectGatewayToElement,
24+
targetType: ElementType.Content | ElementType.ConnectGatewayToElement,
3625
referenceElement: FlowElementReference,
3726
branchIndex?: number
3827
): EditActionResult => {
@@ -46,9 +35,7 @@ export const removeElement = (
4635
break;
4736
case ElementType.ConnectGatewayToElement:
4837
const precedingGatewayElement = (changedFlow.elements[targetId] as unknown) as FlowGatewayDiverging;
49-
const nextElementId = precedingGatewayElement.nextElements[branchIndex].id;
5038
precedingGatewayElement.nextElements.splice(branchIndex, 1);
51-
removeOrphanElement(changedFlow, nextElementId);
5239
if (precedingGatewayElement.nextElements.length === 1) {
5340
// remove gateway as well, now that there is only one path left
5441
replaceAllLinks(changedFlow, targetId, precedingGatewayElement.nextElements[0].id);

src/model/pathValidationUtils.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,15 @@ export const createValidatedElementTree = (flow: FlowModelerProps["flow"], verti
119119
return treeRootElement;
120120
};
121121

122+
export const validateFlow = (flow: FlowModelerProps["flow"]): void => {
123+
checkForCircularReference(flow.firstElementId, flow.elements);
124+
const treeRootElement = createMinimalElementTreeStructure(flow).firstElement;
125+
validatePaths(treeRootElement);
126+
};
127+
122128
export const isFlowValid = (flow: FlowModelerProps["flow"]): boolean => {
123129
try {
124-
checkForCircularReference(flow.firstElementId, flow.elements);
125-
validatePaths(createMinimalElementTreeStructure(flow).firstElement);
130+
validateFlow(flow);
126131
return true;
127132
} catch (error) {
128133
return false;

src/types/EditAction.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ export type SelectableElementType =
99
| ElementType.GatewayConverging
1010
| ElementType.ConnectGatewayToElement;
1111

12-
export type EditActionResult = {
12+
export interface EditActionResult {
1313
changedFlow: FlowModelerProps["flow"];
14-
};
14+
}
1515

1616
export enum DraggableType {
1717
LINK = "link"
@@ -20,7 +20,7 @@ export enum DraggableType {
2020
export interface DraggedLinkContext {
2121
type: DraggableType.LINK;
2222
originType: ElementType.Content | ElementType.ConnectGatewayToElement;
23-
originElement?: FlowElementReference;
23+
originElement: FlowElementReference;
2424
originBranchIndex?: number;
2525
}
2626

test/component/ContentElement.test.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,33 @@
11
import * as React from "react";
2-
import { shallow } from "enzyme";
2+
import { mount, shallow } from "enzyme";
33

44
import { ContentElement } from "../../src/component/ContentElement";
5+
import { FlowElementReference } from "../../src/model/FlowElement";
6+
7+
const mockFlowElementReference = (id: string): FlowElementReference => ({
8+
getId: (): string => id,
9+
getPrecedingElements: (): Array<FlowElementReference> => [],
10+
getFollowingElements: (): Array<FlowElementReference> => []
11+
});
512

613
describe("renders correctly", () => {
714
const onSelect = (): void => {};
815
it("with minimal props", () => {
916
const component = shallow(
10-
<ContentElement elementId="element-id" editMenu={(): React.ReactNode => <div className="edit-menu-placeholder" />} onSelect={onSelect}>
17+
<ContentElement
18+
referenceElement={mockFlowElementReference("element-id")}
19+
editMenu={(): React.ReactNode => <div className="edit-menu-placeholder" />}
20+
onLinkDrop={undefined}
21+
onSelect={onSelect}
22+
>
1123
{"text"}
1224
</ContentElement>
1325
);
1426
expect(component).toMatchSnapshot();
1527
});
1628
it("when not selected", () => {
17-
const component = shallow(
18-
<ContentElement elementId="element-id" editMenu={undefined} onSelect={onSelect}>
29+
const component = mount(
30+
<ContentElement referenceElement={mockFlowElementReference("element-id")} editMenu={undefined} onLinkDrop={undefined} onSelect={onSelect}>
1931
{"text"}
2032
</ContentElement>
2133
);
@@ -28,8 +40,8 @@ describe("calls onSelect", () => {
2840
const onSelect = jest.fn(() => {});
2941
const event = ({ stopPropagation: jest.fn(() => {}) } as unknown) as React.MouseEvent;
3042
it("on click event", () => {
31-
const component = shallow(
32-
<ContentElement elementId={"element-id"} editMenu={undefined} onSelect={onSelect}>
43+
const component = mount(
44+
<ContentElement referenceElement={mockFlowElementReference("element-id")} editMenu={undefined} onLinkDrop={undefined} onSelect={onSelect}>
3345
{"text"}
3446
</ContentElement>
3547
);

test/component/EditMenu.test.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as React from "react";
2+
import { shallow } from "enzyme";
3+
4+
import { EditMenu } from "../../src/component/EditMenu";
5+
import { ElementType } from "../../src/types/GridCellData";
6+
import { createMinimalElementTreeStructure } from "../../src/model/modelUtils";
7+
import { FlowElement } from "../../src/model/FlowElement";
8+
9+
describe("renders correctly", () => {
10+
const { elementsInTree }: { elementsInTree: Map<string, FlowElement> } = createMinimalElementTreeStructure({
11+
firstElementId: "a",
12+
elements: {
13+
a: { nextElements: [{ id: "b" }, { id: "c" }, {}] },
14+
b: { nextElementId: "c" },
15+
c: {}
16+
}
17+
});
18+
const onChange = (): void => {};
19+
it("for start element", () => {
20+
const component = shallow(<EditMenu targetType={ElementType.Start} onChange={onChange} />);
21+
expect(component).toMatchSnapshot();
22+
});
23+
it("for content element", () => {
24+
const component = shallow(<EditMenu targetType={ElementType.Content} referenceElement={elementsInTree.get("b")} onChange={onChange} />);
25+
expect(component).toMatchSnapshot();
26+
});
27+
it("for diverging gateway", () => {
28+
const component = shallow(
29+
<EditMenu targetType={ElementType.GatewayDiverging} referenceElement={elementsInTree.get("a")} onChange={onChange} />
30+
);
31+
expect(component).toMatchSnapshot();
32+
});
33+
it("for converging gateway", () => {
34+
const component = shallow(
35+
<EditMenu targetType={ElementType.GatewayConverging} referenceElement={elementsInTree.get("c")} onChange={onChange} />
36+
);
37+
expect(component).toMatchSnapshot();
38+
});
39+
it("for diverging gateway branch", () => {
40+
const component = shallow(
41+
<EditMenu targetType={ElementType.GatewayConverging} referenceElement={elementsInTree.get("a")} branchIndex={1} onChange={onChange} />
42+
);
43+
expect(component).toMatchSnapshot();
44+
});
45+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as React from "react";
2+
import { DndProvider } from "react-dnd";
3+
import Backend from "react-dnd-html5-backend";
4+
import { mount, shallow } from "enzyme";
5+
6+
import { EditMenuItem } from "../../src/component/EditMenuItem";
7+
import { DraggableType } from "../../src/types/EditAction";
8+
import { ElementType } from "../../src/types/GridCellData";
9+
import { FlowElementReference } from "../../src/model/FlowElement";
10+
11+
describe("renders correctly", () => {
12+
it("for clicking", () => {
13+
const onClick = (): void => {};
14+
const component = shallow(<EditMenuItem options={undefined} defaultClassName="for-clicking" onClick={onClick} />);
15+
const element = component.find(".menu-item.for-clicking");
16+
expect(element.exists()).toBe(true);
17+
expect(element.prop("title")).toBeUndefined();
18+
expect(element.prop("onClick")).toBe(onClick);
19+
});
20+
it("for dragging", () => {
21+
const dragItem = {
22+
type: DraggableType.LINK,
23+
originType: ElementType.Content,
24+
originElement: {
25+
getId: (): string => "id",
26+
getPrecedingElements: (): Array<FlowElementReference> => [],
27+
getFollowingElements: (): Array<FlowElementReference> => []
28+
}
29+
};
30+
const component = mount(
31+
<DndProvider backend={Backend}>
32+
<EditMenuItem options={undefined} defaultClassName="for-dragging" dragItem={dragItem} />
33+
</DndProvider>
34+
);
35+
const element = component.find(".menu-item.for-dragging");
36+
expect(element.exists()).toBe(true);
37+
expect(element.prop("title")).toBeUndefined();
38+
expect(element.prop("onClick")).toBeUndefined();
39+
});
40+
it("considering given options", () => {
41+
const options = { className: "clickable", title: "Click me!" };
42+
const onClick = (): void => {};
43+
const component = shallow(<EditMenuItem options={options} defaultClassName="for-clicking" onClick={onClick} />);
44+
const element = component.find(".clickable");
45+
expect(element.exists()).toBe(true);
46+
expect(element.hasClass("menu-item")).toBe(false);
47+
expect(element.hasClass("for-clicking")).toBe(false);
48+
expect(element.prop("title")).toEqual("Click me!");
49+
expect(element.prop("onClick")).toBe(onClick);
50+
});
51+
});
52+
it("not rendered without dragging or clicking enabled", () => {
53+
const wrapper = shallow(<EditMenuItem options={undefined} defaultClassName="something" />);
54+
expect(wrapper).toEqual({});
55+
});

0 commit comments

Comments
 (0)