@planning-inspectorate/dynamic-forms 3.2.0 → 3.3.1
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/README.md +9 -9
- package/package.json +1 -1
- package/src/components/address/question.js +2 -4
- package/src/components/boolean/question.js +6 -11
- package/src/components/date/question.js +5 -11
- package/src/components/date-period/question.js +5 -9
- package/src/components/date-time/question.js +5 -8
- package/src/components/date-time/question.test.js +3 -24
- package/src/components/manage-list/README.md +21 -2
- package/src/components/manage-list/index.njk +3 -3
- package/src/components/manage-list/question.js +24 -15
- package/src/components/manage-list/question.test.js +38 -1
- package/src/components/manage-list/utils.js +35 -0
- package/src/components/manage-list/utils.test.js +80 -0
- package/src/components/multi-field-input/question.js +7 -11
- package/src/components/multi-field-input/question.test.js +29 -1
- package/src/components/text-entry-redact/question.js +3 -4
- package/src/components/unit-option-entry/question.js +6 -13
- package/src/components/utils/question-has-answer.js +45 -11
- package/src/components/utils/question-utils.js +3 -37
- package/src/controller.js +11 -0
- package/src/controller.test.js +13 -1
- package/src/journey/journey.js +1 -0
- package/src/lib/utils.js +11 -0
- package/src/lib/utils.test.js +19 -0
- package/src/lib/uuid.test.js +0 -12
- package/src/questions/options-question.js +6 -13
- package/src/questions/question.js +17 -31
- package/src/questions/question.test.js +5 -19
- package/src/section-types.d.ts +2 -0
- package/src/section.js +5 -1
- package/src/section.test.js +7 -3
- package/test/check-your-answers.test.js +1 -1
- package/test/mock/uuid.js +13 -0
- package/test/questions.test.js +3 -200
- package/test/utils/question-test-utils.js +200 -0
- package/src/components/macros/answer-list.njk +0 -29
- package/src/middleware/check-not-submitted.js +0 -13
- package/src/middleware/check-not-submitted.test.js +0 -118
- package/src/middleware/dynamic-req-files-to-req-body-files.js +0 -46
- package/src/middleware/dynamic-req-files-to-req-body-files.test.js +0 -221
- package/src/validator/required-file-upload-validator.js +0 -38
- package/src/validator/required-file-upload-validator.test.js +0 -141
- package/src/validator/schemas/required-file-upload-schema.js +0 -35
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { describe, it, mock, beforeEach } from 'node:test';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
import dynamicReqFilesToReqBodyFilesMiddleware from './dynamic-req-files-to-req-body-files.js';
|
|
4
|
-
|
|
5
|
-
describe('dynamic-forms/middleware/dynamic-req-files-to-req-body-files', () => {
|
|
6
|
-
let counter = 0;
|
|
7
|
-
let mockReq;
|
|
8
|
-
let mockRes;
|
|
9
|
-
let filesPropertyPath = `example-files-property-path-${counter}`;
|
|
10
|
-
|
|
11
|
-
const mockgetQuestionByParams = mock.fn(() => {
|
|
12
|
-
return { fieldName: filesPropertyPath };
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
mockReq = () => ({
|
|
17
|
-
cookies: {},
|
|
18
|
-
log: {},
|
|
19
|
-
params: {}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
mockRes = () => ({
|
|
23
|
-
clearCookie: mock.fn(),
|
|
24
|
-
cookie: mock.fn(),
|
|
25
|
-
locals: {
|
|
26
|
-
journeyResponse: mock.fn(),
|
|
27
|
-
journey: { getQuestionByParams: mockgetQuestionByParams }
|
|
28
|
-
},
|
|
29
|
-
redirect: mock.fn(),
|
|
30
|
-
render: mock.fn(),
|
|
31
|
-
sendStatus: mock.fn(),
|
|
32
|
-
status: mock.fn()
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
counter++;
|
|
36
|
-
filesPropertyPath = `example-files-property-path-${counter}`;
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
[
|
|
40
|
-
{
|
|
41
|
-
description: 'should return early if `req.body` is not set',
|
|
42
|
-
given: () => mockReq(),
|
|
43
|
-
expected: (req, res, next) => {
|
|
44
|
-
assert.strictEqual(req.body, undefined);
|
|
45
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
description: 'should return early if `req.files` is not set',
|
|
50
|
-
given: () => ({
|
|
51
|
-
...mockReq(),
|
|
52
|
-
body: {}
|
|
53
|
-
}),
|
|
54
|
-
expected: (req, res, next) => {
|
|
55
|
-
assert.strictEqual(req.body.files, undefined);
|
|
56
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
description: 'should return early if `req.params` is not set',
|
|
61
|
-
given: () => ({
|
|
62
|
-
cookies: {},
|
|
63
|
-
log: {},
|
|
64
|
-
body: {},
|
|
65
|
-
files: {}
|
|
66
|
-
}),
|
|
67
|
-
expected: (req, res, next) => {
|
|
68
|
-
assert.strictEqual(req.body.files, undefined);
|
|
69
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
description:
|
|
74
|
-
'should set `req.body.files = []` if unable to find the given file property path `req.files` - unset',
|
|
75
|
-
given: () => ({
|
|
76
|
-
...mockReq(),
|
|
77
|
-
body: {},
|
|
78
|
-
files: {},
|
|
79
|
-
params: {
|
|
80
|
-
section: 'mock-section',
|
|
81
|
-
question: 'mock-question'
|
|
82
|
-
}
|
|
83
|
-
}),
|
|
84
|
-
expected: (req, res, next) => {
|
|
85
|
-
assert.deepStrictEqual(req.body.files, []);
|
|
86
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
description:
|
|
91
|
-
'should set `req.body.files = []` if unable to find the given file property path `req.files` - wrong path',
|
|
92
|
-
given: () => ({
|
|
93
|
-
...mockReq(),
|
|
94
|
-
body: {},
|
|
95
|
-
files: {
|
|
96
|
-
'some-different-path': {
|
|
97
|
-
x: 'y'
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
params: {
|
|
101
|
-
section: 'mock-section',
|
|
102
|
-
question: 'mock-question'
|
|
103
|
-
}
|
|
104
|
-
}),
|
|
105
|
-
expected: (req, res, next) => {
|
|
106
|
-
assert.deepStrictEqual(req.body.files, []);
|
|
107
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
description: 'should set `req.body.files = [req.files]` when given a single file',
|
|
112
|
-
given: () => ({
|
|
113
|
-
...mockReq(),
|
|
114
|
-
body: {},
|
|
115
|
-
params: {
|
|
116
|
-
section: 'mock-section',
|
|
117
|
-
question: 'mock-question'
|
|
118
|
-
},
|
|
119
|
-
files: {
|
|
120
|
-
[filesPropertyPath]: {
|
|
121
|
-
a: 'b'
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}),
|
|
125
|
-
expected: (req, res, next) => {
|
|
126
|
-
assert.deepStrictEqual(req.body.files, { [filesPropertyPath]: [{ a: 'b' }] });
|
|
127
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
description: 'should set `req.body.files = [...req.files]` when given multiple files',
|
|
132
|
-
given: () => ({
|
|
133
|
-
...mockReq(),
|
|
134
|
-
body: {},
|
|
135
|
-
params: {
|
|
136
|
-
section: 'mock-section',
|
|
137
|
-
question: 'mock-question'
|
|
138
|
-
},
|
|
139
|
-
files: {
|
|
140
|
-
[filesPropertyPath]: [
|
|
141
|
-
{
|
|
142
|
-
a: 'b'
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
c: 'd'
|
|
146
|
-
}
|
|
147
|
-
]
|
|
148
|
-
}
|
|
149
|
-
}),
|
|
150
|
-
expected: (req, res, next) => {
|
|
151
|
-
assert.deepStrictEqual(req.body.files, {
|
|
152
|
-
[filesPropertyPath]: [
|
|
153
|
-
{
|
|
154
|
-
a: 'b'
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
c: 'd'
|
|
158
|
-
}
|
|
159
|
-
]
|
|
160
|
-
});
|
|
161
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
162
|
-
}
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
description: 'should retain existing req.body.files when working with a different key',
|
|
166
|
-
given: () => ({
|
|
167
|
-
...mockReq(),
|
|
168
|
-
body: {
|
|
169
|
-
files: {
|
|
170
|
-
'an-existing-key-value-pair': [
|
|
171
|
-
{
|
|
172
|
-
rgb: 'xyz'
|
|
173
|
-
}
|
|
174
|
-
]
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
files: {
|
|
178
|
-
[filesPropertyPath]: [
|
|
179
|
-
{
|
|
180
|
-
a: 'b'
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
c: 'd'
|
|
184
|
-
}
|
|
185
|
-
]
|
|
186
|
-
},
|
|
187
|
-
params: {
|
|
188
|
-
section: 'mock-section',
|
|
189
|
-
question: 'mock-question'
|
|
190
|
-
}
|
|
191
|
-
}),
|
|
192
|
-
expected: (req, res, next) => {
|
|
193
|
-
assert.deepStrictEqual(req.body.files, {
|
|
194
|
-
'an-existing-key-value-pair': [
|
|
195
|
-
{
|
|
196
|
-
rgb: 'xyz'
|
|
197
|
-
}
|
|
198
|
-
],
|
|
199
|
-
[filesPropertyPath]: [
|
|
200
|
-
{
|
|
201
|
-
a: 'b'
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
c: 'd'
|
|
205
|
-
}
|
|
206
|
-
]
|
|
207
|
-
});
|
|
208
|
-
assert.strictEqual(next.mock.callCount(), 1);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
].forEach(({ description, given, expected }) => {
|
|
212
|
-
it(description, () => {
|
|
213
|
-
const next = mock.fn();
|
|
214
|
-
const req = given();
|
|
215
|
-
|
|
216
|
-
dynamicReqFilesToReqBodyFilesMiddleware()(req, mockRes(), next);
|
|
217
|
-
|
|
218
|
-
expected(req, mockRes(), next);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
});
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import BaseValidator from './base-validator.js';
|
|
2
|
-
import { checkSchema } from 'express-validator';
|
|
3
|
-
|
|
4
|
-
import requiredFileUploadSchema from './schemas/required-file-upload-schema.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @typedef {import('../questions/question.js')} Question
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* enforces that at least one file is present for a question, whether it is being/already uploaded
|
|
12
|
-
* @class
|
|
13
|
-
*/
|
|
14
|
-
export default class RequiredFileUploadValidator extends BaseValidator {
|
|
15
|
-
/**
|
|
16
|
-
* creates an instance of a RequiredFileUploadValidator
|
|
17
|
-
* @param {string} [errorMessage] - custom error message to show on validation failure
|
|
18
|
-
*/
|
|
19
|
-
constructor(errorMessage) {
|
|
20
|
-
super();
|
|
21
|
-
|
|
22
|
-
if (errorMessage) {
|
|
23
|
-
this.errorMessage = errorMessage;
|
|
24
|
-
} else {
|
|
25
|
-
this.errorMessage = 'You must add your documents';
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* validates against path based on questionObj's fieldname
|
|
31
|
-
* @param {Question} questionObj
|
|
32
|
-
*/
|
|
33
|
-
validate(questionObj, journeyResponse) {
|
|
34
|
-
const path = questionObj.fieldName;
|
|
35
|
-
const documentType = questionObj.documentType.name;
|
|
36
|
-
return checkSchema(requiredFileUploadSchema(path, journeyResponse, documentType, this.errorMessage))[0];
|
|
37
|
-
}
|
|
38
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert';
|
|
3
|
-
import RequiredFileUploadValidator from './required-file-upload-validator.js';
|
|
4
|
-
|
|
5
|
-
describe('./src/dynamic-forms/validator/required-file-upload-validator.js', () => {
|
|
6
|
-
describe('validate', () => {
|
|
7
|
-
it('should have a rule for path featuring fieldName', () => {
|
|
8
|
-
const requiredFileUploadValidator = new RequiredFileUploadValidator();
|
|
9
|
-
|
|
10
|
-
const rule = requiredFileUploadValidator
|
|
11
|
-
.validate({
|
|
12
|
-
fieldName: 'test-field-name',
|
|
13
|
-
documentType: {
|
|
14
|
-
name: 'test-doc-type'
|
|
15
|
-
}
|
|
16
|
-
})
|
|
17
|
-
.builder.build();
|
|
18
|
-
|
|
19
|
-
assert.deepStrictEqual(rule.fields, ['test-field-name']);
|
|
20
|
-
assert.strictEqual(!!rule.optional, false);
|
|
21
|
-
assert.strictEqual(rule.stack.length, 1);
|
|
22
|
-
assert.strictEqual(rule.stack[0].validator.name, 'options');
|
|
23
|
-
assert.strictEqual(!!rule.stack[0].validator.negated, false);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should pass if file being uploaded', async () => {
|
|
27
|
-
const questionObj = {
|
|
28
|
-
fieldName: 'test-fieldname',
|
|
29
|
-
documentType: {
|
|
30
|
-
name: 'test-doc-type'
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
const journeyResponse = {};
|
|
34
|
-
const req = { files: { test: 'test123' } };
|
|
35
|
-
|
|
36
|
-
const requiredFileUploadValidator = new RequiredFileUploadValidator();
|
|
37
|
-
const validationResult = await requiredFileUploadValidator.validate(questionObj, journeyResponse).run(req);
|
|
38
|
-
|
|
39
|
-
assert.strictEqual(validationResult.errors.length, 0);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should pass if no file being uploaded and file matching question document type already uploaded', async () => {
|
|
43
|
-
const questionObj = {
|
|
44
|
-
fieldName: 'another-test-fieldname',
|
|
45
|
-
documentType: {
|
|
46
|
-
name: 'test-doc-type'
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
const uploadedFile = {
|
|
50
|
-
file: 'already-uploaded',
|
|
51
|
-
type: 'test-doc-type'
|
|
52
|
-
};
|
|
53
|
-
const journeyResponse = {
|
|
54
|
-
answers: { SubmissionDocumentUpload: [uploadedFile] }
|
|
55
|
-
};
|
|
56
|
-
const req = { body: {} };
|
|
57
|
-
|
|
58
|
-
const requiredFileUploadValidator = new RequiredFileUploadValidator();
|
|
59
|
-
const validationResult = await requiredFileUploadValidator.validate(questionObj, journeyResponse).run(req);
|
|
60
|
-
|
|
61
|
-
assert.strictEqual(validationResult.errors.length, 0);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should fail if no file being uploaded and no file matching question document type already uploaded', async () => {
|
|
65
|
-
const questionObj = {
|
|
66
|
-
fieldName: 'yet-another-test-fieldname',
|
|
67
|
-
documentType: {
|
|
68
|
-
name: 'test-doc-type'
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
const journeyResponse = { answers: { SubmissionDocumentUpload: [] } };
|
|
72
|
-
const req = { body: {} };
|
|
73
|
-
|
|
74
|
-
const requiredFileUploadValidator = new RequiredFileUploadValidator();
|
|
75
|
-
const validationResult = await requiredFileUploadValidator.validate(questionObj, journeyResponse).run(req);
|
|
76
|
-
|
|
77
|
-
assert.strictEqual(validationResult.errors.length, 1);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should fail if no file being uploaded matching question doc type and all existing files matching doc type being removed', async () => {
|
|
81
|
-
const questionObj = {
|
|
82
|
-
fieldName: 'test-fieldname',
|
|
83
|
-
documentType: {
|
|
84
|
-
name: 'test-doc-type'
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const journeyResponse = {
|
|
89
|
-
answers: {
|
|
90
|
-
SubmissionDocumentUpload: [
|
|
91
|
-
{
|
|
92
|
-
file: 'to-be-removed',
|
|
93
|
-
type: 'test-doc-type'
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
file: 'to-be-removed-too',
|
|
97
|
-
type: 'test-doc-type'
|
|
98
|
-
}
|
|
99
|
-
]
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
const req = { body: {} };
|
|
103
|
-
req.body.removedFiles =
|
|
104
|
-
'[{"file": "to-be-removed", "type": "test-doc-type"}, {"file": "to-be-removed-too", "type": "test-doc-type"}]';
|
|
105
|
-
const requiredFileUploadValidator = new RequiredFileUploadValidator();
|
|
106
|
-
const validationResult = await requiredFileUploadValidator.validate(questionObj, journeyResponse).run(req);
|
|
107
|
-
|
|
108
|
-
assert.strictEqual(validationResult.errors.length, 1);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should pass if existing files matching question doc type and only some being removed', async () => {
|
|
112
|
-
const questionObj = {
|
|
113
|
-
fieldName: 'test-fieldname',
|
|
114
|
-
documentType: {
|
|
115
|
-
name: 'test-doc-type'
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
const journeyResponse = {
|
|
119
|
-
answers: {
|
|
120
|
-
SubmissionDocumentUpload: [
|
|
121
|
-
{
|
|
122
|
-
file: 'to-be-removed',
|
|
123
|
-
type: 'test-doc-type'
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
file: 'to-be-kept',
|
|
127
|
-
type: 'test-doc-type'
|
|
128
|
-
}
|
|
129
|
-
]
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
const req = { body: {} };
|
|
133
|
-
req.body.removedFiles = '[{"file": "to-be-removed", "type": "test-doc-type"}]';
|
|
134
|
-
|
|
135
|
-
const requiredFileUploadValidator = new RequiredFileUploadValidator();
|
|
136
|
-
const validationResult = await requiredFileUploadValidator.validate(questionObj, journeyResponse).run(req);
|
|
137
|
-
|
|
138
|
-
assert.strictEqual(validationResult.errors.length, 0);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
const schema = (path, journeyResponse, documentType, errorMsg = 'Select a file to upload') => ({
|
|
2
|
-
[path]: {
|
|
3
|
-
custom: {
|
|
4
|
-
options: async (value, { req }) => {
|
|
5
|
-
if (!req.files) {
|
|
6
|
-
const uploadedFiles = journeyResponse.answers.SubmissionDocumentUpload || [];
|
|
7
|
-
|
|
8
|
-
const relevantUploadedFiles = uploadedFiles.filter((upload) => upload.type === documentType);
|
|
9
|
-
|
|
10
|
-
if (relevantUploadedFiles) {
|
|
11
|
-
//at this stage, uploadedFiles will still contain any files flagged for removal
|
|
12
|
-
//so we need to disregard them when checking uploadedFiles
|
|
13
|
-
let removedFiles = [];
|
|
14
|
-
|
|
15
|
-
if ('removedFiles' in req.body) {
|
|
16
|
-
removedFiles = JSON.parse(req.body.removedFiles) || [];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (relevantUploadedFiles.length - removedFiles.length < 1) {
|
|
20
|
-
throw new Error(errorMsg);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
throw new Error(errorMsg);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
export default schema;
|