@planning-inspectorate/dynamic-forms 3.17.0 → 3.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +70 -246
  2. package/commitlint.config.js +3 -5
  3. package/docs/Email Validation.md +86 -0
  4. package/docs/Getting Started.md +299 -0
  5. package/eslint.config.js +3 -17
  6. package/package.json +4 -1
  7. package/prettier.config.js +7 -0
  8. package/src/components/checkbox/question.d.ts.map +1 -1
  9. package/src/components/checkbox/question.js +3 -2
  10. package/src/components/checkbox/question.test.js +8 -0
  11. package/src/components/conditional/text.njk +1 -0
  12. package/src/components/email/question.d.ts +0 -9
  13. package/src/components/email/question.d.ts.map +1 -1
  14. package/src/components/email/question.js +2 -13
  15. package/src/components/email/question.test.js +19 -0
  16. package/src/components/multi-field-input/question.d.ts +4 -19
  17. package/src/components/multi-field-input/question.d.ts.map +1 -1
  18. package/src/components/multi-field-input/question.js +89 -23
  19. package/src/components/multi-field-input/question.test.js +441 -3
  20. package/src/components/number-entry/question.d.ts.map +1 -1
  21. package/src/components/number-entry/question.js +3 -2
  22. package/src/components/number-entry/question.test.js +17 -0
  23. package/src/components/unit-option-entry/question.d.ts.map +1 -1
  24. package/src/components/unit-option-entry/question.js +3 -1
  25. package/src/components/unit-option-entry/question.test.js +15 -0
  26. package/src/questions/options-question.d.ts +19 -7
  27. package/src/questions/options-question.d.ts.map +1 -1
  28. package/src/questions/options-question.js +9 -7
  29. package/src/questions/question.d.ts +2 -2
  30. package/src/questions/question.js +2 -2
  31. package/test/snapshots/check-your-answers-with-answers.html +1 -1
  32. package/test/utils/question-test-utils.js +2 -1
  33. package/typedefs/journey-types.d.ts +1 -1
  34. package/typedefs/question-props.d.ts +33 -0
  35. package/typedefs/question-types.d.ts +0 -23
  36. package/typedefs/section-types.d.ts +4 -4
  37. package/.prettierrc +0 -12
@@ -1,4 +1,4 @@
1
- import { describe, it } from 'node:test';
1
+ import { describe, it, mock } from 'node:test';
2
2
  import assert from 'node:assert';
3
3
  import MultiFieldInputQuestion from './question.js';
4
4
  import { Journey } from '#journey';
@@ -45,6 +45,11 @@ describe('./src/dynamic-forms/components/multi-field-input/question.js', () => {
45
45
  assert.strictEqual(testQuestion.inputFields, INPUTFIELDS);
46
46
  });
47
47
 
