@defra/forms-engine-plugin 4.0.6 → 4.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.public/stylesheets/application.min.css +1 -1
- package/.public/stylesheets/application.min.css.map +1 -1
- package/.server/client/stylesheets/_location-input.scss +60 -0
- package/.server/client/stylesheets/application.scss +1 -0
- package/.server/client/stylesheets/shared.scss +1 -0
- package/.server/server/forms/components.json +7 -0
- package/.server/server/forms/register-as-a-unicorn-breeder.yaml +40 -1
- package/.server/server/plugins/engine/components/ComponentBase.d.ts +2 -2
- package/.server/server/plugins/engine/components/ComponentBase.js.map +1 -1
- package/.server/server/plugins/engine/components/ComponentCollection.js.map +1 -1
- package/.server/server/plugins/engine/components/DeclarationField.d.ts +81 -0
- package/.server/server/plugins/engine/components/DeclarationField.js +123 -0
- package/.server/server/plugins/engine/components/DeclarationField.js.map +1 -0
- package/.server/server/plugins/engine/components/EastingNorthingField.d.ts +121 -0
- package/.server/server/plugins/engine/components/EastingNorthingField.js +166 -0
- package/.server/server/plugins/engine/components/EastingNorthingField.js.map +1 -0
- package/.server/server/plugins/engine/components/LatLongField.d.ts +121 -0
- package/.server/server/plugins/engine/components/LatLongField.js +164 -0
- package/.server/server/plugins/engine/components/LatLongField.js.map +1 -0
- package/.server/server/plugins/engine/components/LocationFieldBase.d.ts +134 -0
- package/.server/server/plugins/engine/components/LocationFieldBase.js +85 -0
- package/.server/server/plugins/engine/components/LocationFieldBase.js.map +1 -0
- package/.server/server/plugins/engine/components/LocationFieldHelpers.d.ts +108 -0
- package/.server/server/plugins/engine/components/LocationFieldHelpers.js +96 -0
- package/.server/server/plugins/engine/components/LocationFieldHelpers.js.map +1 -0
- package/.server/server/plugins/engine/components/NationalGridFieldNumberField.d.ts +19 -0
- package/.server/server/plugins/engine/components/NationalGridFieldNumberField.js +40 -0
- package/.server/server/plugins/engine/components/NationalGridFieldNumberField.js.map +1 -0
- package/.server/server/plugins/engine/components/OsGridRefField.d.ts +19 -0
- package/.server/server/plugins/engine/components/OsGridRefField.js +56 -0
- package/.server/server/plugins/engine/components/OsGridRefField.js.map +1 -0
- package/.server/server/plugins/engine/components/helpers/components.d.ts +3 -4
- package/.server/server/plugins/engine/components/helpers/components.js +24 -29
- package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
- package/.server/server/plugins/engine/components/index.d.ts +5 -0
- package/.server/server/plugins/engine/components/index.js +5 -0
- package/.server/server/plugins/engine/components/index.js.map +1 -1
- package/.server/server/plugins/engine/components/markdownParser.d.ts +2 -0
- package/.server/server/plugins/engine/components/markdownParser.js +28 -0
- package/.server/server/plugins/engine/components/markdownParser.js.map +1 -0
- package/.server/server/plugins/engine/components/types.d.ts +10 -0
- package/.server/server/plugins/engine/components/types.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/helpers/pages.js +7 -0
- package/.server/server/plugins/engine/pageControllers/helpers/pages.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/validationOptions.js +1 -0
- package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
- package/.server/server/plugins/engine/types/index.d.ts +1 -1
- package/.server/server/plugins/engine/types/index.js.map +1 -1
- package/.server/server/plugins/engine/types.d.ts +2 -2
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/plugins/engine/views/components/_location-field-base.html +53 -0
- package/.server/server/plugins/engine/views/components/declarationfield.html +14 -0
- package/.server/server/plugins/engine/views/components/eastingnorthingfield.html +5 -0
- package/.server/server/plugins/engine/views/components/latlongfield.html +5 -0
- package/.server/server/plugins/engine/views/components/nationalgridfieldnumberfield.html +13 -0
- package/.server/server/plugins/engine/views/components/osgridreffield.html +13 -0
- package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
- package/.server/server/plugins/nunjucks/filters/index.d.ts +1 -0
- package/.server/server/plugins/nunjucks/filters/index.js +1 -0
- package/.server/server/plugins/nunjucks/filters/index.js.map +1 -1
- package/.server/server/plugins/nunjucks/filters/merge.d.ts +7 -0
- package/.server/server/plugins/nunjucks/filters/merge.js +16 -0
- package/.server/server/plugins/nunjucks/filters/merge.js.map +1 -0
- package/.server/server/plugins/nunjucks/filters/merge.test.js +19 -0
- package/.server/server/plugins/nunjucks/filters/merge.test.js.map +1 -0
- package/package.json +3 -3
- package/src/client/stylesheets/_location-input.scss +60 -0
- package/src/client/stylesheets/application.scss +1 -0
- package/src/client/stylesheets/shared.scss +1 -0
- package/src/server/forms/components.json +7 -0
- package/src/server/forms/page-events.yaml +1 -1
- package/src/server/forms/register-as-a-unicorn-breeder.yaml +40 -1
- package/src/server/index.test.ts +1 -0
- package/src/server/plugins/engine/components/ComponentBase.ts +2 -1
- package/src/server/plugins/engine/components/ComponentCollection.ts +1 -0
- package/src/server/plugins/engine/components/DeclarationField.test.ts +426 -0
- package/src/server/plugins/engine/components/DeclarationField.ts +167 -0
- package/src/server/plugins/engine/components/EastingNorthingField.test.ts +665 -0
- package/src/server/plugins/engine/components/EastingNorthingField.ts +224 -0
- package/src/server/plugins/engine/components/LatLongField.test.ts +700 -0
- package/src/server/plugins/engine/components/LatLongField.ts +213 -0
- package/src/server/plugins/engine/components/LocationFieldBase.test.ts +253 -0
- package/src/server/plugins/engine/components/LocationFieldBase.ts +152 -0
- package/src/server/plugins/engine/components/LocationFieldHelpers.test.ts +338 -0
- package/src/server/plugins/engine/components/LocationFieldHelpers.ts +123 -0
- package/src/server/plugins/engine/components/NationalGridFieldNumberField.test.ts +438 -0
- package/src/server/plugins/engine/components/NationalGridFieldNumberField.ts +52 -0
- package/src/server/plugins/engine/components/OsGridRefField.test.ts +469 -0
- package/src/server/plugins/engine/components/OsGridRefField.ts +71 -0
- package/src/server/plugins/engine/components/helpers/components.test.ts +270 -0
- package/src/server/plugins/engine/components/helpers/components.ts +44 -47
- package/src/server/plugins/engine/components/helpers/helpers.test.ts +71 -1
- package/src/server/plugins/engine/components/index.ts +5 -0
- package/src/server/plugins/engine/components/markdownParser.ts +40 -0
- package/src/server/plugins/engine/components/types.ts +14 -0
- package/src/server/plugins/engine/models/SummaryViewModel.test.ts +76 -3
- package/src/server/plugins/engine/models/SummaryViewModel.ts +5 -1
- package/src/server/plugins/engine/outputFormatters/adapter/v1.location.test.ts +356 -0
- package/src/server/plugins/engine/pageControllers/helpers/helpers.test.ts +4 -0
- package/src/server/plugins/engine/pageControllers/helpers/pages.ts +8 -0
- package/src/server/plugins/engine/pageControllers/validationOptions.ts +4 -0
- package/src/server/plugins/engine/types/index.ts +2 -0
- package/src/server/plugins/engine/types.ts +4 -0
- package/src/server/plugins/engine/views/components/_location-field-base.html +53 -0
- package/src/server/plugins/engine/views/components/declarationfield.html +14 -0
- package/src/server/plugins/engine/views/components/eastingnorthingfield.html +5 -0
- package/src/server/plugins/engine/views/components/latlongfield.html +5 -0
- package/src/server/plugins/engine/views/components/nationalgridfieldnumberfield.html +13 -0
- package/src/server/plugins/engine/views/components/osgridreffield.html +13 -0
- package/src/server/plugins/nunjucks/filters/index.js +1 -0
- package/src/server/plugins/nunjucks/filters/merge.js +16 -0
- package/src/server/plugins/nunjucks/filters/merge.test.js +15 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ComponentType,
|
|
3
|
+
type DeclarationFieldComponent
|
|
4
|
+
} from '@defra/forms-model'
|
|
5
|
+
|
|
6
|
+
import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
|
|
7
|
+
import { DeclarationField } from '~/src/server/plugins/engine/components/DeclarationField.js'
|
|
8
|
+
import {
|
|
9
|
+
getAnswer,
|
|
10
|
+
type Field
|
|
11
|
+
} from '~/src/server/plugins/engine/components/helpers/components.js'
|
|
12
|
+
import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
13
|
+
import definition from '~/test/form/definitions/blank.js'
|
|
14
|
+
import { getFormData, getFormState } from '~/test/helpers/component-helpers.js'
|
|
15
|
+
|
|
16
|
+
describe('DeclarationField', () => {
|
|
17
|
+
let model: FormModel
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
model = new FormModel(definition, {
|
|
21
|
+
basePath: 'test'
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('Defaults', () => {
|
|
26
|
+
let def: DeclarationFieldComponent
|
|
27
|
+
let collection: ComponentCollection
|
|
28
|
+
let field: Field
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
def = {
|
|
32
|
+
title: 'Example Declaration Component',
|
|
33
|
+
name: 'myComponent',
|
|
34
|
+
content: 'Lorem ipsum dolar sit amet',
|
|
35
|
+
shortDescription: 'Terms and conditions',
|
|
36
|
+
type: ComponentType.DeclarationField,
|
|
37
|
+
options: {}
|
|
38
|
+
} satisfies DeclarationFieldComponent
|
|
39
|
+
|
|
40
|
+
collection = new ComponentCollection([def], { model })
|
|
41
|
+
field = collection.fields[0]
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('Schema', () => {
|
|
45
|
+
it('uses component title as label as default', () => {
|
|
46
|
+
const { formSchema } = collection
|
|
47
|
+
const { keys } = formSchema.describe()
|
|
48
|
+
|
|
49
|
+
expect(keys).toHaveProperty(
|
|
50
|
+
'myComponent',
|
|
51
|
+
expect.objectContaining({
|
|
52
|
+
flags: expect.objectContaining({
|
|
53
|
+
label: 'Terms and conditions'
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('uses component name as keys', () => {
|
|
60
|
+
const { formSchema } = collection
|
|
61
|
+
const { keys } = formSchema.describe()
|
|
62
|
+
|
|
63
|
+
expect(field.keys).toEqual(['myComponent'])
|
|
64
|
+
expect(field.collection).toBeUndefined()
|
|
65
|
+
|
|
66
|
+
for (const key of field.keys) {
|
|
67
|
+
expect(keys).toHaveProperty(key)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('is required by default', () => {
|
|
72
|
+
const { formSchema } = collection
|
|
73
|
+
const { keys } = formSchema.describe()
|
|
74
|
+
|
|
75
|
+
expect(keys).toHaveProperty(
|
|
76
|
+
'myComponent',
|
|
77
|
+
expect.objectContaining({
|
|
78
|
+
items: expect.arrayContaining([
|
|
79
|
+
expect.objectContaining({
|
|
80
|
+
allow: ['true'],
|
|
81
|
+
flags: expect.objectContaining({
|
|
82
|
+
presence: 'required'
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
])
|
|
86
|
+
})
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('may have unchecked value in addition to true', () => {
|
|
91
|
+
const { formSchema } = collection
|
|
92
|
+
const { keys } = formSchema.describe()
|
|
93
|
+
|
|
94
|
+
expect(keys).toHaveProperty(
|
|
95
|
+
'myComponent',
|
|
96
|
+
expect.objectContaining({
|
|
97
|
+
items: expect.arrayContaining([
|
|
98
|
+
expect.objectContaining({
|
|
99
|
+
allow: ['unchecked'],
|
|
100
|
+
flags: expect.objectContaining({
|
|
101
|
+
result: 'strip'
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
])
|
|
105
|
+
})
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
expect(keys).toHaveProperty(
|
|
109
|
+
'myComponent',
|
|
110
|
+
expect.objectContaining({
|
|
111
|
+
items: expect.arrayContaining([
|
|
112
|
+
expect.objectContaining({
|
|
113
|
+
allow: ['unchecked']
|
|
114
|
+
})
|
|
115
|
+
])
|
|
116
|
+
})
|
|
117
|
+
)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('is optional when configured', () => {
|
|
121
|
+
const collectionOptional = new ComponentCollection(
|
|
122
|
+
[{ ...def, options: { required: false } }],
|
|
123
|
+
{ model }
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const { formSchema } = collectionOptional
|
|
127
|
+
const { keys } = formSchema.describe()
|
|
128
|
+
|
|
129
|
+
expect(keys).toHaveProperty(
|
|
130
|
+
'myComponent',
|
|
131
|
+
expect.objectContaining({
|
|
132
|
+
items: expect.arrayContaining([
|
|
133
|
+
expect.objectContaining({
|
|
134
|
+
allow: ['true'],
|
|
135
|
+
flags: expect.not.objectContaining({
|
|
136
|
+
presence: 'required'
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
])
|
|
140
|
+
})
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const result = collectionOptional.validate(getFormData(['unchecked']))
|
|
144
|
+
expect(result.errors).toBeUndefined()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('accepts valid values', () => {
|
|
148
|
+
const result1 = collection.validate(getFormData(['unchecked', 'true']))
|
|
149
|
+
|
|
150
|
+
expect(result1.errors).toBeUndefined()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('adds errors for empty value', () => {
|
|
154
|
+
const result = collection.validate(getFormData(['unchecked']))
|
|
155
|
+
|
|
156
|
+
expect(result.errors).toEqual([
|
|
157
|
+
expect.objectContaining({
|
|
158
|
+
text: 'You must confirm you understand and agree with the terms and conditions to continue'
|
|
159
|
+
})
|
|
160
|
+
])
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('adds errors for invalid values', () => {
|
|
164
|
+
const result1 = collection.validate(getFormData(['invalid']))
|
|
165
|
+
const result2 = collection.validate(
|
|
166
|
+
// @ts-expect-error - Allow invalid param for test
|
|
167
|
+
getFormData({ unknown: 'invalid' })
|
|
168
|
+
)
|
|
169
|
+
// @ts-expect-error - Allow invalid param for test
|
|
170
|
+
const result3 = collection.validate('false')
|
|
171
|
+
|
|
172
|
+
expect(result1.errors).toBeTruthy()
|
|
173
|
+
expect(result2.errors).toBeTruthy()
|
|
174
|
+
expect(result3.errors).toBeTruthy()
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe('State', () => {
|
|
179
|
+
it('returns text from state', () => {
|
|
180
|
+
const state1 = getFormState(true)
|
|
181
|
+
const state2 = getFormState()
|
|
182
|
+
// context - boolean
|
|
183
|
+
// state - boolean
|
|
184
|
+
// string - I confirm that I understand and accept this declaration
|
|
185
|
+
const answer1 = getAnswer(field, state1)
|
|
186
|
+
const answer2 = getAnswer(field, state2)
|
|
187
|
+
|
|
188
|
+
expect(answer1).toBe('I understand and agree')
|
|
189
|
+
expect(answer2).toBe('')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('returns payload from state', () => {
|
|
193
|
+
const state1 = getFormState(true)
|
|
194
|
+
const state2 = getFormState(null)
|
|
195
|
+
|
|
196
|
+
const payload1 = field.getFormDataFromState(state1)
|
|
197
|
+
const payload2 = field.getFormDataFromState(state2)
|
|
198
|
+
|
|
199
|
+
expect(payload1).toEqual(getFormData('true'))
|
|
200
|
+
expect(payload2).toEqual(getFormData())
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('returns value from state', () => {
|
|
204
|
+
const state1 = getFormState(true)
|
|
205
|
+
const state2 = getFormState(null)
|
|
206
|
+
|
|
207
|
+
const value1 = field.getFormValueFromState(state1)
|
|
208
|
+
const value2 = field.getFormValueFromState(state2)
|
|
209
|
+
|
|
210
|
+
expect(value1).toBe('true')
|
|
211
|
+
expect(value2).toBeUndefined()
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('returns context for conditions and form submission', () => {
|
|
215
|
+
const state1 = getFormState(true)
|
|
216
|
+
const state2 = getFormState(null)
|
|
217
|
+
|
|
218
|
+
const value1 = field.getContextValueFromState(state1)
|
|
219
|
+
const value2 = field.getContextValueFromState(state2)
|
|
220
|
+
|
|
221
|
+
expect(value1).toBe(true)
|
|
222
|
+
expect(value2).toBe(false)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('returns state from payload', () => {
|
|
226
|
+
const payload1 = getFormData(['true'])
|
|
227
|
+
const payload2 = getFormData([])
|
|
228
|
+
const payload3 = getFormData(['unchecked'])
|
|
229
|
+
|
|
230
|
+
const value1 = field.getStateFromValidForm(payload1)
|
|
231
|
+
const value2 = field.getStateFromValidForm(payload2)
|
|
232
|
+
const value3 = field.getStateFromValidForm(payload3)
|
|
233
|
+
|
|
234
|
+
expect(value1).toEqual(getFormState(true))
|
|
235
|
+
expect(value2).toEqual(getFormState(false))
|
|
236
|
+
expect(value3).toEqual(getFormState(false))
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
describe('View model', () => {
|
|
241
|
+
it('sets Nunjucks component defaults', () => {
|
|
242
|
+
const viewModel = field.getViewModel(getFormData([]))
|
|
243
|
+
|
|
244
|
+
expect(viewModel).toEqual(
|
|
245
|
+
expect.objectContaining({
|
|
246
|
+
label: { text: def.title },
|
|
247
|
+
name: 'myComponent',
|
|
248
|
+
attributes: {},
|
|
249
|
+
values: [],
|
|
250
|
+
content: 'Lorem ipsum dolar sit amet',
|
|
251
|
+
id: 'myComponent',
|
|
252
|
+
fieldset: {
|
|
253
|
+
legend: {
|
|
254
|
+
text: 'Example Declaration Component'
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
items: [
|
|
258
|
+
{
|
|
259
|
+
value: 'true',
|
|
260
|
+
text: 'I understand and agree'
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
})
|
|
264
|
+
)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('sets Nunjucks component value when posted', () => {
|
|
268
|
+
def = {
|
|
269
|
+
...def,
|
|
270
|
+
hint: 'Please read and confirm the following'
|
|
271
|
+
} satisfies DeclarationFieldComponent
|
|
272
|
+
|
|
273
|
+
collection = new ComponentCollection([def], { model })
|
|
274
|
+
field = collection.fields[0]
|
|
275
|
+
const viewModel = field.getViewModel(getFormData(['true']))
|
|
276
|
+
|
|
277
|
+
expect(viewModel).toEqual(
|
|
278
|
+
expect.objectContaining({
|
|
279
|
+
values: ['true'],
|
|
280
|
+
hint: {
|
|
281
|
+
text: 'Please read and confirm the following'
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('sets custom message when in def', () => {
|
|
288
|
+
def = {
|
|
289
|
+
...def,
|
|
290
|
+
title: 'Declaration',
|
|
291
|
+
content:
|
|
292
|
+
'Declaration:\n' +
|
|
293
|
+
'By submitting this form, I consent to the collection and processing of my personal data for the purposes described.\n' +
|
|
294
|
+
'I understand that my data may be shared with authorised third parties where required by law',
|
|
295
|
+
options: {
|
|
296
|
+
declarationConfirmationLabel:
|
|
297
|
+
'I consent to the processing of my personal data'
|
|
298
|
+
}
|
|
299
|
+
} satisfies DeclarationFieldComponent
|
|
300
|
+
|
|
301
|
+
collection = new ComponentCollection([def], { model })
|
|
302
|
+
field = collection.fields[0]
|
|
303
|
+
|
|
304
|
+
const viewModel = field.getViewModel(getFormData(['unchecked', 'true']))
|
|
305
|
+
|
|
306
|
+
expect(viewModel).toEqual(
|
|
307
|
+
expect.objectContaining({
|
|
308
|
+
items: [
|
|
309
|
+
{
|
|
310
|
+
value: 'true',
|
|
311
|
+
text: 'I consent to the processing of my personal data'
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
})
|
|
315
|
+
)
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
describe('AllPossibleErrors', () => {
|
|
320
|
+
it('should return errors', () => {
|
|
321
|
+
const errors = field.getAllPossibleErrors()
|
|
322
|
+
expect(errors.baseErrors).not.toBeEmpty()
|
|
323
|
+
expect(errors.advancedSettingsErrors).toBeEmpty()
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
describe('getFormValue', () => {
|
|
328
|
+
test('should return correct value', () => {
|
|
329
|
+
expect(field.getFormValue(undefined)).toBeUndefined()
|
|
330
|
+
expect(field.getFormValue([true])).toEqual([true])
|
|
331
|
+
expect(field.getFormValue([])).toEqual([])
|
|
332
|
+
expect(field.getFormValue({})).toBeUndefined()
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('Validation', () => {
|
|
338
|
+
describe.each([
|
|
339
|
+
{
|
|
340
|
+
description: 'Default',
|
|
341
|
+
component: {
|
|
342
|
+
title: 'Terms and conditions',
|
|
343
|
+
shortDescription: 'The terms and conditions',
|
|
344
|
+
content: 'Lorem ipsum dolar sit amet',
|
|
345
|
+
name: 'myComponent',
|
|
346
|
+
type: ComponentType.DeclarationField,
|
|
347
|
+
options: {}
|
|
348
|
+
} satisfies DeclarationFieldComponent,
|
|
349
|
+
assertions: [
|
|
350
|
+
{
|
|
351
|
+
input: getFormData(['unchecked', 'true']),
|
|
352
|
+
output: {
|
|
353
|
+
value: {
|
|
354
|
+
myComponent: ['true']
|
|
355
|
+
},
|
|
356
|
+
errors: undefined
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
]
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
description: 'Use short description if it exists',
|
|
363
|
+
component: {
|
|
364
|
+
title: 'Terms and conditions',
|
|
365
|
+
shortDescription: 'Terms and conditions',
|
|
366
|
+
content: 'Lorem ipsum dolar sit amet',
|
|
367
|
+
name: 'myComponent',
|
|
368
|
+
type: ComponentType.DeclarationField,
|
|
369
|
+
options: {}
|
|
370
|
+
} satisfies DeclarationFieldComponent,
|
|
371
|
+
assertions: [
|
|
372
|
+
{
|
|
373
|
+
input: getFormData(['unchecked']),
|
|
374
|
+
output: {
|
|
375
|
+
value: { myComponent: [] },
|
|
376
|
+
errors: [
|
|
377
|
+
expect.objectContaining({
|
|
378
|
+
text: 'You must confirm you understand and agree with the terms and conditions to continue'
|
|
379
|
+
})
|
|
380
|
+
]
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
]
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
description: 'Optional field',
|
|
387
|
+
component: {
|
|
388
|
+
title: 'Example text field',
|
|
389
|
+
name: 'myComponent',
|
|
390
|
+
content: 'Lorem ipsum dolar sit amet',
|
|
391
|
+
type: ComponentType.DeclarationField,
|
|
392
|
+
options: {
|
|
393
|
+
required: false
|
|
394
|
+
}
|
|
395
|
+
} satisfies DeclarationFieldComponent,
|
|
396
|
+
assertions: [
|
|
397
|
+
{
|
|
398
|
+
input: getFormData(['unchecked']),
|
|
399
|
+
output: { value: { myComponent: [] } }
|
|
400
|
+
}
|
|
401
|
+
]
|
|
402
|
+
}
|
|
403
|
+
])('$description', ({ component: def, assertions }) => {
|
|
404
|
+
let collection: ComponentCollection
|
|
405
|
+
|
|
406
|
+
beforeEach(() => {
|
|
407
|
+
collection = new ComponentCollection([def], { model })
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it.each([...assertions])(
|
|
411
|
+
'validates custom example',
|
|
412
|
+
({ input, output }) => {
|
|
413
|
+
const result = collection.validate(input)
|
|
414
|
+
expect(result).toEqual(output)
|
|
415
|
+
}
|
|
416
|
+
)
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
describe('isBool', () => {
|
|
421
|
+
test('should return correct boolean', () => {
|
|
422
|
+
expect(DeclarationField.isBool('string')).toBe(false)
|
|
423
|
+
expect(DeclarationField.isBool(true)).toBe(true)
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
})
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { type DeclarationFieldComponent, type Item } from '@defra/forms-model'
|
|
2
|
+
import joi, {
|
|
3
|
+
type ArraySchema,
|
|
4
|
+
type BooleanSchema,
|
|
5
|
+
type StringSchema
|
|
6
|
+
} from 'joi'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
FormComponent,
|
|
10
|
+
isFormValue
|
|
11
|
+
} from '~/src/server/plugins/engine/components/FormComponent.js'
|
|
12
|
+
import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
|
|
13
|
+
import {
|
|
14
|
+
type ErrorMessageTemplateList,
|
|
15
|
+
type FormPayload,
|
|
16
|
+
type FormState,
|
|
17
|
+
type FormStateValue,
|
|
18
|
+
type FormSubmissionError,
|
|
19
|
+
type FormSubmissionState,
|
|
20
|
+
type FormValue
|
|
21
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
22
|
+
|
|
23
|
+
export class DeclarationField extends FormComponent {
|
|
24
|
+
private readonly DEFAULT_DECLARATION_LABEL = 'I understand and agree'
|
|
25
|
+
|
|
26
|
+
declare options: DeclarationFieldComponent['options']
|
|
27
|
+
|
|
28
|
+
declare declarationConfirmationLabel: string
|
|
29
|
+
|
|
30
|
+
declare formSchema: ArraySchema<StringSchema[]>
|
|
31
|
+
declare stateSchema: BooleanSchema
|
|
32
|
+
declare content: string
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
def: DeclarationFieldComponent,
|
|
36
|
+
props: ConstructorParameters<typeof FormComponent>[1]
|
|
37
|
+
) {
|
|
38
|
+
super(def, props)
|
|
39
|
+
|
|
40
|
+
const { options, content } = def
|
|
41
|
+
|
|
42
|
+
let checkboxSchema = joi.string().valid('true')
|
|
43
|
+
|
|
44
|
+
if (options.required !== false) {
|
|
45
|
+
checkboxSchema = checkboxSchema.required()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const formSchema = joi
|
|
49
|
+
.array()
|
|
50
|
+
.items(checkboxSchema, joi.string().valid('unchecked').strip())
|
|
51
|
+
.label(this.label)
|
|
52
|
+
.single()
|
|
53
|
+
.messages({
|
|
54
|
+
'any.required': messageTemplate.declarationRequired as string,
|
|
55
|
+
'any.unknown': messageTemplate.declarationRequired as string,
|
|
56
|
+
'array.includesRequiredUnknowns':
|
|
57
|
+
messageTemplate.declarationRequired as string
|
|
58
|
+
}) as ArraySchema<StringSchema[]>
|
|
59
|
+
|
|
60
|
+
this.formSchema = formSchema
|
|
61
|
+
this.stateSchema = joi.boolean().cast('string').label(this.label).required()
|
|
62
|
+
|
|
63
|
+
this.options = options
|
|
64
|
+
this.content = content
|
|
65
|
+
this.declarationConfirmationLabel =
|
|
66
|
+
options.declarationConfirmationLabel ?? this.DEFAULT_DECLARATION_LABEL
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getFormValueFromState(state: FormSubmissionState) {
|
|
70
|
+
const { name } = this
|
|
71
|
+
return state[name] === true ? 'true' : undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getFormDataFromState(state: FormSubmissionState): FormPayload {
|
|
75
|
+
const { name } = this
|
|
76
|
+
return { [name]: state[name] === true ? 'true' : undefined }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getStateFromValidForm(payload: FormPayload): FormState {
|
|
80
|
+
const { name } = this
|
|
81
|
+
const payloadValue = payload[name]
|
|
82
|
+
const value =
|
|
83
|
+
this.isValue(payloadValue) &&
|
|
84
|
+
payloadValue.length > 0 &&
|
|
85
|
+
payloadValue.every((v) => {
|
|
86
|
+
return v === 'true'
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
return { [name]: value }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getContextValueFromFormValue(value: FormValue | FormPayload): boolean {
|
|
93
|
+
return value === 'true'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getFormValue(value?: FormStateValue | FormState) {
|
|
97
|
+
return this.isValue(value) ? value : undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getDisplayStringFromFormValue(value: FormValue | FormPayload): string {
|
|
101
|
+
return value ? this.declarationConfirmationLabel : ''
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {
|
|
105
|
+
const defaultDeclarationConfirmationLabel =
|
|
106
|
+
'I confirm that I understand and accept this declaration'
|
|
107
|
+
const {
|
|
108
|
+
title,
|
|
109
|
+
hint,
|
|
110
|
+
content,
|
|
111
|
+
declarationConfirmationLabel = defaultDeclarationConfirmationLabel
|
|
112
|
+
} = this
|
|
113
|
+
return {
|
|
114
|
+
...super.getViewModel(payload, errors),
|
|
115
|
+
hint: hint ? { text: hint } : undefined,
|
|
116
|
+
fieldset: {
|
|
117
|
+
legend: {
|
|
118
|
+
text: title
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
content,
|
|
122
|
+
values: payload[this.name],
|
|
123
|
+
items: [
|
|
124
|
+
{
|
|
125
|
+
text: declarationConfirmationLabel,
|
|
126
|
+
value: 'true'
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
isValue(value?: FormStateValue | FormState): value is Item['value'][] {
|
|
133
|
+
if (!Array.isArray(value)) {
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Skip checks when empty
|
|
138
|
+
if (!value.length) {
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return value.every(isFormValue)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* For error preview page that shows all possible errors on a component
|
|
147
|
+
*/
|
|
148
|
+
getAllPossibleErrors(): ErrorMessageTemplateList {
|
|
149
|
+
return DeclarationField.getAllPossibleErrors()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Static version of getAllPossibleErrors that doesn't require a component instance.
|
|
154
|
+
*/
|
|
155
|
+
static getAllPossibleErrors(): ErrorMessageTemplateList {
|
|
156
|
+
return {
|
|
157
|
+
baseErrors: [
|
|
158
|
+
{ type: 'required', template: messageTemplate.declarationRequired }
|
|
159
|
+
],
|
|
160
|
+
advancedSettingsErrors: []
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static isBool(value?: FormStateValue | FormState): value is boolean {
|
|
165
|
+
return isFormValue(value) && typeof value === 'boolean'
|
|
166
|
+
}
|
|
167
|
+
}
|