@defra/forms-engine-plugin 4.0.25 → 4.0.26
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/plugins/engine/components/HiddenField.d.ts +21 -0
- package/.server/server/plugins/engine/components/HiddenField.js +50 -0
- package/.server/server/plugins/engine/components/HiddenField.js.map +1 -0
- package/.server/server/plugins/engine/components/helpers/components.d.ts +1 -1
- package/.server/server/plugins/engine/components/helpers/components.js +3 -0
- package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
- package/.server/server/plugins/engine/components/index.d.ts +1 -0
- package/.server/server/plugins/engine/components/index.js +1 -0
- package/.server/server/plugins/engine/components/index.js.map +1 -1
- package/.server/server/plugins/engine/helpers.js +2 -1
- package/.server/server/plugins/engine/helpers.js.map +1 -1
- package/.server/server/plugins/engine/models/FormModel.d.ts +2 -0
- package/.server/server/plugins/engine/models/FormModel.js +2 -0
- package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/PageController.d.ts +1 -1
- package/.server/server/plugins/engine/pageControllers/PageController.js +2 -8
- package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +7 -0
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/StatusPageController.js +8 -2
- package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +2 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/helpers/state.d.ts +11 -0
- package/.server/server/plugins/engine/pageControllers/helpers/state.js +66 -0
- package/.server/server/plugins/engine/pageControllers/helpers/state.js.map +1 -0
- package/.server/server/plugins/engine/routes/index.js +2 -1
- package/.server/server/plugins/engine/routes/index.js.map +1 -1
- package/.server/server/plugins/engine/services/formsService.d.ts +6 -0
- package/.server/server/plugins/engine/services/formsService.js +10 -0
- package/.server/server/plugins/engine/services/formsService.js.map +1 -1
- package/.server/server/plugins/engine/services/formsService.test.js +14 -0
- package/.server/server/plugins/engine/services/formsService.test.js.map +1 -0
- package/.server/server/plugins/engine/types.d.ts +4 -0
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/plugins/engine/views/components/hiddenfield.html +3 -0
- package/.server/server/plugins/engine/views/confirmation.html +5 -0
- package/.server/server/plugins/engine/views/partials/form.html +9 -1
- package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
- package/.server/server/services/cacheService.d.ts +3 -7
- package/.server/server/services/cacheService.js.map +1 -1
- package/.server/server/types.d.ts +1 -0
- package/.server/server/types.js.map +1 -1
- package/.server/server/utils/file-form-service.d.ts +6 -0
- package/.server/server/utils/file-form-service.js +22 -1
- package/.server/server/utils/file-form-service.js.map +1 -1
- package/.server/server/utils/file-form-service.test.js +86 -0
- package/.server/server/utils/file-form-service.test.js.map +1 -0
- package/package.json +2 -2
- package/src/server/plugins/engine/components/HiddenField.test.ts +188 -0
- package/src/server/plugins/engine/components/HiddenField.ts +68 -0
- package/src/server/plugins/engine/components/helpers/components.ts +5 -0
- package/src/server/plugins/engine/components/helpers/helpers.test.ts +17 -0
- package/src/server/plugins/engine/components/index.ts +1 -0
- package/src/server/plugins/engine/helpers.ts +2 -1
- package/src/server/plugins/engine/models/FormModel.ts +3 -0
- package/src/server/plugins/engine/pageControllers/PageController.test.ts +4 -11
- package/src/server/plugins/engine/pageControllers/PageController.ts +1 -9
- package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +7 -0
- package/src/server/plugins/engine/pageControllers/StatusPageController.ts +9 -2
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +5 -1
- package/src/server/plugins/engine/pageControllers/helpers/state.test.ts +221 -0
- package/src/server/plugins/engine/pageControllers/helpers/state.ts +93 -0
- package/src/server/plugins/engine/routes/index.test.ts +24 -11
- package/src/server/plugins/engine/routes/index.ts +1 -1
- package/src/server/plugins/engine/services/formsService.js +10 -0
- package/src/server/plugins/engine/services/formsService.test.js +21 -0
- package/src/server/plugins/engine/types.ts +5 -0
- package/src/server/plugins/engine/views/components/hiddenfield.html +3 -0
- package/src/server/plugins/engine/views/confirmation.html +5 -0
- package/src/server/plugins/engine/views/partials/form.html +9 -1
- package/src/server/services/cacheService.ts +3 -2
- package/src/server/types.ts +1 -0
- package/src/server/utils/file-form-service.js +27 -1
- package/src/server/utils/file-form-service.test.js +114 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { ComponentType, type Page } from '@defra/forms-model'
|
|
2
|
+
|
|
3
|
+
import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
4
|
+
import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
|
|
5
|
+
import {
|
|
6
|
+
prefillStateFromQueryParameters,
|
|
7
|
+
stripParam
|
|
8
|
+
} from '~/src/server/plugins/engine/pageControllers/helpers/state.js'
|
|
9
|
+
import { type AnyFormRequest } from '~/src/server/plugins/engine/types.js'
|
|
10
|
+
import { type FormsService, type Services } from '~/src/server/types.js'
|
|
11
|
+
|
|
12
|
+
function buildMockPage(
|
|
13
|
+
pagesOverride = {},
|
|
14
|
+
stateOverride = {},
|
|
15
|
+
servicesOverride = {} as Services
|
|
16
|
+
) {
|
|
17
|
+
return {
|
|
18
|
+
model: {
|
|
19
|
+
def: {
|
|
20
|
+
metadata: {
|
|
21
|
+
submission: { code: 'TEST-CODE' }
|
|
22
|
+
} as { submission: { code: string } },
|
|
23
|
+
pages: pagesOverride
|
|
24
|
+
},
|
|
25
|
+
getFormContext: jest.fn().mockReturnValue({
|
|
26
|
+
isForceAccess: false,
|
|
27
|
+
data: {}
|
|
28
|
+
}),
|
|
29
|
+
services: servicesOverride
|
|
30
|
+
} as unknown as FormModel,
|
|
31
|
+
...stateOverride
|
|
32
|
+
} as unknown as PageControllerClass
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('State helpers', () => {
|
|
36
|
+
describe('prefillStateFromQueryParameters', () => {
|
|
37
|
+
const mockGetState = jest.fn()
|
|
38
|
+
const mockMergeState = jest.fn()
|
|
39
|
+
const mockRequestPrefill: AnyFormRequest = {
|
|
40
|
+
app: {},
|
|
41
|
+
yar: { flash: () => [] },
|
|
42
|
+
params: { path: 'test-path' },
|
|
43
|
+
query: {}
|
|
44
|
+
} as unknown as AnyFormRequest
|
|
45
|
+
|
|
46
|
+
it('should not add any state if no params', async () => {
|
|
47
|
+
const mockPagePrefill = buildMockPage([], {
|
|
48
|
+
getState: mockGetState,
|
|
49
|
+
mergeState: mockMergeState
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
expect(
|
|
53
|
+
await prefillStateFromQueryParameters(
|
|
54
|
+
mockRequestPrefill,
|
|
55
|
+
mockPagePrefill
|
|
56
|
+
)
|
|
57
|
+
).toBeFalse()
|
|
58
|
+
expect(mockMergeState).not.toHaveBeenCalled()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should ignore if no params (but some hidden fields)', async () => {
|
|
62
|
+
const mockRequest2 = {
|
|
63
|
+
...mockRequestPrefill,
|
|
64
|
+
query: {}
|
|
65
|
+
} as unknown as AnyFormRequest
|
|
66
|
+
|
|
67
|
+
const mockPagePrefill = buildMockPage(
|
|
68
|
+
[
|
|
69
|
+
{
|
|
70
|
+
components: [
|
|
71
|
+
{
|
|
72
|
+
type: ComponentType.HiddenField,
|
|
73
|
+
name: 'param2'
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: ComponentType.HiddenField,
|
|
77
|
+
name: 'param4'
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
next: []
|
|
81
|
+
} as unknown as Page
|
|
82
|
+
],
|
|
83
|
+
{
|
|
84
|
+
getState: mockGetState,
|
|
85
|
+
mergeState: mockMergeState
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
expect(
|
|
90
|
+
await prefillStateFromQueryParameters(mockRequest2, mockPagePrefill)
|
|
91
|
+
).toBeFalse()
|
|
92
|
+
expect(mockMergeState).not.toHaveBeenCalled()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should only add state where param names match hidden field names', async () => {
|
|
96
|
+
const mockRequest2 = {
|
|
97
|
+
...mockRequestPrefill,
|
|
98
|
+
query: {
|
|
99
|
+
param1: 'val1',
|
|
100
|
+
param2: 'val2',
|
|
101
|
+
param3: 'val3',
|
|
102
|
+
param4: 'val4'
|
|
103
|
+
}
|
|
104
|
+
} as unknown as AnyFormRequest
|
|
105
|
+
|
|
106
|
+
const mockPagePrefill = buildMockPage(
|
|
107
|
+
[
|
|
108
|
+
{
|
|
109
|
+
components: [
|
|
110
|
+
{
|
|
111
|
+
type: ComponentType.HiddenField,
|
|
112
|
+
name: 'param2'
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: ComponentType.HiddenField,
|
|
116
|
+
name: 'param4'
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
next: []
|
|
120
|
+
} as unknown as Page
|
|
121
|
+
],
|
|
122
|
+
{
|
|
123
|
+
getState: mockGetState.mockResolvedValue({}),
|
|
124
|
+
mergeState: mockMergeState
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
expect(
|
|
129
|
+
await prefillStateFromQueryParameters(mockRequest2, mockPagePrefill)
|
|
130
|
+
).toBe(true)
|
|
131
|
+
expect(mockMergeState).toHaveBeenCalledWith(
|
|
132
|
+
expect.anything(),
|
|
133
|
+
expect.anything(),
|
|
134
|
+
{
|
|
135
|
+
param2: 'val2',
|
|
136
|
+
param4: 'val4'
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should call lookup function for formId', async () => {
|
|
142
|
+
const mockRequest3 = {
|
|
143
|
+
...mockRequestPrefill,
|
|
144
|
+
query: {
|
|
145
|
+
formId: 'c644804b-2f23-4c96-a2fc-ad4975974723'
|
|
146
|
+
}
|
|
147
|
+
} as unknown as AnyFormRequest
|
|
148
|
+
|
|
149
|
+
const mockPagePrefill = buildMockPage(
|
|
150
|
+
[
|
|
151
|
+
{
|
|
152
|
+
components: [
|
|
153
|
+
{
|
|
154
|
+
type: ComponentType.HiddenField,
|
|
155
|
+
name: 'formId'
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
next: []
|
|
159
|
+
} as unknown as Page
|
|
160
|
+
],
|
|
161
|
+
{
|
|
162
|
+
getState: mockGetState.mockResolvedValue({}),
|
|
163
|
+
mergeState: mockMergeState
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
formsService: {
|
|
167
|
+
getFormMetadata: jest.fn(),
|
|
168
|
+
getFormMetadataById: jest
|
|
169
|
+
.fn()
|
|
170
|
+
.mockResolvedValue({ title: 'My looked-up form name' }),
|
|
171
|
+
getFormDefinition: jest.fn()
|
|
172
|
+
} as unknown as FormsService
|
|
173
|
+
} as Services
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
await prefillStateFromQueryParameters(mockRequest3, mockPagePrefill)
|
|
177
|
+
expect(mockMergeState).toHaveBeenCalledWith(
|
|
178
|
+
expect.anything(),
|
|
179
|
+
expect.anything(),
|
|
180
|
+
{
|
|
181
|
+
formId: 'c644804b-2f23-4c96-a2fc-ad4975974723',
|
|
182
|
+
formName: 'My looked-up form name'
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('stripParam', () => {
|
|
189
|
+
it('should remove param when exists', () => {
|
|
190
|
+
const params = {
|
|
191
|
+
paramName1: 'val1',
|
|
192
|
+
returnUrl: 'http://somesite.com',
|
|
193
|
+
paramName2: 'val2',
|
|
194
|
+
paramName3: undefined
|
|
195
|
+
}
|
|
196
|
+
expect(stripParam(params, 'returnUrl')).toStrictEqual({
|
|
197
|
+
paramName1: 'val1',
|
|
198
|
+
paramName2: 'val2',
|
|
199
|
+
paramName3: ''
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should handle param missing', () => {
|
|
204
|
+
const params = {
|
|
205
|
+
paramName1: 'val1',
|
|
206
|
+
returnUrl: 'http://somesite.com',
|
|
207
|
+
paramName2: 'val2'
|
|
208
|
+
}
|
|
209
|
+
expect(stripParam(params, 'paramNotThere')).toStrictEqual({
|
|
210
|
+
paramName1: 'val1',
|
|
211
|
+
returnUrl: 'http://somesite.com',
|
|
212
|
+
paramName2: 'val2'
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should handle no params', () => {
|
|
217
|
+
const params = {}
|
|
218
|
+
expect(stripParam(params, 'anyParam')).toBeUndefined()
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { getHiddenFields } from '@defra/forms-model'
|
|
2
|
+
|
|
3
|
+
import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
|
|
4
|
+
import {
|
|
5
|
+
type AnyFormRequest,
|
|
6
|
+
type FormStateValue
|
|
7
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
8
|
+
import { type FormQuery } from '~/src/server/routes/types.js'
|
|
9
|
+
import { type Services } from '~/src/server/types.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A series of functions that can transform a pre-fill input parameter e.g lookup a form title based on form id
|
|
13
|
+
*/
|
|
14
|
+
const paramLookupFunctions = {
|
|
15
|
+
formId: async (val: string, services: Services) => {
|
|
16
|
+
let formTitle
|
|
17
|
+
if (val) {
|
|
18
|
+
const meta = await services.formsService.getFormMetadataById(val)
|
|
19
|
+
formTitle = meta.title
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
key: 'formName',
|
|
23
|
+
value: formTitle
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} as Partial<
|
|
27
|
+
Record<
|
|
28
|
+
string,
|
|
29
|
+
(
|
|
30
|
+
val: string,
|
|
31
|
+
services: Services
|
|
32
|
+
) => Promise<{ key: string; value: string | undefined }>
|
|
33
|
+
>
|
|
34
|
+
>
|
|
35
|
+
|
|
36
|
+
export function stripParam(query: FormQuery, paramToRemove: string) {
|
|
37
|
+
const params = {} as Record<string, FormStateValue | undefined>
|
|
38
|
+
for (const [key, value = ''] of Object.entries(query)) {
|
|
39
|
+
if (key !== paramToRemove) {
|
|
40
|
+
params[key] = value
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return Object.keys(params).length ? (params as FormQuery) : undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Any hidden parameters defined in the FormDefinition may be pre-filled by URL parameter values.
|
|
48
|
+
* Other parameters are ignored for security reasons.
|
|
49
|
+
* @param request
|
|
50
|
+
* @param model
|
|
51
|
+
*/
|
|
52
|
+
export async function prefillStateFromQueryParameters(
|
|
53
|
+
request: AnyFormRequest,
|
|
54
|
+
page: PageControllerClass
|
|
55
|
+
): Promise<boolean> {
|
|
56
|
+
const { model } = page
|
|
57
|
+
|
|
58
|
+
const hiddenFieldNames = new Set(
|
|
59
|
+
getHiddenFields(model.def).map((field) => field.name)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if (!hiddenFieldNames.size) {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Remove 'returnUrl' param
|
|
67
|
+
const query = stripParam(request.query, 'returnUrl')
|
|
68
|
+
|
|
69
|
+
if (!query) {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const params = {} as Record<string, FormStateValue | undefined>
|
|
74
|
+
|
|
75
|
+
for (const [key, value = ''] of Object.entries(query)) {
|
|
76
|
+
if (hiddenFieldNames.has(key)) {
|
|
77
|
+
const lookupFunc = paramLookupFunctions[key]
|
|
78
|
+
if (lookupFunc) {
|
|
79
|
+
const res = await lookupFunc(value, model.services)
|
|
80
|
+
// Store original value and result
|
|
81
|
+
params[key] = value
|
|
82
|
+
params[res.key] = res.value
|
|
83
|
+
} else {
|
|
84
|
+
params[key] = value
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const formData = await page.getState(request)
|
|
90
|
+
await page.mergeState(request, formData, params)
|
|
91
|
+
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Page } from '@defra/forms-model'
|
|
1
2
|
import Boom from '@hapi/boom'
|
|
2
3
|
import { type ResponseObject, type ResponseToolkit } from '@hapi/hapi'
|
|
3
4
|
|
|
@@ -15,9 +16,31 @@ import {
|
|
|
15
16
|
type OnRequestCallback
|
|
16
17
|
} from '~/src/server/plugins/engine/types.js'
|
|
17
18
|
import { type FormResponseToolkit } from '~/src/server/routes/types.js'
|
|
19
|
+
import { type Services } from '~/src/server/types.js'
|
|
18
20
|
|
|
19
21
|
jest.mock('~/src/server/plugins/engine/helpers')
|
|
20
22
|
|
|
23
|
+
function buildMockModel(
|
|
24
|
+
pagesOverride = [] as Page[],
|
|
25
|
+
pagesControllerOverride = [] as PageControllerClass[],
|
|
26
|
+
servicesOverride = {} as Services
|
|
27
|
+
) {
|
|
28
|
+
return {
|
|
29
|
+
def: {
|
|
30
|
+
metadata: {
|
|
31
|
+
submission: { code: 'TEST-CODE' }
|
|
32
|
+
} as { submission: { code: string } },
|
|
33
|
+
pages: pagesOverride
|
|
34
|
+
},
|
|
35
|
+
getFormContext: jest.fn().mockReturnValue({
|
|
36
|
+
isForceAccess: false,
|
|
37
|
+
data: {}
|
|
38
|
+
}),
|
|
39
|
+
pages: pagesControllerOverride,
|
|
40
|
+
services: servicesOverride
|
|
41
|
+
} as unknown as FormModel
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
describe('redirectOrMakeHandler', () => {
|
|
22
45
|
const mockServer = {} as unknown as Parameters<
|
|
23
46
|
typeof redirectOrMakeHandler
|
|
@@ -38,17 +61,7 @@ describe('redirectOrMakeHandler', () => {
|
|
|
38
61
|
|
|
39
62
|
let mockPage: PageControllerClass
|
|
40
63
|
|
|
41
|
-
const mockModel
|
|
42
|
-
def: {
|
|
43
|
-
metadata: {
|
|
44
|
-
submission: { code: 'TEST-CODE' }
|
|
45
|
-
} as { submission: { code: string } }
|
|
46
|
-
},
|
|
47
|
-
getFormContext: jest.fn().mockReturnValue({
|
|
48
|
-
isForceAccess: false,
|
|
49
|
-
data: {}
|
|
50
|
-
})
|
|
51
|
-
} as unknown as FormModel
|
|
64
|
+
const mockModel = buildMockModel()
|
|
52
65
|
|
|
53
66
|
const mockMakeHandler = jest
|
|
54
67
|
.fn()
|
|
@@ -244,7 +244,7 @@ export function makeLoadFormPreHandler(server: Server, options: PluginOptions) {
|
|
|
244
244
|
// Construct the form model
|
|
245
245
|
const model = new FormModel(
|
|
246
246
|
definition,
|
|
247
|
-
{ basePath, versionNumber, ordnanceSurveyApiKey },
|
|
247
|
+
{ basePath, versionNumber, ordnanceSurveyApiKey, formId: id },
|
|
248
248
|
services,
|
|
249
249
|
controllers
|
|
250
250
|
)
|
|
@@ -12,6 +12,16 @@ export function getFormMetadata(_slug) {
|
|
|
12
12
|
throw error
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// eslint-disable-next-line jsdoc/require-returns-check
|
|
16
|
+
/**
|
|
17
|
+
* Dummy function to get form metadata.
|
|
18
|
+
* @param {string} _id - the id of the form
|
|
19
|
+
* @returns {Promise<FormMetadata>}
|
|
20
|
+
*/
|
|
21
|
+
export function getFormMetadataById(_id) {
|
|
22
|
+
throw error
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
// eslint-disable-next-line jsdoc/require-returns-check
|
|
16
26
|
/**
|
|
17
27
|
* Dummy function to get form metadata.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { FormStatus } from '@defra/forms-model'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getFormDefinition,
|
|
5
|
+
getFormMetadata,
|
|
6
|
+
getFormMetadataById
|
|
7
|
+
} from '~/src/server/plugins/engine/services/formsService.js'
|
|
8
|
+
|
|
9
|
+
describe('formsService', () => {
|
|
10
|
+
it('getFormMetadata should throw error', () => {
|
|
11
|
+
expect(() => getFormMetadata('slug')).toThrow()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('getFormMetadataById should throw error', () => {
|
|
15
|
+
expect(() => getFormMetadataById('id')).toThrow()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('getFormDefinition should throw error', () => {
|
|
19
|
+
expect(() => getFormDefinition('id', FormStatus.Draft)).toThrow()
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -97,6 +97,11 @@ export interface FormSubmissionError
|
|
|
97
97
|
text: string // e.g: 'Date field must be a real date'
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
export interface FormConfirmationState {
|
|
101
|
+
confirmed?: true
|
|
102
|
+
formId?: string
|
|
103
|
+
}
|
|
104
|
+
|
|
100
105
|
export interface FormPayloadParams {
|
|
101
106
|
action?: FormAction
|
|
102
107
|
confirm?: true
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
<div class="app-prose-scope">
|
|
15
15
|
{{ submissionGuidance | markdown(3) | safe }}
|
|
16
16
|
</div>
|
|
17
|
+
{% if feedbackLink %}
|
|
18
|
+
<p class="govuk-body">
|
|
19
|
+
<a href="{{ feedbackLink }}" class="govuk-link govuk-link--no-visited-state">What do you think of this service?</a> (takes 30 seconds)
|
|
20
|
+
</p>
|
|
21
|
+
{% endif %}
|
|
17
22
|
</div>
|
|
18
23
|
</div>
|
|
19
24
|
{% endblock %}
|
|
@@ -6,9 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
{{ componentList(components) }}
|
|
8
8
|
|
|
9
|
+
{% if isStartPage %}
|
|
10
|
+
{% set buttonText = "Start now" %}
|
|
11
|
+
{% elif submitButtonText %}
|
|
12
|
+
{% set buttonText = submitButtonText %}
|
|
13
|
+
{% else %}
|
|
14
|
+
{% set buttonText = "Continue" %}
|
|
15
|
+
{% endif %}
|
|
16
|
+
|
|
9
17
|
<div class="govuk-button-group">
|
|
10
18
|
{{ govukButton({
|
|
11
|
-
text:
|
|
19
|
+
text: buttonText,
|
|
12
20
|
isStartButton: isStartPage,
|
|
13
21
|
preventDoubleClick: true
|
|
14
22
|
}) }}
|
|
@@ -6,6 +6,7 @@ import { type createServer } from '~/src/server/index.js'
|
|
|
6
6
|
import {
|
|
7
7
|
type AnyFormRequest,
|
|
8
8
|
type AnyRequest,
|
|
9
|
+
type FormConfirmationState,
|
|
9
10
|
type FormPayload,
|
|
10
11
|
type FormState,
|
|
11
12
|
type FormSubmissionError,
|
|
@@ -55,7 +56,7 @@ export class CacheService {
|
|
|
55
56
|
|
|
56
57
|
async getConfirmationState(
|
|
57
58
|
request: AnyFormRequest
|
|
58
|
-
): Promise<
|
|
59
|
+
): Promise<FormConfirmationState> {
|
|
59
60
|
const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)
|
|
60
61
|
const value = await this.cache.get(key)
|
|
61
62
|
|
|
@@ -64,7 +65,7 @@ export class CacheService {
|
|
|
64
65
|
|
|
65
66
|
async setConfirmationState(
|
|
66
67
|
request: AnyFormRequest,
|
|
67
|
-
confirmationState:
|
|
68
|
+
confirmationState: FormConfirmationState
|
|
68
69
|
) {
|
|
69
70
|
const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)
|
|
70
71
|
const ttl = config.get('confirmationSessionTimeout')
|
package/src/server/types.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { type CacheService } from '~/src/server/services/cacheService.js'
|
|
|
23
23
|
|
|
24
24
|
export interface FormsService {
|
|
25
25
|
getFormMetadata: (slug: string) => Promise<FormMetadata>
|
|
26
|
+
getFormMetadataById: (id: string) => Promise<FormMetadata>
|
|
26
27
|
getFormDefinition: (
|
|
27
28
|
id: string,
|
|
28
29
|
state: FormStatus
|
|
@@ -97,6 +97,23 @@ export class FileFormService {
|
|
|
97
97
|
return metadata
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Get the form metadata by form id
|
|
102
|
+
* @param {string} id - the form id
|
|
103
|
+
* @returns {FormMetadata}
|
|
104
|
+
*/
|
|
105
|
+
getFormMetadataById(id) {
|
|
106
|
+
const metadata = Array.from(this.#metadata.values()).find(
|
|
107
|
+
(form) => form.id === id
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if (!metadata) {
|
|
111
|
+
throw new Error(`Form metadata id '${id}' not found`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return metadata
|
|
115
|
+
}
|
|
116
|
+
|
|
100
117
|
/**
|
|
101
118
|
* Get the form defintion by id
|
|
102
119
|
* @param {string} id - the form id
|
|
@@ -127,6 +144,15 @@ export class FileFormService {
|
|
|
127
144
|
return Promise.resolve(this.getFormMetadata(slug))
|
|
128
145
|
},
|
|
129
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Get the form metadata by form id
|
|
149
|
+
* @param {string} id
|
|
150
|
+
* @returns {Promise<FormMetadata>}
|
|
151
|
+
*/
|
|
152
|
+
getFormMetadataById: (id) => {
|
|
153
|
+
return Promise.resolve(this.getFormMetadataById(id))
|
|
154
|
+
},
|
|
155
|
+
|
|
130
156
|
/**
|
|
131
157
|
* Get the form defintion by id
|
|
132
158
|
* @param {string} id
|
|
@@ -140,5 +166,5 @@ export class FileFormService {
|
|
|
140
166
|
}
|
|
141
167
|
|
|
142
168
|
/**
|
|
143
|
-
* @import { FormMetadata, FormDefinition } from '@defra/forms-model'
|
|
169
|
+
* @import { FormMetadata, FormDefinition, FormStatus } from '@defra/forms-model'
|
|
144
170
|
*/
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { FormStatus } from '~/src/server/routes/types.js'
|
|
4
|
+
import { FileFormService } from '~/src/server/utils/file-form-service.js'
|
|
5
|
+
|
|
6
|
+
describe('File-form-service', () => {
|
|
7
|
+
/** @type {FileFormService} */
|
|
8
|
+
let service
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
const now = new Date()
|
|
11
|
+
const user = { id: 'user', displayName: 'Username' }
|
|
12
|
+
const author = {
|
|
13
|
+
createdAt: now,
|
|
14
|
+
createdBy: user,
|
|
15
|
+
updatedAt: now,
|
|
16
|
+
updatedBy: user
|
|
17
|
+
}
|
|
18
|
+
service = new FileFormService()
|
|
19
|
+
const metadata = {
|
|
20
|
+
organisation: 'Defra',
|
|
21
|
+
teamName: 'Team name',
|
|
22
|
+
teamEmail: 'team@defra.gov.uk',
|
|
23
|
+
submissionGuidance: "Thanks for your submission, we'll be in touch",
|
|
24
|
+
notificationEmail: 'email@domain.com',
|
|
25
|
+
...author,
|
|
26
|
+
live: author
|
|
27
|
+
}
|
|
28
|
+
await service.addForm(
|
|
29
|
+
`${join(import.meta.dirname, '../../../test/form/definitions')}/components.json`,
|
|
30
|
+
{
|
|
31
|
+
...metadata,
|
|
32
|
+
id: '95e92559-968d-44ae-8666-2b1ad3dffd31',
|
|
33
|
+
title: 'Form test',
|
|
34
|
+
slug: 'form-test'
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('metadata by slug', () => {
|
|
40
|
+
it('should get form metadata by slug', () => {
|
|
41
|
+
const meta = service.getFormMetadata('form-test')
|
|
42
|
+
expect(meta.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')
|
|
43
|
+
expect(meta.title).toBe('Form test')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should throw if not found', () => {
|
|
47
|
+
expect(() => service.getFormMetadata('form-test-missing')).toThrow(
|
|
48
|
+
"Form metadata 'form-test-missing' not found"
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('metadata by id', () => {
|
|
54
|
+
it('should get form metadata by id', () => {
|
|
55
|
+
const meta = service.getFormMetadataById(
|
|
56
|
+
'95e92559-968d-44ae-8666-2b1ad3dffd31'
|
|
57
|
+
)
|
|
58
|
+
expect(meta.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')
|
|
59
|
+
expect(meta.title).toBe('Form test')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should throw if not found', () => {
|
|
63
|
+
expect(() => service.getFormMetadataById('id-missing')).toThrow(
|
|
64
|
+
"Form metadata id 'id-missing' not found"
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('definition by id', () => {
|
|
70
|
+
it('should get form definition by id', () => {
|
|
71
|
+
const form = service.getFormDefinition(
|
|
72
|
+
'95e92559-968d-44ae-8666-2b1ad3dffd31'
|
|
73
|
+
)
|
|
74
|
+
expect(form.name).toBe('All components')
|
|
75
|
+
expect(form.startPage).toBe('/all-components')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should throw if not found', () => {
|
|
79
|
+
expect(() => service.getFormDefinition('id-missing')).toThrow(
|
|
80
|
+
"Form definition 'id-missing' not found"
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('toFormsService', () => {
|
|
86
|
+
it('should create interface', async () => {
|
|
87
|
+
const interfaceImpl = service.toFormsService()
|
|
88
|
+
const res1 = await interfaceImpl.getFormMetadata('form-test')
|
|
89
|
+
expect(res1.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')
|
|
90
|
+
expect(res1.title).toBe('Form test')
|
|
91
|
+
|
|
92
|
+
const res2 = await interfaceImpl.getFormMetadataById(
|
|
93
|
+
'95e92559-968d-44ae-8666-2b1ad3dffd31'
|
|
94
|
+
)
|
|
95
|
+
expect(res2.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')
|
|
96
|
+
expect(res2.title).toBe('Form test')
|
|
97
|
+
|
|
98
|
+
const res3 = await interfaceImpl.getFormDefinition(
|
|
99
|
+
'95e92559-968d-44ae-8666-2b1ad3dffd31',
|
|
100
|
+
FormStatus.Draft
|
|
101
|
+
)
|
|
102
|
+
expect(res3?.name).toBe('All components')
|
|
103
|
+
expect(res3?.startPage).toBe('/all-components')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('readForm', () => {
|
|
108
|
+
it('should throw if invalid extension', async () => {
|
|
109
|
+
await expect(
|
|
110
|
+
service.readForm('/some-folder/some-file.bad')
|
|
111
|
+
).rejects.toThrow("Invalid file extension '.bad'")
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
})
|