@planning-inspectorate/dynamic-forms 3.16.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 +124 -189
- package/docs/Email Validation.md +86 -0
- package/docs/Getting Started.md +299 -0
- package/package.json +1 -1
- package/src/components/address/question.d.ts +1 -9
- package/src/components/address/question.d.ts.map +1 -1
- package/src/components/address/question.js +6 -17
- package/src/components/checkbox/question.d.ts +1 -21
- package/src/components/checkbox/question.d.ts.map +1 -1
- package/src/components/checkbox/question.js +38 -50
- package/src/components/checkbox/question.test.js +177 -26
- package/src/components/date/question.d.ts +0 -8
- package/src/components/date/question.d.ts.map +1 -1
- package/src/components/date/question.js +6 -14
- package/src/components/date-period/question.d.ts +1 -5
- package/src/components/date-period/question.d.ts.map +1 -1
- package/src/components/date-period/question.js +1 -1
- package/src/components/date-time/question.d.ts +0 -8
- package/src/components/date-time/question.d.ts.map +1 -1
- package/src/components/date-time/question.js +4 -12
- 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/manage-list/answer-summary-list.njk +1 -1
- package/src/components/manage-list/question.d.ts +1 -5
- package/src/components/manage-list/question.d.ts.map +1 -1
- package/src/components/manage-list/question.js +1 -1
- 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 +0 -28
- package/src/components/number-entry/question.d.ts.map +1 -1
- package/src/components/number-entry/question.js +3 -20
- package/src/components/number-entry/question.test.js +17 -0
- package/src/components/radio/question.d.ts +1 -9
- package/src/components/radio/question.d.ts.map +1 -1
- package/src/components/radio/question.js +29 -46
- package/src/components/radio/question.test.js +139 -60
- package/src/components/select/question.d.ts +1 -15
- package/src/components/select/question.d.ts.map +1 -1
- package/src/components/select/question.js +4 -47
- package/src/components/single-line-input/question.d.ts +0 -4
- package/src/components/single-line-input/question.d.ts.map +1 -1
- package/src/components/single-line-input/question.js +1 -1
- package/src/components/text-entry/question.d.ts +0 -4
- package/src/components/text-entry/question.d.ts.map +1 -1
- package/src/components/text-entry/question.js +1 -1
- package/src/components/text-entry-redact/question.d.ts +2 -6
- package/src/components/text-entry-redact/question.d.ts.map +1 -1
- package/src/components/text-entry-redact/question.js +1 -1
- 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/components/utils/question-has-answer.js +1 -1
- package/src/components/utils/question-utils.d.ts +19 -1
- package/src/components/utils/question-utils.d.ts.map +1 -1
- package/src/components/utils/question-utils.js +31 -2
- package/src/components/utils/question-utils.test.js +89 -2
- package/src/questions/options-question.d.ts +4 -4
- package/src/questions/options-question.d.ts.map +1 -1
- package/src/questions/options-question.js +27 -2
- package/src/questions/options-question.test.js +106 -0
- package/src/questions/question.d.ts +16 -1
- package/src/questions/question.d.ts.map +1 -1
- package/src/questions/question.js +64 -9
- package/src/questions/question.test.js +86 -0
- package/test/journey.js +2 -0
- package/test/questions.js +50 -0
- package/test/snapshots/check-your-answers-not-started.html +22 -0
- package/test/snapshots/check-your-answers-with-answers.html +23 -1
- package/test/snapshots/favourite-activity-reason.html +54 -0
- package/test/snapshots/travel-requirements.html +48 -0
- package/test/utils/question-test-utils.js +38 -7
- package/typedefs/question-props.d.ts +33 -0
- package/typedefs/question-types.d.ts +4 -0
|
@@ -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
|
});
|
|
@@ -6,34 +6,6 @@ export class NumberEntryQuestion extends Question {
|
|
|
6
6
|
suffix: string | undefined;
|
|
7
7
|
label: string | undefined;
|
|
8
8
|
answerForViewModel(answers: any): string | number;
|
|
9
|
-
/**
|
|
10
|
-
* @param {import('#question').QuestionViewModel} viewModel
|
|
11
|
-
*/
|
|
12
|
-
addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
|
|
13
|
-
/**
|
|
14
|
-
* returns the formatted answers values to be used to build task list elements
|
|
15
|
-
* @param {unknown} answer
|
|
16
|
-
* @param {import('#journey').Journey} journey
|
|
17
|
-
* @param {string} sectionSegment
|
|
18
|
-
* @returns {Array<{
|
|
19
|
-
* key: string;
|
|
20
|
-
* value: string | Object;
|
|
21
|
-
* action: {
|
|
22
|
-
* href: string;
|
|
23
|
-
* text: string;
|
|
24
|
-
* visuallyHiddenText: string;
|
|
25
|
-
* };
|
|
26
|
-
* }>}
|
|
27
|
-
*/
|
|
28
|
-
formatAnswerForSummary(sectionSegment: string, journey: import("#journey").Journey, answer: unknown): Array<{
|
|
29
|
-
key: string;
|
|
30
|
-
value: string | Object;
|
|
31
|
-
action: {
|
|
32
|
-
href: string;
|
|
33
|
-
text: string;
|
|
34
|
-
visuallyHiddenText: string;
|
|
35
|
-
};
|
|
36
|
-
}>;
|
|
37
9
|
}
|
|
38
10
|
export default NumberEntryQuestion;
|
|
39
11
|
import { Question } from '#question';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAGA;IACC;;OAEG;IACH,gDAFW,OAAO,+BAA+B,EAAE,yBAAyB,
|
|
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,6 +7,8 @@ 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
13
|
viewFolder: 'number-entry'
|
|
12
14
|
});
|
|
@@ -20,31 +22,12 @@ export class NumberEntryQuestion extends Question {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
|
-
* @param {import('#question').QuestionViewModel} viewModel
|
|
25
|
+
* @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
|
|
24
26
|
*/
|
|
25
27
|
addCustomDataToViewModel(viewModel) {
|
|
26
28
|
viewModel.question.label = this.label;
|
|
27
29
|
viewModel.question.suffix = this.suffix;
|
|
28
30
|
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* returns the formatted answers values to be used to build task list elements
|
|
32
|
-
* @param {unknown} answer
|
|
33
|
-
* @param {import('#journey').Journey} journey
|
|
34
|
-
* @param {string} sectionSegment
|
|
35
|
-
* @returns {Array<{
|
|
36
|
-
* key: string;
|
|
37
|
-
* value: string | Object;
|
|
38
|
-
* action: {
|
|
39
|
-
* href: string;
|
|
40
|
-
* text: string;
|
|
41
|
-
* visuallyHiddenText: string;
|
|
42
|
-
* };
|
|
43
|
-
* }>}
|
|
44
|
-
*/
|
|
45
|
-
formatAnswerForSummary(sectionSegment, journey, answer) {
|
|
46
|
-
return super.formatAnswerForSummary(sectionSegment, journey, answer, false);
|
|
47
|
-
}
|
|
48
31
|
}
|
|
49
32
|
|
|
50
33
|
export default NumberEntryQuestion;
|
|
@@ -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
|
+
});
|
|
@@ -2,17 +2,9 @@ export class RadioQuestion extends OptionsQuestion {
|
|
|
2
2
|
/**
|
|
3
3
|
* @param {import('#typedefs/question-props.d.ts').RadioQuestionParams} params
|
|
4
4
|
*/
|
|
5
|
-
constructor({
|
|
5
|
+
constructor({ label, html, legend, viewFolder, ...parentParams }: import("#typedefs/question-props.d.ts").RadioQuestionParams);
|
|
6
6
|
label: string | undefined;
|
|
7
7
|
legend: string | undefined;
|
|
8
|
-
/**
|
|
9
|
-
* @param {import('#question').QuestionViewModel} viewModel
|
|
10
|
-
*/
|
|
11
|
-
addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
|
|
12
|
-
/**
|
|
13
|
-
* returns the formatted answers values to be used to build task list elements
|
|
14
|
-
*/
|
|
15
|
-
formatAnswerForSummary(sectionSegment: any, journey: any, answer: any): import("../../index.js").SummaryRow[];
|
|
16
8
|
}
|
|
17
9
|
export default RadioQuestion;
|
|
18
10
|
import OptionsQuestion from '../../questions/options-question.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAGA;IACC;;OAEG;IACH,kEAFW,OAAO,+BAA+B,EAAE,mBAAmB,EAWrE;IAFA,0BAAkB;IAClB,2BAAoB;CAuCrB;;4BArD2B,qCAAqC"}
|