Skip to content

Commit 1d9d594

Browse files
committed
chore: add tests, address bugbot comments
1 parent e80d806 commit 1d9d594

File tree

4 files changed

+351
-12
lines changed

4 files changed

+351
-12
lines changed
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
import { IGlobalVariable } from '@plumber/types'
2+
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4+
5+
import {
6+
isCheckboxItems,
7+
processItems,
8+
} from '@/apps/toolbox/common/get-for-each-variables'
9+
import StepError from '@/errors/step'
10+
11+
import action from '../../../actions/for-each'
12+
import {
13+
FOR_EACH_INPUT_SOURCE,
14+
FOR_EACH_ITERATION_KEY,
15+
} from '../../../common/constants'
16+
17+
const mocks = vi.hoisted(() => ({
18+
setActionItem: vi.fn(),
19+
}))
20+
21+
vi.mock('@/apps/toolbox/common/get-for-each-variables', () => ({
22+
isCheckboxItems: vi.fn(),
23+
processItems: vi.fn(),
24+
}))
25+
26+
const mockedIsCheckboxItems = vi.mocked(isCheckboxItems)
27+
const mockedProcessItems = vi.mocked(processItems)
28+
29+
const MOCK_FLOW = [
30+
{
31+
id: 'trigger',
32+
appKey: 'formsg',
33+
key: 'newSubmission',
34+
position: 1,
35+
},
36+
{
37+
id: 'test-step',
38+
appKey: 'tiles',
39+
key: 'findMultipleRows',
40+
position: 2,
41+
},
42+
{
43+
id: 'for-each',
44+
appKey: 'toolbox',
45+
key: action.key,
46+
position: 3,
47+
},
48+
{
49+
id: 'for-each-action',
50+
appKey: 'postman',
51+
key: 'sendTransactionalEmail',
52+
position: 4,
53+
},
54+
]
55+
56+
describe('For each action', () => {
57+
let $: IGlobalVariable
58+
59+
beforeEach(() => {
60+
vi.clearAllMocks()
61+
62+
$ = {
63+
flow: {
64+
id: 'fake-pipe',
65+
steps: MOCK_FLOW,
66+
},
67+
step: {
68+
id: 'for-each',
69+
appKey: 'toolbox',
70+
key: action.key,
71+
position: 3,
72+
parameters: {},
73+
},
74+
app: {
75+
name: 'Toolbox',
76+
},
77+
setActionItem: mocks.setActionItem,
78+
} as unknown as IGlobalVariable
79+
})
80+
81+
afterEach(() => {
82+
vi.restoreAllMocks()
83+
})
84+
85+
describe('checkbox input', () => {
86+
it('should handle valid checkbox input', async () => {
87+
const checkboxItems = ['item1', 'item2', 'item3']
88+
$.step.parameters.items = 'item1,item2,item3'
89+
90+
mockedIsCheckboxItems.mockReturnValue(true)
91+
92+
const result = await action.run($)
93+
94+
expect(mocks.setActionItem).toHaveBeenCalledWith({
95+
raw: {
96+
iterations: 3,
97+
items: checkboxItems,
98+
inputSource: FOR_EACH_INPUT_SOURCE.CHECKBOX,
99+
item: `items.${FOR_EACH_ITERATION_KEY}`,
100+
},
101+
})
102+
103+
expect(result).toEqual({
104+
nextStep: {
105+
command: 'start-for-each',
106+
stepId: 'for-each',
107+
},
108+
})
109+
})
110+
111+
it('should handle checkbox input with whitespace', async () => {
112+
const checkboxItems = ['item1', 'item2', 'item3']
113+
$.step.parameters.items = ' item1,item2,item3 '
114+
115+
mockedIsCheckboxItems.mockReturnValue(true)
116+
117+
await action.run($)
118+
119+
expect(mocks.setActionItem).toHaveBeenCalledWith({
120+
raw: {
121+
iterations: 3,
122+
items: checkboxItems,
123+
inputSource: FOR_EACH_INPUT_SOURCE.CHECKBOX,
124+
item: `items.${FOR_EACH_ITERATION_KEY}`,
125+
},
126+
})
127+
})
128+
129+
it('should handle single checkbox item', async () => {
130+
const checkboxItems = ['single-item']
131+
$.step.parameters.items = 'single-item'
132+
133+
mockedIsCheckboxItems.mockReturnValue(true)
134+
135+
await action.run($)
136+
137+
expect(mocks.setActionItem).toHaveBeenCalledWith({
138+
raw: {
139+
iterations: 1,
140+
items: checkboxItems,
141+
inputSource: FOR_EACH_INPUT_SOURCE.CHECKBOX,
142+
item: `items.${FOR_EACH_ITERATION_KEY}`,
143+
},
144+
})
145+
})
146+
147+
it('should throw error when checkbox items validation fails', async () => {
148+
$.step.parameters.items = ''
149+
150+
await expect(action.run($)).rejects.toThrow(StepError)
151+
await expect(action.run($)).rejects.toThrow('Invalid input list')
152+
})
153+
})
154+
155+
describe('table input', () => {
156+
const validTableData = {
157+
rows: [
158+
{
159+
data: {
160+
col1: 'value1',
161+
col2: 'value2',
162+
},
163+
rowId: 'row-1',
164+
},
165+
{
166+
data: {
167+
col1: 'value3',
168+
col2: 'value4',
169+
},
170+
rowId: 'row-2',
171+
},
172+
],
173+
columns: [
174+
{
175+
id: 'col1',
176+
name: 'Column 1',
177+
value: 'col1-value',
178+
},
179+
{
180+
id: 'col2',
181+
name: 'Column 2',
182+
value: 'col2-value',
183+
},
184+
],
185+
}
186+
187+
it('should handle valid table input (Tiles)', async () => {
188+
$.step.parameters.items = JSON.stringify(validTableData)
189+
190+
const processedResult = {
191+
processedItems: {
192+
rows: validTableData.rows,
193+
columns: validTableData.columns,
194+
},
195+
iterations: 2,
196+
inputSource: FOR_EACH_INPUT_SOURCE.TILES,
197+
}
198+
199+
mockedProcessItems.mockReturnValue(processedResult)
200+
201+
const result = await action.run($)
202+
203+
expect(mockedProcessItems).toHaveBeenCalledWith(validTableData)
204+
expect(mocks.setActionItem).toHaveBeenCalledWith({
205+
raw: {
206+
iterations: 2,
207+
items: processedResult.processedItems,
208+
inputSource: FOR_EACH_INPUT_SOURCE.TILES,
209+
},
210+
})
211+
expect(result).toEqual({
212+
nextStep: {
213+
command: 'start-for-each',
214+
stepId: 'for-each',
215+
},
216+
})
217+
})
218+
219+
it('should handle valid table input (M365 Excel)', async () => {
220+
const excelData = {
221+
...validTableData,
222+
rows: validTableData.rows.map((row) => ({ data: row.data })), // No rowId for Excel
223+
}
224+
225+
$.step.parameters.items = JSON.stringify(excelData)
226+
227+
const processedResult = {
228+
processedItems: {
229+
rows: excelData.rows,
230+
columns: validTableData.columns,
231+
},
232+
iterations: 2,
233+
inputSource: FOR_EACH_INPUT_SOURCE.M365_EXCEL,
234+
}
235+
236+
mockedProcessItems.mockReturnValue(processedResult)
237+
238+
await action.run($)
239+
240+
expect(mockedProcessItems).toHaveBeenCalledWith(excelData)
241+
expect(mocks.setActionItem).toHaveBeenCalledWith({
242+
raw: {
243+
iterations: 2,
244+
items: processedResult.processedItems,
245+
inputSource: FOR_EACH_INPUT_SOURCE.M365_EXCEL,
246+
},
247+
})
248+
})
249+
250+
it('should handle table input with empty rows', async () => {
251+
const emptyTableData = {
252+
rows: [] as any[],
253+
columns: validTableData.columns,
254+
}
255+
256+
$.step.parameters.items = JSON.stringify(emptyTableData)
257+
258+
const processedResult = {
259+
processedItems: {
260+
rows: [] as any[],
261+
columns: validTableData.columns,
262+
},
263+
iterations: 0,
264+
inputSource: null as any,
265+
}
266+
267+
mockedProcessItems.mockReturnValue(processedResult)
268+
269+
await action.run($)
270+
271+
expect(mocks.setActionItem).toHaveBeenCalledWith({
272+
raw: {
273+
iterations: 0,
274+
items: processedResult.processedItems,
275+
inputSource: null,
276+
},
277+
})
278+
})
279+
})
280+
281+
describe('error handling', () => {
282+
it('should throw StepError for empty input', async () => {
283+
$.step.parameters.items = ''
284+
285+
await expect(action.run($)).rejects.toThrow(StepError)
286+
await expect(action.run($)).rejects.toThrow('Invalid input list')
287+
})
288+
289+
it('should throw StepError for whitespace-only input', async () => {
290+
$.step.parameters.items = ' '
291+
292+
await expect(action.run($)).rejects.toThrow(StepError)
293+
await expect(action.run($)).rejects.toThrow('Invalid input list')
294+
})
295+
296+
it('should throw StepError for invalid JSON', async () => {
297+
$.step.parameters.items = '{ invalid json'
298+
299+
await expect(action.run($)).rejects.toThrow(StepError)
300+
await expect(action.run($)).rejects.toThrow('Invalid input list')
301+
})
302+
303+
it('should throw StepError for JSON missing required fields', async () => {
304+
$.step.parameters.items = JSON.stringify({
305+
invalidField: 'value',
306+
})
307+
308+
await expect(action.run($)).rejects.toThrow(StepError)
309+
await expect(action.run($)).rejects.toThrow('Invalid input list')
310+
})
311+
})
312+
})