48
+ it('should default capitaliseAnswer to false', () => {
49
+ const testQuestion = createMultiFieldInputQuestion();
50
+ assert.strictEqual(testQuestion.capitaliseAnswer, false);
51
+ });
52
+
48
53
  it('should throw error if no inputFields parameter is passed to the constructor', () => {
49
54
  assert.throws(() => {
50
55
  createMultiFieldInputQuestion(TITLE, QUESTION, FIELDNAME, VALIDATORS, HTML, HINT, null);
@@ -247,7 +252,7 @@ describe('./src/dynamic-forms/components/multi-field-input/question.js', () => {
247
252
  visuallyHiddenText: 'Question?'
248
253
  },
249
254
  key: 'title',
250
- value: 'planning-permission<br>Test User<br>'
255
+ value: 'planning-permission<br>Test User'
251
256
  }
252
257
  ];
253
258
 
@@ -344,12 +349,445 @@ describe('./src/dynamic-forms/components/multi-field-input/question.js', () => {
344
349
  sections: []
345
350
  });
346
351
 
347
- const expectedValue = 'Line 1\nLine &amp; 2\n';
352
+ const expectedValue = 'Line 1\nLine &amp; 2';
348
353
 
349
354
  const result = question.formatAnswerForSummary('questions', journey);
350
355
 
351
356
  assert.strictEqual(result[0].value, expectedValue);
352
357
  assert.strictEqual(result[0].value.includes('<br>'), false);
353
358
  });
359
+ it('should apply formatSummaryValue when provided', () => {
360
+ const question = new MultiFieldInputQuestion({
361
+ title: TITLE,
362
+ question: QUESTION,
363
+ fieldName: FIELDNAME,
364
+ inputFields: INPUTFIELDS,
365
+ formatSummaryValue: ({ formattedAnswer }) => `Custom: ${formattedAnswer}`
366
+ });
367
+
368
+ const journey = new Journey({
369
+ journeyId: 'TEST',
370
+ makeBaseUrl: () => 'base',
371
+ taskListUrl: 'cases/create-a-case/check-your-answers',
372
+ response: {
373
+ answers: {
374
+ testField1: 'Value 1',
375
+ testField2: 'Value 2'
376
+ }
377
+ },
378
+ journeyTemplate: 'mock template',
379
+ taskListTemplate: 'mock path',
380
+ journeyTitle: 'mock title',
381
+ sections: []
382
+ });
383
+
384
+ const result = question.formatAnswerForSummary('questions', journey);
385
+
386
+ assert.strictEqual(result[0].value, 'Custom: Value 1<br>Value 2');
387
+ });
388
+ it('should apply formatSummaryValue on input fields with context (not escaped, not in form view)', () => {
389
+ // formatSummaryValue receives full context and output is not escaped
390
+ const formatSummaryValue = ({ answer }) => `<strong>${answer}</strong>`;
391
+ const question = new MultiFieldInputQuestion({
392
+ title: TITLE,
393
+ question: QUESTION,
394
+ fieldName: FIELDNAME,
395
+ inputFields: [
396
+ { fieldName: 'testField1', formatSummaryValue, formatJoinString: '<br>' },
397
+ { fieldName: 'testField2', formatSummaryValue, formatJoinString: '<br>' }
398
+ ]
399
+ });
400
+
401
+ const journey = new Journey({
402
+ journeyId: 'TEST',
403
+ makeBaseUrl: () => 'base',
404
+ taskListUrl: 'cases/create-a-case/check-your-answers',
405
+ response: {
406
+ answers: {
407
+ testField1: 'Value 1',
408
+ testField2: 'Value 2'
409
+ }
410
+ },
411
+ journeyTemplate: 'mock template',
412
+ taskListTemplate: 'mock path',
413
+ journeyTitle: 'mock title',
414
+ sections: []
415
+ });
416
+
417
+ // formatSummaryValue should be applied in summary (with HTML not escaped)
418
+ const summaryResult = question.formatAnswerForSummary('questions', journey);
419
+ assert.strictEqual(summaryResult[0].value, '<strong>Value 1</strong><br><strong>Value 2</strong>');
420
+
421
+ // formatSummaryValue should NOT be applied in view model (form inputs)
422
+ const viewResult = question.prepQuestionForRendering({}, journey, {});
423
+ assert.strictEqual(viewResult.question?.value[0].value, 'Value 1');
424
+ assert.strictEqual(viewResult.question?.value[1].value, 'Value 2');
425
+ });
426
+ it('should pass full context to formatSummaryValue including journey and field', () => {
427
+ const formatSummaryValue = mock.fn((ctx) => ctx.answer);
428
+ const question = new MultiFieldInputQuestion({
429
+ title: TITLE,
430
+ question: QUESTION,
431
+ fieldName: FIELDNAME,
432
+ inputFields: [{ fieldName: 'testField1', formatSummaryValue }]
433
+ });
434
+
435
+ const journey = new Journey({
436
+ journeyId: 'TEST',
437
+ makeBaseUrl: () => 'base',
438
+ taskListUrl: 'cases/create-a-case/check-your-answers',
439
+ response: {
440
+ answers: {
441
+ testField1: 'Test Value'
442
+ }
443
+ },
444
+ journeyTemplate: 'mock template',
445
+ taskListTemplate: 'mock path',
446
+ journeyTitle: 'mock title',
447
+ sections: []
448
+ });
449
+
450
+ question.formatAnswerForSummary('test-section', journey);
451
+
452
+ assert.strictEqual(formatSummaryValue.mock.callCount(), 1);
453
+ const capturedContext = formatSummaryValue.mock.calls[0].arguments[0];
454
+ assert.strictEqual(capturedContext.answer, 'Test Value');
455
+ assert.strictEqual(capturedContext.formattedAnswer, 'Test Value');
456
+ assert.strictEqual(capturedContext.question, question);
457
+ assert.strictEqual(capturedContext.journey, journey);
458
+ assert.strictEqual(capturedContext.sectionSegment, 'test-section');
459
+ assert.strictEqual(capturedContext.field.fieldName, 'testField1');
460
+ });
461
+ it('should apply formatPrefix to each field value in summary', () => {
462
+ const question = new MultiFieldInputQuestion({
463
+ title: TITLE,
464
+ question: QUESTION,
465
+ fieldName: FIELDNAME,
466
+ inputFields: [
467
+ { fieldName: 'testField1', formatPrefix: 'Name: ' },
468
+ { fieldName: 'testField2', formatPrefix: 'Email: ' }
469
+ ]
470
+ });
471
+
472
+ const journey = new Journey({
473
+ journeyId: 'TEST',
474
+ makeBaseUrl: () => 'base',
475
+ taskListUrl: 'cases/create-a-case/check-your-answers',
476
+ response: {
477
+ answers: {
478
+ testField1: 'John Doe',
479
+ testField2: 'john@example.com'
480
+ }
481
+ },
482
+ journeyTemplate: 'mock template',
483
+ taskListTemplate: 'mock path',
484
+ journeyTitle: 'mock title',
485
+ sections: []
486
+ });
487
+
488
+ const result = question.formatAnswerForSummary('questions', journey);
489
+
490
+ assert.strictEqual(result[0].value, 'Name: John Doe<br>Email: john@example.com');
491
+ });
492
+ it('should only include answered fields in summary (partial answers)', () => {
493
+ const question = createMultiFieldInputQuestion();
494
+
495
+ const journey = new Journey({
496
+ journeyId: 'TEST',
497
+ makeBaseUrl: () => 'base',
498
+ taskListUrl: 'cases/create-a-case/check-your-answers',
499
+ response: {
500
+ answers: {
501
+ testField1: 'Answered value',
502
+ testField2: null
503
+ }
504
+ },
505
+ journeyTemplate: 'mock template',
506
+ taskListTemplate: 'mock path',
507
+ journeyTitle: 'mock title',
508
+ sections: []
509
+ });
510
+
511
+ const result = question.formatAnswerForSummary('questions', journey);
512
+
513
+ // Only the answered field should be included
514
+ assert.strictEqual(result[0].value, 'Answered value');
515
+ });
516
+ it('should include falsy-but-valid answers such as false and 0 in the summary', () => {
517
+ const question = new MultiFieldInputQuestion({
518
+ title: TITLE,
519
+ question: QUESTION,
520
+ fieldName: FIELDNAME,
521
+ inputFields: [{ fieldName: 'testField1' }, { fieldName: 'testField2' }]
522
+ });
523
+
524
+ const journey = new Journey({
525
+ journeyId: 'TEST',
526
+ makeBaseUrl: () => 'base',
527
+ taskListUrl: 'cases/create-a-case/check-your-answers',
528
+ response: {
529
+ answers: {
530
+ testField1: false,
531
+ testField2: 0
532
+ }
533
+ },
534
+ journeyTemplate: 'mock template',
535
+ taskListTemplate: 'mock path',
536
+ journeyTitle: 'mock title',
537
+ sections: []
538
+ });
539
+
540
+ const result = question.formatAnswerForSummary('questions', journey);
541
+
542
+ assert.strictEqual(result[0].value, 'false<br>0');
543
+ });
544
+ it('should exclude fields with an empty string answer from the summary', () => {
545
+ const question = createMultiFieldInputQuestion();
546
+
547
+ const journey = new Journey({
548
+ journeyId: 'TEST',
549
+ makeBaseUrl: () => 'base',
550
+ taskListUrl: 'cases/create-a-case/check-your-answers',
551
+ response: {
552
+ answers: {
553
+ testField1: 'Answered value',
554
+ testField2: ''
555
+ }
556
+ },
557
+ journeyTemplate: 'mock template',
558
+ taskListTemplate: 'mock path',
559
+ journeyTitle: 'mock title',
560
+ sections: []
561
+ });
562
+
563
+ const result = question.formatAnswerForSummary('questions', journey);
564
+
565
+ // Only the non-empty field should be included, no trailing join string
566
+ assert.strictEqual(result[0].value, 'Answered value');
567
+ });
568
+ it('should escape HTML characters in field values by default', () => {
569
+ const question = createMultiFieldInputQuestion();
570
+
571
+ const journey = new Journey({
572
+ journeyId: 'TEST',
573
+ makeBaseUrl: () => 'base',
574
+ taskListUrl: 'cases/create-a-case/check-your-answers',
575
+ response: {
576
+ answers: {
577
+ testField1: '<script>alert("xss")</script>',
578
+ testField2: 'Safe & Sound'
579
+ }
580
+ },
581
+ journeyTemplate: 'mock template',
582
+ taskListTemplate: 'mock path',
583
+ journeyTitle: 'mock title',
584
+ sections: []
585
+ });
586
+
587
+ const result = question.formatAnswerForSummary('questions', journey);
588
+
589
+ assert.strictEqual(result[0].value, '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;<br>Safe &amp; Sound');
590
+ });
591
+ it('should use default <br> join string when not in ManageListSection', () => {
592
+ const question = new MultiFieldInputQuestion({
593
+ title: TITLE,
594
+ question: QUESTION,
595
+ fieldName: FIELDNAME,
596
+ inputFields: [
597
+ { fieldName: 'testField1' }, // no formatJoinString specified
598
+ { fieldName: 'testField2' }
599
+ ]
600
+ });
601
+
602
+ const journey = new Journey({
603
+ journeyId: 'TEST',
604
+ makeBaseUrl: () => 'base',
605
+ taskListUrl: 'cases/create-a-case/check-your-answers',
606
+ response: {
607
+ answers: {
608
+ testField1: 'Value 1',
609
+ testField2: 'Value 2'
610
+ }
611
+ },
612
+ journeyTemplate: 'mock template',
613
+ taskListTemplate: 'mock path',
614
+ journeyTitle: 'mock title',
615
+ sections: []
616
+ });
617
+
618
+ const result = question.formatAnswerForSummary('questions', journey);
619
+
620
+ // Default join string should be <br> (not \n)
621
+ assert.ok(result[0].value.includes('<br>'));
622
+ assert.strictEqual(result[0].value, 'Value 1<br>Value 2');
623
+ });
624
+ it('should apply formatTextFunction to field values in summary', () => {
625
+ const formatTextFunction = (value) => `Formatted: ${value}`;
626
+ const question = new MultiFieldInputQuestion({
627
+ title: TITLE,
628
+ question: QUESTION,
629
+ fieldName: FIELDNAME,
630
+ inputFields: [
631
+ { fieldName: 'testField1', formatTextFunction },
632
+ { fieldName: 'testField2', formatTextFunction }
633
+ ]
634
+ });
635
+
636
+ const journey = new Journey({
637
+ journeyId: 'TEST',
638
+ makeBaseUrl: () => 'base',
639
+ taskListUrl: 'cases/create-a-case/check-your-answers',
640
+ response: {
641
+ answers: {
642
+ testField1: 'Value 1',
643
+ testField2: 'Value 2'
644
+ }
645
+ },
646
+ journeyTemplate: 'mock template',
647
+ taskListTemplate: 'mock path',
648
+ journeyTitle: 'mock title',
649
+ sections: []
650
+ });
651
+
652
+ const result = question.formatAnswerForSummary('questions', journey);
653
+
654
+ assert.strictEqual(result[0].value, 'Formatted: Value 1<br>Formatted: Value 2');
655
+ });
656
+ it('should escape output of formatTextFunction in summary', () => {
657
+ const formatTextFunction = (value) => `<b>${value}</b>`;
658
+ const question = new MultiFieldInputQuestion({
659
+ title: TITLE,
660
+ question: QUESTION,
661
+ fieldName: FIELDNAME,
662
+ inputFields: [{ fieldName: 'testField1', formatTextFunction }]
663
+ });
664
+
665
+ const journey = new Journey({
666
+ journeyId: 'TEST',
667
+ makeBaseUrl: () => 'base',
668
+ taskListUrl: 'cases/create-a-case/check-your-answers',
669
+ response: {
670
+ answers: {
671
+ testField1: 'Value 1'
672
+ }
673
+ },
674
+ journeyTemplate: 'mock template',
675
+ taskListTemplate: 'mock path',
676
+ journeyTitle: 'mock title',
677
+ sections: []
678
+ });
679
+
680
+ const result = question.formatAnswerForSummary('questions', journey);
681
+
682
+ assert.strictEqual(result[0].value, '&lt;b&gt;Value 1&lt;/b&gt;');
683
+ });
684
+ it('should apply formatTextFunction before formatSummaryValue, passing it as formattedAnswer', () => {
685
+ const formatTextFunction = (value) => value.toUpperCase();
686
+ const formatSummaryValue = mock.fn(({ answer, formattedAnswer }) => `[${answer}] ${formattedAnswer}`);
687
+ const question = new MultiFieldInputQuestion({
688
+ title: TITLE,
689
+ question: QUESTION,
690
+ fieldName: FIELDNAME,
691
+ inputFields: [{ fieldName: 'testField1', formatTextFunction, formatSummaryValue }]
692
+ });
693
+
694
+ const journey = new Journey({
695
+ journeyId: 'TEST',
696
+ makeBaseUrl: () => 'base',
697
+ taskListUrl: 'cases/create-a-case/check-your-answers',
698
+ response: {
699
+ answers: {
700
+ testField1: 'Value 1'
701
+ }
702
+ },
703
+ journeyTemplate: 'mock template',
704
+ taskListTemplate: 'mock path',
705
+ journeyTitle: 'mock title',
706
+ sections: []
707
+ });
708
+
709
+ const result = question.formatAnswerForSummary('questions', journey);
710
+
711
+ assert.strictEqual(formatSummaryValue.mock.callCount(), 1);
712
+ const capturedContext = formatSummaryValue.mock.calls[0].arguments[0];
713
+ // answer should remain the raw value
714
+ assert.strictEqual(capturedContext.answer, 'Value 1');
715
+ // formattedAnswer should reflect formatTextFunction output (escaped)
716
+ assert.strictEqual(capturedContext.formattedAnswer, 'VALUE 1');
717
+ assert.strictEqual(result[0].value, '[Value 1] VALUE 1');
718
+ });
719
+ it('should use custom formatJoinString when provided', () => {
720
+ const question = new MultiFieldInputQuestion({
721
+ title: TITLE,
722
+ question: QUESTION,
723
+ fieldName: FIELDNAME,
724
+ inputFields: [
725
+ { fieldName: 'testField1', formatJoinString: ' - ' },
726
+ { fieldName: 'testField2', formatJoinString: '' }
727
+ ]
728
+ });
729
+
730
+ const journey = new Journey({
731
+ journeyId: 'TEST',
732
+ makeBaseUrl: () => 'base',
733
+ taskListUrl: 'cases/create-a-case/check-your-answers',
734
+ response: {
735
+ answers: {
736
+ testField1: 'Value 1',
737
+ testField2: 'Value 2'
738
+ }
739
+ },
740
+ journeyTemplate: 'mock template',
741
+ taskListTemplate: 'mock path',
742
+ journeyTitle: 'mock title',
743
+ sections: []
744
+ });
745
+
746
+ const result = question.formatAnswerForSummary('questions', journey);
747
+
748
+ assert.strictEqual(result[0].value, 'Value 1 - Value 2');
749
+ });
750
+ });
751
+ describe('formatAnswer', () => {
752
+ it('should return notStartedText for null', () => {
753
+ const question = createMultiFieldInputQuestion();
754
+ assert.strictEqual(question.formatAnswer(null), 'Not started');
755
+ });
756
+ it('should return notStartedText for undefined', () => {
757
+ const question = createMultiFieldInputQuestion();
758
+ assert.strictEqual(question.formatAnswer(undefined), 'Not started');
759
+ });
760
+ it('should return empty string for empty string', () => {
761
+ const question = createMultiFieldInputQuestion();
762
+ assert.strictEqual(question.formatAnswer(''), '');
763
+ });
764
+ it('should escape HTML and convert newlines to <br>', () => {
765
+ const question = createMultiFieldInputQuestion();
766
+ assert.strictEqual(question.formatAnswer('Line 1\nLine <2>'), 'Line 1<br>Line &lt;2&gt;');
767
+ });
768
+ it('should not apply nl2br when isInManageListSection is true', () => {
769
+ const question = createMultiFieldInputQuestion();
770
+ question.isInManageListSection = true;
771
+ assert.strictEqual(question.formatAnswer('Line 1\nLine 2'), 'Line 1\nLine 2');
772
+ });
773
+ it('should capitalise answer when capitaliseAnswer is true', () => {
774
+ const question = new MultiFieldInputQuestion({
775
+ title: TITLE,
776
+ question: QUESTION,
777
+ fieldName: FIELDNAME,
778
+ inputFields: INPUTFIELDS,
779
+ capitaliseAnswer: true
780
+ });
781
+ assert.strictEqual(question.formatAnswer('hello world'), 'Hello world');
782
+ });
783
+ it('should not capitalise answer when capitaliseAnswer is false (default)', () => {
784
+ const question = createMultiFieldInputQuestion();
785
+ assert.strictEqual(question.formatAnswer('hello world'), 'hello world');
786
+ });
787
+ it('should coerce non-string values to string before formatting', () => {
788
+ const question = createMultiFieldInputQuestion();
789
+ assert.strictEqual(question.formatAnswer(123), '123');
790
+ assert.strictEqual(question.formatAnswer(true), 'true');
791
+ });
354
792
  });
