Skip to content

Commit 1c6ef1f

Browse files
Initialise evaluationState using empty state (#210)
* Initialise evaluationState using empty state * Remove engine condition in initialiseContext * Added unit test to prove stale state is handled for conditions * Fix V1 evaluation context tests * Add evaluation context test for all components --------- Co-authored-by: Jez Barnsley <jbarnsley.github@gmail.com>
1 parent 8a4e04a commit 1c6ef1f

4 files changed

Lines changed: 3078 additions & 27 deletions

File tree

src/server/plugins/engine/models/FormModel.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
ConditionsModel,
44
ControllerPath,
55
ControllerType,
6-
Engine,
76
SchemaVersion,
87
convertConditionWrapperFromV2,
98
formDefinitionSchema,
@@ -19,6 +18,7 @@ import {
1918
type ConditionWrapperV2,
2019
type ConditionsModelData,
2120
type DateUnits,
21+
type Engine,
2222
type FormDefinition,
2323
type List,
2424
type Page
@@ -363,10 +363,7 @@ export class FormModel {
363363
// Add page to context
364364
context.relevantPages.push(nextPage)
365365

366-
// Engine.V2 is excluded here as this will have already been done in initialiseContext()
367-
if (this.engine !== Engine.V2) {
368-
this.assignEvaluationState(context, nextPage)
369-
}
366+
this.assignEvaluationState(context, nextPage)
370367

371368
this.assignRelevantState(context, nextPage)
372369

@@ -392,12 +389,19 @@ export class FormModel {
392389
}
393390

394391
private initialiseContext(context: FormContext) {
395-
// For the V2 engine, we initialise `evaluationState` for all keys.
392+
// Initialise `evaluationState` for all keys using empty state.
396393
// This is because the current condition evaluation library (eval-expr)
397394
// will throw if an expression uses a key that is undefined.
398-
if (this.engine === Engine.V2) {
399-
for (const page of this.pages) {
400-
this.assignEvaluationState(context, page)
395+
const emptyState = Object.freeze({})
396+
397+
for (const page of this.pages) {
398+
const { collection, pageDef } = page
399+
400+
if (!hasRepeater(pageDef)) {
401+
Object.assign(
402+
context.evaluationState,
403+
collection.getContextValueFromState(emptyState)
404+
)
401405
}
402406
}
403407
}

src/server/plugins/engine/pageControllers/QuestionPageController.test.ts

Lines changed: 235 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type PageQuestion } from '@defra/forms-model'
22

3+
import { getForm } from '~/src/server/plugins/engine/configureEnginePlugin.js'
34
import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
45
import { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
56
import {
@@ -8,6 +9,8 @@ import {
89
} from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
910
import { serverWithSaveAndExit } from '~/src/server/plugins/engine/pageControllers/__stubs__/server.js'
1011
import {
12+
FileStatus,
13+
UploadStatus,
1114
type FormContext,
1215
type FormPageViewModel,
1316
type FormState,
@@ -321,7 +324,11 @@ describe('QuestionPageController', () => {
321324
'AddressLine2',
322325
'Town',
323326
'Postcode'
324-
]
327+
],
328+
applicantTwoAddress: null,
329+
applicantTwoFirstName: null,
330+
applicantTwoLastName: null,
331+
applicantTwoMiddleName: null
325332
})
326333

327334
// Our context should know which pages are relevant
@@ -358,15 +365,18 @@ describe('QuestionPageController', () => {
358365
'/summary'
359366
])
360367

361-
// Our context should no longer know anything about our applicant
362-
expect(stateAfter).not.toHaveProperty('numberOfApplicants')
363-
expect(stateAfter).not.toHaveProperty('applicantOneFirstName')
364-
expect(stateAfter).not.toHaveProperty('applicantOneMiddleName')
365-
expect(stateAfter).not.toHaveProperty('applicantOneLastName')
366-
expect(stateAfter).not.toHaveProperty('applicantOneAddress')
367-
368+
// Our evaluation context should have default values for irrelevant fields
368369
expect(stateAfter).toEqual({
369-
ukPassport: false
370+
ukPassport: false,
371+
numberOfApplicants: null,
372+
applicantOneFirstName: null,
373+
applicantOneMiddleName: null,
374+
applicantOneLastName: null,
375+
applicantOneAddress: null,
376+
applicantTwoAddress: null,
377+
applicantTwoFirstName: null,
378+
applicantTwoLastName: null,
379+
applicantTwoMiddleName: null
370380
})
371381
})
372382

@@ -521,6 +531,206 @@ describe('QuestionPageController', () => {
521531
}
522532
])
523533
})
534+
535+
it('correctly initialises default values', async () => {
536+
const components = await getForm(
537+
'../../../../test/form/definitions/components.json'
538+
)
539+
const { pages } = components
540+
541+
const model = new FormModel(components, {
542+
basePath: 'test'
543+
})
544+
545+
const controller = new QuestionPageController(model, pages[0])
546+
547+
const state: FormSubmissionState = { $$__referenceNumber: 'foobar' }
548+
549+
let request = buildFormContextRequest({
550+
method: 'get',
551+
url: new URL('http://example.com/test/all-components'),
552+
path: '/test/all-components',
553+
params: {
554+
path: 'all-components',
555+
slug: 'test'
556+
},
557+
query: {},
558+
app: { model }
559+
})
560+
561+
// Calculate our context based on the page we're attempting to load and the above state we provide
562+
let context = controller.model.getFormContext(request, state)
563+
564+
// Context paths should be all pages up to the one we requested
565+
expect(context.paths).toEqual(['/all-components'])
566+
567+
// Our context should have default values for all input fields
568+
expect(context.evaluationState).toEqual({
569+
textField: null,
570+
multilineTextField: null,
571+
numberField: null,
572+
datePartsField: null,
573+
monthYearField: null,
574+
yesNoField: null,
575+
emailAddressField: null,
576+
telephoneNumberField: null,
577+
addressField: null,
578+
radiosField: null,
579+
selectField: null,
580+
autocompleteField: null,
581+
checkboxesSingle: [],
582+
checkboxesMultiple: [],
583+
checkboxesSingleNumber: [],
584+
checkboxesMultipleNumber: [],
585+
fileUpload: null
586+
})
587+
588+
Object.assign(state, {
589+
textField: 'Text field',
590+
multilineTextField: 'Multiline text field',
591+
numberField: 1,
592+
datePartsField__day: 12,
593+
datePartsField__month: 12,
594+
datePartsField__year: 2012,
595+
monthYearField__month: 12,
596+
monthYearField__year: 2012,
597+
yesNoField: 'true',
598+
emailAddressField: 'user@email.com',
599+
telephoneNumberField: '+447900000000',
600+
addressField__addressLine1: 'Address line 1',
601+
addressField__addressLine2: 'Address line 2',
602+
addressField__town: 'Town or city',
603+
addressField__county: 'Cheshire',
604+
addressField__postcode: 'CW1 1AB',
605+
radiosField: 'privateLimitedCompany',
606+
selectField: 910400000,
607+
autocompleteField: 910400044,
608+
checkboxesSingle: ['Shetland'],
609+
checkboxesMultiple: ['Arabian', 'Shire', 'Race'],
610+
checkboxesSingleNumber: [1],
611+
checkboxesMultipleNumber: [0, 1]
612+
})
613+
614+
request = buildFormContextRequest({
615+
method: 'get',
616+
url: new URL('http://example.com/test/methodology-statement'),
617+
path: '/test/methodology-statement',
618+
params: {
619+
path: 'methodology-statement',
620+
slug: 'test'
621+
},
622+
query: {},
623+
app: { model }
624+
})
625+
626+
// Calculate our context based on the page we're attempting to load and the above state we provide
627+
context = controller.model.getFormContext(request, state)
628+
629+
// Context paths should be all pages up to the one we requested
630+
expect(context.paths).toEqual([
631+
'/all-components',
632+
'/methodology-statement'
633+
])
634+
635+
// Our context should have evaluation state values for relevant fields and default values for everything else
636+
expect(context.evaluationState).toEqual({
637+
textField: 'Text field',
638+
multilineTextField: 'Multiline text field',
639+
numberField: 1,
640+
datePartsField: '2012-12-12',
641+
monthYearField: '2012-12',
642+
yesNoField: null,
643+
emailAddressField: 'user@email.com',
644+
telephoneNumberField: '+447900000000',
645+
addressField: [
646+
'Address line 1',
647+
'Address line 2',
648+
'Town or city',
649+
'Cheshire',
650+
'CW1 1AB'
651+
],
652+
radiosField: 'privateLimitedCompany',
653+
selectField: 910400000,
654+
autocompleteField: 910400044,
655+
checkboxesSingle: ['Shetland'],
656+
checkboxesMultiple: ['Arabian', 'Shire', 'Race'],
657+
checkboxesSingleNumber: [1],
658+
checkboxesMultipleNumber: [0, 1],
659+
fileUpload: null
660+
})
661+
662+
Object.assign(state, {
663+
fileUpload: [
664+
{
665+
uploadId: '348e1878-59c0-4d2e-a52b-e5042ad729f0',
666+
status: {
667+
uploadStatus: UploadStatus.ready,
668+
metadata: {
669+
retrievalKey: 'enrique.chase@defra.gov.uk'
670+
},
671+
form: {
672+
file: {
673+
fileId: 'fd5db541-179c-4107-a4d0-149d09672ffc',
674+
filename: 'test.jpg',
675+
fileStatus: FileStatus.complete,
676+
contentLength: 3671
677+
}
678+
},
679+
numberOfRejectedFiles: 0
680+
}
681+
}
682+
]
683+
})
684+
685+
request = buildFormContextRequest({
686+
method: 'get',
687+
url: new URL('http://example.com/test/summary'),
688+
path: '/test/summary',
689+
params: {
690+
path: 'summary',
691+
slug: 'test'
692+
},
693+
query: {},
694+
app: { model }
695+
})
696+
697+
// Calculate our context based on the page we're attempting to load and the above state we provide
698+
context = controller.model.getFormContext(request, state)
699+
700+
// Context paths should be all pages up to the one we requested
701+
expect(context.paths).toEqual([
702+
'/all-components',
703+
'/methodology-statement',
704+
'/summary'
705+
])
706+
707+
// Our context should now have evaluation state values all fields
708+
expect(context.evaluationState).toEqual({
709+
textField: 'Text field',
710+
multilineTextField: 'Multiline text field',
711+
numberField: 1,
712+
datePartsField: '2012-12-12',
713+
monthYearField: '2012-12',
714+
yesNoField: null,
715+
emailAddressField: 'user@email.com',
716+
telephoneNumberField: '+447900000000',
717+
addressField: [
718+
'Address line 1',
719+
'Address line 2',
720+
'Town or city',
721+
'Cheshire',
722+
'CW1 1AB'
723+
],
724+
radiosField: 'privateLimitedCompany',
725+
selectField: 910400000,
726+
autocompleteField: 910400044,
727+
checkboxesSingle: ['Shetland'],
728+
checkboxesMultiple: ['Arabian', 'Shire', 'Race'],
729+
checkboxesSingleNumber: [1],
730+
checkboxesMultipleNumber: [0, 1],
731+
fileUpload: ['fd5db541-179c-4107-a4d0-149d09672ffc']
732+
})
733+
})
524734
})
525735

