|
5 | 5 | * a knob-paint bug once threw only when a card had a coloured knob — invisible |
6 | 6 | * on an idle canvas, fatal on node creation. |
7 | 7 | */ |
8 | | -import { act } from 'react' |
| 8 | +import { act, useLayoutEffect } from 'react' |
9 | 9 | import { |
10 | 10 | normalizeWorkflowEdgeSourceHandle, |
11 | 11 | normalizeWorkflowEdgeTargetHandle, |
12 | 12 | } from '@sim/workflow-types/workflow' |
13 | 13 | import { createRoot, type Root } from 'react-dom/client' |
14 | | -import { ReactFlowProvider } from 'reactflow' |
| 14 | +import { type Edge, ReactFlowProvider, useStoreApi as useReactFlowStoreApi } from 'reactflow' |
15 | 15 | import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' |
16 | 16 | import { CONTAINER_DIMENSIONS } from '../dimensions' |
17 | 17 | import { |
@@ -83,6 +83,47 @@ function mount(element: React.ReactElement) { |
83 | 83 | return { host, root } |
84 | 84 | } |
85 | 85 |
|
| 86 | +const NO_EDGES: Edge[] = [] |
| 87 | + |
| 88 | +function SubflowMountFixture({ |
| 89 | + id, |
| 90 | + kind, |
| 91 | + parentId, |
| 92 | + edges = NO_EDGES, |
| 93 | +}: { |
| 94 | + id: string |
| 95 | + kind: 'loop' | 'parallel' |
| 96 | + parentId?: string |
| 97 | + edges?: Edge[] |
| 98 | +}) { |
| 99 | + const reactFlowStore = useReactFlowStoreApi() |
| 100 | + |
| 101 | + useLayoutEffect(() => { |
| 102 | + reactFlowStore.setState({ edges }) |
| 103 | + }, [edges, reactFlowStore]) |
| 104 | + |
| 105 | + return ( |
| 106 | + <SubflowNodeView |
| 107 | + id={id} |
| 108 | + data={{ kind, name: kind === 'loop' ? 'Loop' : 'Parallel', parentId, isPreview: true }} |
| 109 | + isEnabled |
| 110 | + isLocked={false} |
| 111 | + isFocused={false} |
| 112 | + nestingLevel={parentId ? 1 : 0} |
| 113 | + canEditWorkflow={false} |
| 114 | + onSelect={() => undefined} |
| 115 | + /> |
| 116 | + ) |
| 117 | +} |
| 118 | + |
| 119 | +function getSubflowSilhouette(host: HTMLElement, caseName: string) { |
| 120 | + const path = host.querySelector<SVGPathElement>( |
| 121 | + `[data-subflow-case="${caseName}"] [data-type="subflowNode"] > svg > path[fill="var(--border-1)"]` |
| 122 | + ) |
| 123 | + expect(path).toBeTruthy() |
| 124 | + return path?.getAttribute('d') |
| 125 | +} |
| 126 | + |
86 | 127 | afterEach(() => { |
87 | 128 | act(() => { |
88 | 129 | mountedRoots.forEach((root) => root.unmount()) |
@@ -980,6 +1021,62 @@ describe('WorkflowBlockBorder mount', () => { |
980 | 1021 | ) |
981 | 1022 | }) |
982 | 1023 |
|
| 1024 | + it.each(['loop', 'parallel'] as const)( |
| 1025 | + 'only paints the fixed %s end port when its topology needs it', |
| 1026 | + (kind) => { |
| 1027 | + const endHandleId = `${kind}-end-source` |
| 1028 | + const nestedConnectedEdges: Edge[] = [ |
| 1029 | + { |
| 1030 | + id: `${kind}-end-edge`, |
| 1031 | + source: `${kind}-nested-connected`, |
| 1032 | + sourceHandle: endHandleId, |
| 1033 | + target: `${kind}-sibling`, |
| 1034 | + targetHandle: 'target', |
| 1035 | + }, |
| 1036 | + ] |
| 1037 | + const { host } = mount( |
| 1038 | + <div> |
| 1039 | + <ReactFlowProvider> |
| 1040 | + <div data-subflow-case='top-level'> |
| 1041 | + <SubflowMountFixture id={`${kind}-top-level`} kind={kind} /> |
| 1042 | + </div> |
| 1043 | + </ReactFlowProvider> |
| 1044 | + <ReactFlowProvider> |
| 1045 | + <div data-subflow-case='nested-idle'> |
| 1046 | + <SubflowMountFixture |
| 1047 | + id={`${kind}-nested-idle`} |
| 1048 | + kind={kind} |
| 1049 | + parentId={`${kind}-parent`} |
| 1050 | + /> |
| 1051 | + </div> |
| 1052 | + </ReactFlowProvider> |
| 1053 | + <ReactFlowProvider> |
| 1054 | + <div data-subflow-case='nested-connected'> |
| 1055 | + <SubflowMountFixture |
| 1056 | + id={`${kind}-nested-connected`} |
| 1057 | + kind={kind} |
| 1058 | + parentId={`${kind}-parent`} |
| 1059 | + edges={nestedConnectedEdges} |
| 1060 | + /> |
| 1061 | + </div> |
| 1062 | + </ReactFlowProvider> |
| 1063 | + </div> |
| 1064 | + ) |
| 1065 | + |
| 1066 | + const topLevelPath = getSubflowSilhouette(host, 'top-level') |
| 1067 | + const nestedIdlePath = getSubflowSilhouette(host, 'nested-idle') |
| 1068 | + const nestedConnectedPath = getSubflowSilhouette(host, 'nested-connected') |
| 1069 | + |
| 1070 | + expect(nestedIdlePath).not.toBe(topLevelPath) |
| 1071 | + expect(nestedConnectedPath).toBe(topLevelPath) |
| 1072 | + for (const caseName of ['top-level', 'nested-idle', 'nested-connected']) { |
| 1073 | + const subflow = host.querySelector(`[data-subflow-case="${caseName}"]`) |
| 1074 | + expect(subflow?.querySelector('[data-handleid="target"]')).toBeTruthy() |
| 1075 | + expect(subflow?.querySelector(`[data-handleid="${endHandleId}"]`)).toBeTruthy() |
| 1076 | + } |
| 1077 | + } |
| 1078 | + ) |
| 1079 | + |
983 | 1080 | it('retracts a selected loop action swell after hover ends', () => { |
984 | 1081 | vi.useFakeTimers() |
985 | 1082 | vi.stubGlobal( |
|
0 commit comments