@defra/forms-engine-plugin 0.1.8 → 0.1.10
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/.server/server/forms/report-a-terrorist.json +7 -7
- package/.server/server/forms/runner-components-test.json +10 -10
- package/.server/server/forms/test.yaml +363 -0
- package/.server/server/utils/file-form-service.js +134 -0
- package/.server/server/utils/file-form-service.js.map +1 -0
- package/.server/server/utils/file-form-service.test.js +52 -0
- package/.server/server/utils/file-form-service.test.js.map +1 -0
- package/README.md +3 -1
- package/package.json +6 -3
- package/src/server/forms/report-a-terrorist.json +7 -7
- package/src/server/forms/runner-components-test.json +10 -10
- package/src/server/forms/test.yaml +363 -0
- package/src/server/plugins/engine/components/AutocompleteField.test.ts +5 -5
- package/src/server/plugins/engine/components/CheckboxesField.test.ts +7 -7
- package/src/server/plugins/engine/components/List.test.ts +3 -0
- package/src/server/plugins/engine/components/RadiosField.test.ts +5 -5
- package/src/server/plugins/engine/components/SelectField.test.ts +5 -5
- package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +4 -0
- package/src/server/utils/file-form-service.js +144 -0
- package/src/server/utils/file-form-service.test.js +79 -0
|
@@ -240,7 +240,7 @@ describe.each([
|
|
|
240
240
|
it.each([...options.examples])(
|
|
241
241
|
'returns text from state (single)',
|
|
242
242
|
(item) => {
|
|
243
|
-
const state1 = getFormState([item.
|
|
243
|
+
const state1 = getFormState([item.value])
|
|
244
244
|
const state2 = getFormState(null)
|
|
245
245
|
|
|
246
246
|
const answer1 = getAnswer(field, state1)
|
|
@@ -260,7 +260,7 @@ describe.each([
|
|
|
260
260
|
const item1 = options.examples[0]
|
|
261
261
|
const item2 = options.examples[2]
|
|
262
262
|
|
|
263
|
-
const state = getFormState([item1.
|
|
263
|
+
const state = getFormState([item1.value, item2.value])
|
|
264
264
|
const answer = getAnswer(field, state)
|
|
265
265
|
|
|
266
266
|
expect(answer).toBe(outdent`
|
|
@@ -272,7 +272,7 @@ describe.each([
|
|
|
272
272
|
})
|
|
273
273
|
|
|
274
274
|
it.each([...options.examples])('returns payload from state', (item) => {
|
|
275
|
-
const state1 = getFormState([item.
|
|
275
|
+
const state1 = getFormState([item.value])
|
|
276
276
|
const state2 = getFormState(null)
|
|
277
277
|
|
|
278
278
|
const payload1 = field.getFormDataFromState(state1)
|
|
@@ -283,7 +283,7 @@ describe.each([
|
|
|
283
283
|
})
|
|
284
284
|
|
|
285
285
|
it.each([...options.examples])('returns value from state', (item) => {
|
|
286
|
-
const state1 = getFormState([item.
|
|
286
|
+
const state1 = getFormState([item.value])
|
|
287
287
|
const state2 = getFormState(null)
|
|
288
288
|
|
|
289
289
|
const value1 = field.getFormValueFromState(state1)
|
|
@@ -296,13 +296,13 @@ describe.each([
|
|
|
296
296
|
it.each([...options.examples])(
|
|
297
297
|
'returns context for conditions and form submission',
|
|
298
298
|
(item) => {
|
|
299
|
-
const state1 = getFormState([item.
|
|
299
|
+
const state1 = getFormState([item.value])
|
|
300
300
|
const state2 = getFormState(null)
|
|
301
301
|
|
|
302
302
|
const value1 = field.getContextValueFromState(state1)
|
|
303
303
|
const value2 = field.getContextValueFromState(state2)
|
|
304
304
|
|
|
305
|
-
expect(value1).toEqual([item.
|
|
305
|
+
expect(value1).toEqual([item.value])
|
|
306
306
|
expect(value2).toEqual([])
|
|
307
307
|
}
|
|
308
308
|
)
|
|
@@ -314,7 +314,7 @@ describe.each([
|
|
|
314
314
|
const value1 = field.getStateFromValidForm(payload1)
|
|
315
315
|
const value2 = field.getStateFromValidForm(payload2)
|
|
316
316
|
|
|
317
|
-
expect(value1).toEqual(getFormState([item.
|
|
317
|
+
expect(value1).toEqual(getFormState([item.value]))
|
|
318
318
|
expect(value2).toEqual(getFormState(null))
|
|
319
319
|
})
|
|
320
320
|
})
|
|
@@ -52,18 +52,21 @@ describe('List', () => {
|
|
|
52
52
|
it('returns list items', () => {
|
|
53
53
|
expect(guidance).toHaveProperty('items', [
|
|
54
54
|
{
|
|
55
|
+
id: '52fc51fc-c75a-4b08-9c9e-6bd99b9bc49b',
|
|
55
56
|
text: '1 day',
|
|
56
57
|
value: 1,
|
|
57
58
|
description:
|
|
58
59
|
'Valid for 24 hours from the start time that you select'
|
|
59
60
|
},
|
|
60
61
|
{
|
|
62
|
+
id: '56b7b34f-23b3-4446-ac8e-b2443d18588e',
|
|
61
63
|
text: '8 day',
|
|
62
64
|
value: 8,
|
|
63
65
|
description:
|
|
64
66
|
'Valid for 8 consecutive days from the start time that you select'
|
|
65
67
|
},
|
|
66
68
|
{
|
|
69
|
+
id: '1af54fbc-eec2-4e1e-bd53-2415abf62677',
|
|
67
70
|
text: '12 months',
|
|
68
71
|
value: 365,
|
|
69
72
|
description:
|
|
@@ -170,7 +170,7 @@ describe.each([
|
|
|
170
170
|
|
|
171
171
|
describe('State', () => {
|
|
172
172
|
it.each([...options.examples])('returns text from state', (item) => {
|
|
173
|
-
const state1 = getFormState(item.
|
|
173
|
+
const state1 = getFormState(item.value)
|
|
174
174
|
const state2 = getFormState(null)
|
|
175
175
|
|
|
176
176
|
const answer1 = getAnswer(field, state1)
|
|
@@ -181,7 +181,7 @@ describe.each([
|
|
|
181
181
|
})
|
|
182
182
|
|
|
183
183
|
it.each([...options.examples])('returns payload from state', (item) => {
|
|
184
|
-
const state1 = getFormState(item.
|
|
184
|
+
const state1 = getFormState(item.value)
|
|
185
185
|
const state2 = getFormState(null)
|
|
186
186
|
|
|
187
187
|
const payload1 = field.getFormDataFromState(state1)
|
|
@@ -192,7 +192,7 @@ describe.each([
|
|
|
192
192
|
})
|
|
193
193
|
|
|
194
194
|
it.each([...options.examples])('returns value from state', (item) => {
|
|
195
|
-
const state1 = getFormState(item.
|
|
195
|
+
const state1 = getFormState(item.value)
|
|
196
196
|
const state2 = getFormState(null)
|
|
197
197
|
|
|
198
198
|
const value1 = field.getFormValueFromState(state1)
|
|
@@ -205,7 +205,7 @@ describe.each([
|
|
|
205
205
|
it.each([...options.examples])(
|
|
206
206
|
'returns context for conditions and form submission',
|
|
207
207
|
(item) => {
|
|
208
|
-
const state1 = getFormState(item.
|
|
208
|
+
const state1 = getFormState(item.value)
|
|
209
209
|
const state2 = getFormState(null)
|
|
210
210
|
|
|
211
211
|
const value1 = field.getContextValueFromState(state1)
|
|
@@ -223,7 +223,7 @@ describe.each([
|
|
|
223
223
|
const value1 = field.getStateFromValidForm(payload1)
|
|
224
224
|
const value2 = field.getStateFromValidForm(payload2)
|
|
225
225
|
|
|
226
|
-
expect(value1).toEqual(getFormState(item.
|
|
226
|
+
expect(value1).toEqual(getFormState(item.value))
|
|
227
227
|
expect(value2).toEqual(getFormState(null))
|
|
228
228
|
})
|
|
229
229
|
})
|
|
@@ -171,7 +171,7 @@ describe.each([
|
|
|
171
171
|
|
|
172
172
|
describe('State', () => {
|
|
173
173
|
it.each([...options.examples])('returns text from state', (item) => {
|
|
174
|
-
const state1 = getFormState(item.
|
|
174
|
+
const state1 = getFormState(item.value)
|
|
175
175
|
const state2 = getFormState(null)
|
|
176
176
|
|
|
177
177
|
const answer1 = getAnswer(field, state1)
|
|
@@ -182,7 +182,7 @@ describe.each([
|
|
|
182
182
|
})
|
|
183
183
|
|
|
184
184
|
it.each([...options.examples])('returns payload from state', (item) => {
|
|
185
|
-
const state1 = getFormState(item.
|
|
185
|
+
const state1 = getFormState(item.value)
|
|
186
186
|
const state2 = getFormState(null)
|
|
187
187
|
|
|
188
188
|
const payload1 = field.getFormDataFromState(state1)
|
|
@@ -193,7 +193,7 @@ describe.each([
|
|
|
193
193
|
})
|
|
194
194
|
|
|
195
195
|
it.each([...options.examples])('returns value from state', (item) => {
|
|
196
|
-
const state1 = getFormState(item.
|
|
196
|
+
const state1 = getFormState(item.value)
|
|
197
197
|
const state2 = getFormState(null)
|
|
198
198
|
|
|
199
199
|
const value1 = field.getFormValueFromState(state1)
|
|
@@ -206,7 +206,7 @@ describe.each([
|
|
|
206
206
|
it.each([...options.examples])(
|
|
207
207
|
'returns context for conditions and form submission',
|
|
208
208
|
(item) => {
|
|
209
|
-
const state1 = getFormState(item.
|
|
209
|
+
const state1 = getFormState(item.value)
|
|
210
210
|
const state2 = getFormState(null)
|
|
211
211
|
|
|
212
212
|
const value1 = field.getContextValueFromState(state1)
|
|
@@ -224,7 +224,7 @@ describe.each([
|
|
|
224
224
|
const value1 = field.getStateFromValidForm(payload1)
|
|
225
225
|
const value2 = field.getStateFromValidForm(payload2)
|
|
226
226
|
|
|
227
|
-
expect(value1).toEqual(getFormState(item.
|
|
227
|
+
expect(value1).toEqual(getFormState(item.value))
|
|
228
228
|
expect(value2).toEqual(getFormState(null))
|
|
229
229
|
})
|
|
230
230
|
})
|
|
@@ -443,6 +443,7 @@ describe('QuestionPageController', () => {
|
|
|
443
443
|
expect(filtered[1].model.label?.text).toBe('Select from the list')
|
|
444
444
|
expect(filtered[1].model.items).toEqual([
|
|
445
445
|
{
|
|
446
|
+
id: expect.any(String),
|
|
446
447
|
checked: false,
|
|
447
448
|
condition: 'isBarnOwl',
|
|
448
449
|
selected: false,
|
|
@@ -450,6 +451,7 @@ describe('QuestionPageController', () => {
|
|
|
450
451
|
value: '1'
|
|
451
452
|
},
|
|
452
453
|
{
|
|
454
|
+
id: expect.any(String),
|
|
453
455
|
checked: false,
|
|
454
456
|
condition: 'isBarnOwl',
|
|
455
457
|
selected: false,
|
|
@@ -499,6 +501,7 @@ describe('QuestionPageController', () => {
|
|
|
499
501
|
expect(filtered[1].model.label?.text).toBe('Select from the list')
|
|
500
502
|
expect(filtered[1].model.items).toEqual([
|
|
501
503
|
{
|
|
504
|
+
id: expect.any(String),
|
|
502
505
|
checked: false,
|
|
503
506
|
condition: 'notBarnOwl',
|
|
504
507
|
selected: false,
|
|
@@ -506,6 +509,7 @@ describe('QuestionPageController', () => {
|
|
|
506
509
|
value: '3'
|
|
507
510
|
},
|
|
508
511
|
{
|
|
512
|
+
id: expect.any(String),
|
|
509
513
|
checked: false,
|
|
510
514
|
condition: 'notBarnOwl',
|
|
511
515
|
selected: false,
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fs from 'fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import YAML from 'yaml'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* FileFormService class
|
|
8
|
+
*/
|
|
9
|
+
export class FileFormService {
|
|
10
|
+
/**
|
|
11
|
+
* The map of form metadatas by slug
|
|
12
|
+
* @type {Map<string, FormMetadata>}
|
|
13
|
+
*/
|
|
14
|
+
#metadata = new Map()
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The map of form definitions by id
|
|
18
|
+
* @type {Map<string, FormDefinition>}
|
|
19
|
+
*/
|
|
20
|
+
#definition = new Map()
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Add form from a file
|
|
24
|
+
* @param {string} filepath - the file path
|
|
25
|
+
* @param {FormMetadata} metadata - the metadata to use for this form
|
|
26
|
+
* @returns {Promise<FormDefinition>}
|
|
27
|
+
*/
|
|
28
|
+
async addForm(filepath, metadata) {
|
|
29
|
+
const definition = await this.readForm(filepath)
|
|
30
|
+
|
|
31
|
+
this.#metadata.set(metadata.slug, metadata)
|
|
32
|
+
this.#definition.set(metadata.id, definition)
|
|
33
|
+
|
|
34
|
+
return definition
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read the form definition from file
|
|
39
|
+
* @param {string} filepath - the file path
|
|
40
|
+
* @returns {Promise<FormDefinition>}
|
|
41
|
+
*/
|
|
42
|
+
async readForm(filepath) {
|
|
43
|
+
const ext = path.extname(filepath).toLowerCase()
|
|
44
|
+
|
|
45
|
+
switch (ext) {
|
|
46
|
+
case '.json':
|
|
47
|
+
return this.readJsonForm(filepath)
|
|
48
|
+
case '.yaml':
|
|
49
|
+
return this.readYamlForm(filepath)
|
|
50
|
+
default:
|
|
51
|
+
throw new Error(`Invalid file extension '${ext}'`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read the form definition from a json file
|
|
57
|
+
* @param {string} filepath - the file path
|
|
58
|
+
* @returns {Promise<FormDefinition>}
|
|
59
|
+
*/
|
|
60
|
+
async readJsonForm(filepath) {
|
|
61
|
+
/**
|
|
62
|
+
* @type {FormDefinition}
|
|
63
|
+
*/
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
65
|
+
const definition = JSON.parse(await fs.readFile(filepath, 'utf8'))
|
|
66
|
+
|
|
67
|
+
return definition
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Read the form definition from a yaml file
|
|
72
|
+
* @param {string} filepath - the file path
|
|
73
|
+
* @returns {Promise<FormDefinition>}
|
|
74
|
+
*/
|
|
75
|
+
async readYamlForm(filepath) {
|
|
76
|
+
/**
|
|
77
|
+
* @type {FormDefinition}
|
|
78
|
+
*/
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
80
|
+
const definition = YAML.parse(await fs.readFile(filepath, 'utf8'))
|
|
81
|
+
|
|
82
|
+
return definition
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the form metadata by slug
|
|
87
|
+
* @param {string} slug - the form slug
|
|
88
|
+
* @returns {FormMetadata}
|
|
89
|
+
*/
|
|
90
|
+
getFormMetadata(slug) {
|
|
91
|
+
const metadata = this.#metadata.get(slug)
|
|
92
|
+
|
|
93
|
+
if (!metadata) {
|
|
94
|
+
throw new Error(`Form metadata '${slug}' not found`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return metadata
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get the form defintion by id
|
|
102
|
+
* @param {string} id - the form id
|
|
103
|
+
* @returns {FormDefinition}
|
|
104
|
+
*/
|
|
105
|
+
getFormDefinition(id) {
|
|
106
|
+
const definition = this.#definition.get(id)
|
|
107
|
+
|
|
108
|
+
if (!definition) {
|
|
109
|
+
throw new Error(`Form definition '${id}' not found`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return definition
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Returns a FormsService compliant interface
|
|
117
|
+
* @returns {import('~/src/server/types.js').FormsService}
|
|
118
|
+
*/
|
|
119
|
+
toFormsService() {
|
|
120
|
+
return {
|
|
121
|
+
/**
|
|
122
|
+
* Get the form metadata by slug
|
|
123
|
+
* @param {string} slug
|
|
124
|
+
* @returns {Promise<FormMetadata>}
|
|
125
|
+
*/
|
|
126
|
+
getFormMetadata: (slug) => {
|
|
127
|
+
return Promise.resolve(this.getFormMetadata(slug))
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get the form defintion by id
|
|
132
|
+
* @param {string} id
|
|
133
|
+
* @returns {Promise<FormDefinition>}
|
|
134
|
+
*/
|
|
135
|
+
getFormDefinition: (id) => {
|
|
136
|
+
return Promise.resolve(this.getFormDefinition(id))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @import { FormMetadata, FormDefinition } from '@defra/forms-model'
|
|
144
|
+
*/
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { FormStatus } from '~/src/server/routes/types.js'
|
|
2
|
+
import { FileFormService } from '~/src/server/utils/file-form-service.js'
|
|
3
|
+
|
|
4
|
+
// Create the metadata which is shared for all forms
|
|
5
|
+
const now = new Date()
|
|
6
|
+
const user = { id: 'user', displayName: 'Username' }
|
|
7
|
+
const author = {
|
|
8
|
+
createdAt: now,
|
|
9
|
+
createdBy: user,
|
|
10
|
+
updatedAt: now,
|
|
11
|
+
updatedBy: user
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const metadata = {
|
|
15
|
+
id: '95e92559-968d-44ae-8666-2b1ad3dffd31',
|
|
16
|
+
slug: 'example-form',
|
|
17
|
+
title: 'Example form',
|
|
18
|
+
organisation: 'Defra',
|
|
19
|
+
teamName: 'Team name',
|
|
20
|
+
teamEmail: 'team@defra.gov.uk',
|
|
21
|
+
submissionGuidance: "Thanks for your submission, we'll be in touch",
|
|
22
|
+
notificationEmail: 'email@domain.com',
|
|
23
|
+
...author,
|
|
24
|
+
live: author
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('File Form Service', () => {
|
|
28
|
+
it('should load JSON files from disk', async () => {
|
|
29
|
+
const loader = new FileFormService()
|
|
30
|
+
|
|
31
|
+
const definition = await loader.addForm(
|
|
32
|
+
'src/server/forms/test.json',
|
|
33
|
+
metadata
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const formsService = loader.toFormsService()
|
|
37
|
+
expect(await formsService.getFormMetadata(metadata.slug)).toBe(metadata)
|
|
38
|
+
expect(
|
|
39
|
+
await formsService.getFormDefinition(metadata.id, FormStatus.Draft)
|
|
40
|
+
).toBe(definition)
|
|
41
|
+
|
|
42
|
+
expect(() => loader.getFormMetadata('invalid-slug')).toThrow(
|
|
43
|
+
"Form metadata 'invalid-slug' not found"
|
|
44
|
+
)
|
|
45
|
+
expect(() => loader.getFormDefinition('invalid-id')).toThrow(
|
|
46
|
+
"Form definition 'invalid-id' not found"
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should load YAML files from disk', async () => {
|
|
51
|
+
const loader = new FileFormService()
|
|
52
|
+
|
|
53
|
+
const definition = await loader.addForm(
|
|
54
|
+
'src/server/forms/test.yaml',
|
|
55
|
+
metadata
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const formsService = loader.toFormsService()
|
|
59
|
+
expect(await formsService.getFormMetadata(metadata.slug)).toBe(metadata)
|
|
60
|
+
expect(
|
|
61
|
+
await formsService.getFormDefinition(metadata.id, FormStatus.Draft)
|
|
62
|
+
).toBe(definition)
|
|
63
|
+
|
|
64
|
+
expect(() => loader.getFormMetadata('invalid-slug')).toThrow(
|
|
65
|
+
"Form metadata 'invalid-slug' not found"
|
|
66
|
+
)
|
|
67
|
+
expect(() => loader.getFormDefinition('invalid-id')).toThrow(
|
|
68
|
+
"Form definition 'invalid-id' not found"
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it("should throw if the file isn't JSON or YAML", async () => {
|
|
73
|
+
const loader = new FileFormService()
|
|
74
|
+
|
|
75
|
+
await expect(
|
|
76
|
+
loader.addForm('src/server/forms/test.txt', metadata)
|
|
77
|
+
).rejects.toThrow("Invalid file extension '.txt'")
|
|
78
|
+
})
|
|
79
|
+
})
|