@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.
- package/README.md +70 -246
- package/docs/Email Validation.md +86 -0
- package/docs/Getting Started.md +299 -0
- package/package.json +1 -1
- package/src/components/checkbox/question.d.ts.map +1 -1
- package/src/components/checkbox/question.js +3 -2
- package/src/components/checkbox/question.test.js +8 -0
- package/src/components/email/question.d.ts +0 -9
- package/src/components/email/question.d.ts.map +1 -1
- package/src/components/email/question.js +2 -13
- package/src/components/email/question.test.js +19 -0
- package/src/components/multi-field-input/question.d.ts +4 -19
- package/src/components/multi-field-input/question.d.ts.map +1 -1
- package/src/components/multi-field-input/question.js +89 -23
- package/src/components/multi-field-input/question.test.js +441 -3
- package/src/components/number-entry/question.d.ts.map +1 -1
- package/src/components/number-entry/question.js +3 -2
- package/src/components/number-entry/question.test.js +17 -0
- package/src/components/unit-option-entry/question.d.ts.map +1 -1
- package/src/components/unit-option-entry/question.js +3 -1
- package/src/components/unit-option-entry/question.test.js +15 -0
- package/src/questions/question.d.ts +2 -2
- package/src/questions/question.js +2 -2
- package/test/snapshots/check-your-answers-with-answers.html +1 -1
- package/test/utils/question-test-utils.js +2 -1
- package/typedefs/question-props.d.ts +33 -0
- package/typedefs/question-types.d.ts +0 -23
|
@@ -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
|
|
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 & 2
|
|
352
|
+
const expectedValue = 'Line 1\nLine & 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, '<script>alert("xss")</script><br>Safe & 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, '<b>Value 1</b>');
|
|
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 <2>');
|
|
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,
|
|
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,
|
|
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
|
|
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-
|
|
52
|
-
formatSummaryValue: import("#typedefs/question-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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.
|