|
| 1 | +import { beforeEach, describe, expect, it, vi } from 'vitest' |
| 2 | + |
| 3 | +// Mock hooks |
| 4 | +const mockCollaborativeUpdates = { |
| 5 | + collaborativeUpdateLoopType: vi.fn(), |
| 6 | + collaborativeUpdateParallelType: vi.fn(), |
| 7 | + collaborativeUpdateIterationCount: vi.fn(), |
| 8 | + collaborativeUpdateIterationCollection: vi.fn(), |
| 9 | +} |
| 10 | + |
| 11 | +const mockStoreData = { |
| 12 | + loops: {}, |
| 13 | + parallels: {}, |
| 14 | +} |
| 15 | + |
| 16 | +vi.mock('@/hooks/use-collaborative-workflow', () => ({ |
| 17 | + useCollaborativeWorkflow: () => mockCollaborativeUpdates, |
| 18 | +})) |
| 19 | + |
| 20 | +vi.mock('@/stores/workflows/workflow/store', () => ({ |
| 21 | + useWorkflowStore: () => mockStoreData, |
| 22 | +})) |
| 23 | + |
| 24 | +vi.mock('@/components/ui/badge', () => ({ |
| 25 | + Badge: ({ children, ...props }: any) => ( |
| 26 | + <div data-testid='badge' {...props}> |
| 27 | + {children} |
| 28 | + </div> |
| 29 | + ), |
| 30 | +})) |
| 31 | + |
| 32 | +vi.mock('@/components/ui/input', () => ({ |
| 33 | + Input: (props: any) => <input data-testid='input' {...props} />, |
| 34 | +})) |
| 35 | + |
| 36 | +vi.mock('@/components/ui/popover', () => ({ |
| 37 | + Popover: ({ children }: any) => <div data-testid='popover'>{children}</div>, |
| 38 | + PopoverContent: ({ children }: any) => <div data-testid='popover-content'>{children}</div>, |
| 39 | + PopoverTrigger: ({ children }: any) => <div data-testid='popover-trigger'>{children}</div>, |
| 40 | +})) |
| 41 | + |
| 42 | +vi.mock('@/components/ui/tag-dropdown', () => ({ |
| 43 | + checkTagTrigger: vi.fn(() => ({ show: false })), |
| 44 | + TagDropdown: ({ children }: any) => <div data-testid='tag-dropdown'>{children}</div>, |
| 45 | +})) |
| 46 | + |
| 47 | +vi.mock('react-simple-code-editor', () => ({ |
| 48 | + default: (props: any) => <textarea data-testid='code-editor' {...props} />, |
| 49 | +})) |
| 50 | + |
| 51 | +describe('IterationBadges', () => { |
| 52 | + const defaultProps = { |
| 53 | + nodeId: 'test-node-1', |
| 54 | + data: { |
| 55 | + width: 500, |
| 56 | + height: 300, |
| 57 | + isPreview: false, |
| 58 | + }, |
| 59 | + iterationType: 'loop' as const, |
| 60 | + } |
| 61 | + |
| 62 | + beforeEach(() => { |
| 63 | + vi.clearAllMocks() |
| 64 | + mockStoreData.loops = {} |
| 65 | + mockStoreData.parallels = {} |
| 66 | + }) |
| 67 | + |
| 68 | + describe('Component Interface', () => { |
| 69 | + it.concurrent('should accept required props', () => { |
| 70 | + expect(defaultProps.nodeId).toBeDefined() |
| 71 | + expect(defaultProps.data).toBeDefined() |
| 72 | + expect(defaultProps.iterationType).toBeDefined() |
| 73 | + }) |
| 74 | + |
| 75 | + it.concurrent('should handle loop iteration type prop', () => { |
| 76 | + const loopProps = { ...defaultProps, iterationType: 'loop' as const } |
| 77 | + expect(loopProps.iterationType).toBe('loop') |
| 78 | + }) |
| 79 | + |
| 80 | + it.concurrent('should handle parallel iteration type prop', () => { |
| 81 | + const parallelProps = { ...defaultProps, iterationType: 'parallel' as const } |
| 82 | + expect(parallelProps.iterationType).toBe('parallel') |
| 83 | + }) |
| 84 | + }) |
| 85 | + |
| 86 | + describe('Configuration System', () => { |
| 87 | + it.concurrent('should use correct config for loop type', () => { |
| 88 | + const CONFIG = { |
| 89 | + loop: { |
| 90 | + typeLabels: { for: 'For Loop', forEach: 'For Each' }, |
| 91 | + typeKey: 'loopType' as const, |
| 92 | + storeKey: 'loops' as const, |
| 93 | + maxIterations: 100, |
| 94 | + configKeys: { |
| 95 | + iterations: 'iterations' as const, |
| 96 | + items: 'forEachItems' as const, |
| 97 | + }, |
| 98 | + }, |
| 99 | + } |
| 100 | + |
| 101 | + expect(CONFIG.loop.typeLabels.for).toBe('For Loop') |
| 102 | + expect(CONFIG.loop.typeLabels.forEach).toBe('For Each') |
| 103 | + expect(CONFIG.loop.maxIterations).toBe(100) |
| 104 | + expect(CONFIG.loop.storeKey).toBe('loops') |
| 105 | + }) |
| 106 | + |
| 107 | + it.concurrent('should use correct config for parallel type', () => { |
| 108 | + const CONFIG = { |
| 109 | + parallel: { |
| 110 | + typeLabels: { count: 'Parallel Count', collection: 'Parallel Each' }, |
| 111 | + typeKey: 'parallelType' as const, |
| 112 | + storeKey: 'parallels' as const, |
| 113 | + maxIterations: 20, |
| 114 | + configKeys: { |
| 115 | + iterations: 'count' as const, |
| 116 | + items: 'distribution' as const, |
| 117 | + }, |
| 118 | + }, |
| 119 | + } |
| 120 | + |
| 121 | + expect(CONFIG.parallel.typeLabels.count).toBe('Parallel Count') |
| 122 | + expect(CONFIG.parallel.typeLabels.collection).toBe('Parallel Each') |
| 123 | + expect(CONFIG.parallel.maxIterations).toBe(20) |
| 124 | + expect(CONFIG.parallel.storeKey).toBe('parallels') |
| 125 | + }) |
| 126 | + }) |
| 127 | + |
| 128 | + describe('Type Determination Logic', () => { |
| 129 | + it.concurrent('should default to "for" for loop type', () => { |
| 130 | + type IterationType = 'loop' | 'parallel' |
| 131 | + const determineDefaultType = (iterationType: IterationType) => { |
| 132 | + return iterationType === 'loop' ? 'for' : 'count' |
| 133 | + } |
| 134 | + |
| 135 | + const currentType = determineDefaultType('loop') |
| 136 | + expect(currentType).toBe('for') |
| 137 | + }) |
| 138 | + |
| 139 | + it.concurrent('should default to "count" for parallel type', () => { |
| 140 | + type IterationType = 'loop' | 'parallel' |
| 141 | + const determineDefaultType = (iterationType: IterationType) => { |
| 142 | + return iterationType === 'loop' ? 'for' : 'count' |
| 143 | + } |
| 144 | + |
| 145 | + const currentType = determineDefaultType('parallel') |
| 146 | + expect(currentType).toBe('count') |
| 147 | + }) |
| 148 | + |
| 149 | + it.concurrent('should use explicit loopType when provided', () => { |
| 150 | + type IterationType = 'loop' | 'parallel' |
| 151 | + const determineType = (explicitType: string | undefined, iterationType: IterationType) => { |
| 152 | + return explicitType || (iterationType === 'loop' ? 'for' : 'count') |
| 153 | + } |
| 154 | + |
| 155 | + const currentType = determineType('forEach', 'loop') |
| 156 | + expect(currentType).toBe('forEach') |
| 157 | + }) |
| 158 | + |
| 159 | + it.concurrent('should use explicit parallelType when provided', () => { |
| 160 | + type IterationType = 'loop' | 'parallel' |
| 161 | + const determineType = (explicitType: string | undefined, iterationType: IterationType) => { |
| 162 | + return explicitType || (iterationType === 'loop' ? 'for' : 'count') |
| 163 | + } |
| 164 | + |
| 165 | + const currentType = determineType('collection', 'parallel') |
| 166 | + expect(currentType).toBe('collection') |
| 167 | + }) |
| 168 | + }) |
| 169 | + |
| 170 | + describe('Count Mode Detection', () => { |
| 171 | + it.concurrent('should be in count mode for loop + for combination', () => { |
| 172 | + type IterationType = 'loop' | 'parallel' |
| 173 | + type LoopType = 'for' | 'forEach' |
| 174 | + type ParallelType = 'count' | 'collection' |
| 175 | + |
| 176 | + const iterationType: IterationType = 'loop' |
| 177 | + const currentType: LoopType = 'for' |
| 178 | + const isCountMode = iterationType === 'loop' && currentType === 'for' |
| 179 | + |
| 180 | + expect(isCountMode).toBe(true) |
| 181 | + }) |
| 182 | + |
| 183 | + it.concurrent('should be in count mode for parallel + count combination', () => { |
| 184 | + type IterationType = 'loop' | 'parallel' |
| 185 | + type ParallelType = 'count' | 'collection' |
| 186 | + |
| 187 | + const iterationType: IterationType = 'parallel' |
| 188 | + const currentType: ParallelType = 'count' |
| 189 | + const isCountMode = iterationType === 'parallel' && currentType === 'count' |
| 190 | + |
| 191 | + expect(isCountMode).toBe(true) |
| 192 | + }) |
| 193 | + |
| 194 | + it.concurrent('should not be in count mode for loop + forEach combination', () => { |
| 195 | + type IterationType = 'loop' | 'parallel' |
| 196 | + |
| 197 | + const testCountMode = (iterationType: IterationType, currentType: string) => { |
| 198 | + return iterationType === 'loop' && currentType === 'for' |
| 199 | + } |
| 200 | + |
| 201 | + const isCountMode = testCountMode('loop', 'forEach') |
| 202 | + expect(isCountMode).toBe(false) |
| 203 | + }) |
| 204 | + |
| 205 | + it.concurrent('should not be in count mode for parallel + collection combination', () => { |
| 206 | + type IterationType = 'loop' | 'parallel' |
| 207 | + |
| 208 | + const testCountMode = (iterationType: IterationType, currentType: string) => { |
| 209 | + return iterationType === 'parallel' && currentType === 'count' |
| 210 | + } |
| 211 | + |
| 212 | + const isCountMode = testCountMode('parallel', 'collection') |
| 213 | + expect(isCountMode).toBe(false) |
| 214 | + }) |
| 215 | + }) |
| 216 | + |
| 217 | + describe('Configuration Values', () => { |
| 218 | + it.concurrent('should handle default iteration count', () => { |
| 219 | + const data = { count: undefined } |
| 220 | + const configIterations = data.count ?? 5 |
| 221 | + expect(configIterations).toBe(5) |
| 222 | + }) |
| 223 | + |
| 224 | + it.concurrent('should use provided iteration count', () => { |
| 225 | + const data = { count: 10 } |
| 226 | + const configIterations = data.count ?? 5 |
| 227 | + expect(configIterations).toBe(10) |
| 228 | + }) |
| 229 | + |
| 230 | + it.concurrent('should handle string collection', () => { |
| 231 | + const collection = '[1, 2, 3, 4, 5]' |
| 232 | + const collectionString = |
| 233 | + typeof collection === 'string' ? collection : JSON.stringify(collection) || '' |
| 234 | + expect(collectionString).toBe('[1, 2, 3, 4, 5]') |
| 235 | + }) |
| 236 | + |
| 237 | + it.concurrent('should handle object collection', () => { |
| 238 | + const collection = { items: [1, 2, 3] } |
| 239 | + const collectionString = |
| 240 | + typeof collection === 'string' ? collection : JSON.stringify(collection) || '' |
| 241 | + expect(collectionString).toBe('{"items":[1,2,3]}') |
| 242 | + }) |
| 243 | + |
| 244 | + it.concurrent('should handle array collection', () => { |
| 245 | + const collection = [1, 2, 3, 4, 5] |
| 246 | + const collectionString = |
| 247 | + typeof collection === 'string' ? collection : JSON.stringify(collection) || '' |
| 248 | + expect(collectionString).toBe('[1,2,3,4,5]') |
| 249 | + }) |
| 250 | + }) |
| 251 | + |
| 252 | + describe('Preview Mode Handling', () => { |
| 253 | + it.concurrent('should handle preview mode for loops', () => { |
| 254 | + const previewProps = { |
| 255 | + ...defaultProps, |
| 256 | + data: { ...defaultProps.data, isPreview: true }, |
| 257 | + iterationType: 'loop' as const, |
| 258 | + } |
| 259 | + |
| 260 | + expect(previewProps.data.isPreview).toBe(true) |
| 261 | + // In preview mode, collaborative functions shouldn't be called |
| 262 | + expect(mockCollaborativeUpdates.collaborativeUpdateLoopType).not.toHaveBeenCalled() |
| 263 | + }) |
| 264 | + |
| 265 | + it.concurrent('should handle preview mode for parallels', () => { |
| 266 | + const previewProps = { |
| 267 | + ...defaultProps, |
| 268 | + data: { ...defaultProps.data, isPreview: true }, |
| 269 | + iterationType: 'parallel' as const, |
| 270 | + } |
| 271 | + |
| 272 | + expect(previewProps.data.isPreview).toBe(true) |
| 273 | + // In preview mode, collaborative functions shouldn't be called |
| 274 | + expect(mockCollaborativeUpdates.collaborativeUpdateParallelType).not.toHaveBeenCalled() |
| 275 | + }) |
| 276 | + }) |
| 277 | + |
| 278 | + describe('Store Integration', () => { |
| 279 | + it.concurrent('should access loops store for loop iteration type', () => { |
| 280 | + const nodeId = 'loop-node-1' |
| 281 | + ;(mockStoreData.loops as any)[nodeId] = { iterations: 10 } |
| 282 | + |
| 283 | + const nodeConfig = (mockStoreData.loops as any)[nodeId] |
| 284 | + expect(nodeConfig).toBeDefined() |
| 285 | + expect(nodeConfig.iterations).toBe(10) |
| 286 | + }) |
| 287 | + |
| 288 | + it.concurrent('should access parallels store for parallel iteration type', () => { |
| 289 | + const nodeId = 'parallel-node-1' |
| 290 | + ;(mockStoreData.parallels as any)[nodeId] = { count: 5 } |
| 291 | + |
| 292 | + const nodeConfig = (mockStoreData.parallels as any)[nodeId] |
| 293 | + expect(nodeConfig).toBeDefined() |
| 294 | + expect(nodeConfig.count).toBe(5) |
| 295 | + }) |
| 296 | + |
| 297 | + it.concurrent('should handle missing node configuration gracefully', () => { |
| 298 | + const nodeId = 'missing-node' |
| 299 | + const nodeConfig = (mockStoreData.loops as any)[nodeId] |
| 300 | + expect(nodeConfig).toBeUndefined() |
| 301 | + }) |
| 302 | + }) |
| 303 | + |
| 304 | + describe('Max Iterations Limits', () => { |
| 305 | + it.concurrent('should enforce max iterations for loops (100)', () => { |
| 306 | + const maxIterations = 100 |
| 307 | + const testValue = 150 |
| 308 | + const clampedValue = Math.min(maxIterations, testValue) |
| 309 | + expect(clampedValue).toBe(100) |
| 310 | + }) |
| 311 | + |
| 312 | + it.concurrent('should enforce max iterations for parallels (20)', () => { |
| 313 | + const maxIterations = 20 |
| 314 | + const testValue = 50 |
| 315 | + const clampedValue = Math.min(maxIterations, testValue) |
| 316 | + expect(clampedValue).toBe(20) |
| 317 | + }) |
| 318 | + |
| 319 | + it.concurrent('should allow values within limits', () => { |
| 320 | + const loopMaxIterations = 100 |
| 321 | + const parallelMaxIterations = 20 |
| 322 | + |
| 323 | + expect(Math.min(loopMaxIterations, 50)).toBe(50) |
| 324 | + expect(Math.min(parallelMaxIterations, 10)).toBe(10) |
| 325 | + }) |
| 326 | + }) |
| 327 | + |
| 328 | + describe('Collaborative Update Functions', () => { |
| 329 | + it.concurrent('should have the correct collaborative functions available', () => { |
| 330 | + expect(mockCollaborativeUpdates.collaborativeUpdateLoopType).toBeDefined() |
| 331 | + expect(mockCollaborativeUpdates.collaborativeUpdateParallelType).toBeDefined() |
| 332 | + expect(mockCollaborativeUpdates.collaborativeUpdateIterationCount).toBeDefined() |
| 333 | + expect(mockCollaborativeUpdates.collaborativeUpdateIterationCollection).toBeDefined() |
| 334 | + }) |
| 335 | + |
| 336 | + it.concurrent('should call correct function for loop type updates', () => { |
| 337 | + const handleTypeChange = (newType: string, iterationType: string, nodeId: string) => { |
| 338 | + if (iterationType === 'loop') { |
| 339 | + mockCollaborativeUpdates.collaborativeUpdateLoopType(nodeId, newType) |
| 340 | + } else { |
| 341 | + mockCollaborativeUpdates.collaborativeUpdateParallelType(nodeId, newType) |
| 342 | + } |
| 343 | + } |
| 344 | + |
| 345 | + handleTypeChange('forEach', 'loop', 'test-node') |
| 346 | + expect(mockCollaborativeUpdates.collaborativeUpdateLoopType).toHaveBeenCalledWith( |
| 347 | + 'test-node', |
| 348 | + 'forEach' |
| 349 | + ) |
| 350 | + }) |
| 351 | + |
| 352 | + it.concurrent('should call correct function for parallel type updates', () => { |
| 353 | + const handleTypeChange = (newType: string, iterationType: string, nodeId: string) => { |
| 354 | + if (iterationType === 'loop') { |
| 355 | + mockCollaborativeUpdates.collaborativeUpdateLoopType(nodeId, newType) |
| 356 | + } else { |
| 357 | + mockCollaborativeUpdates.collaborativeUpdateParallelType(nodeId, newType) |
| 358 | + } |
| 359 | + } |
| 360 | + |
| 361 | + handleTypeChange('collection', 'parallel', 'test-node') |
| 362 | + expect(mockCollaborativeUpdates.collaborativeUpdateParallelType).toHaveBeenCalledWith( |
| 363 | + 'test-node', |
| 364 | + 'collection' |
| 365 | + ) |
| 366 | + }) |
| 367 | + }) |
| 368 | + |
| 369 | + describe('Input Sanitization', () => { |
| 370 | + it.concurrent('should sanitize numeric input by removing non-digits', () => { |
| 371 | + const testInput = 'abc123def456' |
| 372 | + const sanitized = testInput.replace(/[^0-9]/g, '') |
| 373 | + expect(sanitized).toBe('123456') |
| 374 | + }) |
| 375 | + |
| 376 | + it.concurrent('should handle empty input', () => { |
| 377 | + const testInput = '' |
| 378 | + const sanitized = testInput.replace(/[^0-9]/g, '') |
| 379 | + expect(sanitized).toBe('') |
| 380 | + }) |
| 381 | + |
| 382 | + it.concurrent('should preserve valid numeric input', () => { |
| 383 | + const testInput = '42' |
| 384 | + const sanitized = testInput.replace(/[^0-9]/g, '') |
| 385 | + expect(sanitized).toBe('42') |
| 386 | + }) |
| 387 | + }) |
| 388 | +}) |
0 commit comments