@planning-inspectorate/dynamic-forms 3.21.0 → 3.23.0
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 +52 -1
- package/docs/Dynamic Sections.md +47 -0
- package/package.json +1 -1
- package/src/components/manage-list/question.d.ts.map +1 -1
- package/src/components/manage-list/question.js +5 -0
- package/src/components/manage-list/question.test.js +3 -1
- package/src/components/multi-field-input/question.d.ts.map +1 -1
- package/src/components/multi-field-input/question.js +7 -5
- package/src/components/multi-field-input/question.test.js +126 -213
- package/src/components/number-entry/index.njk +2 -3
- package/src/components/number-entry/question.d.ts +3 -2
- package/src/components/number-entry/question.d.ts.map +1 -1
- package/src/components/number-entry/question.js +25 -2
- package/src/components/number-entry/question.test.js +65 -0
- package/src/components/text-entry-redact/question.d.ts.map +1 -1
- package/src/components/text-entry-redact/question.js +3 -1
- package/src/components/text-entry-redact/question.test.js +9 -0
- package/src/components/unit-option-entry/question.d.ts.map +1 -1
- package/src/components/unit-option-entry/question.js +3 -1
- package/src/components/unit-option-entry/question.test.js +9 -0
- package/src/controller.d.ts +15 -3
- package/src/controller.d.ts.map +1 -1
- package/src/controller.js +19 -11
- package/src/controller.test.js +182 -153
- package/src/dynamic-section.d.ts +40 -0
- package/src/dynamic-section.d.ts.map +1 -0
- package/src/dynamic-section.js +51 -0
- package/src/dynamic-section.test.js +113 -0
- package/src/index.d.ts +2 -1
- package/src/index.js +2 -1
- package/src/index.test.js +3 -0
- package/src/journey/journey.d.ts +10 -0
- package/src/journey/journey.d.ts.map +1 -1
- package/src/journey/journey.js +15 -0
- package/src/lib/answer-utils.d.ts +22 -0
- package/src/lib/answer-utils.d.ts.map +1 -0
- package/src/lib/answer-utils.js +34 -0
- package/src/{components/manage-list/utils.test.js → lib/answer-utils.test.js} +9 -11
- package/src/lib/session-answer-store.d.ts.map +1 -1
- package/src/lib/session-answer-store.js +14 -6
- package/src/lib/session-answer-store.test.js +43 -0
- package/src/middleware/redirect-to-unanswered-question.d.ts.map +1 -1
- package/src/middleware/redirect-to-unanswered-question.js +4 -3
- package/src/middleware/redirect-to-unanswered-question.test.js +32 -0
- package/src/questions/question.d.ts +8 -3
- package/src/questions/question.d.ts.map +1 -1
- package/src/questions/question.js +16 -6
- package/src/questions/question.test.js +19 -0
- package/src/section.d.ts +19 -0
- package/src/section.d.ts.map +1 -1
- package/src/section.js +50 -9
- package/src/section.test.js +9 -0
- package/test/dynamic-journey.test.js +372 -0
- package/typedefs/question-props.d.ts +3 -2
- package/typedefs/question-types.d.ts +3 -0
- package/src/components/manage-list/utils.d.ts +0 -18
- package/src/components/manage-list/utils.d.ts.map +0 -1
- package/src/components/manage-list/utils.js +0 -35
package/src/controller.test.js
CHANGED
|
@@ -3,11 +3,13 @@ import assert from 'node:assert';
|
|
|
3
3
|
import { list, question, buildSave } from './controller.js';
|
|
4
4
|
|
|
5
5
|
import { Journey } from './journey/journey.js';
|
|
6
|
-
import { SECTION_STATUS } from './section.js';
|
|
6
|
+
import { Section, SECTION_STATUS } from './section.js';
|
|
7
7
|
|
|
8
8
|
import { mockReq, mockRes } from '#test/utils/utils.js';
|
|
9
9
|
import { BOOLEAN_OPTIONS } from '#src/components/boolean/question.js';
|
|
10
10
|
import { MANAGE_LIST_ACTIONS } from '#src/components/manage-list/manage-list-actions.js';
|
|
11
|
+
import { DynamicSection } from '#src/dynamic-section.js';
|
|
12
|
+
import { Question } from '#question';
|
|
11
13
|
|
|
12
14
|
const mockBaseUrl = '/manage-appeals/questionnaire';
|
|
13
15
|
const mockRef = '123456';
|
|
@@ -18,148 +20,83 @@ const mockAnswer = 'Not started';
|
|
|
18
20
|
|
|
19
21
|
function testSetup() {
|
|
20
22
|
const sections = [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
value: mockAnswer,
|
|
59
|
-
action: {
|
|
60
|
-
href: '/manage-appeals/questionnaire/123456/segment-1/title-1b',
|
|
61
|
-
text: 'Answer',
|
|
62
|
-
visuallyHiddenText: 'Answer'
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
])
|
|
66
|
-
}
|
|
67
|
-
]
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
name: 'Section 2',
|
|
71
|
-
segment: 'segment-2',
|
|
72
|
-
getStatus: () => {
|
|
73
|
-
return SECTION_STATUS.IN_PROGRESS;
|
|
74
|
-
},
|
|
75
|
-
isComplete: () => {
|
|
76
|
-
return true;
|
|
23
|
+
new Section('Section 1', 'segment-1'),
|
|
24
|
+
new Section('Section 2', 'segment-2'),
|
|
25
|
+
new DynamicSection('Section 3', 'segment-3', 'arrayOfAnswers')
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Create a new question and set taskList if needed
|
|
29
|
+
* Also mocks formatAnswerForSummary so it can be overridden later.
|
|
30
|
+
* Default mock uses the original implementation
|
|
31
|
+
*
|
|
32
|
+
* @param {import('#typedefs/question-types.d.ts').QuestionParameters} params
|
|
33
|
+
* @param {boolean} [taskList]
|
|
34
|
+
* @returns {Question}
|
|
35
|
+
*/
|
|
36
|
+
const newQuestion = (params, taskList) => {
|
|
37
|
+
const q = new Question(params);
|
|
38
|
+
if (typeof taskList === 'boolean') {
|
|
39
|
+
q.taskList = taskList;
|
|
40
|
+
}
|
|
41
|
+
mock.method(q, 'formatAnswerForSummary');
|
|
42
|
+
return q;
|
|
43
|
+
};
|
|
44
|
+
// mock/override what we need
|
|
45
|
+
mock.method(sections[0], 'getStatus', () => SECTION_STATUS.COMPLETE);
|
|
46
|
+
mock.method(sections[0], 'isComplete', () => true);
|
|
47
|
+
sections[0].questions = [
|
|
48
|
+
newQuestion({
|
|
49
|
+
title: 'Title 1a',
|
|
50
|
+
question: 'Why?',
|
|
51
|
+
fieldName: 'title-1a',
|
|
52
|
+
viewFolder: 'mock'
|
|
53
|
+
}),
|
|
54
|
+
newQuestion(
|
|
55
|
+
{
|
|
56
|
+
title: 'Title 1b',
|
|
57
|
+
question: 'Who?',
|
|
58
|
+
fieldName: 'title-1b',
|
|
59
|
+
viewFolder: 'mock'
|
|
77
60
|
},
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
href: '/manage-appeals/questionnaire/123456/segment-2/title-2b',
|
|
109
|
-
text: 'Answer',
|
|
110
|
-
visuallyHiddenText: 'Answer'
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
]
|
|
114
|
-
}
|
|
115
|
-
]
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
name: 'Section 3',
|
|
119
|
-
segment: 'segment-3',
|
|
120
|
-
getStatus: () => {
|
|
121
|
-
return SECTION_STATUS.NOT_STARTED;
|
|
61
|
+
false
|
|
62
|
+
)
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
mock.method(sections[1], 'getStatus', () => SECTION_STATUS.IN_PROGRESS);
|
|
66
|
+
mock.method(sections[1], 'isComplete', () => true);
|
|
67
|
+
sections[1].questions = [
|
|
68
|
+
newQuestion({
|
|
69
|
+
title: 'Title 2a',
|
|
70
|
+
question: 'How?',
|
|
71
|
+
fieldName: 'title-2a',
|
|
72
|
+
viewFolder: 'mock'
|
|
73
|
+
}),
|
|
74
|
+
newQuestion({
|
|
75
|
+
title: 'Title 2b',
|
|
76
|
+
question: 'What?',
|
|
77
|
+
fieldName: 'title-2b',
|
|
78
|
+
viewFolder: 'mock'
|
|
79
|
+
})
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
mock.method(sections[2], 'getStatus', () => SECTION_STATUS.NOT_STARTED);
|
|
83
|
+
mock.method(sections[2], 'isComplete', () => false);
|
|
84
|
+
sections[2].questions = [
|
|
85
|
+
newQuestion(
|
|
86
|
+
{
|
|
87
|
+
title: 'Title 3a',
|
|
88
|
+
question: 'When?',
|
|
89
|
+
fieldName: 'title-3a',
|
|
90
|
+
viewFolder: 'mock'
|
|
122
91
|
},
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
formatAnswerForSummary: () => [
|
|
132
|
-
{
|
|
133
|
-
key: 'Title 3a',
|
|
134
|
-
value: mockAnswer,
|
|
135
|
-
action: {
|
|
136
|
-
href: '/manage-appeals/questionnaire/123456/segment-3/title-3a',
|
|
137
|
-
text: 'Answer',
|
|
138
|
-
visuallyHiddenText: 'Answer'
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
]
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
title: 'Title 3b',
|
|
145
|
-
question: 'Really?',
|
|
146
|
-
taskList: true,
|
|
147
|
-
fieldName: 'title-3b',
|
|
148
|
-
shouldDisplay: () => true,
|
|
149
|
-
formatAnswerForSummary: () => [
|
|
150
|
-
{
|
|
151
|
-
key: 'Title 3b',
|
|
152
|
-
value: mockAnswer,
|
|
153
|
-
action: {
|
|
154
|
-
href: '/manage-appeals/questionnaire/123456/segment-3/title-3b',
|
|
155
|
-
text: 'Answer',
|
|
156
|
-
visuallyHiddenText: 'Answer'
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
]
|
|
160
|
-
}
|
|
161
|
-
]
|
|
162
|
-
}
|
|
92
|
+
false
|
|
93
|
+
),
|
|
94
|
+
newQuestion({
|
|
95
|
+
title: 'Title 3b',
|
|
96
|
+
question: 'Really?',
|
|
97
|
+
fieldName: 'title-3b',
|
|
98
|
+
viewFolder: 'mock'
|
|
99
|
+
})
|
|
163
100
|
];
|
|
164
101
|
|
|
165
102
|
const journeyParams = {
|
|
@@ -186,7 +123,7 @@ function testSetup() {
|
|
|
186
123
|
|
|
187
124
|
const mockJourney = new Journey({ response: mockResponse, ...journeyParams });
|
|
188
125
|
|
|
189
|
-
res.locals.journeyResponse =
|
|
126
|
+
res.locals.journeyResponse = mockResponse;
|
|
190
127
|
res.locals.journey = mockJourney;
|
|
191
128
|
|
|
192
129
|
const sampleQuestionObj = {
|
|
@@ -238,12 +175,12 @@ describe('dynamic-form/controller', () => {
|
|
|
238
175
|
mockJourney.sections[0].questions[0].formatAnswerForSummary.mock.mockImplementation(() => [
|
|
239
176
|
{
|
|
240
177
|
key: 'Title 1a',
|
|
241
|
-
value:
|
|
178
|
+
value: 'Yes',
|
|
242
179
|
action: [
|
|
243
180
|
{
|
|
244
181
|
href: '/manage-appeals/questionnaire/123456/segment-1/title-1a',
|
|
245
|
-
text: '
|
|
246
|
-
visuallyHiddenText: '
|
|
182
|
+
text: 'Change',
|
|
183
|
+
visuallyHiddenText: 'Why?'
|
|
247
184
|
},
|
|
248
185
|
{
|
|
249
186
|
href: '/manage-appeals/questionnaire/123456/segment-1/title-1a/edit',
|
|
@@ -344,6 +281,35 @@ describe('dynamic-form/controller', () => {
|
|
|
344
281
|
visuallyHiddenText: 'Answer'
|
|
345
282
|
});
|
|
346
283
|
});
|
|
284
|
+
|
|
285
|
+
it('should render dynamic sections answers', async () => {
|
|
286
|
+
const { req, res, mockJourney } = testSetup();
|
|
287
|
+
const dynamicSection = mockJourney.sections[2];
|
|
288
|
+
|
|
289
|
+
// show all questions
|
|
290
|
+
dynamicSection.questions[0].taskList = true;
|
|
291
|
+
mockJourney.response.answers = {
|
|
292
|
+
arrayOfAnswers: [
|
|
293
|
+
{ id: 'id-1', 'title-3a': 'First answer', 'title-3b': 'Second answer' },
|
|
294
|
+
// should match these answers - based on section segment
|
|
295
|
+
{ id: 'segment-3', 'title-3a': 'Another answer', 'title-3b': 'A fourth answer' }
|
|
296
|
+
]
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
await list(req, res, '', {});
|
|
300
|
+
|
|
301
|
+
const mockFn = res.render.mock;
|
|
302
|
+
assert.strictEqual(mockFn.callCount(), 1);
|
|
303
|
+
const args = mockFn.calls[0].arguments[1];
|
|
304
|
+
assert.ok(args.summaryListData);
|
|
305
|
+
const section = args.summaryListData.sections[2];
|
|
306
|
+
assert.strictEqual(section.heading, 'Section 3');
|
|
307
|
+
const rows = section.list.rows;
|
|
308
|
+
assert.strictEqual(rows.length, 2);
|
|
309
|
+
// answers extracted from the array
|
|
310
|
+
assert.strictEqual(rows[0].value.html, 'Another answer');
|
|
311
|
+
assert.strictEqual(rows[1].value.html, 'A fourth answer');
|
|
312
|
+
});
|
|
347
313
|
});
|
|
348
314
|
|
|
349
315
|
describe('question', () => {
|
|
@@ -562,6 +528,9 @@ describe('dynamic-form/controller', () => {
|
|
|
562
528
|
isManageListItem: undefined,
|
|
563
529
|
manageListItemRemove: false,
|
|
564
530
|
manageListQuestionFieldName: undefined,
|
|
531
|
+
isDynamicSection: false,
|
|
532
|
+
dynamicSectionId: undefined,
|
|
533
|
+
dynamicSectionFieldName: undefined,
|
|
565
534
|
data: undefined
|
|
566
535
|
}
|
|
567
536
|
]);
|
|
@@ -623,7 +592,10 @@ describe('dynamic-form/controller', () => {
|
|
|
623
592
|
data: undefined,
|
|
624
593
|
isManageListItem: undefined,
|
|
625
594
|
manageListItemRemove: false,
|
|
626
|
-
manageListQuestionFieldName: undefined
|
|
595
|
+
manageListQuestionFieldName: undefined,
|
|
596
|
+
isDynamicSection: false,
|
|
597
|
+
dynamicSectionId: undefined,
|
|
598
|
+
dynamicSectionFieldName: undefined
|
|
627
599
|
}
|
|
628
600
|
],
|
|
629
601
|
'save data args'
|
|
@@ -678,7 +650,7 @@ describe('dynamic-form/controller', () => {
|
|
|
678
650
|
};
|
|
679
651
|
sampleQuestionObjWithActions.getDataToSave.mock.mockImplementationOnce(() => ({
|
|
680
652
|
answers: {
|
|
681
|
-
[
|
|
653
|
+
[sampleQuestionObjWithActions.fieldName]: 'my-answer',
|
|
682
654
|
aBooleanToTest: true
|
|
683
655
|
}
|
|
684
656
|
}));
|
|
@@ -749,6 +721,63 @@ describe('dynamic-form/controller', () => {
|
|
|
749
721
|
assert.strictEqual(Object.keys(res.locals.journeyResponse.answers).length, 0);
|
|
750
722
|
});
|
|
751
723
|
|
|
724
|
+
it('should call saveData with dynamic section properties', async () => {
|
|
725
|
+
const { req, res, mockJourney, sampleQuestionObj, journeyParams } = testSetup();
|
|
726
|
+
const journeyId = 'has-questionnaire';
|
|
727
|
+
const dynamicSection = mockJourney.sections[2];
|
|
728
|
+
|
|
729
|
+
sampleQuestionObj.getDataToSave.mock.mockImplementationOnce(() => ({
|
|
730
|
+
answers: {
|
|
731
|
+
[sampleQuestionObj.fieldName]: 'my-answer'
|
|
732
|
+
}
|
|
733
|
+
}));
|
|
734
|
+
|
|
735
|
+
req.params = {
|
|
736
|
+
referenceId: mockRef,
|
|
737
|
+
section: dynamicSection.segment,
|
|
738
|
+
question: dynamicSection.questions[0].fieldName
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
res.locals.journeyResponse = {
|
|
742
|
+
...journeyParams,
|
|
743
|
+
answers: {}
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
req.body = {
|
|
747
|
+
sampleFieldName: true,
|
|
748
|
+
sampleFieldName_sub: 'send this',
|
|
749
|
+
notSampleFieldName: 'do not send this'
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
mockJourney.getQuestionByParams = mock.fn();
|
|
753
|
+
mockJourney.getQuestionByParams.mock.mockImplementationOnce(() => sampleQuestionObj);
|
|
754
|
+
|
|
755
|
+
const saveData = mock.fn();
|
|
756
|
+
await buildSave(saveData)(req, res, journeyId);
|
|
757
|
+
|
|
758
|
+
assert.strictEqual(sampleQuestionObj.getDataToSave.mock.callCount(), 1);
|
|
759
|
+
assert.strictEqual(saveData.mock.callCount(), 1);
|
|
760
|
+
assert.deepStrictEqual(saveData.mock.calls[0].arguments, [
|
|
761
|
+
{
|
|
762
|
+
req,
|
|
763
|
+
res,
|
|
764
|
+
journeyId: journeyParams.journeyId,
|
|
765
|
+
referenceId: journeyParams.referenceId,
|
|
766
|
+
isManageListItem: undefined,
|
|
767
|
+
manageListItemRemove: false,
|
|
768
|
+
manageListQuestionFieldName: undefined,
|
|
769
|
+
isDynamicSection: true,
|
|
770
|
+
dynamicSectionId: 'segment-3',
|
|
771
|
+
dynamicSectionFieldName: 'arrayOfAnswers',
|
|
772
|
+
data: { answers: { [sampleQuestionObj.fieldName]: 'my-answer' } }
|
|
773
|
+
}
|
|
774
|
+
]);
|
|
775
|
+
// journey response should be edited if redirecting to next question
|
|
776
|
+
assert.deepStrictEqual(res.locals.journeyResponse.answers?.arrayOfAnswers, [
|
|
777
|
+
{ id: 'segment-3', [sampleQuestionObj.fieldName]: 'my-answer' }
|
|
778
|
+
]);
|
|
779
|
+
});
|
|
780
|
+
|
|
752
781
|
describe('manageListQuestions', () => {
|
|
753
782
|
const setupJourney = () => {
|
|
754
783
|
const q = {
|
|
@@ -834,13 +863,13 @@ const _getmockSummaryListData = (mockJourney) => {
|
|
|
834
863
|
items: [
|
|
835
864
|
{
|
|
836
865
|
href: `${mockBaseUrl}/${mockRef}/${mockJourney.sections[0].segment}/${mockJourney.sections[0].questions[0].fieldName}`,
|
|
837
|
-
text: '
|
|
838
|
-
visuallyHiddenText:
|
|
866
|
+
text: 'Change',
|
|
867
|
+
visuallyHiddenText: mockJourney.sections[0].questions[0].question
|
|
839
868
|
}
|
|
840
869
|
]
|
|
841
870
|
},
|
|
842
871
|
key: { text: mockJourney.sections[0].questions[0].title },
|
|
843
|
-
value: { html: '
|
|
872
|
+
value: { html: 'Yes' }
|
|
844
873
|
}
|
|
845
874
|
]
|
|
846
875
|
}
|
|
@@ -856,7 +885,7 @@ const _getmockSummaryListData = (mockJourney) => {
|
|
|
856
885
|
{
|
|
857
886
|
href: `${mockBaseUrl}/${mockRef}/${mockJourney.sections[1].segment}/${mockJourney.sections[1].questions[0].fieldName}`,
|
|
858
887
|
text: 'Answer',
|
|
859
|
-
visuallyHiddenText:
|
|
888
|
+
visuallyHiddenText: mockJourney.sections[1].questions[0].question
|
|
860
889
|
}
|
|
861
890
|
]
|
|
862
891
|
},
|
|
@@ -869,7 +898,7 @@ const _getmockSummaryListData = (mockJourney) => {
|
|
|
869
898
|
{
|
|
870
899
|
href: `${mockBaseUrl}/${mockRef}/${mockJourney.sections[1].segment}/${mockJourney.sections[1].questions[1].fieldName}`,
|
|
871
900
|
text: 'Answer',
|
|
872
|
-
visuallyHiddenText:
|
|
901
|
+
visuallyHiddenText: mockJourney.sections[1].questions[1].question
|
|
873
902
|
}
|
|
874
903
|
]
|
|
875
904
|
},
|
|
@@ -890,7 +919,7 @@ const _getmockSummaryListData = (mockJourney) => {
|
|
|
890
919
|
{
|
|
891
920
|
href: `${mockBaseUrl}/${mockRef}/${mockJourney.sections[2].segment}/${mockJourney.sections[2].questions[1].fieldName}`,
|
|
892
921
|
text: 'Answer',
|
|
893
|
-
visuallyHiddenText:
|
|
922
|
+
visuallyHiddenText: mockJourney.sections[2].questions[1].question
|
|
894
923
|
}
|
|
895
924
|
]
|
|
896
925
|
},
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use a dynamic section for sections which are generated from an array of objects
|
|
3
|
+
*
|
|
4
|
+
* This can be useful for adding a section for each entry in a ManageList, or each row in a database
|
|
5
|
+
* The controller logic, when encountering a DynamicSection, will check for an answer object within
|
|
6
|
+
* `journeyResponse.answers[fieldName]`, matching by the `id` property
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* answers = {
|
|
10
|
+
* myField: 'some answer',
|
|
11
|
+
* list: [
|
|
12
|
+
* { id: 'person-id-1', name: 'Grogu' },
|
|
13
|
+
* { id: 'person-id-2', name: 'Mando' }
|
|
14
|
+
* ]
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* In this example, create a DynamicSection for each list entry:
|
|
19
|
+
*
|
|
20
|
+
* ```
|
|
21
|
+
* function createPersonSection(person) {
|
|
22
|
+
* return new DynamicSection(person.name, person.id, 'list')
|
|
23
|
+
* .addQuestion(questions.name)
|
|
24
|
+
* // ... add questions
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* When using with buildSave, be sure to handle the parameters: `isDynamicSection`, `dynamicSectionId`,`dynamicSectionFieldName`
|
|
29
|
+
*/
|
|
30
|
+
export class DynamicSection extends Section {
|
|
31
|
+
/**
|
|
32
|
+
* @param name
|
|
33
|
+
* @param segment
|
|
34
|
+
* @param fieldName the key used in the answers object, which will be an array of answers for this section
|
|
35
|
+
*/
|
|
36
|
+
constructor(name: any, segment: any, fieldName: any);
|
|
37
|
+
fieldName: any;
|
|
38
|
+
}
|
|
39
|
+
import { Section } from '#section';
|
|
40
|
+
//# sourceMappingURL=dynamic-section.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-section.d.ts","sourceRoot":"","sources":["dynamic-section.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH;IACC;;;;OAIG;IACH,qDAIC;IADA,eAA0B;CAU3B;wBAlDuB,UAAU"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Section } from '#section';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Use a dynamic section for sections which are generated from an array of objects
|
|
5
|
+
*
|
|
6
|
+
* This can be useful for adding a section for each entry in a ManageList, or each row in a database
|
|
7
|
+
* The controller logic, when encountering a DynamicSection, will check for an answer object within
|
|
8
|
+
* `journeyResponse.answers[fieldName]`, matching by the `id` property
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* answers = {
|
|
12
|
+
* myField: 'some answer',
|
|
13
|
+
* list: [
|
|
14
|
+
* { id: 'person-id-1', name: 'Grogu' },
|
|
15
|
+
* { id: 'person-id-2', name: 'Mando' }
|
|
16
|
+
* ]
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* In this example, create a DynamicSection for each list entry:
|
|
21
|
+
*
|
|
22
|
+
* ```
|
|
23
|
+
* function createPersonSection(person) {
|
|
24
|
+
* return new DynamicSection(person.name, person.id, 'list')
|
|
25
|
+
* .addQuestion(questions.name)
|
|
26
|
+
* // ... add questions
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* When using with buildSave, be sure to handle the parameters: `isDynamicSection`, `dynamicSectionId`,`dynamicSectionFieldName`
|
|
31
|
+
*/
|
|
32
|
+
export class DynamicSection extends Section {
|
|
33
|
+
/**
|
|
34
|
+
* @param name
|
|
35
|
+
* @param segment
|
|
36
|
+
* @param fieldName the key used in the answers object, which will be an array of answers for this section
|
|
37
|
+
*/
|
|
38
|
+
constructor(name, segment, fieldName) {
|
|
39
|
+
super(name, segment);
|
|
40
|
+
|
|
41
|
+
this.fieldName = fieldName;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Indicate that this section is dynamic, and answers should be pulled from an array
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
get isDynamicSection() {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, it, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { DynamicSection } from '#src/dynamic-section.js';
|
|
4
|
+
import { SECTION_STATUS } from '#section';
|
|
5
|
+
import { Question } from '#question';
|
|
6
|
+
|
|
7
|
+
// use a real question to avoid mocking lots of logic
|
|
8
|
+
const mockQuestion = (fieldName = 'fieldName', required = true) =>
|
|
9
|
+
new Question({
|
|
10
|
+
fieldName: fieldName,
|
|
11
|
+
title: 'question',
|
|
12
|
+
question: 'question',
|
|
13
|
+
viewFolder: 'folder',
|
|
14
|
+
validators: [
|
|
15
|
+
{
|
|
16
|
+
isRequired() {
|
|
17
|
+
return required;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('dynamic-section', () => {
|
|
24
|
+
describe('responseForSection', () => {
|
|
25
|
+
it('should return inner object for dynamic section', () => {
|
|
26
|
+
const section = new DynamicSection('s1', 'id-1', 'myList');
|
|
27
|
+
const answers = { id: 'id-1', myField: 'answer' };
|
|
28
|
+
const res = {
|
|
29
|
+
answers: {
|
|
30
|
+
myList: [{ id: 'id-2' }, { id: 'id-3' }, answers]
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const got = section.getResponse(res);
|
|
34
|
+
assert.strictEqual(got?.answers, answers);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('getStatus', () => {
|
|
39
|
+
it('should use section answers for status', () => {
|
|
40
|
+
const s1Answers = { id: 'S', fieldOne: 'answer' };
|
|
41
|
+
const mockJourneyResponse = {
|
|
42
|
+
answers: {
|
|
43
|
+
visitFrequently: 'Answer 1',
|
|
44
|
+
anotherFieldName: 'Answer 2',
|
|
45
|
+
myList: [s1Answers]
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const requiredQuestion = mockQuestion('visitFrequently');
|
|
50
|
+
|
|
51
|
+
const section = new DynamicSection('s1', 'S', 'myList');
|
|
52
|
+
section.addQuestion(requiredQuestion);
|
|
53
|
+
|
|
54
|
+
mock.method(requiredQuestion, 'shouldDisplay');
|
|
55
|
+
mock.method(requiredQuestion, 'isAnswered');
|
|
56
|
+
|
|
57
|
+
section.getStatus(mockJourneyResponse);
|
|
58
|
+
|
|
59
|
+
assert.strictEqual(requiredQuestion.shouldDisplay.mock.callCount(), 1);
|
|
60
|
+
assert.strictEqual(requiredQuestion.isAnswered.mock.callCount(), 2);
|
|
61
|
+
assert.strictEqual(requiredQuestion.shouldDisplay.mock.calls[0].arguments[0].answers, s1Answers);
|
|
62
|
+
assert.strictEqual(requiredQuestion.isAnswered.mock.calls[0].arguments[0].answers, s1Answers);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should return NOT_STARTED when no answers are given', () => {
|
|
66
|
+
const mockJourneyResponse = {
|
|
67
|
+
answers: {}
|
|
68
|
+
};
|
|
69
|
+
const section = new DynamicSection('s1', 'S', 'myList');
|
|
70
|
+
section.addQuestion(mockQuestion());
|
|
71
|
+
const result = section.getStatus(mockJourneyResponse);
|
|
72
|
+
assert.strictEqual(result, SECTION_STATUS.NOT_STARTED);
|
|
73
|
+
const isComplete = section.isComplete(mockJourneyResponse);
|
|
74
|
+
assert.strictEqual(isComplete, false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return IN_PROGRESS when at least one answer is given', () => {
|
|
78
|
+
const mockJourneyResponse = {
|
|
79
|
+
answers: {
|
|
80
|
+
myList: [{ id: 'S', visitFrequently: 'Answer 1' }]
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const section = new DynamicSection('s1', 'S', 'myList');
|
|
85
|
+
section.addQuestion(mockQuestion());
|
|
86
|
+
section.addQuestion(mockQuestion('visitFrequently'));
|
|
87
|
+
const result = section.getStatus(mockJourneyResponse);
|
|
88
|
+
assert.strictEqual(result, SECTION_STATUS.IN_PROGRESS);
|
|
89
|
+
const isComplete = section.isComplete(mockJourneyResponse);
|
|
90
|
+
assert.strictEqual(isComplete, false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should return COMPLETE when all required answers are given', () => {
|
|
94
|
+
const s1Answers = { id: 'S', visitFrequently: 'answer', anotherFieldName: true };
|
|
95
|
+
const mockJourneyResponse = {
|
|
96
|
+
answers: {
|
|
97
|
+
myList: [s1Answers]
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const section = new DynamicSection('s1', 'S', 'myList');
|
|
102
|
+
section.addQuestion(mockQuestion('visitFrequently'));
|
|
103
|
+
section.addQuestion(mockQuestion('anotherFieldName'));
|
|
104
|
+
section.addQuestion(mockQuestion('someQuestion', false));
|
|
105
|
+
|
|
106
|
+
const result = section.getStatus(mockJourneyResponse);
|
|
107
|
+
assert.strictEqual(result, SECTION_STATUS.COMPLETE);
|
|
108
|
+
|
|
109
|
+
const isComplete = section.isComplete(mockJourneyResponse);
|
|
110
|
+
assert.strictEqual(isComplete, true);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
package/src/index.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export * from "./lib/utils.js";
|
|
|
32
32
|
export * from "./middleware/build-get-journey.js";
|
|
33
33
|
export * from "./middleware/redirect-to-unanswered-question.js";
|
|
34
34
|
export * from "./questions/options-question.js";
|
|
35
|
+
export * from "./dynamic-section.js";
|
|
36
|
+
export * from "./section.js";
|
|
35
37
|
export * from "./validator/address-validator.js";
|
|
36
38
|
export * from "./validator/base-validator.js";
|
|
37
39
|
export * from "./validator/conditional-required-validator.js";
|
|
@@ -58,6 +60,5 @@ export { JourneyResponse } from "./journey/journey-response.js";
|
|
|
58
60
|
export { Question } from "./questions/question.js";
|
|
59
61
|
export { createQuestions } from "./questions/create-questions.js";
|
|
60
62
|
export { questionClasses } from "./questions/questions.js";
|
|
61
|
-
export { Section } from "./section.js";
|
|
62
63
|
export { default as validate } from "./validator/validator.js";
|
|
63
64
|
//# sourceMappingURL=index.d.ts.map
|
package/src/index.js
CHANGED
|
@@ -55,7 +55,8 @@ export * from './questions/options-question.js';
|
|
|
55
55
|
export { questionClasses } from './questions/questions.js';
|
|
56
56
|
|
|
57
57
|
// Section
|
|
58
|
-
export
|
|
58
|
+
export * from './dynamic-section.js';
|
|
59
|
+
export * from './section.js';
|
|
59
60
|
|
|
60
61
|
// Validators
|
|
61
62
|
export * from './validator/address-validator.js';
|