packages/backend/src/helpers/__tests__/compute-for-each-parameters.test.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { IExecutionStep, IJSONObject } from '@plumber/types'
1+
import { IExecutionStep } from '@plumber/types'
22

33
import { randomUUID } from 'crypto'
44
import { describe, expect, it } from 'vitest'
55

66
import {
7+
FOR_EACH_ITERATION_KEY,
78
TOOLBOX_ACTIONS,
89
TOOLBOX_APP_KEY,
910
} from '@/apps/toolbox/common/constants'
@@ -480,13 +481,10 @@ describe('computeForEachParameters', () => {
480481
})
481482

482483
it('should handle missing forEachContext gracefully', () => {
483-
const data: IJSONObject = {
484-
stringProp: 'test string',
485-
}
486-
const keyPath = 'stringProp'
484+
const keyPath = `items.columns.${FOR_EACH_ITERATION_KEY}.value`
487485
const executionStep = mockExecutionStepsCheckbox[2]
488486
const result = computeForEachParameters({
489-
data,
487+
data: executionStep.dataOut,
490488
keyPath,
491489
executionSteps: mockExecutionStepsCheckbox,
492490
executionStep,
@@ -496,4 +494,23 @@ describe('computeForEachParameters', () => {
496494

497495
expect(result).toBe(keyPath) // returns keyPath when context is missing
498496
})
497+
498+
it('should handle undefined metadata.iteration gracefully', () => {
499+
const keyPath = `items.columns.${FOR_EACH_ITERATION_KEY}.value`
500+
const executionStep = mockExecutionStepsTable[1] // for-each step
501+
502+
const result = computeForEachParameters({
503+
data: executionStep.dataOut,
504+
keyPath,
505+
executionSteps: mockExecutionStepsTable,
506+
executionStep,
507+
stepId: randomForEachStepId,
508+
forEachContext: {
509+
...baseForEachContext,
510+
executionStepMetadata: {},
511+
},
512+
})
513+
514+
expect(result).toBe('')
515+
})
499516
})

packages/backend/src/helpers/compute-for-each-parameters.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,21 @@ export function computeForEachParameters({
9393
let forEachKeyPath = get(data, keyPath)
9494
const currentStepIndex = stepPositions?.[executionStep?.stepId] || -1
9595

96-
// handling of checkboxes which is just a flat array
97-
forEachKeyPath = String(forEachKeyPath).replace(
98-
FOR_EACH_ITERATION_KEY,
99-
`${testRun ? 0 : metadata?.iteration - 1}`,
100-
)
96+
if (testRun) {
97+
forEachKeyPath = String(forEachKeyPath).replace(FOR_EACH_ITERATION_KEY, '0')
98+
} else if (metadata?.iteration) {
99+
forEachKeyPath = String(forEachKeyPath).replace(
100+
FOR_EACH_ITERATION_KEY,
101+
`${metadata.iteration - 1}`,
102+
)
103+
}
104+
105+
if (testRun || metadata?.iteration) {
106+
forEachKeyPath = String(forEachKeyPath).replace(
107+
FOR_EACH_ITERATION_KEY,
108+
`${testRun ? 0 : metadata.iteration - 1}`,
109+
)
110+
}
101111

102112
if (
103113
executionStep.appKey === TOOLBOX_APP_KEY &&

0 commit comments

Comments
 (0)