Skip to content

Commit 29b681d

Browse files
committed
feat(agentflow): update canvas add node drag and drop behaviour
- fix style issues and refactor components using design token with dark mode theme support - add and update tests with additional jest config to mock canvas and libs
1 parent b63d771 commit 29b681d

38 files changed

+1817
-316
lines changed

packages/agentflow/.eslintrc.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,18 @@ module.exports = {
142142
'no-console': 'off',
143143
'@typescript-eslint/no-non-null-assertion': 'off'
144144
}
145+
},
146+
{
147+
files: ['src/__mocks__/**/*.{ts,tsx}'],
148+
rules: {
149+
'@typescript-eslint/no-explicit-any': 'off'
150+
}
151+
},
152+
{
153+
files: ['src/__test_utils__/**/*.js'],
154+
rules: {
155+
'@typescript-eslint/no-require-imports': 'off'
156+
}
145157
}
146158
]
147159
}

packages/agentflow/TESTS.md

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ These modules carry the highest risk. Test in the same PR when modifying.
2727
| `src/core/utils/` | `getUniqueNodeId`, `getUniqueNodeLabel`, `initializeDefaultNodeData`, `initNode`, `generateExportFlowData`, `isValidConnectionAgentflowV2` | ✅ Done |
2828
| `src/core/node-catalog/` | `filterNodesByComponents`, `isAgentflowNode`, `groupNodesByCategory` | ✅ Done |
2929
| `src/core/node-config/` | `getAgentflowIcon`, `getNodeColor` | ✅ Done |
30+
| `src/core/theme/tokens.ts` | All design tokens — node colors (15 types, uniqueness), light/dark variants (backgrounds, borders, text), spacing scale (8px base), semantic colors, ReactFlow colors, shadows, border radius, gradients | ✅ Done |
31+
| `src/core/theme/cssVariables.ts` | `generateCSSVariables()` — generates valid CSS strings, includes all variables (colors, spacing, shadows, etc.), correct light/dark mode values, proper formatting (px suffixes), consistency with tokens | ✅ Done |
32+
| `src/core/theme/createAgentflowTheme.ts` | `createAgentflowTheme()` — MUI theme creation, palette mode (light/dark), background/text/border colors from tokens, custom card palette extension, 8px spacing, border radius, theme consistency | ✅ Done |
3033
| `src/infrastructure/api/client.ts` | `createApiClient` — headers, auth token, 401 interceptor | ✅ Done |
3134
| `src/infrastructure/api/chatflows.ts` | All CRUD + `generateAgentflow` + `getChatModels`, FlowData serialization | ✅ Done |
3235
| `src/infrastructure/api/nodes.ts` | `getAllNodes`, `getNodeByName`, `getNodeIconUrl` | ✅ Done |
@@ -64,17 +67,67 @@ Mostly JSX with minimal logic. Only add tests if business logic is introduced.
6467
| `src/atoms/NodeInputHandler.tsx` | If input rendering or position calculation logic changes | ⬜ Not yet |
6568
| `src/features/canvas/components/ConnectionLine.tsx` | If edge label determination logic becomes more complex | ⬜ Not yet |
6669
| `src/features/canvas/components/NodeStatusIndicator.tsx` | If status-to-color/icon mapping expands | ⬜ Not yet |
67-
| `src/Agentflow.tsx` | Integration test when component orchestration is stable | ⬜ Not yet |
70+
| `src/Agentflow.tsx` | Integration test — dark mode integration (data-dark-mode attribute, Controls class), ThemeProvider wrapper, theme switching, CSS variables injection/cleanup, header rendering, component structure, generate flow dialog (open/close/onGenerated), imperative ref | ✅ Done |
6871

6972
Files that are pure styling or data constants (`styled.ts`, `nodeIcons.ts`, `MainCard.tsx`, `Input.tsx`, etc.) do not need dedicated tests.
7073

