@planning-inspectorate/dynamic-forms 3.16.0 → 3.17.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 (62) hide show
  1. package/README.md +112 -1
  2. package/package.json +1 -1
  3. package/src/components/address/question.d.ts +1 -9
  4. package/src/components/address/question.d.ts.map +1 -1
  5. package/src/components/address/question.js +6 -17
  6. package/src/components/checkbox/question.d.ts +1 -21
  7. package/src/components/checkbox/question.d.ts.map +1 -1
  8. package/src/components/checkbox/question.js +36 -49
  9. package/src/components/checkbox/question.test.js +170 -27
  10. package/src/components/date/question.d.ts +0 -8
  11. package/src/components/date/question.d.ts.map +1 -1
  12. package/src/components/date/question.js +6 -14
  13. package/src/components/date-period/question.d.ts +1 -5
  14. package/src/components/date-period/question.d.ts.map +1 -1
  15. package/src/components/date-period/question.js +1 -1
  16. package/src/components/date-time/question.d.ts +0 -8
  17. package/src/components/date-time/question.d.ts.map +1 -1
  18. package/src/components/date-time/question.js +4 -12
  19. package/src/components/manage-list/answer-summary-list.njk +1 -1
  20. package/src/components/manage-list/question.d.ts +1 -5
  21. package/src/components/manage-list/question.d.ts.map +1 -1
  22. package/src/components/manage-list/question.js +1 -1
  23. package/src/components/number-entry/question.d.ts +0 -28
  24. package/src/components/number-entry/question.d.ts.map +1 -1
  25. package/src/components/number-entry/question.js +3 -21
  26. package/src/components/radio/question.d.ts +1 -9
  27. package/src/components/radio/question.d.ts.map +1 -1
  28. package/src/components/radio/question.js +29 -46
  29. package/src/components/radio/question.test.js +139 -60
  30. package/src/components/select/question.d.ts +1 -15
  31. package/src/components/select/question.d.ts.map +1 -1
  32. package/src/components/select/question.js +4 -47
  33. package/src/components/single-line-input/question.d.ts +0 -4
  34. package/src/components/single-line-input/question.d.ts.map +1 -1
  35. package/src/components/single-line-input/question.js +1 -1
  36. package/src/components/text-entry/question.d.ts +0 -4
  37. package/src/components/text-entry/question.d.ts.map +1 -1
  38. package/src/components/text-entry/question.js +1 -1
  39. package/src/components/text-entry-redact/question.d.ts +2 -6
  40. package/src/components/text-entry-redact/question.d.ts.map +1 -1
  41. package/src/components/text-entry-redact/question.js +1 -1
  42. package/src/components/utils/question-has-answer.js +1 -1
  43. package/src/components/utils/question-utils.d.ts +19 -1
  44. package/src/components/utils/question-utils.d.ts.map +1 -1
  45. package/src/components/utils/question-utils.js +31 -2
  46. package/src/components/utils/question-utils.test.js +89 -2
  47. package/src/questions/options-question.d.ts +4 -4
  48. package/src/questions/options-question.d.ts.map +1 -1
  49. package/src/questions/options-question.js +27 -2
  50. package/src/questions/options-question.test.js +106 -0
  51. package/src/questions/question.d.ts +16 -1
  52. package/src/questions/question.d.ts.map +1 -1
  53. package/src/questions/question.js +64 -9
  54. package/src/questions/question.test.js +86 -0
  55. package/test/journey.js +2 -0
  56. package/test/questions.js +50 -0
  57. package/test/snapshots/check-your-answers-not-started.html +22 -0
  58. package/test/snapshots/check-your-answers-with-answers.html +22 -0
  59. package/test/snapshots/favourite-activity-reason.html +54 -0
  60. package/test/snapshots/travel-requirements.html +48 -0
  61. package/test/utils/question-test-utils.js +36 -6
  62. package/typedefs/question-types.d.ts +27 -0
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAMA;IACC,kBAAiB;IACjB,kBAAiB;IAEjB;;OAEG;IACH,yDAFW,OAAO,+BAA+B,EAAE,sBAAsB,EASxE;IAFA,mBAA4B;IAC5B,mBAA4B;IAkB7B;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,eAAe,GACb,OAAO,CAAE,MAAM,CAAC,CAqB5B;IAED;;MAqCC;IAED;;OAEG;IACH;;;;QAQC;;CAsBD;;yBAxIwB,6BAA6B"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAMA;IACC,kBAAiB;IACjB,kBAAiB;IAEjB;;OAEG;IACH,yDAFW,OAAO,+BAA+B,EAAE,sBAAsB,EASxE;IAFA,mBAA4B;IAC5B,mBAA4B;IAkB7B;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,eAAe,GACb,OAAO,CAAE,MAAM,CAAC,CAqB5B;IAED;;MAqCC;;CA2BD;;yBAhIwB,6BAA6B"}
@@ -102,19 +102,11 @@ export class DateTimeQuestion extends Question {
102
102
  }
