@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.
@@ -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.state])
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.state, item2.state])
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.state])
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.state])
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.state])
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.state])
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.state]))
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.state)
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.state)
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.state)
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.state)
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.state))
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.state)
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.state)
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.state)
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.state)
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.state))
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
+ })