@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
package/README.md CHANGED
@@ -286,6 +286,117 @@ const questionProps = {
286
286
  const questions = createQuestions(questionProps, questionClasses, {});
287
287
  ```
288
288
 
289
+ ### Custom Summary Formatting
290
+
291
+ The `formatSummaryValue` option allows you to customise how answers are displayed on check-your-answers pages without needing to create a custom question class.
292
+
293
+ #### Basic Usage
294
+
295
+ Pass a `formatSummaryValue` function when creating a question:
296
+
297
+ ```javascript
298
+ import { RadioQuestion } from '@planning-inspectorate/dynamic-forms';
299
+
300
+ new RadioQuestion({
301
+ fieldName: 'status',
302
+ title: 'Application status',
303
+ question: 'What is the application status?',
304
+ options: [
305
+ { text: 'Approved', value: 'approved' },
306
+ { text: 'Rejected', value: 'rejected' }
307
+ ],
308
+ formatSummaryValue: ({ formattedAnswer }) => `<strong>${formattedAnswer}</strong>`
309
+ });
310
+ ```
311
+
312
+ #### Formatter Context
313
+
314
+ The formatter function receives a context object with the following properties:
315
+
316
+ | Property | Type | Description |
317
+ |----------|------|------------------------------------------------|
318
+ | `answer` | `unknown` | The raw answer value |
319
+ | `formattedAnswer` | `string` | The default formatted display value |
320
+ | `question` | `Question` | The question instance |
321
+ | `journey` | `Journey` | The journey instance |
322
+ | `sectionSegment` | `string` | The current section segment |
323
+ | `selectedOptions` | `Option[] \| undefined` | For OptionsQuestions (such as Checkbox or Radio), the matched option object(s) as an array |
324
+
325
+ #### Answer Formats
326
+
327
+ Different options-based questions store answers in different formats. The `selectedOptions` array normalises these formats for your formatter:
328
+
329
+ | Question Type | Answer Format | `selectedOptions` Result |
330
+ |---------------|---------------|--------------------------|
331
+ | **Radio / Select** | Single string value: `"approved"` | `[{ text: 'Approved', value: 'approved' }]` |
332
+ | **Radio / Select** | Conditional object: `{ value: "yes", conditional: "details" }` | `[{ text: 'Yes', value: 'yes', conditional: {...} }]` |
333
+ | **Checkbox** | Comma-separated string: `"option1,option2"` | `[{ text: 'Option 1', value: 'option1' }, { text: 'Option 2', value: 'option2' }]` |
334
+ | **Checkbox** | Single conditional object: `{ value: "other", conditional: "details" }` | `[{ text: 'Other', value: 'other', conditional: {...} }]` |
335
+
336
+ For single-select questions (Radio, Select), use `selectedOptions[0]` to access the selected option. For multi-select questions (Checkbox), iterate over the array.
337
+
338
+ #### Examples
339
+
340
+ **Bold formatting:**
341
+
342
+ ```javascript
343
+ formatSummaryValue: ({ formattedAnswer }) => `<strong>${escapeHtml(formattedAnswer)}</strong>`
344
+ ```
345
+
346
+ **Conditional styling based on answer:**
347
+
348
+ ```javascript
349
+ formatSummaryValue: ({ selectedOptions }) => {
350
+ const selectedOption = selectedOptions[0];
351
+ const color = selectedOption?.value === 'approved' ? 'green' : 'red';
352
+ return `<span style="color: ${color}">${escapeHtml(selectedOption?.text ?? '-')}</span>`;
353
+ }
354
+ ```
355
+
356
+ **Adding a suffix from another answer:**
357
+
358
+ ```javascript
359
+ const suffixes = { solid: ' (tonnes)', liquid: ' (litres)' };
360
+
361
+ formatSummaryValue: ({ answer, formattedAnswer }) => {
362
+ const suffix = suffixes[answer] ?? '';
363
+ return `${formattedAnswer}${suffix}`;
364
+ }
365
+ ```
366
+
367
+ **Accessing other journey answers:**
368
+
369
+ ```javascript
370
+ formatSummaryValue: ({ formattedAnswer, journey }) => {
371
+ const capacity = journey.response.answers.wasteCapacity;
372
+ return capacity ? `${formattedAnswer}: ${capacity}` : formattedAnswer;
373
+ }
374
+ ```
375
+
376
+ **Using selectedOptions for single-select questions (Radio/Select):**
377
+
378
+ ```javascript
379
+ formatSummaryValue: ({ selectedOptions }) => {
380
+ const selectedOption = selectedOptions[0];
381
+ // Access the full option object including any custom properties
382
+ const warning = selectedOption?.warning;
383
+ return warning ? `${escapeHtml(selectedOption?.text ?? '-')} ⚠️` : escapeHtml(selectedOption?.text ?? '-');
384
+ }
385
+ ```
386
+
387
+ **Using selectedOptions for multi-select questions (Checkbox):**
388
+
389
+ ```javascript
390
+ formatSummaryValue: ({ selectedOptions }) => {
391
+ // Join all selected option texts with custom formatting
392
+ return selectedOptions
393
+ .map(opt => `<span class="tag">${escapeHtml(opt.text)}</span>`)
394
+ .join(' ');
395
+ }
396
+ ```
397
+
398
+ > **Note:** `formattedAnswer` is already escaped by dynamic-forms; you are responsible for escaping any additional user-controlled values you include (for example `answer`, option text from `selectedOptions`, or values read from `journey.response.answers`).
399
+
289
400
  ## Contributing
290
401
 
291
402
  When contributing to this package, ensure changes are generic and not service-specific. Speak to the R&D devs if you are not sure. Prefer configuration over hardcoding values, and ensure the code is well documented.
@@ -321,7 +432,7 @@ To update any snapshots with rendering changes (or new questions), run `node --t
321
432
 
322
433
  When implement a new component, extend the base `Question` class. Common methods to override are:
323
434
 
324
- * `formatAnswerForSummary` - used for check-your-answers display
435
+ * `formatAnswer` - used for check-your-answers display
325
436
  * `getDataToSave` - answer data to save
326
437
  * `addCustomDataToViewModel` - customise the view model with extra data/configuration
327
438
  * `answerForViewModel` - customise the view model answer value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planning-inspectorate/dynamic-forms",
3
- "version": "3.16.0",
3
+ "version": "3.17.0",
4
4
  "description": "This package is for building GDS forms using a configuration-based approach.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -22,10 +22,6 @@ export class AddressQuestion extends Question {
22
22
  * @returns {*|string}
23
23
  */
24
24
  answerForViewModel(answers: Record<string, any>): any | string;
25
- /**
26
- * @param {import('#question').QuestionViewModel} viewModel
27
- */
28
- addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
29
25
  /**
30
26
  * @param {Object<string, any>} answer
31
27
  * @returns The formatted address to be presented in the UI
@@ -36,11 +32,7 @@ export class AddressQuestion extends Question {
36
32
  /**
37
33
  * returns the formatted answers values to be used to build task list elements
38
34
  */
39
- formatAnswerForSummary(sectionSegment: any, journey: any, answer: any): {
40
- key: string;
41
- value: string;
42
- action: import("../../index.js").ActionView | import("../../index.js").ActionView[] | undefined;
43
- }[];
35
+ formatAnswer(answer: any): string;
44
36
  formatLabelFromRequiredFields(fieldName: any): "" | " (optional)";
45
37
  }