103
103
 
104
104
  /**
105
- * returns the formatted answers values to be used to build task list elements
105
+ * Formats a date-time answer for display in the summary.
106
+ * @param {unknown} answer - the date value
107
+ * @returns {string} the formatted date and time
106
108
  */
107
- formatAnswerForSummary(sectionSegment, journey, answer) {
108
- return [
109
- {
110
- key: this.title,
111
- value: this.#formatDateTimeValue(answer),
112
- action: this.getAction(sectionSegment, journey, answer)
113
- }
114
- ];
115
- }
116
-
117
- #formatDateTimeValue(answer) {
109
+ formatAnswer(answer) {
118
110
  if (!answer) return this.notStartedText;
119
111
 
120
112
  const formattedDate = formatDateForDisplay(answer, { format: this.dateFormat });
@@ -2,7 +2,7 @@
2
2
  {% for question in answers %}
3
3
  <li>
4
4
  {% for value in question %}
5
- {{ value.answer }}{% if not loop.last %}<br>{% endif %}
5
+ {{ value.answer | safe }}{% if not loop.last %}<br>{% endif %}
6
6
 
7
7
  {% endfor %}
8
8
  </li>
@@ -11,10 +11,6 @@ export class ManageListQuestion extends Question {
11
11
  */
12
12
  constructor(params: import("#typedefs/question-props.d.ts").ManageListQuestionParams);
13
13
  get confirmationQuestionParam(): string;
14
- /**
15
- * @param {import('#question').QuestionViewModel} viewModel
16
- */
17
- addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
18
14
  getDataToSave(req: any, journeyResponse: any): Promise<{
19
15
  answers: {
20
16
  [x: string]: any;
@@ -26,7 +22,7 @@ export class ManageListQuestion extends Question {
26
22
  formatAnswerForSummary(sectionSegment: any, journey: any, answer: any): {
27
23
  key: string;
28
24
  value: string;
29
- action: import("../../index.js").ActionView | import("../../index.js").ActionView[] | undefined;
25
+ action: import("#typedefs/question-types.d.ts").ActionView | import("#typedefs/question-types.d.ts").ActionView[] | undefined;
30
26
  }[];
31
27
  renderConfirmationAction(res: any, itemToRemove: any, viewModel: any): void;
32
28
  set section(section: import("../../section.js").Section);
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAKA;;;;;;GAMG;AAEH;IAOC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,wBAAwB,EAe1E;IAcD,wCAEC;IAED;;OAEG;IACH,oCAFW,OAAO,WAAW,EAAE,iBAAiB,QAmB/C;IAmCD;;;;OAMC;IAED;;OAEG;IACH;;;;QAsBC;IAED,4EAQC;IAoCD,yDAEC;IATD,kDAKC;;CAKD;;;;;;mBAxLa,MAAM;;;;;;;;;;;;;;yBAPK,WAAW"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAKA;;;;;;GAMG;AAEH;IAOC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,wBAAwB,EAe1E;IAcD,wCAEC;IAyDD;;;;OAMC;IAED;;OAEG;IACH;;;;QAsBC;IAED,4EAQC;IAoCD,yDAEC;IATD,kDAKC;;CAKD;;;;;;mBAxLa,MAAM;;;;;;;;;;;;;;yBAPK,WAAW"}
@@ -53,7 +53,7 @@ export class ManageListQuestion extends Question {
53
53
  }
54
54
 
55
55
  /**
56
- * @param {import('#question').QuestionViewModel} viewModel
56
+ * @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
57
57
  */
58
58
  addCustomDataToViewModel(viewModel) {
59
59
  viewModel.question.addAnotherLink = this.#addAnotherLink;
@@ -6,34 +6,6 @@ export class NumberEntryQuestion extends Question {
6
6
  suffix: string | undefined;
7
7
  label: string | undefined;
8
8
  answerForViewModel(answers: any): string | number;
9
- /**
10
- * @param {import('#question').QuestionViewModel} viewModel
11
- */
12
- addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
13
- /**
14
- * returns the formatted answers values to be used to build task list elements
15
- * @param {unknown} answer
16
- * @param {import('#journey').Journey} journey
17
- * @param {string} sectionSegment
18
- * @returns {Array<{
19
- * key: string;
20
- * value: string | Object;
21
- * action: {
22
- * href: string;
23
- * text: string;
24
- * visuallyHiddenText: string;
25
- * };
26
- * }>}
27
- */
28
- formatAnswerForSummary(sectionSegment: string, journey: import("#journey").Journey, answer: unknown): Array<{
29
- key: string;
30
- value: string | Object;
31
- action: {
32
- href: string;
33
- text: string;
34
- visuallyHiddenText: string;
35
- };
36
- }>;
37
9
  }
38
10
  export default NumberEntryQuestion;
39
11
  import { Question } from '#question';
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAGA;IACC;;OAEG;IACH,gDAFW,OAAO,+BAA+B,EAAE,yBAAyB,EAU3E;IAFA,2BAAoB;IACpB,0BAAkB;IAGnB,kDAEC;IAED;;OAEG;IACH,oCAFW,OAAO,WAAW,EAAE,iBAAiB,QAK/C;IAED;;;;;;;;;;;;;;OAcG;IACH,uCAXW,MAAM,WADN,OAAO,UAAU,EAAE,OAAO,UAD1B,OAAO,GAGL,KAAK,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QACvB,MAAM,EAAE;YACN,IAAI,EAAE,MAAM,CAAC;YACb,IAAI,EAAE,MAAM,CAAC;YACb,kBAAkB,EAAE,MAAM,CAAC;SAC5B,CAAC;KACH,CAAC,CAIJ;CACD;;yBA/CwB,WAAW"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAGA;IACC;;OAEG;IACH,gDAFW,OAAO,+BAA+B,EAAE,yBAAyB,EAW3E;IAFA,2BAAoB;IACpB,0BAAkB;IAGnB,kDAEC;CASD;;yBA7BwB,WAAW"}
@@ -8,7 +8,8 @@ export class NumberEntryQuestion extends Question {
8
8
  constructor({ label, suffix, ...parentParams }) {
9
9
  super({
10
10
  ...parentParams,
11
- viewFolder: 'number-entry'
11
+ viewFolder: 'number-entry',
12
+ capitaliseAnswer: false
12
13
  });
13
14
 
14
15
  this.suffix = suffix;
@@ -20,31 +21,12 @@ export class NumberEntryQuestion extends Question {
20
21
  }
21
22
 
22
23
  /**
23
- * @param {import('#question').QuestionViewModel} viewModel
24
+ * @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
24
25
  */
25
26
  addCustomDataToViewModel(viewModel) {
26
27
  viewModel.question.label = this.label;
27
28
  viewModel.question.suffix = this.suffix;
28
29
  }
29
-
30
- /**
31
- * returns the formatted answers values to be used to build task list elements
32
- * @param {unknown} answer
33
- * @param {import('#journey').Journey} journey
34
- * @param {string} sectionSegment
35
- * @returns {Array<{
36
- * key: string;
37
- * value: string | Object;
38
- * action: {
39
- * href: string;
40
- * text: string;
41
- * visuallyHiddenText: string;
42
- * };
43
- * }>}
44
- */
45
- formatAnswerForSummary(sectionSegment, journey, answer) {
46
- return super.formatAnswerForSummary(sectionSegment, journey, answer, false);
47
- }
48
30
  }
49
31
 
50
32
  export default NumberEntryQuestion;
@@ -2,17 +2,9 @@ export class RadioQuestion extends OptionsQuestion {
2
2
  /**
3
3
  * @param {import('#typedefs/question-props.d.ts').RadioQuestionParams} params
4
4
  */
5
- constructor({ title, question, fieldName, viewFolder, url, hint, pageTitle, description, label, html, legend, options, validators, actionLink, editable, viewData }: import("#typedefs/question-props.d.ts").RadioQuestionParams);
5
+ constructor({ label, html, legend, viewFolder, ...parentParams }: import("#typedefs/question-props.d.ts").RadioQuestionParams);
6
6
  label: string | undefined;
7
7
  legend: string | undefined;
8
- /**
9
- * @param {import('#question').QuestionViewModel} viewModel
10
- */
11
- addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
12
- /**
13
- * returns the formatted answers values to be used to build task list elements
14
- */
15
- formatAnswerForSummary(sectionSegment: any, journey: any, answer: any): import("../../index.js").SummaryRow[];
16
8
  }
17
9
  export default RadioQuestion;
18
10
  import OptionsQuestion from '../../questions/options-question.js';
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;IACC;;OAEG;IACH,qKAFW,OAAO,+BAA+B,EAAE,mBAAmB,EAuCrE;IAFA,0BAAkB;IAClB,2BAAoB;IAGrB;;OAEG;IACH,oCAFW,OAAO,WAAW,EAAE,iBAAiB,QAK/C;IAED;;OAEG;IACH,8GAcC;CACD;;4BAvE2B,qCAAqC"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAGA;IACC;;OAEG;IACH,kEAFW,OAAO,+BAA+B,EAAE,mBAAmB,EAWrE;IAFA,0BAAkB;IAClB,2BAAoB;CAuCrB;;4BArD2B,qCAAqC"}
@@ -1,41 +1,14 @@
1
+ import escape from 'escape-html';
1
2
  import OptionsQuestion from '../../questions/options-question.js';
2
3
 
3
4
  export class RadioQuestion extends OptionsQuestion {
4
5
  /**
5
6
  * @param {import('#typedefs/question-props.d.ts').RadioQuestionParams} params
6
7
  */
7
- constructor({
8
- title,
9
- question,
10
- fieldName,
11
- viewFolder,
12
- url,
13
- hint,
14
- pageTitle,
15
- description,
16
- label,
17
- html,
18
- legend,
19
- options,
20
- validators,
21
- actionLink,
22
- editable,
23
- viewData
24
- }) {
8
+ constructor({ label, html, legend, viewFolder, ...parentParams }) {
25
9
  super({
26
- title,
27
- question,
28
- viewFolder: !viewFolder ? 'radio' : viewFolder,
29
- fieldName,
30
- url,
31
- hint,
32
- pageTitle,
33
- description,
34
- options,
35
- validators,
36
- actionLink,
37
- editable,
38
- viewData
10
+ ...parentParams,
11
+ viewFolder: viewFolder || 'radio'
39
12
  });
40
13
 
41
14
  this.html = html;
@@ -44,7 +17,7 @@ export class RadioQuestion extends OptionsQuestion {
44
17
  }
45
18
 
46
19
  /**
47
- * @param {import('#question').QuestionViewModel} viewModel
20
+ * @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
48
21
  */
49
22
  addCustomDataToViewModel(viewModel) {
50
23
  viewModel.question.label = this.label;
@@ -52,22 +25,32 @@ export class RadioQuestion extends OptionsQuestion {
52
25
  }
53
26
 
54
27
  /**
55
- * returns the formatted answers values to be used to build task list elements
28
+ * Formats an answer value for display in the summary.
29
+ * Handles object answers with conditional fields (e.g. { value: 'yes', conditional: { yes: 'details' } })
30
+ *
31
+ * @param {unknown} answer - the raw answer value
32
+ * @returns {string} the formatted answer for display
56
33
  */
57
- formatAnswerForSummary(sectionSegment, journey, answer) {
58
- if (answer?.conditional) {
59
- const selectedOption = this.options.find((option) => option.value === answer.value);
60
- const conditionalAnswerText = selectedOption.conditional?.label
61
- ? `${selectedOption.conditional.label} ${answer.conditional}`
62
- : answer.conditional;
63
- const formattedAnswer = [selectedOption.text, conditionalAnswerText].join('\n');
64
- return super.formatAnswerForSummary(sectionSegment, journey, formattedAnswer, false);
65
- } else if (answer) {
66
- const selectedOption = this.options.find((option) => option.value === answer);
67
- const selectedText = selectedOption?.text || '';
68
- return super.formatAnswerForSummary(sectionSegment, journey, selectedText, false);
34
+ formatAnswer(answer) {
35
+ if (answer === null || answer === undefined || answer === '') {
36
+ return this.notStartedText;
69
37
  }
70
- return super.formatAnswerForSummary(sectionSegment, journey, answer);
38
+
39
+ // Handle simple string answers
40
+ if (typeof answer !== 'object' || answer.value === undefined) {
41
+ return super.formatAnswer(answer);
42
+ }
43
+
44
+ // Handle object answers with conditional fields
45
+ const option = this.options.find((opt) => opt.value === answer.value);
46
+ const optionText = escape(option ? option.text : answer.value);
47
+
48
+ const conditionalValue = answer.conditional?.[answer.value];
49
+ if (conditionalValue) {
50
+ const label = option?.conditional?.label ? `${escape(option.conditional.label)} ` : '';
51
+ return `${optionText}<br>${label}${escape(conditionalValue)}`;
52
+ }
53
+ return optionText;
71
54
  }
72
55
  }
73
56
 
@@ -96,78 +96,157 @@ describe('./src/dynamic-forms/components/radio/question.js', () => {
96
96
  assert.strictEqual(preppedQuestion.question.label, LABEL);
97
97
  });
98
98
 
99
- it('should customise answer for summary when question has a conditional field filled in', () => {
100
- const radioQuestion = new RadioQuestion({
101
- title: TITLE,
102
- question: QUESTION,
103
- description: DESCRIPTION,
104
- fieldName: FIELDNAME,
105
- viewFolder: VIEWFOLDER,
106
- html: HTML,
107
- label: LABEL,
108
- options: OPTIONS
99
+ describe('formatAnswer', () => {
100
+ it('should delegate simple string answers to parent class', () => {
101
+ const radioQuestion = new RadioQuestion({
102
+ title: TITLE,
103
+ question: QUESTION,
104
+ description: DESCRIPTION,
105
+ fieldName: FIELDNAME,
106
+ options: [
107
+ { text: 'Option 1', value: 'op-1' },
108
+ { text: 'Option 2', value: 'op-2' }
109
+ ]
110
+ });
111
+
112
+ const result = radioQuestion.formatAnswer('op-1');
113
+ assert.strictEqual(result, 'Option 1');
109
114
  });
110
115
 
111
- const rowParams = radioQuestion.formatAnswerForSummary(SECTION, JOURNEY, {
112
- value: 'yes',
113
- conditional: 'test'
116
+ it('should format object answer with value only (no conditional)', () => {
117
+ const radioQuestion = new RadioQuestion({
118
+ title: TITLE,
119
+ question: QUESTION,
120
+ description: DESCRIPTION,
121
+ fieldName: FIELDNAME,
122
+ options: OPTIONS
123
+ });
124
+
125
+ const result = radioQuestion.formatAnswer({ value: 'no' });
126
+ assert.strictEqual(result, 'No');
114
127
  });
115
- assert.strictEqual(rowParams[0].value, 'Yes<br>test');
116
- });
117
128
 
118
- it('should use option text (not value) for display', () => {
119
- const radioQuestion = new RadioQuestion({
120
- title: TITLE,
121
- question: QUESTION,
122
- description: DESCRIPTION,
123
- fieldName: FIELDNAME,
124
- viewFolder: VIEWFOLDER,
125
- html: HTML,
126
- label: LABEL,
127
- options: [
128
- { text: 'Option 1', value: 'op-1' },
129
- { text: 'Option 2', value: 'op-2' }
130
- ]
129
+ it('should format object answer with conditional', () => {
130
+ const radioQuestion = new RadioQuestion({
131
+ title: TITLE,
132
+ question: QUESTION,
133
+ description: DESCRIPTION,
134
+ fieldName: FIELDNAME,
135
+ options: OPTIONS
136
+ });
137
+
138
+ const result = radioQuestion.formatAnswer({
139
+ value: 'yes',
140
+ conditional: { yes: 'test details' }
141
+ });
142
+ assert.strictEqual(result, 'Yes<br>test details');
131
143
  });
132
144
 
133
- const rowParams = radioQuestion.formatAnswerForSummary(SECTION, JOURNEY, 'op-1');
134
- assert.strictEqual(rowParams[0].value, 'Option 1');
135
- });
145
+ it('should format object answer with conditional and label', () => {
146
+ const optionsWithLabel = [...OPTIONS];
147
+ optionsWithLabel[0] = {
148
+ ...optionsWithLabel[0],
149
+ conditional: {
150
+ ...optionsWithLabel[0].conditional,
151
+ label: 'label:'
152
+ }
153
+ };
154
+ const radioQuestion = new RadioQuestion({
155
+ title: TITLE,
156
+ question: QUESTION,
157
+ description: DESCRIPTION,
158
+ fieldName: FIELDNAME,
159
+ options: optionsWithLabel
160
+ });
161
+
162
+ const result = radioQuestion.formatAnswer({
163
+ value: 'yes',
164
+ conditional: { yes: 'test details' }
165
+ });
166
+ assert.strictEqual(result, 'Yes<br>label: test details');
167
+ });
136
168
 
137
- it('should customise answer for summary and add label when question has a conditional field filled in and conditional field has a label', () => {
138
- const optionsWithLabel = [...OPTIONS];
139
- optionsWithLabel[0].conditional.label = 'label:';
140
- const radioQuestion = new RadioQuestion({
141
- title: TITLE,
142
- question: QUESTION,
143
- description: DESCRIPTION,
144
- fieldName: FIELDNAME,
145
- viewFolder: VIEWFOLDER,
146
- html: HTML,
147
- label: LABEL,
148
- options: optionsWithLabel
169
+ it('should fall back to value when option not found in object answer', () => {
170
+ const radioQuestion = new RadioQuestion({
171
+ title: TITLE,
172
+ question: QUESTION,
173
+ description: DESCRIPTION,
174
+ fieldName: FIELDNAME,
175
+ options: OPTIONS
176
+ });
177
+
178
+ const result = radioQuestion.formatAnswer({ value: 'unknown' });
179
+ assert.strictEqual(result, 'unknown');
149
180
  });
150
181
 
151
- const rowParams = radioQuestion.formatAnswerForSummary(SECTION, JOURNEY, {
152
- value: 'yes',
153
- conditional: 'test'
182
+ it('should escape HTML in option text', () => {
183
+ const radioQuestion = new RadioQuestion({
184
+ title: TITLE,
185
+ question: QUESTION,
186
+ description: DESCRIPTION,
187
+ fieldName: FIELDNAME,
188
+ options: [{ text: '<script>alert("xss")</script>', value: 'xss' }]
189
+ });
190
+
191
+ const result = radioQuestion.formatAnswer('xss');
192
+ assert.strictEqual(result, '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;');
154
193
  });
155
- assert.strictEqual(rowParams[0].value, 'Yes<br>label: test');
156
- });
157
194
 
158
- it('should not customise answer for summary when question does not have a conditional field filled in', () => {
159
- const radioQuestion = new RadioQuestion({
160
- title: TITLE,
161
- question: QUESTION,
162
- description: DESCRIPTION,
163
- fieldName: FIELDNAME,
164
- viewFolder: VIEWFOLDER,
165
- html: HTML,
166
- label: LABEL,
167
- options: OPTIONS
195
+ it('should escape HTML in conditional value', () => {
196
+ const radioQuestion = new RadioQuestion({
197
+ title: TITLE,
198
+ question: QUESTION,
199
+ description: DESCRIPTION,
200
+ fieldName: FIELDNAME,
201
+ options: [
202
+ {
203
+ text: 'Yes',
204
+ value: 'yes',
205
+ conditional: { fieldName: 'details', type: 'text' }
206
+ }
207
+ ]
208
+ });
209
+
210
+ const result = radioQuestion.formatAnswer({
211
+ value: 'yes',
212
+ conditional: { yes: '<img src=x onerror=alert(1)>' }
213
+ });
214
+ assert.strictEqual(result, 'Yes<br>&lt;img src=x onerror=alert(1)&gt;');
168
215
  });
169
216
 
170
- const rowParams = radioQuestion.formatAnswerForSummary(SECTION, JOURNEY, 'no');
171
- assert.strictEqual(rowParams[0].value, 'No');
217
+ it('should escape HTML in conditional label', () => {
218
+ const radioQuestion = new RadioQuestion({
219
+ title: TITLE,
220
+ question: QUESTION,
221
+ description: DESCRIPTION,
222
+ fieldName: FIELDNAME,
223
+ options: [
224
+ {
225
+ text: 'Yes',
226
+ value: 'yes',
227
+ conditional: { fieldName: 'details', type: 'text', label: '<b>Label</b>' }
228
+ }
229
+ ]
230
+ });
231
+
232
+ const result = radioQuestion.formatAnswer({
233
+ value: 'yes',
234
+ conditional: { yes: 'safe value' }
235
+ });
236
+ assert.strictEqual(result, 'Yes<br>&lt;b&gt;Label&lt;/b&gt; safe value');
237
+ });
238
+
239
+ it('should escape HTML in fallback value when option not found', () => {
240
+ const radioQuestion = new RadioQuestion({
241
+ title: TITLE,
242
+ question: QUESTION,
243
+ description: DESCRIPTION,
244
+ fieldName: FIELDNAME,
245
+ options: OPTIONS
246
+ });
247
+
248
+ const result = radioQuestion.formatAnswer({ value: '<script>xss</script>' });
249
+ assert.strictEqual(result, '&lt;script&gt;xss&lt;/script&gt;');
250
+ });
172
251
  });
173
252
  });
@@ -2,23 +2,9 @@ export class SelectQuestion extends OptionsQuestion {
2
2
  /**
3
3
  * @param {import('#typedefs/question-props.d.ts').SelectQuestionParams} params
4
4
  */
5
- constructor({ title, question, fieldName, viewFolder, url, hint, pageTitle, description, label, html, legend, disableAccessibleAutocomplete, options, validators, viewData }: import("#typedefs/question-props.d.ts").SelectQuestionParams);
5
+ constructor({ label, html, legend, disableAccessibleAutocomplete, viewFolder, ...parentParams }: import("#typedefs/question-props.d.ts").SelectQuestionParams);
6
6
  label: string | undefined;
7
7
  legend: string | undefined;
8
- /**
9
- * @param {import('#question').QuestionViewModel} viewModel
10
- */
11
- addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
12
- /**
13
- * returns the formatted answers values to be used to build task list elements
14
- * note: only supports a single answer
15
- *
16
- * @param {unknown} answer
17
- * @param {Journey} journey
18
- * @param {string} sectionSegment
19
- * @returns {Array.<Object>}
20
- */
21
- formatAnswerForSummary(sectionSegment: string, journey: Journey, answer: unknown): Array<Object>;
22
8
  #private;
23
9
  }
24
10
  export default SelectQuestion;
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;IAEC;;OAEG;IACH,8KAFW,OAAO,+BAA+B,EAAE,oBAAoB,EAqCtE;IAHA,0BAAkB;IAClB,2BAAoB;IAIrB;;OAEG;IACH,oCAFW,OAAO,WAAW,EAAE,iBAAiB,QAM/C;IAED;;;;;;;;OAQG;IACH,uCAHW,MAAM,WADN,OAAO,UADP,OAAO,GAGL,KAAK,CAAE,MAAM,CAAC,CAS1B;;CACD;;4BAtE2B,qCAAqC"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;IAEC;;OAEG;IACH,iGAFW,OAAO,+BAA+B,EAAE,oBAAoB,EAYtE;IAHA,0BAAkB;IAClB,2BAAoB;;CAYrB;;4BA3B2B,qCAAqC"}
@@ -5,35 +5,10 @@ export class SelectQuestion extends OptionsQuestion {
5
5
  /**
6
6
  * @param {import('#typedefs/question-props.d.ts').SelectQuestionParams} params
7
7
  */
8
- constructor({
9
- title,
10
- question,
11
- fieldName,
12
- viewFolder,
13
- url,
14
- hint,
15
- pageTitle,
16
- description,
17
- label,
18
- html,
19
- legend,
20
- disableAccessibleAutocomplete,
21
- options,
22
- validators,
23
- viewData
24
- }) {
8
+ constructor({ label, html, legend, disableAccessibleAutocomplete, viewFolder, ...parentParams }) {
25
9
  super({
26
- title,
27
- question,
28
- viewFolder: !viewFolder ? 'select' : viewFolder,
29
- fieldName,
30
- url,
31
- hint,
32
- pageTitle,
33
- description,
34
- options,
35
- validators,
36
- viewData
10
+ ...parentParams,
11
+ viewFolder: viewFolder || 'select'
37
12
  });
38
13
 
39
14
  this.html = html;
@@ -43,31 +18,13 @@ export class SelectQuestion extends OptionsQuestion {
43
18
  }
44
19
 
45
20
  /**
46
- * @param {import('#question').QuestionViewModel} viewModel
21
+ * @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
47
22
  */
48
23
  addCustomDataToViewModel(viewModel) {
49
24
  viewModel.question.label = this.label;
50
25
  viewModel.question.legend = this.legend;
51
26
  viewModel.question.disableAccessibleAutocomplete = this.#disableAccessibleAutocomplete;
52
27
  }
53
-
54
- /**
55
- * returns the formatted answers values to be used to build task list elements
56
- * note: only supports a single answer
57
- *
58
- * @param {unknown} answer
59
- * @param {Journey} journey
60
- * @param {string} sectionSegment
61
- * @returns {Array.<Object>}
62
- */
63
- formatAnswerForSummary(sectionSegment, journey, answer) {
64
- if (answer) {
65
- const selectedOption = this.options.find((option) => option.value === answer);
66
- const selectedText = selectedOption?.text || '';
67
- return super.formatAnswerForSummary(sectionSegment, journey, selectedText, false);
68
- }
69
- return super.formatAnswerForSummary(sectionSegment, journey, answer);
70
- }
71
28
  }
72
29
 
73
30
  export default SelectQuestion;
@@ -10,10 +10,6 @@ export class SingleLineInputQuestion extends Question {
10
10
  inputAttributes: Record<string, string>;
11
11
  label: string | undefined;
12
12
  classes: string;
13
- /**
14
- * @param {import('#question').QuestionViewModel} viewModel
15
- */
16
- addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
17
13
  }
18
14
  export default SingleLineInputQuestion;
19
15
  import { Question } from '#question';
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;;GAEG;AACH;IAIC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,6BAA6B,EAW/E;IAfD,qCAAqC;IACrC,iBADW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CACjB;IAWf,0BAAyB;IAEzB,gBAAmC;IAGpC;;OAEG;IACH,oCAFW,OAAO,WAAW,EAAE,iBAAiB,QAU/C;CACD;;yBAnCwB,WAAW"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;;GAEG;AACH;IAIC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,6BAA6B,EAW/E;IAfD,qCAAqC;IACrC,iBADW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CACjB;IAWf,0BAAyB;IAEzB,gBAAmC;CAepC;;yBAnCwB,WAAW"}
@@ -22,7 +22,7 @@ export class SingleLineInputQuestion extends Question {
22
22
  }
23
23
 
24
24
  /**
25
- * @param {import('#question').QuestionViewModel} viewModel
25
+ * @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
26
26
  */
27
27
  addCustomDataToViewModel(viewModel) {
28
28
  viewModel.question.label = this.label;
@@ -21,10 +21,6 @@ export class TextEntryQuestion extends Question {
21
21
  constructor({ textEntryCheckbox, label, ...parentParams }: import("#typedefs/question-props.d.ts").TextEntryQuestionParams);
22
22
  textEntryCheckbox: import("#typedefs/question-props.d.ts").TextEntryCheckbox | undefined;
23
23
  label: string | undefined;
24
- /**
25
- * @param {import('#question').QuestionViewModel} viewModel
26
- */
27
- addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
28
24
  }
29
25
  export default TextEntryQuestion;
30
26
  export type TextEntryCheckbox = {
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AACA;;;;;GAKG;AAEH;;;;;;GAMG;AAEH;;GAEG;AACH;IACC;;OAEG;IACH,2DAFW,OAAO,+BAA+B,EAAE,uBAAuB,EAUzE;IAFA,yFAA0C;IAC1C,0BAAkB;IAGnB;;OAEG;IACH,oCAFW,OAAO,WAAW,EAAE,iBAAiB,QAK/C;CACD;;;YA9Ba,MAAM;UACN,MAAM;UACN,MAAM;;;yBAZK,WAAW"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AACA;;;;;GAKG;AAEH;;;;;;GAMG;AAEH;;GAEG;AACH;IACC;;OAEG;IACH,2DAFW,OAAO,+BAA+B,EAAE,uBAAuB,EAUzE;IAFA,yFAA0C;IAC1C,0BAAkB;CAUnB;;;YA9Ba,MAAM;UACN,MAAM;UACN,MAAM;;;yBAZK,WAAW"}