@planning-inspectorate/dynamic-forms 3.17.0 → 3.18.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.
@@ -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: {
@@ -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) {
@@ -146,7 +146,7 @@
146
146
  Travel Companions
147
147
  </dt>
148
148
  <dd class="govuk-summary-list__value">
149
- test<br>1<br>
149
+ test<br>1
150
150
  </dd>
151
151
  <dd class="govuk-summary-list__actions">
152
152
  <a class="govuk-link" href="questions/companions">Change<span class="govuk-visually-hidden"> Enter the names and ages of your travel companions.</span></a>
@@ -235,7 +235,8 @@ export function mockAnswer(q) {
235
235
  for (const field of q.inputFields) {
236
236
  res += mockInputFieldValue(field) + '<br>';
237
237
  }
238
- return res;
238
+ // Remove trailing <br>
239
+ return res.slice(0, -4);
239
240
  }
240
241
  break;
241
242
  case COMPONENT_TYPES.ADDRESS: {
@@ -1,4 +1,27 @@
1
1
  import type { QuestionParameters } from './question-types.d.ts';
2
+ import type { Question } from '../src/questions/question.js';
3
+ import type { Journey } from '../src/journey/journey.js';
4
+
5
+ /**
6
+ * Context passed to the custom summary formatter
7
+ */
8
+ export interface SummaryFormatterContext<TAnswer = unknown> {
9
+ /** The raw answer value */
10
+ answer: TAnswer;
11
+ /** The default display value */
12
+ formattedAnswer: string;
13
+ /** The question instance */
14
+ question: Question;
15
+ /** The journey instance */
16
+ journey: Journey;
17
+ /** The section segment */
18
+ sectionSegment: string;
19
+ }
20
+
21
+ /**
22
+ * Custom function to format the summary display value
23
+ */
24
+ export type SummaryValueFormatter<TAnswer = unknown> = (context: SummaryFormatterContext<TAnswer>) => string;
2
25
 
3
26
  export type QuestionTypes =
4
27
  | 'boolean'
@@ -74,6 +97,7 @@ interface InputField {
74
97
  formatJoinString?: string; // used by formatAnswerForSummary (e.g. task list display), effective default to line break
75
98
  formatPrefix?: string; // used by formatAnswerForSummary (e.g. task list display), to prefix answer
76
99
  formatTextFunction?: (text: string) => string; // used to format the answer for display and value in question
100
+ formatSummaryValue?: (context: InputFieldSummaryContext) => string; // used to format the answer for summary display only (not escaped)
77
101
  attributes?: Record<string, string>; // used to add HTML attributes to the field
78
102
  suffix?: Affix; // used to add a suffix to the field
79
103
  prefix?: Affix; // used to add a prefix to the field
@@ -83,6 +107,15 @@ interface InputField {
83
107
  pattern?: string; // Used for backwards-compatibility for older iOS devices in numeric input fields
84
108
  }
85
109
 
110
+ /**
111
+ * Context passed to InputField.formatSummaryValue
112
+ * Extends the base SummaryFormatterContext with field-specific properties
113
+ */
114
+ interface InputFieldSummaryContext extends SummaryFormatterContext {
115
+ /** The input field configuration */
116
+ field: InputField;
117
+ }
118
+
86
119
  /*
87
120
  * UnitOptions are the options displayed in the radio format - in this case the value
88
121
  * represents the unit.