355
793
  });
@@ -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,EAW3E;IAFA,2BAAoB;IACpB,0BAAkB;IAGnB,kDAEC;CASD;;yBA7BwB,WAAW"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAGA;IACC;;OAEG;IACH,gDAFW,OAAO,+BAA+B,EAAE,yBAAyB,EAY3E;IAFA,2BAAoB;IACpB,0BAAkB;IAGnB,kDAEC;CASD;;yBA9BwB,WAAW"}
@@ -7,9 +7,10 @@ export class NumberEntryQuestion extends Question {
7
7
  */
8
8
  constructor({ label, suffix, ...parentParams }) {
9
9
  super({
10
+ // default but allow overrides
11
+ capitaliseAnswer: false,
10
12
  ...parentParams,
11
- viewFolder: 'number-entry',
12
- capitaliseAnswer: false
13
+ viewFolder: 'number-entry'
13
14
  });
14
15
 
15
16
  this.suffix = suffix;
@@ -0,0 +1,17 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import NumberEntryQuestion from '#src/components/number-entry/question.js';
4
+
5
+ describe('number-entry', () => {
6
+ it('should allow capitaliseAnswer override', () => {
7
+ const question = new NumberEntryQuestion({
8
+ title: 'Question',
9
+ question: 'Question',
10
+ fieldName: 'question',
11
+ url: 'Question',
12
+ capitaliseAnswer: true
13
+ });
14
+
15
+ assert.strictEqual(question.capitaliseAnswer, true);
16
+ });
17
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;IAIC;;;OAGG;IACH,uEAHW,OAAO,+BAA+B,EAAE,6BAA6B,oBACrE,MAAM,CAAC,MAAM,WAAW,EAkBlC;IAvBD,gCAAgC;IAChC,SADW,KAAK,CAAC,UAAU,CAAC,CACpB;IAkBP,6BAAgD;IAEhD,0BAAkB;IAClB,yBAA+C;IAGhD;;;;;;;;;;;;OAYG;IACH,kCAXW,OAAO,kBAAkB,EAAE,OAAO,WAClC,OAAO,UAAU,EAAE,OAAO,mBAC1B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YACvB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACvB,OAAO,+BAA+B,EAAE,+BAA+B,GACrE,OAAO,+BAA+B,EAAE,iBAAiB,GAAG;QACpE,QAAQ,EAAE,OAAO,+BAA+B,EAAE,iBAAiB,CAAC,UAAU,CAAC,GAAG;YAChF,OAAO,EAAC,UAAU,EAAE,CAAA;SACrB,CAAA;KACF,CAmDH;IAED;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,eAAe,GACb,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAgCzD;IAED;;OAEG;IACH,6HAaC;CACD;;;;;;;;;;yBAnKY;IACR,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE;QACX,SAAS,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,OAAO,CAAC;QACZ,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAA;CACH;yBA5BsB,6BAA6B"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;IAIC;;;OAGG;IACH,uEAHW,OAAO,+BAA+B,EAAE,6BAA6B,oBACrE,MAAM,CAAC,MAAM,WAAW,EAoBlC;IAzBD,gCAAgC;IAChC,SADW,KAAK,CAAC,UAAU,CAAC,CACpB;IAoBP,6BAAgD;IAEhD,0BAAkB;IAClB,yBAA+C;IAGhD;;;;;;;;;;;;OAYG;IACH,kCAXW,OAAO,kBAAkB,EAAE,OAAO,WAClC,OAAO,UAAU,EAAE,OAAO,mBAC1B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YACvB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACvB,OAAO,+BAA+B,EAAE,+BAA+B,GACrE,OAAO,+BAA+B,EAAE,iBAAiB,GAAG;QACpE,QAAQ,EAAE,OAAO,+BAA+B,EAAE,iBAAiB,CAAC,UAAU,CAAC,GAAG;YAChF,OAAO,EAAC,UAAU,EAAE,CAAA;SACrB,CAAA;KACF,CAmDH;IAED;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,eAAe,GACb,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAgCzD;IAED;;OAEG;IACH,6HAaC;CACD;;;;;;;;;;yBArKY;IACR,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE;QACX,SAAS,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,OAAO,CAAC;QACZ,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAA;CACH;yBA5BsB,6BAA6B"}
@@ -41,6 +41,8 @@ export class UnitOptionEntryQuestion extends Question {
41
41
  constructor({ conditionalFieldName, options, label, ...parentParams }, methodOverrides) {
42
42
  super(
43
43
  {
44
+ // Prevent capitalisation of answers, but allow override
45
+ capitaliseAnswer: false,
44
46
  ...parentParams,
45
47
  viewFolder: 'unit-option-entry'
46
48
  },
@@ -173,7 +175,7 @@ export class UnitOptionEntryQuestion extends Question {
173
175
  if (isNaN(answerQuantity)) throw new Error('Conditional answer had an unexpected type');
174
176
 
175
177
  const formattedAnswer = `${answerQuantity} ${answer}`;
176
- return super.formatAnswerForSummary(sectionSegment, journey, formattedAnswer, false);
178
+ return super.formatAnswerForSummary(sectionSegment, journey, formattedAnswer);
177
179
  }
178
180
  }
179
181
 
@@ -53,6 +53,21 @@ describe('./src/dynamic-forms/components/unit-option-entry/question.js', () => {
53
53
  assert.strictEqual(unitOptionEntryQuestion.options, OPTIONS);
54
54
  });
55
55
 
56
+ it('should default capitaliseAnswer to false', () => {
57
+ assert.strictEqual(unitOptionEntryQuestion.capitaliseAnswer, false);
58
+ });
59
+
60
+ it('should allow capitaliseAnswer to be set via constructor', () => {
61
+ const customQuestion = new UnitOptionEntryQuestion({
62
+ title: TITLE,
63
+ question: QUESTION,
64
+ fieldName: FIELDNAME,
65
+ conditionalFieldName: CONDITIONAL_FIELDNAME,
66
+ options: OPTIONS,
67
+ capitaliseAnswer: true
68
+ });
69
+ assert.strictEqual(customQuestion.capitaliseAnswer, true);
70
+ });
56
71
  it('should handle decimal string formatting conversion', () => {
57
72
  const journey = {
58
73
  response: {
@@ -2,7 +2,7 @@
2
2
  * @typedef {{
3
3
  * text: string;
4
4
  * value: string;
5
- * hint?: object;
5
+ * hint?: object;
6
6
  * checked?: boolean | undefined;
7
7
  * attributes?: Record<string, string>;
8
8
  * behaviour?: 'exclusive';
@@ -10,14 +10,16 @@
10
10
  * question: string;
11
11
  * type: string;
12
12
  * fieldName: string;
13
- * inputClasses?: string;
14
- * html?: string;
13
+ * inputClasses?: string;
14
+ * html?: string;
15
15
  * value?: unknown;
16
- * label?: string;
17
- * hint?: string
16
+ * label?: string;
17
+ * hint?: string
18
+ * prefix?: { text?: string; html?: string; classes?: string };
19
+ * suffix?: { text?: string; html?: string; classes?: string };
18
20
  * };
19
- * conditionalText?: {
20
- * html: string;
21
+ * conditionalText?: {
22
+ * html: string;
21
23
  * }
22
24
  *}} Option
23
25
  */
@@ -63,6 +65,16 @@ export type Option = {
63
65
  value?: unknown;
64
66
  label?: string;
65
67
  hint?: string;
68
+ prefix?: {
69
+ text?: string;
70
+ html?: string;
71
+ classes?: string;
72
+ };
73
+ suffix?: {
74
+ text?: string;
75
+ html?: string;
76
+ classes?: string;
77
+ };
66
78
  };
67
79
  conditionalText?: {
68
80
  html: string;
@@ -1 +1 @@
1
- {"version":3,"file":"options-question.d.ts","sourceRoot":"","sources":["options-question.js"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;GAEG;AACH;;GAEG;AAEH;IAIC;;OAEG;IACH,oBAFW,yBAAyB,EAenC;IAnBD,4BAA4B;IAC5B,SADW,KAAK,CAAC,MAAM,CAAC,CAChB;IAiBP,yBAA+C;IAGhD;;;;;;;;OAQG;IACH,kCAPW,OAAO,UAAU,EAAE,OAAO,WAC1B,OAAO,UAAU,EAAE,OAAO,mBAC1B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YACvB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACvB,OAAO,+BAA+B,EAAE,+BAA+B,GACrE,OAAO,+BAA+B,EAAE,iBAAiB,CAkDrE;CA2DD;;qBAxKY;IACR,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,WAAW,CAAC,EAAE;QACZ,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QACd,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,IAAI,CAAC,EAAE,MAAM,CAAC;QAClB,KAAK,CAAC,EAAE,OAAO,CAAC;QACZ,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAA;KAClB,CAAC;IACA,eAAe,CAAC,EAAE;QAChB,IAAI,EAAE,MAAM,CAAC;KAChB,CAAA;CACH;+BAIU,OAAO,+BAA+B,EAAE,iBAAiB,GAAG;IAAE,QAAQ,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;CAAE;wCAG/F,OAAO,+BAA+B,EAAE,kBAAkB,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;CAAE;yBApC3E,eAAe"}
1
+ {"version":3,"file":"options-question.d.ts","sourceRoot":"","sources":["options-question.js"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;GAEG;AACH;;GAEG;AAEH;IAIC;;OAEG;IACH,oBAFW,yBAAyB,EAenC;IAnBD,4BAA4B;IAC5B,SADW,KAAK,CAAC,MAAM,CAAC,CAChB;IAiBP,yBAA+C;IAGhD;;;;;;;;OAQG;IACH,kCAPW,OAAO,UAAU,EAAE,OAAO,WAC1B,OAAO,UAAU,EAAE,OAAO,mBAC1B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YACvB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACvB,OAAO,+BAA+B,EAAE,+BAA+B,GACrE,OAAO,+BAA+B,EAAE,iBAAiB,CAkDrE;CA2DD;;qBA1KY;IACR,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,WAAW,CAAC,EAAE;QACZ,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QACnB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,IAAI,CAAC,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,OAAO,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,MAAM,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC5D,MAAM,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KAC5D,CAAC;IACH,eAAe,CAAC,EAAE;QAChB,IAAI,EAAE,MAAM,CAAC;KACb,CAAA;CACH;+BAIU,OAAO,+BAA+B,EAAE,iBAAiB,GAAG;IAAE,QAAQ,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;CAAE;wCAG/F,OAAO,+BAA+B,EAAE,kBAAkB,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;CAAE;yBAtC3E,eAAe"}
@@ -11,7 +11,7 @@ const defaultOptionJoinString = ',';
11
11
  * @typedef {{
12
12
  * text: string;
13
13
  * value: string;
14
- * hint?: object;
14
+ * hint?: object;
15
15
  * checked?: boolean | undefined;
16
16
  * attributes?: Record<string, string>;
17
17
  * behaviour?: 'exclusive';
@@ -19,14 +19,16 @@ const defaultOptionJoinString = ',';
19
19
  * question: string;
20
20
  * type: string;
21
21
  * fieldName: string;
22
- * inputClasses?: string;
23
- * html?: string;
22
+ * inputClasses?: string;
23
+ * html?: string;
24
24
  * value?: unknown;
25
- * label?: string;
26
- * hint?: string
25
+ * label?: string;
26
+ * hint?: string
27
+ * prefix?: { text?: string; html?: string; classes?: string };
28
+ * suffix?: { text?: string; html?: string; classes?: string };
27
29
  * };
28
- * conditionalText?: {
29
- * html: string;
30
+ * conditionalText?: {
31
+ * html: string;
30
32
  * }
31
33
  *}} Option
32
34
  */
@@ -48,8 +48,8 @@ export class Question {
48
48
  answerActionText: string;
49
49
  /** @type {string} text to display for 'add' link */
50
50
  addActionText: string;
51
- /** @type {import('#typedefs/question-types.d.ts').SummaryValueFormatter|undefined} custom function to format the summary display value */
52
- formatSummaryValue: import("#typedefs/question-types.d.ts").SummaryValueFormatter | undefined;
51
+ /** @type {import('#typedefs/question-props.d.ts').SummaryValueFormatter|undefined} custom function to format the summary display value */
52
+ formatSummaryValue: import("#typedefs/question-props.d.ts").SummaryValueFormatter | undefined;
53
53
  /**
54
54
  * @param {import('../journey/journey-response.js').JourneyResponse} [response]
55
55
  * @returns {boolean}
@@ -49,7 +49,7 @@ export class Question {
49
49
  answerActionText = 'Answer';
50
50
  /** @type {string} text to display for 'add' link */
51
51
  addActionText = 'Add';
52
- /** @type {import('#typedefs/question-types.d.ts').SummaryValueFormatter|undefined} custom function to format the summary display value */
52
+ /** @type {import('#typedefs/question-props.d.ts').SummaryValueFormatter|undefined} custom function to format the summary display value */
53
53
  formatSummaryValue;
54
54
 
55
55
  /**
@@ -163,7 +163,7 @@ export class Question {
163
163
 
164
164
  /**
165
165
  * Applies custom summary formatting if a formatSummaryValue function is provided
166
- * @param {import('#typedefs/question-types.d.ts').SummaryFormatterContext} context - the context for formatting
166
+ * @param {import('#typedefs/question-props.d.ts').SummaryFormatterContext} context - the context for formatting
167
167
  * @returns {string}
168
168
  */
169
169
  #applyCustomSummaryFormatter(context) {