diff --git a/src/server/plugins/engine/index.ts b/src/server/plugins/engine/index.ts index 3b73bb5d3..4d82a2ab3 100644 --- a/src/server/plugins/engine/index.ts +++ b/src/server/plugins/engine/index.ts @@ -31,6 +31,9 @@ const globals = { export const VIEW_PATH = 'src/server/plugins/engine/views' export const PLUGIN_PATH = 'node_modules/@defra/forms-engine-plugin' +export const STATE_NOT_YET_VALIDATED = '__stateNotYetValidated' +export const CURRENT_PAGE_PATH_KEY = '__currentPagePath' + export const prepareNunjucksEnvironment = function ( env: Environment, pluginOptions: PluginOptions diff --git a/src/server/plugins/engine/models/FormModel.ts b/src/server/plugins/engine/models/FormModel.ts index 6050a33b4..c2aff279e 100644 --- a/src/server/plugins/engine/models/FormModel.ts +++ b/src/server/plugins/engine/models/FormModel.ts @@ -48,6 +48,7 @@ import { createPage, type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js' +import { copyNotYetValidatedState } from '~/src/server/plugins/engine/pageControllers/helpers/state.js' import { validationOptions as opts } from '~/src/server/plugins/engine/pageControllers/validationOptions.js' import * as defaultServices from '~/src/server/plugins/engine/services/index.js' import { @@ -401,6 +402,9 @@ export class FormModel { // Add paths for navigation this.assignPaths(context) + // Handle restoration of payload from say a 'save-and-exit' request + copyNotYetValidatedState(request, context) + return context } diff --git a/src/server/plugins/engine/pageControllers/QuestionPageController.ts b/src/server/plugins/engine/pageControllers/QuestionPageController.ts index ec778d6a1..314c9ce58 100644 --- a/src/server/plugins/engine/pageControllers/QuestionPageController.ts +++ b/src/server/plugins/engine/pageControllers/QuestionPageController.ts @@ -28,7 +28,10 @@ import { } from '~/src/server/plugins/engine/helpers.js' import { type FormModel } from '~/src/server/plugins/engine/models/index.js' import { PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js' -import { prefillStateFromQueryParameters } from '~/src/server/plugins/engine/pageControllers/helpers/state.js' +import { + clearNotYetValidatedState, + prefillStateFromQueryParameters +} from '~/src/server/plugins/engine/pageControllers/helpers/state.js' import { type AnyFormRequest, type FormContext, @@ -324,7 +327,8 @@ export class QuestionPageController extends PageController { const cacheService = getCacheService(request.server) - return cacheService.setState(request, state) + // Clear any 'not yet validated' state before saving to cache + return cacheService.setState(request, clearNotYetValidatedState(state)) } async mergeState( diff --git a/src/server/plugins/engine/pageControllers/helpers/state.test.ts b/src/server/plugins/engine/pageControllers/helpers/state.test.ts index a3d008d78..d833c5c84 100644 --- a/src/server/plugins/engine/pageControllers/helpers/state.test.ts +++ b/src/server/plugins/engine/pageControllers/helpers/state.test.ts @@ -3,10 +3,15 @@ import { ComponentType, type Page } from '@defra/forms-model' import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js' import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js' import { + copyNotYetValidatedState, prefillStateFromQueryParameters, stripParam } from '~/src/server/plugins/engine/pageControllers/helpers/state.js' -import { type AnyFormRequest } from '~/src/server/plugins/engine/types.js' +import { + type AnyFormRequest, + type FormContext, + type FormContextRequest +} from '~/src/server/plugins/engine/types.js' import { type FormsService, type Services } from '~/src/server/types.js' function buildMockPage( @@ -218,4 +223,73 @@ describe('State helpers', () => { expect(stripParam(params, 'anyParam')).toBeUndefined() }) }) + + describe('copyNotYetValidatedState', () => { + it('should ignore if no invalid state', () => { + const mockRequest = {} as FormContextRequest + const mockContext = { + state: { abc: '123' }, + payload: {} + } as unknown as FormContext + copyNotYetValidatedState(mockRequest, mockContext) + expect(mockContext.state).toEqual({ abc: '123' }) + expect(mockContext.payload).toEqual({}) + }) + + it('should ignore if wrong path', () => { + const mockRequest = { + url: { + pathname: '/form-page1' + } + } as unknown as FormContextRequest + const mockContext = { + state: { + abc: '123', + __stateNotYetValidated: { + def: '456', + __currentPagePath: '/root' + } + }, + payload: {} + } as unknown as FormContext + copyNotYetValidatedState(mockRequest, mockContext) + expect(mockContext.state).toEqual({ + abc: '123', + __stateNotYetValidated: { + def: '456', + __currentPagePath: '/root' + } + }) + expect(mockContext.payload).toEqual({}) + }) + + it('should apply if correct path', () => { + const mockRequest = { + url: { + pathname: '/form-page1' + } + } as unknown as FormContextRequest + const mockContext = { + state: { + abc: '123', + __stateNotYetValidated: { + def: '456', + __currentPagePath: '/form-page1' + } + }, + payload: {} + } as unknown as FormContext + copyNotYetValidatedState(mockRequest, mockContext) + expect(mockContext.state).toEqual({ + abc: '123', + __stateNotYetValidated: { + def: '456', + __currentPagePath: '/form-page1' + } + }) + expect(mockContext.payload).toEqual({ + def: '456' + }) + }) + }) }) diff --git a/src/server/plugins/engine/pageControllers/helpers/state.ts b/src/server/plugins/engine/pageControllers/helpers/state.ts index 01357329e..6d7e2c621 100644 --- a/src/server/plugins/engine/pageControllers/helpers/state.ts +++ b/src/server/plugins/engine/pageControllers/helpers/state.ts @@ -1,9 +1,17 @@ import { getHiddenFields } from '@defra/forms-model' +import { + CURRENT_PAGE_PATH_KEY, + STATE_NOT_YET_VALIDATED +} from '~/src/server/plugins/engine/index.js' import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js' import { type AnyFormRequest, - type FormStateValue + type FormContext, + type FormContextRequest, + type FormStateValue, + type FormSubmissionState, + type FormValue } from '~/src/server/plugins/engine/types.js' import { type FormQuery } from '~/src/server/routes/types.js' import { type Services } from '~/src/server/types.js' @@ -91,3 +99,44 @@ export async function prefillStateFromQueryParameters( return true } + +/** + * Copies any potentially invalid state into the payload, and removes those values from state + * NOTE - this method has a side-effect on 'context.state' and 'context.payload' + * @param request - the form request + * @param context - the form context + */ +export function copyNotYetValidatedState( + request: FormContextRequest, + context: FormContext +) { + const potentiallyInvalidState = context.state[STATE_NOT_YET_VALIDATED] as + | Record + | undefined + if (!potentiallyInvalidState) { + return + } + + const originalPath = potentiallyInvalidState[CURRENT_PAGE_PATH_KEY] + + if (originalPath && originalPath === request.url.pathname) { + context.payload = { + ...context.payload, + ...potentiallyInvalidState, + [CURRENT_PAGE_PATH_KEY]: undefined + } + } +} + +/** + * Remove any temporary 'not yet validated' state now that it's been validated + * @param state - the form state + */ +export function clearNotYetValidatedState( + state: FormSubmissionState +): FormSubmissionState { + if (state[STATE_NOT_YET_VALIDATED]) { + state[STATE_NOT_YET_VALIDATED] = undefined + } + return state +}