526736
describe('Form validation', () => {
@@ -1005,7 +1215,11 @@ describe('QuestionPageController V2', () => {
10051215
'AddressLine2',
10061216
'Town',
10071217
'Postcode'
1008-
]
1218+
],
1219+
applicantTwoAddress: null,
1220+
applicantTwoFirstName: null,
1221+
applicantTwoLastName: null,
1222+
applicantTwoMiddleName: null
10091223
})
10101224

10111225
// Our context should know which pages are relevant
@@ -1042,15 +1256,18 @@ describe('QuestionPageController V2', () => {
10421256
'/summary'
10431257
])
10441258

1045-
// Our context should no longer know anything about our applicant
1046-
expect(stateAfter).not.toHaveProperty('numberOfApplicants')
1047-
expect(stateAfter).not.toHaveProperty('applicantOneFirstName')
1048-
expect(stateAfter).not.toHaveProperty('applicantOneMiddleName')
1049-
expect(stateAfter).not.toHaveProperty('applicantOneLastName')
1050-
expect(stateAfter).not.toHaveProperty('applicantOneAddress')
1051-
1259+
// Our evaluation context should have default values for irrelevant fields
10521260
expect(stateAfter).toEqual({
1053-
ukPassport: false
1261+
ukPassport: false,
1262+
numberOfApplicants: null,
1263+
applicantOneFirstName: null,
1264+
applicantOneMiddleName: null,
1265+
applicantOneLastName: null,
1266+
applicantOneAddress: null,
1267+
applicantTwoAddress: null,
1268+
applicantTwoFirstName: null,
1269+
applicantTwoLastName: null,
1270+
applicantTwoMiddleName: null
10541271
})
10551272
})
10561273

0 commit comments

Comments
 (0)