46
38
  export default AddressQuestion;
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAOA;IACC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,yBAAyB,EAqB3E;IAXE;;;;;;kBAA8C;IAIhD;;;;;;MAMC;IAiBF;;;OAGG;IACH,4BAHW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjB,GAAC,GAAC,MAAM,CAqBpB;IAED;;OAEG;IACH,oCAFW,OAAO,WAAW,EAAE,iBAAiB,QAI/C;IA8BD;;;OAGG;IACH;;eAUC;IAED;;OAEG;IACH;;;;QAgBC;IACD,kEAMC;CACD;;yBArJwB,WAAW"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAOA;IACC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,yBAAyB,EAqB3E;IAXE;;;;;;kBAA8C;IAIhD;;;;;;MAMC;IAiBF;;;OAGG;IACH,4BAHW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjB,GAAC,GAAC,MAAM,CAqBpB;IAqCD;;;OAGG;IACH;;eAUC;IAED;;OAEG;IACH,kCAIC;IAED,kEAMC;CACD;;yBA1IwB,WAAW"}
@@ -70,7 +70,7 @@ export class AddressQuestion extends Question {
70
70
  }
71
71
 
72
72
  /**
73
- * @param {import('#question').QuestionViewModel} viewModel
73
+ * @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
74
74
  */
