Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/server/plugins/engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/server/plugins/engine/models/FormModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
76 changes: 75 additions & 1 deletion src/server/plugins/engine/pageControllers/helpers/state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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'
})
})
})
})
51 changes: 50 additions & 1 deletion src/server/plugins/engine/pageControllers/helpers/state.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<string, FormValue>
| 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
}
Loading