Skip to content

Commit 653c616

Browse files
Release v1.45.0 (#1070)
## What's included in this release? ### Features 1. Hide FormSG question variables under "see more" 2. Allow users to toggle between using mock data or last test submission in formsg test step ### Chores 1. Skip s3 upload scanning in dev environment 2. remove the rest of material UI artifacts ### Fixes 1. Fix attachment variables not showing after rebase 2. Error when re-selecting excel file after selecting table and column ## Tests - [x] Check that Response variables now show question texts - [x] Check that Question variables are collapsed by default - [x] Check that attachment variables are back - [x] Check that no error appears when re-selecting excel file - [x] Check that users can toggle between mock data and last test submission --------- Co-authored-by: kevin <[email protected]>
2 parents 611637e + 5cb5502 commit 653c616

File tree

82 files changed

+1538
-2059
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+1538
-2059
lines changed

package-lock.json

Lines changed: 3 additions & 502 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,5 @@
106106
"tsconfig-paths": "^4.2.0",
107107
"type-fest": "4.10.3"
108108
},
109-
"version": "1.44.1"
109+
"version": "1.45.0"
110110
}

packages/backend/src/apps/formsg/__tests__/triggers/new-submission.test.ts

Lines changed: 224 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,31 @@ import {
22
IDataOutMetadata,
33
IDataOutMetadatum,
44
IExecutionStep,
5+
IGlobalVariable,
56
IJSONObject,
67
} from '@plumber/types'
78

8-
import { beforeEach, describe, expect, it } from 'vitest'
9+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
910

1011
import { ADDRESS_LABELS } from '../../common/constants'
1112
import trigger from '../../triggers/new-submission'
1213

