Skip to content

Commit 843b577

Browse files
committed
Test changes
1 parent 2ce579a commit 843b577

4 files changed

Lines changed: 463 additions & 10 deletions

File tree

packages/agentflow/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ module.exports = {
3737
coverageThreshold: {
3838
'./src/*.ts': { branches: 80, functions: 80, lines: 80, statements: 80 },
3939
'./src/Agentflow.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
40+
'./src/atoms/ArrayInput.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
4041
'./src/core/': { branches: 80, functions: 80, lines: 80, statements: 80 },
4142
'./src/features/canvas/components/ConnectionLine.tsx': { branches: 80, functions: 80, lines: 80, statements: 80 },
4243
// Only getMinimumNodeHeight() is tested; the component is Tier 3 UI with no business logic
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import { fireEvent, render, screen } from '@testing-library/react'
2+
3+
import type { InputParam, NodeData } from '@/core/types'
4+
5+
import { ArrayInput } from './ArrayInput'
6+
7+
// --- Mocks ---
8+
const mockOnDataChange = jest.fn()
9+
10+
jest.mock('./NodeInputHandler', () => ({
11+
NodeInputHandler: ({
12+
inputParam,
13+
onDataChange
14+
}: {
15+
inputParam: InputParam
16+
data: NodeData
17+
onDataChange: (args: { inputParam: InputParam; newValue: unknown }) => void
18+
}) => (
19+
<div data-testid={`input-handler-${inputParam.name}`}>
20+
<label>{inputParam.label}</label>
21+
<input data-testid={`input-${inputParam.name}`} onChange={(e) => onDataChange({ inputParam, newValue: e.target.value })} />
22+
</div>
23+
)
24+
}))
25+
26+
jest.mock('@tabler/icons-react', () => ({
27+
IconPlus: () => <span data-testid='icon-plus' />,
28+
IconTrash: () => <span data-testid='icon-trash' />
29+
}))
30+
31+
describe('ArrayInput', () => {
32+
const mockInputParam: InputParam = {
33+
id: 'test-array',
34+
name: 'testArray',
35+
label: 'Test Item',
36+
type: 'array',
37+
array: [
38+
{ id: 'field1', name: 'field1', label: 'Field 1', type: 'string', default: '' } as InputParam,
39+
{ id: 'field2', name: 'field2', label: 'Field 2', type: 'number', default: 0 } as InputParam
40+
]
41+
}
42+
43+
const mockNodeData: NodeData = {
44+
id: 'node-1',
45+
name: 'testNode',
46+
label: 'Test Node',
47+
inputValues: {}
48+
} as NodeData
49+
50+
beforeEach(() => {
51+
jest.clearAllMocks()
52+
})
53+
54+
// Test 1: Render existing items
55+
it('should render existing items correctly', () => {
56+
const dataWithItems: NodeData = {
57+
...mockNodeData,
58+
inputValues: {
59+
testArray: [
60+
{ field1: 'value1', field2: 10 },
61+
{ field1: 'value2', field2: 20 }
62+
]
63+
}
64+
} as NodeData
65+
66+
render(<ArrayInput inputParam={mockInputParam} data={dataWithItems} onDataChange={mockOnDataChange} />)
67+
68+
// Verify both items are rendered
69+
expect(screen.getByText('0')).toBeInTheDocument()
70+
expect(screen.getByText('1')).toBeInTheDocument()
71+
72+
// Verify field handlers are rendered for both items
73+
expect(screen.getAllByTestId('input-handler-field1')).toHaveLength(2)
74+
expect(screen.getAllByTestId('input-handler-field2')).toHaveLength(2)
75+
})
76+
77+
// Test 2: Render Add button
78+
it('should render Add button with correct label', () => {
79+
render(<ArrayInput inputParam={mockInputParam} data={mockNodeData} onDataChange={mockOnDataChange} />)
80+
81+
const addButton = screen.getByRole('button', { name: /Add Test Item/i })
82+
expect(addButton).toBeInTheDocument()
83+
expect(screen.getByTestId('icon-plus')).toBeInTheDocument()
84+
})
85+
86+
// Test 3: Add new item
87+
it('should add new item and call onDataChange with new array', () => {
88+
render(<ArrayInput inputParam={mockInputParam} data={mockNodeData} onDataChange={mockOnDataChange} />)
89+
90+
const addButton = screen.getByRole('button', { name: /Add Test Item/i })
91+
fireEvent.click(addButton)
92+
93+
// Verify onDataChange was called with new array containing default values
94+
expect(mockOnDataChange).toHaveBeenCalledWith({
95+
inputParam: mockInputParam,
96+
newValue: [{ field1: '', field2: 0 }]
97+
})
98+
})
99+
100+
// Test 4: Delete item
101+
it('should delete item and verify item removed from array', () => {
102+
const dataWithItems: NodeData = {
103+
...mockNodeData,
104+
inputValues: {
105+
testArray: [
106+
{ field1: 'value1', field2: 10 },
107+
{ field1: 'value2', field2: 20 }
108+
]
109+
}
110+
} as NodeData
111+
112+
render(<ArrayInput inputParam={mockInputParam} data={dataWithItems} onDataChange={mockOnDataChange} />)
113+
114+
// Get all delete buttons (IconTrash buttons)
115+
const deleteButtons = screen.getAllByTitle('Delete')
116+
117+
// Click the first delete button
118+
fireEvent.click(deleteButtons[0])
119+
120+
// Verify onDataChange was called with updated array (first item removed)
121+
expect(mockOnDataChange).toHaveBeenCalledWith({
122+
inputParam: mockInputParam,
123+
newValue: [{ field1: 'value2', field2: 20 }]
124+
})
125+
})
126+
127+
// Test 5: Handle field changes
128+
it('should handle nested field changes and update parent array', () => {
129+
const dataWithItems: NodeData = {
130+
...mockNodeData,
131+
inputValues: {
132+
testArray: [{ field1: 'initial', field2: 5 }]
133+
}
134+
} as NodeData
135+
136+
render(<ArrayInput inputParam={mockInputParam} data={dataWithItems} onDataChange={mockOnDataChange} />)
137+
138+
// Change field1 value
139+
const field1Input = screen.getByTestId('input-field1')
140+
fireEvent.change(field1Input, { target: { value: 'updated' } })
141+
142+
// Verify parent array was updated
143+
expect(mockOnDataChange).toHaveBeenCalledWith({
144+
inputParam: mockInputParam,
145+
newValue: [{ field1: 'updated', field2: 5 }]
146+
})
147+
})
148+
149+
// Test 6: Empty array initialization
150+
it('should render with empty array and only show Add button', () => {
151+
render(<ArrayInput inputParam={mockInputParam} data={mockNodeData} onDataChange={mockOnDataChange} />)
152+
153+
// Verify no items are rendered
154+
expect(screen.queryByText('0')).not.toBeInTheDocument()
155+
156+
// Verify Add button is present
157+
expect(screen.getByRole('button', { name: /Add Test Item/i })).toBeInTheDocument()
158+
})
159+
160+
// Test 7: Respect disabled prop
161+
it('should disable buttons when disabled prop is true', () => {
162+
const dataWithItems: NodeData = {
163+
...mockNodeData,
164+
inputValues: {
165+
testArray: [{ field1: 'value1', field2: 10 }]
166+
}
167+
} as NodeData
168+
169+
render(<ArrayInput inputParam={mockInputParam} data={dataWithItems} disabled={true} onDataChange={mockOnDataChange} />)
170+
171+
// Verify Add button is disabled
172+
const addButton = screen.getByRole('button', { name: /Add Test Item/i })
173+
expect(addButton).toBeDisabled()
174+
175+
// Verify Delete button is disabled
176+
const deleteButton = screen.getByTitle('Delete')
177+
expect(deleteButton).toBeDisabled()
178+
})
179+
180+
// Test 8: Filter hidden fields
181+
it('should not render fields with display set to false', () => {
182+
const inputParamWithHiddenField: InputParam = {
183+
...mockInputParam,
184+
array: [
185+
{ id: 'visible', name: 'visible', label: 'Visible Field', type: 'string', display: true } as InputParam,
186+
{ id: 'hidden', name: 'hidden', label: 'Hidden Field', type: 'string', display: false } as InputParam
187+
]
188+
}
189+
190+
const dataWithItems: NodeData = {
191+
...mockNodeData,
192+
inputValues: {
193+
testArray: [{ visible: 'test', hidden: 'should-not-show' }]
194+
}
195+
} as NodeData
196+
197+
render(<ArrayInput inputParam={inputParamWithHiddenField} data={dataWithItems} onDataChange={mockOnDataChange} />)
198+
199+
// Verify visible field is rendered
200+
expect(screen.getByTestId('input-handler-visible')).toBeInTheDocument()
201+
202+
// Verify hidden field is NOT rendered
203+
expect(screen.queryByTestId('input-handler-hidden')).not.toBeInTheDocument()
204+
})
205+
206+
// Test 9: Multiple items
207+
it('should render multiple items with correct indices', () => {
208+
const dataWithMultipleItems: NodeData = {
209+
...mockNodeData,
210+
inputValues: {
211+
testArray: [
212+
{ field1: 'item1', field2: 1 },
213+
{ field1: 'item2', field2: 2 },
214+
{ field1: 'item3', field2: 3 },
215+
{ field1: 'item4', field2: 4 }
216+
]
217+
}
218+
} as NodeData
219+
220+
render(<ArrayInput inputParam={mockInputParam} data={dataWithMultipleItems} onDataChange={mockOnDataChange} />)
221+
222+
// Verify all indices are shown
223+
expect(screen.getByText('0')).toBeInTheDocument()
224+
expect(screen.getByText('1')).toBeInTheDocument()
225+
expect(screen.getByText('2')).toBeInTheDocument()
226+
expect(screen.getByText('3')).toBeInTheDocument()
227+
228+
// Verify all field handlers are rendered (4 items * 2 fields each = 8 handlers)
229+
expect(screen.getAllByTestId('input-handler-field1')).toHaveLength(4)
230+
expect(screen.getAllByTestId('input-handler-field2')).toHaveLength(4)
231+
})
232+
233+
// Test 10: Default values
234+
it('should initialize new items with field default values', () => {
235+
const inputParamWithDefaults: InputParam = {
236+
id: 'test-array',
237+
name: 'testArray',
238+
label: 'Test Item',
239+
type: 'array',
240+
array: [
241+
{ id: 'name', name: 'name', label: 'Name', type: 'string', default: 'John Doe' } as InputParam,
242+
{ id: 'age', name: 'age', label: 'Age', type: 'number', default: 25 } as InputParam,
243+
{ id: 'active', name: 'active', label: 'Active', type: 'boolean', default: true } as InputParam
244+
]
245+
}
246+
247+
render(<ArrayInput inputParam={inputParamWithDefaults} data={mockNodeData} onDataChange={mockOnDataChange} />)
248+
249+
const addButton = screen.getByRole('button', { name: /Add Test Item/i })
250+
fireEvent.click(addButton)
251+
252+
// Verify new item initialized with correct default values
253+
expect(mockOnDataChange).toHaveBeenCalledWith({
254+
inputParam: inputParamWithDefaults,
255+
newValue: [{ name: 'John Doe', age: 25, active: true }]
256+
})
257+
})
258+
259+
// minItems constraint
260+
it('should respect minItems constraint and disable delete when minimum reached', () => {
261+
const dataWithItems: NodeData = {
262+
...mockNodeData,
263+
inputValues: {
264+
testArray: [
265+
{ field1: 'value1', field2: 10 },
266+
{ field1: 'value2', field2: 20 }
267+
]
268+
}
269+
} as NodeData
270+
271+
render(<ArrayInput inputParam={mockInputParam} data={dataWithItems} onDataChange={mockOnDataChange} minItems={2} />)
272+
273+
// Both delete buttons should be disabled when at minItems limit
274+
const deleteButtons = screen.getAllByTitle('Delete')
275+
expect(deleteButtons[0]).toBeDisabled()
276+
expect(deleteButtons[1]).toBeDisabled()
277+
})
278+
})

packages/agentflow/src/atoms/ArrayInput.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ export function ArrayInput({ inputParam, data, disabled = false, onDataChange, m
3333
setArrayItems(parsedArray)
3434

3535
// Initialize parameter definitions for each item
36-
// For now, simple replication of inputParam.array (no show/hide logic)
37-
const initialParams = parsedArray.map(() => inputParam.array?.map((field) => ({ ...field, display: true })) || [])
36+
const initialParams = parsedArray.map(() => inputParam.array?.map((field) => ({ ...field })) || [])
3837
setItemParameters(initialParams)
3938
}, [data.inputValues, inputParam.name, inputParam.array])
4039

0 commit comments

Comments
 (0)