74+
## Test Utilities
75+
76+
### Factory Functions (`src/__test_utils__/factories.ts`)
77+
78+
Use factory functions to create test fixtures with sensible defaults:
79+
80+
```typescript
81+
import { makeFlowNode, makeFlowEdge, makeNodeData } from '@test-utils/factories'
82+
83+
// Create test nodes
84+
const node = makeFlowNode('node-1', {
85+
type: 'agentflowNode',
86+
data: { name: 'llmAgentflow', label: 'LLM' }
87+
})
88+
89+
// Create test edges
90+
const edge = makeFlowEdge('node-1', 'node-2')
91+
92+
// Create node data (for palette/search tests)
93+
const nodeData = makeNodeData({ name: 'llmAgentflow', label: 'LLM' })
94+
```
95+
96+
### Custom Jest Environment
97+
98+
**File**: `src/__test_utils__/jest-environment-jsdom.js`
99+
100+
Prevents the `canvas` native module from being loaded during jsdom initialization. The canvas package requires native compilation which fails in many environments, but it's only an optional dependency of jsdom and not needed for React component tests.
101+
102+
This custom environment intercepts `require('canvas')` at the module level and returns a mock before jsdom tries to load the native binary.
103+
104+
### Module Mocks
105+
106+
**ReactFlow Mock** (`src/__mocks__/reactflow.tsx`): Provides mock implementations of ReactFlow components and hooks.
107+
108+
Key features:
109+
- Uses `forwardRef` for MUI `styled()` compatibility (prevents emotion errors)
110+
- Uses `useState` internally to maintain stable references (prevents infinite re-render loops)
111+
- Exports all commonly used ReactFlow components (`Controls`, `MiniMap`, `Background`, etc.)
112+
- Mocks hooks (`useNodesState`, `useEdgesState`, `useReactFlow`)
113+
114+
**Axios Mock** (`src/__mocks__/axios.ts`): Prevents network errors by mocking all HTTP requests. Returns empty arrays/objects for all API calls to silence network warnings in tests.
115+
116+
**CSS Mock** (`src/__mocks__/styleMock.js`): Empty object export for CSS imports.
117+
71118
## Configuration
72119

73-
- **Jest config**: `jest.config.js` — two projects: `unit` (node env, `.test.ts`) and `components` (jsdom env, `.test.tsx`)
120+
- **Jest config**: `jest.config.js` — two projects: `unit` (node env, `.test.ts`) and `components` (custom jsdom env, `.test.tsx`)
121+
- **Test environment**: Component tests use custom jsdom environment (`src/__test_utils__/jest-environment-jsdom.js`) to handle canvas loading
122+
- **Import aliases**: `@test-utils` maps to `src/__test_utils__` for convenient imports
74123
- **Coverage thresholds**: uniform 80% floor (`branches`, `functions`, `lines`, `statements`) enforced per-path:
124+
- `./src/Agentflow.tsx`
75125
- `./src/core/`
76-
- `./src/infrastructure/api/`
77126
- `./src/features/node-palette/search.ts`
78-
- **Exclusions**: `src/infrastructure/api/hooks/useApi.ts` is excluded from coverage collection (potentially deprecated — check before investing in tests)
127+
- `./src/infrastructure/api/`
128+
- **Coverage exclusions**:
129+
- `src/__test_utils__/**` — test utilities
130+
- `src/__mocks__/**` — module mocks
131+
- `src/infrastructure/api/hooks/useApi.ts` — potentially deprecated
79132
- **CI**: `pnpm test:coverage` runs in GitHub Actions between lint and build
80133
- **Reports**: `coverage/lcov-report/index.html` for detailed HTML report