14+
const pushTriggerItemMock = vi.fn()
15+
const getLastExecutionStepMock = vi.fn()
16+
17+
const mocks = vi.hoisted(() => ({
18+
getMockData: vi.fn(),
19+
getFormDetailsFromGlobalVariable: vi.fn(),
20+
}))
21+
22+
vi.mock('../../triggers/new-submission/get-mock-data', () => ({
23+
default: mocks.getMockData,
24+
}))
25+
26+
vi.mock('../../common/webhook-settings', () => ({
27+
getFormDetailsFromGlobalVariable: mocks.getFormDetailsFromGlobalVariable,
28+
}))
29+
1330
describe('new submission trigger', () => {
1431
let executionStep: IExecutionStep
1532

@@ -26,6 +43,11 @@ describe('new submission trigger', () => {
2643
attr: 'name',
2744
},
2845
},
46+
headerFieldId: {
47+
question: 'Section Header',
48+
fieldType: 'section',
49+
order: 2,
50+
},
2951
},
3052
verifiedSubmitterInfo: {
3153
uinFin: 'S1234567B',
@@ -37,6 +59,158 @@ describe('new submission trigger', () => {
3759
} as unknown as IExecutionStep
3860
})
3961

62+
describe('testRun', () => {
63+
const $ = {
64+
auth: { data: { formId: '123' } },
65+
user: { email: '[email protected]' },
66+
pushTriggerItem: pushTriggerItemMock,
67+
getLastExecutionStep: getLastExecutionStepMock,
68+
} as unknown as IGlobalVariable
69+
70+
const mockData = {
71+
formId: '123',
72+
responses: {
73+
mockTextFieldId: {
74+
question: 'What is your name?',
75+
answer: 'herp derp',
76+
},
77+
},
78+
}
79+
80+
const actualData = {
81+
formId: '123',
82+
submissionTime: '2025-06-17T14:25:14.195+08:00',
83+
responses: {
84+
textFieldId: {
85+
question: 'What is your age?',
86+
answer: 10,
87+
},
88+
},
89+
}
90+
91+
beforeEach(() => {
92+
mocks.getMockData.mockResolvedValue(mockData)
93+
mocks.getFormDetailsFromGlobalVariable.mockReturnValue({
94+
formId: '123',
95+
})
96+
})
97+
98+
afterEach(() => {
99+
vi.clearAllMocks()
100+
})
101+
102+
it('should use mock data if preferMock is true and there is no past submission', async () => {
103+
getLastExecutionStepMock.mockResolvedValue(null)
104+
await trigger.testRun($, { preferMock: true })
105+
expect(pushTriggerItemMock).toHaveBeenCalledWith({
106+
raw: mockData,
107+
meta: {
108+
internalId: '',
109+
isMock: true,
110+
lastTestSubmissionDate: undefined,
111+
},
112+
})
113+
})
114+
115+
it('should use mock data if preferMock is true even though there is past submission', async () => {
116+
getLastExecutionStepMock.mockResolvedValue({
117+
dataOut: actualData,
118+
createdAt: '2025-06-16 07:06:30.155+00',
119+
})
120+
await trigger.testRun($, { preferMock: true })
121+
expect(pushTriggerItemMock).toHaveBeenCalledWith({
122+
raw: mockData,
123+
meta: {
124+
internalId: '',
125+
isMock: true,
126+
lastTestSubmissionDate: new Date(
127+
'2025-06-17T14:25:14.195+08:00',
128+
).toISOString(),
129+
},
130+
})
131+
})
132+
133+
it('should use mock data if testRunMetadata is undefined and there is no past submission', async () => {
134+
getLastExecutionStepMock.mockResolvedValue(null)
135+
await trigger.testRun($, undefined)
136+
expect(pushTriggerItemMock).toHaveBeenCalledWith({
137+
raw: mockData,
138+
meta: {
139+
internalId: '',
140+
isMock: true,
141+
lastTestSubmissionDate: undefined,
142+
},
143+
})
144+
})
145+
146+
it('should use last test submission if testRunMetadata is undefined and there is past submission', async () => {
147+
getLastExecutionStepMock.mockResolvedValue({
148+
dataOut: actualData,
149+
createdAt: '2025-06-16 07:06:30.155+00',
150+
})
151+
await trigger.testRun($, undefined)
152+
expect(pushTriggerItemMock).toHaveBeenCalledWith({
153+
raw: actualData,
154+
meta: {
155+
internalId: '',
156+
isMock: false,
157+
lastTestSubmissionDate: new Date(
158+
'2025-06-17T14:25:14.195+08:00',
159+
).toISOString(),
160+
},
161+
})
162+
})
163+
164+
it('should use last test submission if preferMock is false and there is past submission', async () => {
165+
getLastExecutionStepMock.mockResolvedValue({
166+
dataOut: actualData,
167+
createdAt: '2025-06-16 07:06:30.155+00',
168+
})
169+
await trigger.testRun($, { preferMock: false })
170+
expect(pushTriggerItemMock).toHaveBeenCalledWith({
171+
raw: actualData,
172+
meta: {
173+
internalId: '',
174+
isMock: false,
175+
lastTestSubmissionDate: new Date(
176+
'2025-06-17T14:25:14.195+08:00',
177+
).toISOString(),
178+
},
179+
})
180+
})
181+
182+
it('should use mock data if preferMock is false and there is no past submission', async () => {
183+
getLastExecutionStepMock.mockResolvedValue(null)
184+
await trigger.testRun($, { preferMock: false })
185+
expect(pushTriggerItemMock).toHaveBeenCalledWith({
186+
raw: mockData,
187+
meta: {
188+
internalId: '',
189+
isMock: true,
190+
lastTestSubmissionDate: undefined,
191+
},
192+
})
193+
})
194+
195+
it('should store the createdAt time if submissionTime is not available', async () => {
196+
getLastExecutionStepMock.mockResolvedValue({
197+
dataOut: { ...actualData, submissionTime: undefined },
198+
createdAt: '2025-06-16 07:06:30.155+00',
199+
})
200+
await trigger.testRun($, { preferMock: false })
201+
202+
expect(pushTriggerItemMock).toHaveBeenCalledWith({
203+
raw: { ...actualData, submissionTime: undefined },
204+
meta: {
205+
internalId: '',
206+
isMock: false,
207+
lastTestSubmissionDate: new Date(
208+
'2025-06-16 07:06:30.155+00',
209+
).toISOString(),
210+
},
211+
})
212+
})
213+
})
40214
describe('dataOut metadata', () => {
41215
it('ensures that only question, answer and answerArray props are visible', async () => {
42216
const metadata = await trigger.getDataOutMetadata(executionStep)
@@ -64,10 +238,12 @@ describe('new submission trigger', () => {
64238
expect(metadata.fields.textFieldId.question.label).toEqual('Question 1')
65239
})
66240

67-
it('changes the answer label to "Response #n"', async () => {
241+
it('changes the answer label to "1. What is your name?"', async () => {
68242
const metadata = await trigger.getDataOutMetadata(executionStep)
69243

70-
expect(metadata.fields.textFieldId.answer.label).toEqual('Response 1')
244+
expect(metadata.fields.textFieldId.answer.label).toEqual(
245+
'1. What is your name?',
246+
)
71247
})
72248

73249
it('positions the answer after the question', async () => {
@@ -78,20 +254,29 @@ describe('new submission trigger', () => {
78254
)
79255
})
80256

81-
it('sets label and order to null if question number is undefined', async () => {
257+
it('should computes order for questions and answers even if order is not provided', async () => {
82258
const fields = executionStep.dataOut.fields as IJSONObject
83-
fields.textFieldId = {
84-
question: 'What is your name?',
85-
answer: 'herp derp',
86-
fieldType: 'textField',
259+
delete fields.textFieldId
260+
// generate a few fields
261+
for (let i = 0; i < 10; i++) {
262+
fields[`textFieldId${i + 1}`] = {
263+
question: `What is your name? ${i}`,
264+
answer: 'herp derp',
265+
fieldType: 'textField',
266+
order: i < 5 ? i + 1 : null,
267+
}
87268
}
88269

89270
const metadata = await trigger.getDataOutMetadata(executionStep)
90271

91-
expect(metadata.fields.textFieldId.question.order).toBeNull()
92-
expect(metadata.fields.textFieldId.answer.order).toBeNull()
93-
expect(metadata.fields.textFieldId.question.label).toBeNull()
94-
expect(metadata.fields.textFieldId.answer.label).toBeNull()
272+
expect(metadata.fields.textFieldId1.question.order).toBe(1)
273+
expect(metadata.fields.textFieldId1.answer.order).toBe(1.1)
274+
expect(metadata.fields.textFieldId2.question.order).toBe(2)
275+
expect(metadata.fields.textFieldId2.answer.order).toBe(2.1)
276+
expect(metadata.fields.textFieldId5.question.order).toBe(5)
277+
expect(metadata.fields.textFieldId5.answer.order).toBe(5.1)
278+
expect(metadata.fields.textFieldId6.question.order).toBe(6)
279+
expect(metadata.fields.textFieldId6.answer.order).toBe(6.1)
95280
})
96281

97282
it('sets a label for SingPass verified NRIC/FIN', async () => {
@@ -170,6 +355,20 @@ describe('new submission trigger', () => {
170355
expect(metadata.fields.fileFieldId.answer.label).toEqual('Attach a file.')
171356
})
172357

358+
it('collapses header fields', async () => {
359+
const metadata = await trigger.getDataOutMetadata(executionStep)
360+
expect(
361+
metadata.fields.headerFieldId.question.isCollapsedByDefault,
362+
).toEqual(true)
363+
})
364+
365+
it('collapses question variables', async () => {
366+
const metadata = await trigger.getDataOutMetadata(executionStep)
367+
expect(metadata.fields.textFieldId.question.isCollapsedByDefault).toEqual(
368+
true,
369+
)
370+
})
371+
173372
it('hides attachment questions', async () => {
174373
executionStep.dataOut.fields = {
175374
fileFieldId: {
@@ -277,7 +476,8 @@ describe('new submission trigger for answer array fields', () => {
277476
answerArray: ['lunch', 'dinner'],
278477
},
279478
textFieldId2: {
280-
question: 'What are your hobbies? When do you do them?',
479+
question:
480+
'What are your hobbies? When do you do them? (activity, time)',
281481
fieldType: 'table',
282482
order: 2,
283483
answerArray: [
@@ -327,7 +527,7 @@ describe('new submission trigger for answer array fields', () => {
327527
// type will be array instead of text!
328528
expect(array).toEqual({
329529
type: 'array',
330-
label: 'Response 1',
530+
label: '1. Have you had your meals?',
331531
order: 1.1,
332532
})
333533
})
@@ -345,8 +545,10 @@ describe('new submission trigger for answer array fields', () => {
345545
for (let i = 0; i < array.length; i++) {
346546
const nestedArray = array[i]
347547
for (let j = 0; j < array.length; j++) {
348-
expect(nestedArray[j].label).toEqual(
349-
`Response 2, Row ${i + 1} Column ${j + 1}`,
548+
expect(nestedArray[j].label).toBe(
549+
`2. Row ${i + 1} ${
550+
j === 0 ? 'activity' : 'time'
551+
} - What are your hobbies? When do you do them?`,
350552
)
351553
}
352554
}
@@ -370,7 +572,9 @@ describe('new submission trigger for answer array fields', () => {
370572

371573
expect(addressMetadata).toHaveLength(5)
372574
ADDRESS_LABELS.forEach((label, index) => {
373-
expect(addressMetadata[index].label).toEqual(`Response 4, ${label}`)
575+
expect(addressMetadata[index].label).toEqual(
576+
`4.${index + 1}. ${label} - What is your address?`,
577+
)
374578
})
375579
})
376580

@@ -379,7 +583,9 @@ describe('new submission trigger for answer array fields', () => {
379583
const addressMetadata = metadata.fields.addressFieldPartial
380584
.answerArray as IDataOutMetadatum[]
381585
ADDRESS_LABELS.forEach((label, index) => {
382-
expect(addressMetadata[index].label).toEqual(`Response 5, ${label}`)
586+
expect(addressMetadata[index].label).toEqual(
587+
`5.${index + 1}. ${label} - What is your address?`,
588+
)
383589
})
384590
})
385591
})

packages/backend/src/apps/formsg/auth/decrypt-form-response.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ export async function decryptFormResponse(
135135
}
136136
}
137137

138+
// delete metadata from fields
139+
delete rest['isHeader']
140+
138141
if (rest.fieldType === 'nric' && !!rest.answer) {
139142
const filteredAnswer = filterNric($, rest.answer)
140143
if (!filteredAnswer) {

0 commit comments

Comments
 (0)