@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.
Files changed (58) hide show
  1. package/README.md +52 -1
  2. package/docs/Dynamic Sections.md +47 -0
  3. package/package.json +1 -1
  4. package/src/components/manage-list/question.d.ts.map +1 -1
  5. package/src/components/manage-list/question.js +5 -0
  6. package/src/components/manage-list/question.test.js +3 -1
  7. package/src/components/multi-field-input/question.d.ts.map +1 -1
  8. package/src/components/multi-field-input/question.js +7 -5
  9. package/src/components/multi-field-input/question.test.js +126 -213
  10. package/src/components/number-entry/index.njk +2 -3
  11. package/src/components/number-entry/question.d.ts +3 -2
  12. package/src/components/number-entry/question.d.ts.map +1 -1
  13. package/src/components/number-entry/question.js +25 -2
  14. package/src/components/number-entry/question.test.js +65 -0
  15. package/src/components/text-entry-redact/question.d.ts.map +1 -1
  16. package/src/components/text-entry-redact/question.js +3 -1
  17. package/src/components/text-entry-redact/question.test.js +9 -0
  18. package/src/components/unit-option-entry/question.d.ts.map +1 -1
  19. package/src/components/unit-option-entry/question.js +3 -1
  20. package/src/components/unit-option-entry/question.test.js +9 -0
  21. package/src/controller.d.ts +15 -3
  22. package/src/controller.d.ts.map +1 -1
  23. package/src/controller.js +19 -11
  24. package/src/controller.test.js +182 -153
  25. package/src/dynamic-section.d.ts +40 -0
  26. package/src/dynamic-section.d.ts.map +1 -0
  27. package/src/dynamic-section.js +51 -0
  28. package/src/dynamic-section.test.js +113 -0
  29. package/src/index.d.ts +2 -1
  30. package/src/index.js +2 -1
  31. package/src/index.test.js +3 -0
  32. package/src/journey/journey.d.ts +10 -0
  33. package/src/journey/journey.d.ts.map +1 -1
  34. package/src/journey/journey.js +15 -0
  35. package/src/lib/answer-utils.d.ts +22 -0
  36. package/src/lib/answer-utils.d.ts.map +1 -0
  37. package/src/lib/answer-utils.js +34 -0
  38. package/src/{components/manage-list/utils.test.js → lib/answer-utils.test.js} +9 -11
  39. package/src/lib/session-answer-store.d.ts.map +1 -1
  40. package/src/lib/session-answer-store.js +14 -6
  41. package/src/lib/session-answer-store.test.js +43 -0
  42. package/src/middleware/redirect-to-unanswered-question.d.ts.map +1 -1
  43. package/src/middleware/redirect-to-unanswered-question.js +4 -3
  44. package/src/middleware/redirect-to-unanswered-question.test.js +32 -0
  45. package/src/questions/question.d.ts +8 -3
  46. package/src/questions/question.d.ts.map +1 -1
  47. package/src/questions/question.js +16 -6
  48. package/src/questions/question.test.js +19 -0
  49. package/src/section.d.ts +19 -0
  50. package/src/section.d.ts.map +1 -1
  51. package/src/section.js +50 -9
  52. package/src/section.test.js +9 -0
  53. package/test/dynamic-journey.test.js +372 -0
  54. package/typedefs/question-props.d.ts +3 -2
  55. package/typedefs/question-types.d.ts +3 -0
  56. package/src/components/manage-list/utils.d.ts +0 -18
  57. package/src/components/manage-list/utils.d.ts.map +0 -1
  58. package/src/components/manage-list/utils.js +0 -35
@@ -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
- name: 'Section 1',
23
- segment: 'segment-1',
24
- getStatus: () => {
25
- return SECTION_STATUS.COMPLETE;
26
- },
27
- isComplete: () => {
28
- return true;
29
- },
30
- questions: [
31
- {
32
- title: 'Title 1a',
33
- question: 'Why?',
34
- taskList: true,
35
- fieldName: 'title-1a',
36
- shouldDisplay: () => true,
37
- formatAnswerForSummary: mock.fn(() => [
38
- {
39
- key: 'Title 1a',
40
- value: mockAnswer,
41
- action: {
42
- href: '/manage-appeals/questionnaire/123456/segment-1/title-1a',
43
- text: 'Answer',
44
- visuallyHiddenText: 'Answer'
45
- }
46
- }
47
- ])
48
- },
49
- {
50
- title: 'Title 1b',
51
- question: 'Who?',
52
- taskList: false,
53
- fieldName: 'title-1b',
54
- shouldDisplay: () => true,
55
- formatAnswerForSummary: mock.fn(() => [
56
- {
57
- key: 'Title 1b',
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
- questions: [
79
- {
80
- title: 'Title 2a',
81
- question: 'How?',
82
- taskList: true,
83
- fieldName: 'title-2a',
84
- shouldDisplay: () => true,
85
- formatAnswerForSummary: () => [
86
- {
87
- key: 'Title 2a',
88
- value: mockAnswer,
89
- action: {
90
- href: '/manage-appeals/questionnaire/123456/segment-2/title-2a',
91
- text: 'Answer',
92
- visuallyHiddenText: 'Answer'
93
- }
94
- }
95
- ]
96
- },
97
- {
98
- title: 'Title 2b',
99
- question: 'What?',
100
- taskList: true,
101
- fieldName: 'title-2b',
102
- shouldDisplay: () => true,
103
- formatAnswerForSummary: () => [
104
- {
105
- key: 'Title 2b',
106
- value: mockAnswer,
107
- action: {
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
- isComplete: mock.fn(),
124
- questions: [
125
- {
126
- title: 'Title 3a',
127
- question: 'When?',
128
- taskList: false,
129
- fieldName: 'title-3a',
130
- shouldDisplay: () => true,
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: mockAnswer,
178
+ value: 'Yes',
242
179
  action: [
243
180
  {
244
181
  href: '/manage-appeals/questionnaire/123456/segment-1/title-1a',
245
- text: 'Answer',
246
- visuallyHiddenText: 'Answer'
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
- [sampleQuestionObj.fieldName]: 'my-answer',
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: 'Answer',
838
- visuallyHiddenText: 'Answer'
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: 'Not started' }
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: 'Answer'
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: 'Answer'
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: 'Answer'
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 { Section } from './section.js';
58
+ export * from './dynamic-section.js';
59
+ export * from './section.js';
59
60
 
60
61
  // Validators
61
62
  export * from './validator/address-validator.js';