packages/agentflow/examples/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ function LoadingFallback() {
100100

101101
export default function App() {
102102
const [selectedExample, setSelectedExample] = useState<ExampleId>('basic')
103+
const [showProps, setShowProps] = useState(true)
103104
// Config loaded from environment variables
104105

105106
const currentExample = examples.find((e) => e.id === selectedExample)
@@ -175,6 +176,8 @@ export default function App() {
175176
exampleName={currentExample.name}
176177
props={actualProps as Record<string, string | boolean>}
177178
exampleId={selectedExample}
179+
showProps={showProps}
180+
onToggleProps={setShowProps}
178181
/>
179182
)}
180183

packages/agentflow/examples/src/PropsDisplay.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,15 @@
44
* Displays Agentflow component props in an expandable accordion format
55
*/
66

7-
import { useEffect, useState } from 'react'
8-
97
interface PropsDisplayProps {
108
exampleName: string
119
props: Record<string, string | boolean>
1210
exampleId: string
11+
showProps: boolean
12+
onToggleProps: (show: boolean) => void
1313
}
1414

15-
export function PropsDisplay({ exampleName, props, exampleId }: PropsDisplayProps) {
16-
const [showProps, setShowProps] = useState(false)
17-
18-
// Auto-expand props when example changes
19-
useEffect(() => {
20-
setShowProps(true)
21-
}, [exampleId])
22-
15+
export function PropsDisplay({ exampleName, props, exampleId, showProps, onToggleProps }: PropsDisplayProps) {
2316
return (
2417
<div
2518
key={exampleId}
@@ -31,7 +24,7 @@ export function PropsDisplay({ exampleName, props, exampleId }: PropsDisplayProp
3124
>
3225
{/* Accordion Header */}
3326
<button
34-
onClick={() => setShowProps(!showProps)}
27+
onClick={() => onToggleProps(!showProps)}
3528
style={{
3629
width: '100%',
3730
padding: '12px 16px',

packages/agentflow/examples/src/demos/AllNodeTypesExample.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ export function AllNodeTypesExample() {
219219
const agentflowRef = useRef<AgentFlowInstance>(null)
220220

221221
return (
222-
<div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
222+
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
223223
{/* Info Header */}
224224
<div
225225
style={{

packages/agentflow/examples/src/demos/CustomUIExample.tsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -253,21 +253,23 @@ export function CustomUIExample() {
253253
const agentflowRef = useRef<AgentFlowInstance>(null)
254254

255255
return (
256-
<div style={{ width: '100%', height: '100vh' }}>
257-
<Agentflow
258-
ref={agentflowRef}
259-
apiBaseUrl={apiBaseUrl}
260-
token={token ?? undefined}
261-
initialFlow={initialFlow}
262-
renderHeader={(props) => <CustomHeader {...props} />}
263-
renderNodePalette={(props) => <CustomPalette {...props} />}
264-
showDefaultHeader={false}
265-
showDefaultPalette={false}
266-
onSave={(flow) => {
267-
console.log('Saving flow:', flow)
268-
alert('Flow saved! Check console.')
269-
}}
270-
/>
256+
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
257+
<div style={{ flex: 1 }}>
258+
<Agentflow
259+
ref={agentflowRef}
260+
apiBaseUrl={apiBaseUrl}
261+
token={token ?? undefined}
262+
initialFlow={initialFlow}
263+
renderHeader={(props) => <CustomHeader {...props} />}
264+
renderNodePalette={(props) => <CustomPalette {...props} />}
265+
showDefaultHeader={false}
266+
showDefaultPalette={false}
267+
onSave={(flow) => {
268+
console.log('Saving flow:', flow)
269+
alert('Flow saved! Check console.')
270+
}}
271+
/>
272+
</div>
271273
</div>
272274
)
273275
}

packages/agentflow/examples/src/demos/DarkModeExample.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export function DarkModeExample() {
104104
return (
105105
<ThemeProvider theme={darkTheme}>
106106
<CssBaseline />
107-
<div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
107+
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
108108
{/* Theme Toggle */}
109109
<div
110110
style={{
@@ -140,7 +140,7 @@ export function DarkModeExample() {
140140
apiBaseUrl={apiBaseUrl}
141141
token={token ?? undefined}
142142
initialFlow={sampleFlow}
143-
theme={isDark ? 'dark' : 'light'}
143+
isDarkMode={isDark}
144144
showDefaultHeader={false}
145145
/>
146146
</div>
@@ -153,6 +153,6 @@ export const DarkModeExampleProps = {
153153
apiBaseUrl: '{from environment variables}',
154154
token: '{from environment variables}',
155155
initialFlow: 'FlowData (sample flow)',
156-
theme: '{isDark ? "dark" : "light"}',
156+
isDarkMode: '{isDark}',
157157
showDefaultHeader: false
158158
}

packages/agentflow/examples/src/demos/FilteredComponentsExample.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export function FilteredComponentsExample() {
7777
const currentPreset = presets[selectedPreset]
7878

7979
return (
80-
<div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
80+
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
8181
{/* Preset Selector */}
8282
<div
8383
style={{

packages/agentflow/examples/src/demos/MultiNodeFlow.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,17 @@ export function MultiNodeFlow() {
149149
const agentflowRef = useRef<AgentFlowInstance>(null)
150150

151151
return (
152-
<div style={{ width: '100%', height: '100vh' }}>
153-
<Agentflow
154-
ref={agentflowRef}
155-
apiBaseUrl={apiBaseUrl}
156-
token={token ?? undefined}
157-
initialFlow={translationFlow}
158-
showDefaultHeader={true}
159-
onFlowChange={(flow) => console.log('Flow changed:', flow.nodes.length, 'nodes')}
160-
/>
152+
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
153+
<div style={{ flex: 1 }}>
154+
<Agentflow
155+
ref={agentflowRef}
156+
apiBaseUrl={apiBaseUrl}
157+
token={token ?? undefined}
158+
initialFlow={translationFlow}
159+
showDefaultHeader={true}
160+
onFlowChange={(flow) => console.log('Flow changed:', flow.nodes.length, 'nodes')}
161+
/>
162+
</div>
161163
</div>
162164
)
163165
}

packages/agentflow/examples/src/demos/StatusIndicatorsExample.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export function StatusIndicatorsExample() {
190190
}
191191

192192
return (
193-
<div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
193+
<div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }}>
194194
{/* Controls */}
195195
<div
196196
style={{

0 commit comments

Comments
 (0)