75
75
  addCustomDataToViewModel(viewModel) {
76
76
  viewModel.question.labels = this.addressLabels;
@@ -123,23 +123,12 @@ export class AddressQuestion extends Question {
123
123
  /**
124
124
  * returns the formatted answers values to be used to build task list elements
125
125
  */
126
- formatAnswerForSummary(sectionSegment, journey, answer) {
127
- let formattedAnswer = this.notStartedText;
128
-
129
- if (answer) {
130
- formattedAnswer = nl2br(escape(this.format(answer)));
131
- } else if (answer === null) {
132
- formattedAnswer = '';
133
- }
134
-
135
- return [
136
- {
137
- key: `${this.title}`,
138
- value: formattedAnswer,
139
- action: this.getAction(sectionSegment, journey, answer)
140
- }
141
- ];
126
+ formatAnswer(answer) {
127
+ if (answer === null) return '';
128
+ if (!answer) return this.notStartedText;
129
+ return nl2br(escape(this.format(answer)));
142
130
  }
131
+
143
132
  formatLabelFromRequiredFields(fieldName) {
144
133
  if (this.requiredFields && this.requiredFields[fieldName]) {
145
134
  return '';
@@ -1,29 +1,9 @@
1
- /**
2
- * @typedef ConditionalAnswerObject
3
- * @type {object}
4
- * @property {string} value the checkbox answer
5
- * @property {string} conditional the conditional text input
6
- */
7
1
  export class CheckboxQuestion extends OptionsQuestion {
8
2
  /**
9
3
  * @param {import('#typedefs/question-props.d.ts').CheckboxQuestionParams} params
10
4
  */
11
- constructor({ title, question, fieldName, url, pageTitle, description, options, validators, viewData }: import("#typedefs/question-props.d.ts").CheckboxQuestionParams);
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[];
5
+ constructor({ ...parentParams }: import("#typedefs/question-props.d.ts").CheckboxQuestionParams);
16
6
  }
17
7
  export default CheckboxQuestion;
18
- export type ConditionalAnswerObject = {
19
- /**
20
- * the checkbox answer
21
- */
22
- value: string;
23
- /**
24
- * the conditional text input
25
- */
26
- conditional: string;
27
- };
28
8
  import OptionsQuestion from '../../questions/options-question.js';
29
9
  //# sourceMappingURL=question.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAKA;;;;;GAKG;AAEH;IACC;;OAEG;IACH,wGAFW,OAAO,+BAA+B,EAAE,sBAAsB,EAiBxE;IAED;;OAEG;IACH,8GAqCC;CACD;;;;;;WAlEa,MAAM;;;;iBACN,MAAM;;4BATQ,qCAAqC"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAKA;IACC;;OAEG;IACH,iCAFW,OAAO,+BAA+B,EAAE,sBAAsB,EAUxE;CA4CD;;4BA5D2B,qCAAqC"}
@@ -1,76 +1,63 @@
1
+ import escape from 'escape-html';
1
2
  import OptionsQuestion from '../../questions/options-question.js';
2
- import questionUtils from '../utils/question-utils.js';
3
3
 
4
4
  const defaultOptionJoinString = ',';
5
5
 
6
- /**
7
- * @typedef ConditionalAnswerObject
8
- * @type {object}
9
- * @property {string} value the checkbox answer
10
- * @property {string} conditional the conditional text input
11
- */
12
-
13
6
  export class CheckboxQuestion extends OptionsQuestion {
14
7
  /**
15
8
  * @param {import('#typedefs/question-props.d.ts').CheckboxQuestionParams} params
16
9
  */
17
- constructor({ title, question, fieldName, url, pageTitle, description, options, validators, viewData }) {
10
+ constructor({ ...parentParams }) {
18
11
  super({
19
- title,
20
- question,
12
+ ...parentParams,
21
13
  viewFolder: 'checkbox',
22
- fieldName,
23
- url,
24
- pageTitle,
25
- description,
26
- options,
27
- validators,
28
- viewData
14
+ capitaliseAnswer: false
29
15
  });
30
16
 
31
17
  this.optionJoinString = defaultOptionJoinString;
32
18
  }
33
19
 
34
20
  /**
35
- * returns the formatted answers values to be used to build task list elements
21
+ * Formats an answer value for display in the summary.
22
+ * Handles object answers with conditional fields.
23
+ * - Single value with conditional: { value: 'yes', conditional: { yes: 'details' } }
24
+ * - Multiple values with conditionals: { value: 'yes,no', conditional: { yes: 'details1', no: 'details2' } }
25
+ *
26
+ * @param {unknown} answer - the raw answer value
27
+ * @returns {string} the formatted answer for display
36
28
  */
37
- formatAnswerForSummary(sectionSegment, journey, answer) {
38
- if (!answer) {
39
- return super.formatAnswerForSummary(sectionSegment, journey, answer, false);
29
+ formatAnswer(answer) {
30
+ if (answer === null || answer === undefined || answer === '') {
31
+ return this.notStartedText;
40
32
  }
41
33
 
42
- // answer is single ConditionalAnswerObject
43
- if (answer?.conditional) {
44
- const selectedOption = this.options.find((option) => option.value === answer.value);
45
-
46
- const conditionalAnswerText = selectedOption.conditional?.label
47
- ? `${selectedOption.conditional.label} ${answer.conditional}`
48
- : answer.conditional;
49
-
50
- const formattedConditionalText = [selectedOption.text, conditionalAnswerText].join('\n');
51
-
52
- return super.formatAnswerForSummary(sectionSegment, journey, formattedConditionalText, false);
34
+ // Handle simple string answers (e.g. '1,2')
35
+ if (typeof answer !== 'object' || answer.value === undefined) {
36
+ return super.formatAnswer(answer);
53
37
  }
54
38
 
55
- // answer is a string
56
- const answerArray = answer.split(this.optionJoinString);
39
+ // Handle object answers with conditional fields
40
+ // TODO DF-51 treat as an array
41
+ const answerValues = String(answer.value)
42
+ .split(this.optionJoinString)
43
+ .map((v) => v.trim());
44
+ const conditionals = answer.conditional || {};
45
+
46
+ const formattedParts = answerValues.map((value) => {
47
+ const option = this.options.find((opt) => opt.value === value);
48
+ const optionText = escape(option ? option.text : value);
57
49
 
58
- const formattedAnswer = this.options
59
- .filter((option) => answerArray.includes(option.value))
60
- .map((option) => {
61
- if (option.conditional) {
62
- const conditionalAnswer =
63
- journey.response.answers[
64
- questionUtils.getConditionalFieldName(this.fieldName, option.conditional.fieldName)
65
- ];
66
- return [option.text, conditionalAnswer].join('\n');
67
- }
50
+ // Check if this option has a conditional answer
51
+ const conditionalValue = conditionals[value];
52
+ if (conditionalValue) {
53
+ const label = option?.conditional?.label ? `${escape(option.conditional.label)} ` : '';
54
+ return `${optionText}<br>${label}${escape(conditionalValue)}`;
55
+ }
68
56
 
69
- return option.text;
70
- })
71
- .join('\n');
57
+ return optionText;
58
+ });
72
59
 
73
- return super.formatAnswerForSummary(sectionSegment, journey, formattedAnswer, false);
60
+ return formattedParts.join('<br>');
74
61
  }
75
62
  }
76
63
 
@@ -54,35 +54,178 @@ describe('./src/dynamic-forms/components/checkbox/question.js', () => {
54
54
  assert.deepStrictEqual(question.options, OPTIONS);
55
55
  });
56
56
 
57
- it('should return option label when formatAnswerForSummary is called with one answer', () => {
58
- const question = new CheckboxQuestion(CHECKBOX_PARAMS);
59
- question.getAction = () => {};
60
- const formattedAnswer = question.formatAnswerForSummary({}, JOURNEY, '1');
61
- assert.strictEqual(formattedAnswer[0].value, 'a');
62
- });
57
+ describe('formatAnswer', () => {
58
+ it('should delegate simple string answers to parent class', () => {
59
+ const question = new CheckboxQuestion(CHECKBOX_PARAMS);
60
+ // Simple string should use parent's formatAnswer
61
+ const result = question.formatAnswer('1,2');
62
+ assert.strictEqual(result, 'a<br>b');
63
+ });
63
64
 
64
- it('should return formatted option labels when formatAnswerForSummary is called with a string representing several non-conditional answers', () => {
65
- const question = new CheckboxQuestion(CHECKBOX_PARAMS);
66
- question.getAction = () => {};
67
- const formattedAnswer = question.formatAnswerForSummary({}, JOURNEY, '1,2');
68
- assert.strictEqual(formattedAnswer[0].value, 'a<br>b');
69
- });
65
+ it('should format single answer with conditional', () => {
66
+ const question = new CheckboxQuestion(CHECKBOX_PARAMS);
67
+ const answer = {
68
+ value: '3',
69
+ conditional: {
70
+ 3: 'conditional details'
71
+ }
72
+ };
73
+ const result = question.formatAnswer(answer);
74
+ assert.strictEqual(result, 'c<br>conditional details');
75
+ });
70
76
 
71
- it('should return formatted option labels when formatAnswerForSummary is called with a string representing several answers including a conditional', () => {
72
- const question = new CheckboxQuestion(CHECKBOX_PARAMS);
73
- question.getAction = () => {};
74
- const formattedAnswer = question.formatAnswerForSummary({}, JOURNEY, '1,2,3');
75
- assert.strictEqual(formattedAnswer[0].value, `a<br>b<br>c<br>${CONDITIONAL_ANSWER_TEXT}`);
76
- });
77
+ it('should format single answer with conditional and label', () => {
78
+ const optionsWithLabel = [
79
+ { text: 'Option A', value: 'a' },
80
+ {
81
+ text: 'Option B',
82
+ value: 'b',
83
+ conditional: {
84
+ fieldName: 'details',
85
+ label: 'Please provide details:'
86
+ }
87
+ }
88
+ ];
89
+ const question = new CheckboxQuestion({
90
+ ...CHECKBOX_PARAMS,
91
+ options: optionsWithLabel
92
+ });
93
+ const answer = {
94
+ value: 'b',
95
+ conditional: {
96
+ b: 'my details'
97
+ }
98
+ };
99
+ const result = question.formatAnswer(answer);
100
+ assert.strictEqual(result, 'Option B<br>Please provide details: my details');
101
+ });
77
102
 
78
- it('should return a formatted option label when formatAnswerForSummary is called with a single conditional answer', () => {
79
- const question = new CheckboxQuestion(CHECKBOX_PARAMS);
80
- const conditionalAnswer = {
81
- value: '3',
82
- conditional: CONDITIONAL_ANSWER_TEXT
83
- };
84
- question.getAction = () => {};
85
- const formattedAnswer = question.formatAnswerForSummary({}, JOURNEY, conditionalAnswer);
86
- assert.strictEqual(formattedAnswer[0].value, `c<br>${CONDITIONAL_ANSWER_TEXT}`);
103
+ it('should format multiple answers where one has conditional', () => {
104
+ const question = new CheckboxQuestion(CHECKBOX_PARAMS);
105
+ const answer = {
106
+ value: '1,3',
107
+ conditional: {
108
+ 3: 'conditional for c'
109
+ }
110
+ };
111
+ const result = question.formatAnswer(answer);
112
+ assert.strictEqual(result, 'a<br>c<br>conditional for c');
113
+ });
114
+
115
+ it('should format multiple answers with multiple conditionals', () => {
116
+ const optionsWithMultipleConditionals = [
117
+ {
118
+ text: 'Option A',
119
+ value: 'a',
120
+ conditional: { fieldName: 'details-a' }
121
+ },
122
+ { text: 'Option B', value: 'b' },
123
+ {
124
+ text: 'Option C',
125
+ value: 'c',
126
+ conditional: { fieldName: 'details-c' }
127
+ }
128
+ ];
129
+ const question = new CheckboxQuestion({
130
+ ...CHECKBOX_PARAMS,
131
+ options: optionsWithMultipleConditionals
132
+ });
133
+ const answer = {
134
+ value: 'a,c',
135
+ conditional: {
136
+ a: 'details for A',
137
+ c: 'details for C'
138
+ }
139
+ };
140
+ const result = question.formatAnswer(answer);
141
+ assert.strictEqual(result, 'Option A<br>details for A<br>Option C<br>details for C');
142
+ });
143
+
144
+ it('should handle object answer with empty conditional', () => {
145
+ const question = new CheckboxQuestion(CHECKBOX_PARAMS);
146
+ const answer = {
147
+ value: '1,2',
148
+ conditional: {}
149
+ };
150
+ const result = question.formatAnswer(answer);
151
+ assert.strictEqual(result, 'a<br>b');
152
+ });
153
+
154
+ it('should handle object answer with null conditional', () => {
155
+ const question = new CheckboxQuestion(CHECKBOX_PARAMS);
156
+ const answer = {
157
+ value: '1,2',
158
+ conditional: null
159
+ };
160
+ const result = question.formatAnswer(answer);
161
+ assert.strictEqual(result, 'a<br>b');
162
+ });
163
+
164
+ it('should fall back to value when option not found in object answer', () => {
165
+ const question = new CheckboxQuestion(CHECKBOX_PARAMS);
166
+ const answer = {
167
+ value: 'unknown',
168
+ conditional: {}
169
+ };
170
+ const result = question.formatAnswer(answer);
171
+ assert.strictEqual(result, 'unknown');
172
+ });
173
+
174
+ it('should escape HTML in option text', () => {
175
+ const question = new CheckboxQuestion({
176
+ ...CHECKBOX_PARAMS,
177
+ options: [{ text: '<script>alert("xss")</script>', value: '1' }]
178
+ });
179
+ const result = question.formatAnswer('1');
180
+ assert.strictEqual(result, '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;');
181
+ });
182
+
183
+ it('should escape HTML in conditional value', () => {
184
+ const question = new CheckboxQuestion({
185
+ ...CHECKBOX_PARAMS,
186
+ options: [
187
+ {
188
+ text: 'Option',
189
+ value: '1',
190
+ conditional: { fieldName: 'details', type: 'text' }
191
+ }
192
+ ]
193
+ });
194
+ const answer = {
195
+ value: '1',
196
+ conditional: { 1: '<img src=x onerror=alert(1)>' }
197
+ };
198
+ const result = question.formatAnswer(answer);
199
+ assert.strictEqual(result, 'Option<br>&lt;img src=x onerror=alert(1)&gt;');
200
+ });
201
+
202
+ it('should escape HTML in conditional label', () => {
203
+ const question = new CheckboxQuestion({
204
+ ...CHECKBOX_PARAMS,
205
+ options: [
206
+ {
207
+ text: 'Option',
208
+ value: '1',
209
+ conditional: { fieldName: 'details', type: 'text', label: '<b>Label</b>' }
210
+ }
211
+ ]
212
+ });
213
+ const answer = {
214
+ value: '1',
215
+ conditional: { 1: 'safe value' }
216
+ };
217
+ const result = question.formatAnswer(answer);
218
+ assert.strictEqual(result, 'Option<br>&lt;b&gt;Label&lt;/b&gt; safe value');
219
+ });
220
+
221
+ it('should escape HTML in fallback value when option not found', () => {
222
+ const question = new CheckboxQuestion(CHECKBOX_PARAMS);
223
+ const answer = {
224
+ value: '<script>xss</script>',
225
+ conditional: {}
226
+ };
227
+ const result = question.formatAnswer(answer);
228
+ assert.strictEqual(result, '&lt;script&gt;xss&lt;/script&gt;');
229
+ });
87
230
  });
88
231
  });
@@ -16,14 +16,6 @@ export class DateQuestion extends Question {
16
16
  answerForViewModel(answers: any, isPayload: any): {
17
17
  [x: string]: any;
18
18
  };
19
- /**
20
- * returns the formatted answers values to be used to build task list elements
21
- */
22
- formatAnswerForSummary(sectionSegment: any, journey: any, answer: any): {
23
- key: string;
24
- value: string;
25
- action: import("../../index.js").ActionView | import("../../index.js").ActionView[] | undefined;
26
- }[];
27
19
  }
28
20
  export default DateQuestion;
29
21
  import { Question } from '../../questions/question.js';
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAKA;;GAEG;AACH;IACC;;OAEG;IACH,6CAFW,OAAO,+BAA+B,EAAE,kBAAkB,EAQpE;IADA,mBAA4B;IAW7B;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,OAAO,mBAAmB,EAAE,eAAe,GACzC,OAAO,CAAE,MAAM,CAAC,CAY5B;IAED;;MAwBC;IAED;;OAEG;IACH;;;;QAaC;CACD;;yBAxFwB,6BAA6B"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAKA;;GAEG;AACH;IACC;;OAEG;IACH,6CAFW,OAAO,+BAA+B,EAAE,kBAAkB,EAQpE;IADA,mBAA4B;IAW7B;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,OAAO,mBAAmB,EAAE,eAAe,GACzC,OAAO,CAAE,MAAM,CAAC,CAY5B;IAED;;MAwBC;CAWD;;yBAhFwB,6BAA6B"}
@@ -71,21 +71,13 @@ export class DateQuestion extends Question {
71
71
  }
72
72
 
73
73
  /**
74
- * returns the formatted answers values to be used to build task list elements
74
+ * Formats a date answer for display in the summary.
75
+ * @param {unknown} answer - the date value
76
+ * @returns {string} the formatted date
75
77
  */
76
- formatAnswerForSummary(sectionSegment, journey, answer) {
77
- let formattedAnswer;
78
-
79
- if (answer) {
80
- formattedAnswer = formatDateForDisplay(answer, { format: this.dateFormat });
81
- } else {
82
- formattedAnswer = this.notStartedText;
83
- }
84
-
85
- const action = this.getAction(sectionSegment, journey, answer);
86
- const key = this.title ?? this.question;
87
-
88
- return [{ key: key, value: formattedAnswer, action: action }];
78
+ formatAnswer(answer) {
79
+ if (!answer) return this.notStartedText;
80
+ return formatDateForDisplay(answer, { format: this.dateFormat });
89
81
  }
90
82
  }
91
83
 
@@ -25,17 +25,13 @@ export class DatePeriodQuestion extends Question {
25
25
  answerForViewModel(answers: any, isPayload: any): {
26
26
  [x: string]: any;
27
27
  };
28
- /**
29
- * @param {import('#question').QuestionViewModel} viewModel
30
- */
31
- addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
32
28
  /**
33
29
  * returns the formatted answers values to be used to build task list elements
34
30
  */
35
31
  formatAnswerForSummary(sectionSegment: any, journey: any, answer: any): {
36
32
  key: string;
37
33
  value: string;
38
- action: import("../../index.js").ActionView | import("../../index.js").ActionView[] | undefined;
34
+ action: import("#typedefs/question-types.d.ts").ActionView | import("#typedefs/question-types.d.ts").ActionView[] | undefined;
39
35
  }[];
40
36
  }
41
37
  export default DatePeriodQuestion;
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAOA;;;GAGG;AACH;IACC;;OAEG;IACH,mGAFW,OAAO,+BAA+B,EAAE,wBAAwB,EAuB1E;IAPA,mBAA4B;IAC5B;;;MAAsD;IAEtD,8BAA0B;IAC1B,4BAAsB;IACtB,kEAA+D;IAC/D,gEAA2D;IAkB5D;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,OAAO,mBAAmB,EAAE,eAAe,GACzC,OAAO,CAAE,MAAM,CAAC,CAgC5B;IAED;;MAuCC;IAED;;OAEG;IACH,oCAFW,OAAO,WAAW,EAAE,iBAAiB,QAM/C;IAED;;OAEG;IACH;;;;QAyBC;CACD;;yBAzKwB,WAAW"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAOA;;;GAGG;AACH;IACC;;OAEG;IACH,mGAFW,OAAO,+BAA+B,EAAE,wBAAwB,EAuB1E;IAPA,mBAA4B;IAC5B;;;MAAsD;IAEtD,8BAA0B;IAC1B,4BAAsB;IACtB,kEAA+D;IAC/D,gEAA2D;IAkB5D;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,OAAO,mBAAmB,EAAE,eAAe,GACzC,OAAO,CAAE,MAAM,CAAC,CAgC5B;IAED;;MAuCC;IAWD;;OAEG;IACH;;;;QAyBC;CACD;;yBAzKwB,WAAW"}
@@ -131,7 +131,7 @@ export class DatePeriodQuestion extends Question {
131
131
  }
132
132
 
133
133
  /**
134
- * @param {import('#question').QuestionViewModel} viewModel
134
+ * @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
135
135
  */
136
136
  addCustomDataToViewModel(viewModel) {
137
137
  viewModel.labels = this.labels;
@@ -16,14 +16,6 @@ export class DateTimeQuestion extends Question {
16
16
  answerForViewModel(answers: any, isPayload: any): {
17
17
  [x: string]: any;
18
18
  };
19
- /**
20
- * returns the formatted answers values to be used to build task list elements
21
- */
22
- formatAnswerForSummary(sectionSegment: any, journey: any, answer: any): {
23
- key: string;
24
- value: string;
25
- action: import("../../index.js").ActionView | import("../../index.js").ActionView[] | undefined;
26
- }[];
27
19
  #private;
28
20
  }
29
21
  export default DateTimeQuestion;