@defra/forms-engine-plugin 1.3.1 → 1.4.1
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/.server/server/plugins/engine/index.js +1 -1
- package/.server/server/plugins/engine/index.js.map +1 -1
- package/.server/server/plugins/engine/models/FormModel.js +2 -2
- package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
- package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +1 -0
- package/.server/server/plugins/engine/models/SummaryViewModel.js +1 -0
- package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
- package/.server/server/plugins/engine/options.js +4 -1
- package/.server/server/plugins/engine/options.js.map +1 -1
- package/.server/server/plugins/engine/options.test.js +20 -0
- package/.server/server/plugins/engine/options.test.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.d.ts +5 -0
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +27 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/StartPageController.d.ts +2 -0
- package/.server/server/plugins/engine/pageControllers/StartPageController.js +3 -0
- package/.server/server/plugins/engine/pageControllers/StartPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +1 -0
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +4 -0
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
- package/.server/server/plugins/engine/plugin.js +5 -2
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/routes/exit.d.ts +46 -0
- package/.server/server/plugins/engine/routes/exit.js +36 -0
- package/.server/server/plugins/engine/routes/exit.js.map +1 -0
- package/.server/server/plugins/engine/types.d.ts +6 -2
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/plugins/engine/views/exit.html +31 -0
- package/.server/server/plugins/engine/views/partials/form.html +17 -6
- package/.server/server/routes/types.d.ts +2 -1
- package/.server/server/routes/types.js +1 -0
- package/.server/server/routes/types.js.map +1 -1
- package/.server/server/schemas/index.js +1 -1
- package/.server/server/schemas/index.js.map +1 -1
- package/.server/server/services/cacheService.d.ts +2 -0
- package/.server/server/services/cacheService.js +9 -5
- package/.server/server/services/cacheService.js.map +1 -1
- package/package.json +1 -1
- package/src/server/index.test.ts +39 -0
- package/src/server/plugins/engine/components/helpers.test.ts +31 -0
- package/src/server/plugins/engine/index.ts +1 -3
- package/src/server/plugins/engine/models/FormModel.test.ts +85 -11
- package/src/server/plugins/engine/models/FormModel.ts +5 -2
- package/src/server/plugins/engine/models/SummaryViewModel.test.ts +59 -0
- package/src/server/plugins/engine/models/SummaryViewModel.ts +1 -0
- package/src/server/plugins/engine/options.js +4 -1
- package/src/server/plugins/engine/options.test.js +20 -0
- package/src/server/plugins/engine/pageControllers/PageController.test.ts +25 -0
- package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +178 -1
- package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +28 -1
- package/src/server/plugins/engine/pageControllers/StartPageController.ts +4 -0
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +5 -0
- package/src/server/plugins/engine/plugin.ts +5 -1
- package/src/server/plugins/engine/routes/exit.ts +47 -0
- package/src/server/plugins/engine/types.ts +10 -4
- package/src/server/plugins/engine/views/exit.html +31 -0
- package/src/server/plugins/engine/views/partials/form.html +17 -6
- package/src/server/routes/types.ts +2 -1
- package/src/server/schemas/index.ts +2 -1
- package/src/server/services/cacheService.test.ts +45 -0
- package/src/server/services/cacheService.ts +20 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cacheService.js","names":["Hoek","config","partition","ADDITIONAL_IDENTIFIER","CacheService","cache","generateKey","customFetcher","logger","constructor","server","cacheName","options","keyGenerator","sessionHydrator","log","defaultKeyGenerator","bind","undefined","segment","getState","request","cached","get","Key","rehydrated","set","setState","state","key","ttl","getConfirmationState","Confirmation","value","setConfirmationState","confirmationState","clearState","yar","id","drop","getFlash","messages","flash","Array","isArray","length","at","setFlash","message","Error","params","slug","additionalIdentifier","baseKey","merge","update","mergeArrays"],"sources":["../../../src/server/services/cacheService.ts"],"sourcesContent":["import { type Request, type Server } from '@hapi/hapi'\nimport * as Hoek from '@hapi/hoek'\n\nimport { config } from '~/src/config/index.js'\nimport { type createServer } from '~/src/server/index.js'\nimport {\n type FormPayload,\n type FormState,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\n\nconst partition = 'cache'\n\nexport enum ADDITIONAL_IDENTIFIER {\n Confirmation = ':confirmation'\n}\n\nexport class CacheService {\n /**\n * This service is responsible for getting, storing or deleting a user's session data in the cache. This service has been registered by {@link createServer}\n */\n cache\n generateKey?: (request: Request | FormRequest | FormRequestPayload) => string\n customFetcher?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState | null>\n\n logger: Server['logger']\n\n constructor({\n server,\n cacheName,\n options\n }: {\n server: Server\n cacheName?: string\n options?: {\n keyGenerator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => string\n sessionHydrator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState | null>\n }\n }) {\n const { keyGenerator, sessionHydrator } = options ?? {}\n if (!cacheName) {\n server.log(\n 'warn',\n 'You are using the default hapi cache. Please provide a cache name in plugin registration options.'\n )\n }\n this.generateKey = keyGenerator ?? this.defaultKeyGenerator.bind(this)\n this.customFetcher = sessionHydrator ?? undefined\n this.cache = server.cache({ cache: cacheName, segment: 'formSubmission' })\n this.logger = server.logger\n }\n\n async getState(\n request: Request | FormRequest | FormRequestPayload\n ): Promise<FormSubmissionState> {\n let cached = await this.cache.get(this.Key(request))\n\n // If nothing in Redis, attempt to rehydrate from backend DB\n if (!cached && this.customFetcher) {\n const rehydrated = await this.customFetcher(request)\n\n if (rehydrated != null) {\n await this.cache.set(\n this.Key(request),\n rehydrated,\n config.get('sessionTimeout')\n )\n cached = await this.getState(request)\n }\n }\n\n return cached ?? {}\n }\n\n async setState(\n request: FormRequest | FormRequestPayload,\n state: FormSubmissionState\n ) {\n const key = this.Key(request)\n const ttl = config.get('sessionTimeout')\n\n await this.cache.set(key, state, ttl)\n return this.getState(request)\n }\n\n async getConfirmationState(\n request: FormRequest | FormRequestPayload\n ): Promise<{ confirmed?: true }> {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const value = await this.cache.get(key)\n\n return value ?? {}\n }\n\n async setConfirmationState(\n request: FormRequest | FormRequestPayload,\n confirmationState: { confirmed?: true }\n ) {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const ttl = config.get('confirmationSessionTimeout')\n\n return this.cache.set(key, confirmationState, ttl)\n }\n\n async clearState(request: FormRequest | FormRequestPayload) {\n if (request.yar.id) {\n await this.cache.drop(this.Key(request))\n }\n }\n\n getFlash(\n request: FormRequest | FormRequestPayload\n ): { errors: FormSubmissionError[] } | undefined {\n const key = this.Key(request)\n const messages = request.yar.flash(key.id)\n\n if (Array.isArray(messages) && messages.length) {\n return messages.at(0) as { errors: FormSubmissionError[] }\n }\n }\n\n setFlash(\n request: FormRequest | FormRequestPayload,\n message: { errors: FormSubmissionError[] }\n ) {\n const key = this.Key(request)\n\n request.yar.flash(key.id, message)\n }\n\n private defaultKeyGenerator(\n request: Request | FormRequest | FormRequestPayload\n ): string {\n if (!request.yar.id) {\n throw new Error('No session ID found')\n }\n\n const state = request.params.state ?? ''\n const slug = request.params.slug ?? ''\n return `${request.yar.id}:${state}:${slug}:`\n }\n\n /**\n * The key used to store user session data against.\n * If there are multiple forms on the same runner instance, for example `form-a` and `form-a-feedback` this will prevent CacheService from clearing data from `form-a` if a user gave feedback before they finished `form-a`\n * @param request - hapi request object\n * @param additionalIdentifier - appended to the id\n */\n Key(\n request: Request | FormRequest | FormRequestPayload,\n additionalIdentifier?: ADDITIONAL_IDENTIFIER\n ) {\n const baseKey = this.generateKey\n ? this.generateKey(request)\n : this.defaultKeyGenerator(request)\n\n return {\n segment: partition,\n id: `${baseKey}${additionalIdentifier ?? ''}`\n }\n }\n}\n\n/**\n * State merge helper\n * 1. Merges objects (form fields)\n * 2. Overwrites arrays\n */\nexport function merge<StateType extends FormState | FormPayload>(\n state: StateType,\n update: object\n): StateType {\n return Hoek.merge(state, update, {\n mergeArrays: false\n })\n}\n"],"mappings":"AACA,OAAO,KAAKA,IAAI,MAAM,YAAY;AAElC,SAASC,MAAM;AAaf,MAAMC,SAAS,GAAG,OAAO;AAEzB,WAAYC,qBAAqB,0BAArBA,qBAAqB;EAArBA,qBAAqB;EAAA,OAArBA,qBAAqB;AAAA;AAIjC,OAAO,MAAMC,YAAY,CAAC;EACxB;AACF;AACA;EACEC,KAAK;EACLC,WAAW;EACXC,aAAa;EAIbC,MAAM;EAENC,WAAWA,CAAC;IACVC,MAAM;IACNC,SAAS;IACTC;EAYF,CAAC,EAAE;IACD,MAAM;MAAEC,YAAY;MAAEC;IAAgB,CAAC,GAAGF,OAAO,IAAI,CAAC,CAAC;IACvD,IAAI,CAACD,SAAS,EAAE;MACdD,MAAM,CAACK,GAAG,CACR,MAAM,EACN,mGACF,CAAC;IACH;IACA,IAAI,CAACT,WAAW,GAAGO,YAAY,IAAI,IAAI,CAACG,mBAAmB,CAACC,IAAI,CAAC,IAAI,CAAC;IACtE,IAAI,CAACV,aAAa,GAAGO,eAAe,IAAII,SAAS;IACjD,IAAI,CAACb,KAAK,GAAGK,MAAM,CAACL,KAAK,CAAC;MAAEA,KAAK,EAAEM,SAAS;MAAEQ,OAAO,EAAE;IAAiB,CAAC,CAAC;IAC1E,IAAI,CAACX,MAAM,GAAGE,MAAM,CAACF,MAAM;EAC7B;EAEA,MAAMY,QAAQA,CACZC,OAAmD,EACrB;IAC9B,IAAIC,MAAM,GAAG,MAAM,IAAI,CAACjB,KAAK,CAACkB,GAAG,CAAC,IAAI,CAACC,GAAG,CAACH,OAAO,CAAC,CAAC;;IAEpD;IACA,IAAI,CAACC,MAAM,IAAI,IAAI,CAACf,aAAa,EAAE;MACjC,MAAMkB,UAAU,GAAG,MAAM,IAAI,CAAClB,aAAa,CAACc,OAAO,CAAC;MAEpD,IAAII,UAAU,IAAI,IAAI,EAAE;QACtB,MAAM,IAAI,CAACpB,KAAK,CAACqB,GAAG,CAClB,IAAI,CAACF,GAAG,CAACH,OAAO,CAAC,EACjBI,UAAU,EACVxB,MAAM,CAACsB,GAAG,CAAC,gBAAgB,CAC7B,CAAC;QACDD,MAAM,GAAG,MAAM,IAAI,CAACF,QAAQ,CAACC,OAAO,CAAC;MACvC;IACF;IAEA,OAAOC,MAAM,IAAI,CAAC,CAAC;EACrB;EAEA,MAAMK,QAAQA,CACZN,OAAyC,EACzCO,KAA0B,EAC1B;IACA,MAAMC,GAAG,GAAG,IAAI,CAACL,GAAG,CAACH,OAAO,CAAC;IAC7B,MAAMS,GAAG,GAAG7B,MAAM,CAACsB,GAAG,CAAC,gBAAgB,CAAC;IAExC,MAAM,IAAI,CAAClB,KAAK,CAACqB,GAAG,CAACG,GAAG,EAAED,KAAK,EAAEE,GAAG,CAAC;IACrC,OAAO,IAAI,CAACV,QAAQ,CAACC,OAAO,CAAC;EAC/B;EAEA,MAAMU,oBAAoBA,CACxBV,OAAyC,EACV;IAC/B,MAAMQ,GAAG,GAAG,IAAI,CAACL,GAAG,CAACH,OAAO,EAAElB,qBAAqB,CAAC6B,YAAY,CAAC;IACjE,MAAMC,KAAK,GAAG,MAAM,IAAI,CAAC5B,KAAK,CAACkB,GAAG,CAACM,GAAG,CAAC;IAEvC,OAAOI,KAAK,IAAI,CAAC,CAAC;EACpB;EAEA,MAAMC,oBAAoBA,CACxBb,OAAyC,EACzCc,iBAAuC,EACvC;IACA,MAAMN,GAAG,GAAG,IAAI,CAACL,GAAG,CAACH,OAAO,EAAElB,qBAAqB,CAAC6B,YAAY,CAAC;IACjE,MAAMF,GAAG,GAAG7B,MAAM,CAACsB,GAAG,CAAC,4BAA4B,CAAC;IAEpD,OAAO,IAAI,CAAClB,KAAK,CAACqB,GAAG,CAACG,GAAG,EAAEM,iBAAiB,EAAEL,GAAG,CAAC;EACpD;EAEA,MAAMM,UAAUA,CAACf,OAAyC,EAAE;IAC1D,IAAIA,OAAO,CAACgB,GAAG,CAACC,EAAE,EAAE;MAClB,MAAM,IAAI,CAACjC,KAAK,CAACkC,IAAI,CAAC,IAAI,CAACf,GAAG,CAACH,OAAO,CAAC,CAAC;IAC1C;EACF;EAEAmB,QAAQA,CACNnB,OAAyC,EACM;IAC/C,MAAMQ,GAAG,GAAG,IAAI,CAACL,GAAG,CAACH,OAAO,CAAC;IAC7B,MAAMoB,QAAQ,GAAGpB,OAAO,CAACgB,GAAG,CAACK,KAAK,CAACb,GAAG,CAACS,EAAE,CAAC;IAE1C,IAAIK,KAAK,CAACC,OAAO,CAACH,QAAQ,CAAC,IAAIA,QAAQ,CAACI,MAAM,EAAE;MAC9C,OAAOJ,QAAQ,CAACK,EAAE,CAAC,CAAC,CAAC;IACvB;EACF;EAEAC,QAAQA,CACN1B,OAAyC,EACzC2B,OAA0C,EAC1C;IACA,MAAMnB,GAAG,GAAG,IAAI,CAACL,GAAG,CAACH,OAAO,CAAC;IAE7BA,OAAO,CAACgB,GAAG,CAACK,KAAK,CAACb,GAAG,CAACS,EAAE,EAAEU,OAAO,CAAC;EACpC;EAEQhC,mBAAmBA,CACzBK,OAAmD,EAC3C;IACR,IAAI,CAACA,OAAO,CAACgB,GAAG,CAACC,EAAE,EAAE;MACnB,MAAM,IAAIW,KAAK,CAAC,qBAAqB,CAAC;IACxC;IAEA,MAAMrB,KAAK,GAAGP,OAAO,CAAC6B,MAAM,CAACtB,KAAK,IAAI,EAAE;IACxC,MAAMuB,IAAI,GAAG9B,OAAO,CAAC6B,MAAM,CAACC,IAAI,IAAI,EAAE;IACtC,OAAO,GAAG9B,OAAO,CAACgB,GAAG,CAACC,EAAE,IAAIV,KAAK,IAAIuB,IAAI,GAAG;EAC9C;;EAEA;AACF;AACA;AACA;AACA;AACA;EACE3B,GAAGA,CACDH,OAAmD,EACnD+B,oBAA4C,EAC5C;IACA,MAAMC,OAAO,GAAG,IAAI,CAAC/C,WAAW,GAC5B,IAAI,CAACA,WAAW,CAACe,OAAO,CAAC,GACzB,IAAI,CAACL,mBAAmB,CAACK,OAAO,CAAC;IAErC,OAAO;MACLF,OAAO,EAAEjB,SAAS;MAClBoC,EAAE,EAAE,GAAGe,OAAO,GAAGD,oBAAoB,IAAI,EAAE;IAC7C,CAAC;EACH;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASE,KAAKA,CACnB1B,KAAgB,EAChB2B,MAAc,EACH;EACX,OAAOvD,IAAI,CAACsD,KAAK,CAAC1B,KAAK,EAAE2B,MAAM,EAAE;IAC/BC,WAAW,EAAE;EACf,CAAC,CAAC;AACJ","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"cacheService.js","names":["Hoek","config","partition","ADDITIONAL_IDENTIFIER","CacheService","cache","generateKey","customFetcher","customPersister","logger","constructor","server","cacheName","options","keyGenerator","sessionHydrator","sessionPersister","log","defaultKeyGenerator","bind","undefined","segment","getState","request","key","Key","cached","get","rehydrated","set","setState","state","ttl","getConfirmationState","Confirmation","value","setConfirmationState","confirmationState","clearState","yar","id","drop","getFlash","messages","flash","Array","isArray","length","at","setFlash","message","Error","params","slug","additionalIdentifier","baseKey","merge","update","mergeArrays"],"sources":["../../../src/server/services/cacheService.ts"],"sourcesContent":["import { type Request, type Server } from '@hapi/hapi'\nimport * as Hoek from '@hapi/hoek'\n\nimport { config } from '~/src/config/index.js'\nimport { type createServer } from '~/src/server/index.js'\nimport {\n type FormPayload,\n type FormState,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload\n} from '~/src/server/routes/types.js'\n\nconst partition = 'cache'\n\nexport enum ADDITIONAL_IDENTIFIER {\n Confirmation = ':confirmation'\n}\n\nexport class CacheService {\n /**\n * This service is responsible for getting, storing or deleting a user's session data in the cache. This service has been registered by {@link createServer}\n */\n cache\n generateKey?: (request: Request | FormRequest | FormRequestPayload) => string\n customFetcher?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState | null>\n\n customPersister?: (\n key: string,\n state: FormSubmissionState,\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<void>\n\n logger: Server['logger']\n\n constructor({\n server,\n cacheName,\n options\n }: {\n server: Server\n cacheName?: string\n options?: {\n keyGenerator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => string\n sessionHydrator?: (\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<FormSubmissionState | null>\n sessionPersister?: (\n key: string,\n state: FormSubmissionState,\n request: Request | FormRequest | FormRequestPayload\n ) => Promise<void>\n }\n }) {\n const { keyGenerator, sessionHydrator, sessionPersister } = options ?? {}\n if (!cacheName) {\n server.log(\n 'warn',\n 'You are using the default hapi cache. Please provide a cache name in plugin registration options.'\n )\n }\n this.generateKey = keyGenerator ?? this.defaultKeyGenerator.bind(this)\n this.customFetcher = sessionHydrator ?? undefined\n this.customPersister = sessionPersister ?? undefined\n this.cache = server.cache({ cache: cacheName, segment: 'formSubmission' })\n this.logger = server.logger\n }\n\n async getState(\n request: Request | FormRequest | FormRequestPayload\n ): Promise<FormSubmissionState> {\n const key = this.Key(request)\n\n let cached = await this.cache.get(key)\n\n // If nothing in Redis, attempt to rehydrate from backend DB\n if (!cached && this.customFetcher) {\n const rehydrated = await this.customFetcher(request)\n\n if (rehydrated != null) {\n await this.cache.set(key, rehydrated, config.get('sessionTimeout'))\n cached = await this.getState(request)\n }\n }\n\n return cached ?? {}\n }\n\n async setState(\n request: FormRequest | FormRequestPayload,\n state: FormSubmissionState\n ) {\n const key = this.Key(request)\n const ttl = config.get('sessionTimeout')\n\n await this.cache.set(key, state, ttl)\n\n return this.getState(request)\n }\n\n async getConfirmationState(\n request: FormRequest | FormRequestPayload\n ): Promise<{ confirmed?: true }> {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const value = await this.cache.get(key)\n\n return value ?? {}\n }\n\n async setConfirmationState(\n request: FormRequest | FormRequestPayload,\n confirmationState: { confirmed?: true }\n ) {\n const key = this.Key(request, ADDITIONAL_IDENTIFIER.Confirmation)\n const ttl = config.get('confirmationSessionTimeout')\n\n return this.cache.set(key, confirmationState, ttl)\n }\n\n async clearState(request: FormRequest | FormRequestPayload) {\n if (request.yar.id) {\n await this.cache.drop(this.Key(request))\n }\n }\n\n getFlash(\n request: FormRequest | FormRequestPayload\n ): { errors: FormSubmissionError[] } | undefined {\n const key = this.Key(request)\n const messages = request.yar.flash(key.id)\n\n if (Array.isArray(messages) && messages.length) {\n return messages.at(0) as { errors: FormSubmissionError[] }\n }\n }\n\n setFlash(\n request: FormRequest | FormRequestPayload,\n message: { errors: FormSubmissionError[] }\n ) {\n const key = this.Key(request)\n\n request.yar.flash(key.id, message)\n }\n\n private defaultKeyGenerator(\n request: Request | FormRequest | FormRequestPayload\n ): string {\n if (!request.yar.id) {\n throw new Error('No session ID found')\n }\n\n const state = (request.params.state as string) || ''\n const slug = (request.params.slug as string) || ''\n return `${request.yar.id}:${state}:${slug}:`\n }\n\n /**\n * The key used to store user session data against.\n * If there are multiple forms on the same runner instance, for example `form-a` and `form-a-feedback` this will prevent CacheService from clearing data from `form-a` if a user gave feedback before they finished `form-a`\n * @param request - hapi request object\n * @param additionalIdentifier - appended to the id\n */\n Key(\n request: Request | FormRequest | FormRequestPayload,\n additionalIdentifier?: ADDITIONAL_IDENTIFIER\n ) {\n const baseKey = this.generateKey\n ? this.generateKey(request)\n : this.defaultKeyGenerator(request)\n\n return {\n segment: partition,\n id: `${baseKey}${additionalIdentifier ?? ''}`\n }\n }\n}\n\n/**\n * State merge helper\n * 1. Merges objects (form fields)\n * 2. Overwrites arrays\n */\nexport function merge<StateType extends FormState | FormPayload>(\n state: StateType,\n update: object\n): StateType {\n return Hoek.merge(state, update, {\n mergeArrays: false\n })\n}\n"],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
package/src/server/index.test.ts
CHANGED
|
@@ -639,3 +639,42 @@ describe('prepareEnvironment', () => {
|
|
|
639
639
|
)
|
|
640
640
|
})
|
|
641
641
|
})
|
|
642
|
+
|
|
643
|
+
describe('Exit route handlers', () => {
|
|
644
|
+
let server: Server
|
|
645
|
+
|
|
646
|
+
beforeAll(async () => {
|
|
647
|
+
server = await createServer({
|
|
648
|
+
services: defaultServices
|
|
649
|
+
})
|
|
650
|
+
await server.initialize()
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
afterAll(async () => {
|
|
654
|
+
await server.stop()
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
beforeEach(() => {
|
|
658
|
+
jest.mocked(getFormMetadata).mockResolvedValue(fixtures.form.metadata)
|
|
659
|
+
server.app.models.clear()
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
test('GET /exit returns 200 with exit page content', async () => {
|
|
663
|
+
jest.mocked(getFormMetadata).mockResolvedValueOnce({
|
|
664
|
+
...fixtures.form.metadata,
|
|
665
|
+
live: fixtures.form.state
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
jest.mocked(getFormDefinition).mockResolvedValue(fixtures.form.definition)
|
|
669
|
+
|
|
670
|
+
const options = {
|
|
671
|
+
method: 'GET',
|
|
672
|
+
url: `${FORM_PREFIX}/slug/exit`
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const res = await server.inject(options)
|
|
676
|
+
|
|
677
|
+
expect(res.statusCode).toBe(StatusCodes.OK)
|
|
678
|
+
expect(res.result).toContain('Your progress has been saved')
|
|
679
|
+
})
|
|
680
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ComponentDef } from '@defra/forms-model'
|
|
2
2
|
|
|
3
|
+
import { ComponentBase } from '~/src/server/plugins/engine/components/ComponentBase.js'
|
|
3
4
|
import { createComponent } from '~/src/server/plugins/engine/components/helpers.js'
|
|
4
5
|
import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
5
6
|
import definition from '~/test/form/definitions/basic.js'
|
|
@@ -22,3 +23,33 @@ describe('helpers tests', () => {
|
|
|
22
23
|
).toThrow('Component type invalid-type does not exist')
|
|
23
24
|
})
|
|
24
25
|
})
|
|
26
|
+
|
|
27
|
+
describe('ComponentBase tests', () => {
|
|
28
|
+
test('should handle save and return functionality', () => {
|
|
29
|
+
const mockComponentDef = {
|
|
30
|
+
type: 'TextField',
|
|
31
|
+
name: 'testField',
|
|
32
|
+
title: 'Test Field'
|
|
33
|
+
} as ComponentDef
|
|
34
|
+
|
|
35
|
+
const component = new ComponentBase(mockComponentDef, { model: formModel })
|
|
36
|
+
|
|
37
|
+
expect(component.name).toBe('testField')
|
|
38
|
+
expect(component.title).toBe('Test Field')
|
|
39
|
+
expect(component.type).toBe('TextField')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('should handle context correctly', () => {
|
|
43
|
+
const mockComponentDef = {
|
|
44
|
+
type: 'TextField',
|
|
45
|
+
name: 'contextField',
|
|
46
|
+
title: 'Context Field'
|
|
47
|
+
} as ComponentDef
|
|
48
|
+
|
|
49
|
+
const component = new ComponentBase(mockComponentDef, { model: formModel })
|
|
50
|
+
|
|
51
|
+
expect(component.model).toBe(formModel)
|
|
52
|
+
expect(component.name).toBe('contextField')
|
|
53
|
+
expect(component.title).toBe('Context Field')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -33,9 +33,7 @@ export const prepareNunjucksEnvironment = function (
|
|
|
33
33
|
env.addFilter(name, nunjucksFilter)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
env.addFilter('markdown', (text: string) =>
|
|
37
|
-
markdownToHtml(text, pluginOptions.baseUrl)
|
|
38
|
-
)
|
|
36
|
+
env.addFilter('markdown', (text: string) => markdownToHtml(text))
|
|
39
37
|
|
|
40
38
|
for (const [name, nunjucksGlobal] of Object.entries(globals)) {
|
|
41
39
|
env.addGlobal(name, nunjucksGlobal)
|
|
@@ -12,7 +12,7 @@ import definition from '~/test/form/definitions/conditions-escaping.js'
|
|
|
12
12
|
import conditionsListDefinition from '~/test/form/definitions/conditions-list.js'
|
|
13
13
|
import relativeDatesDefinition from '~/test/form/definitions/conditions-relative-dates-v2.js'
|
|
14
14
|
import fieldsRequiredDefinition from '~/test/form/definitions/fields-required.js'
|
|
15
|
-
import joinedConditionsDefinition from '~/test/form/definitions/joined-conditions-
|
|
15
|
+
import joinedConditionsDefinition from '~/test/form/definitions/joined-conditions-simple-v2.js'
|
|
16
16
|
|
|
17
17
|
jest.mock('~/src/server/plugins/engine/date-helper.ts')
|
|
18
18
|
|
|
@@ -357,15 +357,15 @@ describe('FormModel - Joined Conditions', () => {
|
|
|
357
357
|
const joinedCondition =
|
|
358
358
|
model.conditions['db43c6bc-9ce6-478b-8345-4fff5eff2ba3']
|
|
359
359
|
expect(joinedCondition).toBeDefined()
|
|
360
|
-
expect(joinedCondition?.displayName).toBe('
|
|
360
|
+
expect(joinedCondition?.displayName).toBe('is Bob AND over 18')
|
|
361
361
|
|
|
362
|
-
const stateAllTrue = {
|
|
362
|
+
const stateAllTrue = { userName: 'Bob', isOverEighteen: true }
|
|
363
363
|
expect(joinedCondition?.fn(stateAllTrue)).toBe(true)
|
|
364
364
|
|
|
365
|
-
const statePartialTrue = {
|
|
365
|
+
const statePartialTrue = { userName: 'Alice', isOverEighteen: true }
|
|
366
366
|
expect(joinedCondition?.fn(statePartialTrue)).toBe(false)
|
|
367
367
|
|
|
368
|
-
const stateFalse = {
|
|
368
|
+
const stateFalse = { userName: 'Alice', isOverEighteen: false }
|
|
369
369
|
expect(joinedCondition?.fn(stateFalse)).toBe(false)
|
|
370
370
|
})
|
|
371
371
|
|
|
@@ -379,18 +379,92 @@ describe('FormModel - Joined Conditions', () => {
|
|
|
379
379
|
})
|
|
380
380
|
|
|
381
381
|
const joinedConditionPage = model.pages.find(
|
|
382
|
-
(page) => page.path === '/
|
|
382
|
+
(page) => page.path === '/simple-and-page'
|
|
383
383
|
)
|
|
384
384
|
|
|
385
385
|
expect(joinedConditionPage?.condition).toBeDefined()
|
|
386
386
|
|
|
387
|
-
const trueState = {
|
|
387
|
+
const trueState = { userName: 'Bob', isOverEighteen: true }
|
|
388
388
|
expect(joinedConditionPage?.condition?.fn(trueState)).toBe(true)
|
|
389
389
|
|
|
390
|
-
const falseState = {
|
|
390
|
+
const falseState = { userName: 'Bob', isOverEighteen: false }
|
|
391
391
|
expect(joinedConditionPage?.condition?.fn(falseState)).toBe(false)
|
|
392
392
|
})
|
|
393
393
|
|
|
394
|
+
it('should handle V1 joined conditions without aliases', () => {
|
|
395
|
+
formDefinitionV2Schema.validate = jest
|
|
396
|
+
.fn()
|
|
397
|
+
.mockReturnValue({ value: definition })
|
|
398
|
+
|
|
399
|
+
const model = new FormModel(definition, {
|
|
400
|
+
basePath: 'test'
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
expect(model.conditions).toBeDefined()
|
|
404
|
+
expect(Object.keys(model.conditions)).toHaveLength(1)
|
|
405
|
+
|
|
406
|
+
const joinedCondition = model.conditions.ZCXeMz
|
|
407
|
+
expect(joinedCondition).toBeDefined()
|
|
408
|
+
expect(joinedCondition?.displayName).toBe('test')
|
|
409
|
+
|
|
410
|
+
const testState = { NIJphU: "ap'ostrophe's", iraEpG: "shouldn't've" }
|
|
411
|
+
expect(joinedCondition?.fn(testState)).toBe(true)
|
|
412
|
+
|
|
413
|
+
const testStateFalse = { NIJphU: 'other', iraEpG: "shouldn't've" }
|
|
414
|
+
expect(joinedCondition?.fn(testStateFalse)).toBe(false)
|
|
415
|
+
|
|
416
|
+
const context = model.toConditionContext(testState, model.conditions)
|
|
417
|
+
|
|
418
|
+
expect(context).not.toHaveProperty('cond_ZCXeMz')
|
|
419
|
+
|
|
420
|
+
expect(context).toHaveProperty('ZCXeMz')
|
|
421
|
+
|
|
422
|
+
expect(context).toHaveProperty('NIJphU', "ap'ostrophe's")
|
|
423
|
+
expect(context).toHaveProperty('iraEpG', "shouldn't've")
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it('should use schema version to determine condition aliases', () => {
|
|
427
|
+
const v1Definition = { ...definition, schema: SchemaVersion.V1 }
|
|
428
|
+
formDefinitionV2Schema.validate = jest
|
|
429
|
+
.fn()
|
|
430
|
+
.mockReturnValue({ value: v1Definition })
|
|
431
|
+
|
|
432
|
+
const v1Model = new FormModel(v1Definition, { basePath: 'test' })
|
|
433
|
+
expect(v1Model.schemaVersion).toBe(SchemaVersion.V1)
|
|
434
|
+
|
|
435
|
+
const v1TestState = { NIJphU: "ap'ostrophe's", iraEpG: "shouldn't've" }
|
|
436
|
+
const v1Context = v1Model.toConditionContext(
|
|
437
|
+
v1TestState,
|
|
438
|
+
v1Model.conditions
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
expect(v1Context).toHaveProperty('ZCXeMz')
|
|
442
|
+
expect(v1Context).not.toHaveProperty('cond_ZCXeMz')
|
|
443
|
+
|
|
444
|
+
formDefinitionV2Schema.validate = jest
|
|
445
|
+
.fn()
|
|
446
|
+
.mockReturnValue({ value: joinedConditionsDefinition })
|
|
447
|
+
|
|
448
|
+
const v2Model = new FormModel(joinedConditionsDefinition, {
|
|
449
|
+
basePath: 'test'
|
|
450
|
+
})
|
|
451
|
+
expect(v2Model.schemaVersion).toBe(SchemaVersion.V2)
|
|
452
|
+
|
|
453
|
+
const v2TestState = { userName: 'Bob', isOverEighteen: true }
|
|
454
|
+
const v2Context = v2Model.toConditionContext(
|
|
455
|
+
v2TestState,
|
|
456
|
+
v2Model.conditions
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
expect(v2Context).toHaveProperty('cond_d15aff7a622440a28e5f51a5af2f7910')
|
|
460
|
+
expect(v2Context).toHaveProperty('cond_d1f9fcc7f09847e79d314f5ee57ba985')
|
|
461
|
+
expect(v2Context).toHaveProperty('cond_db43c6bc9ce6478b83454fff5eff2ba3')
|
|
462
|
+
|
|
463
|
+
expect(v2Context).not.toHaveProperty('d15aff7a-6224-40a2-8e5f-51a5af2f7910')
|
|
464
|
+
expect(v2Context).not.toHaveProperty('d1f9fcc7-f098-47e7-9d31-4f5ee57ba985')
|
|
465
|
+
expect(v2Context).not.toHaveProperty('db43c6bc-9ce6-478b-8345-4fff5eff2ba3')
|
|
466
|
+
})
|
|
467
|
+
|
|
394
468
|
describe('generateConditionAlias', () => {
|
|
395
469
|
it('should generate valid JavaScript identifiers from condition IDs', () => {
|
|
396
470
|
formDefinitionV2Schema.validate = jest
|
|
@@ -401,7 +475,7 @@ describe('FormModel - Joined Conditions', () => {
|
|
|
401
475
|
basePath: 'test'
|
|
402
476
|
})
|
|
403
477
|
|
|
404
|
-
const evaluationState = {
|
|
478
|
+
const evaluationState = { userName: 'Bob', isOverEighteen: true }
|
|
405
479
|
|
|
406
480
|
const context = model.toConditionContext(
|
|
407
481
|
evaluationState,
|
|
@@ -428,8 +502,8 @@ describe('FormModel - Joined Conditions', () => {
|
|
|
428
502
|
model.conditions['db43c6bc-9ce6-478b-8345-4fff5eff2ba3']
|
|
429
503
|
expect(joinedCondition).toBeDefined()
|
|
430
504
|
|
|
431
|
-
const stateTrue = {
|
|
432
|
-
const stateFalse = {
|
|
505
|
+
const stateTrue = { userName: 'Bob', isOverEighteen: true }
|
|
506
|
+
const stateFalse = { userName: 'Alice', isOverEighteen: false }
|
|
433
507
|
|
|
434
508
|
expect(joinedCondition?.fn(stateTrue)).toBe(true)
|
|
435
509
|
expect(joinedCondition?.fn(stateFalse)).toBe(false)
|
|
@@ -278,9 +278,12 @@ export class FormModel {
|
|
|
278
278
|
const context = { ...evaluationState }
|
|
279
279
|
|
|
280
280
|
for (const conditionId in conditions) {
|
|
281
|
-
const
|
|
281
|
+
const propertyName =
|
|
282
|
+
this.schemaVersion === SchemaVersion.V2
|
|
283
|
+
? generateConditionAlias(conditionId)
|
|
284
|
+
: conditionId
|
|
282
285
|
|
|
283
|
-
Object.defineProperty(context,
|
|
286
|
+
Object.defineProperty(context, propertyName, {
|
|
284
287
|
get() {
|
|
285
288
|
return conditions[conditionId]?.fn(evaluationState)
|
|
286
289
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
FormModel,
|
|
4
4
|
SummaryViewModel
|
|
5
5
|
} from '~/src/server/plugins/engine/models/index.js'
|
|
6
|
+
import { SummaryPageController } from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'
|
|
6
7
|
import {
|
|
7
8
|
createPage,
|
|
8
9
|
type PageControllerClass
|
|
@@ -249,3 +250,61 @@ describe('SummaryViewModel', () => {
|
|
|
249
250
|
})
|
|
250
251
|
})
|
|
251
252
|
})
|
|
253
|
+
|
|
254
|
+
describe('SummaryPageController', () => {
|
|
255
|
+
let model: FormModel
|
|
256
|
+
let controller: SummaryPageController
|
|
257
|
+
let request: FormContextRequest
|
|
258
|
+
|
|
259
|
+
beforeEach(() => {
|
|
260
|
+
model = new FormModel(definition, {
|
|
261
|
+
basePath: `${FORM_PREFIX}/test`
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
controller = new SummaryPageController(model, definition.pages[2])
|
|
265
|
+
|
|
266
|
+
request = {
|
|
267
|
+
method: 'get',
|
|
268
|
+
url: new URL('http://example.com/repeat/pizza-order/summary'),
|
|
269
|
+
path: '/repeat/pizza-order/summary',
|
|
270
|
+
params: {
|
|
271
|
+
path: 'pizza-order',
|
|
272
|
+
slug: 'repeat'
|
|
273
|
+
},
|
|
274
|
+
query: {},
|
|
275
|
+
app: { model }
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
describe('Save and Return functionality', () => {
|
|
280
|
+
it('should show save and return button on summary page', () => {
|
|
281
|
+
expect(controller.shouldShowSaveAndReturn()).toBe(true)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('should handle save and return from summary page', () => {
|
|
285
|
+
const state: FormState = {
|
|
286
|
+
$$__referenceNumber: 'foobar',
|
|
287
|
+
orderType: 'collection',
|
|
288
|
+
pizza: []
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const context = model.getFormContext(request, state)
|
|
292
|
+
const viewModel = controller.getViewModel(request, context)
|
|
293
|
+
|
|
294
|
+
expect(viewModel).toHaveProperty('allowSaveAndReturn', true)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('should display correct page title', () => {
|
|
298
|
+
const state: FormState = {
|
|
299
|
+
$$__referenceNumber: 'foobar',
|
|
300
|
+
orderType: 'collection',
|
|
301
|
+
pizza: []
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const context = model.getFormContext(request, state)
|
|
305
|
+
const viewModel = controller.getViewModel(request, context)
|
|
306
|
+
|
|
307
|
+
expect(viewModel.pageTitle).toBe('Check your answers')
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
})
|
|
@@ -19,7 +19,10 @@ const pluginRegistrationOptionsSchema = Joi.object({
|
|
|
19
19
|
viewContext: Joi.function().required(),
|
|
20
20
|
preparePageEventRequestOptions: Joi.function().optional(),
|
|
21
21
|
onRequest: Joi.function().optional(),
|
|
22
|
-
baseUrl: Joi.string().uri().required()
|
|
22
|
+
baseUrl: Joi.string().uri().required(),
|
|
23
|
+
keyGenerator: Joi.function().optional(),
|
|
24
|
+
sessionHydrator: Joi.function().optional(),
|
|
25
|
+
sessionPersister: Joi.function().optional()
|
|
23
26
|
})
|
|
24
27
|
|
|
25
28
|
/**
|
|
@@ -16,6 +16,26 @@ describe('validatePluginOptions', () => {
|
|
|
16
16
|
expect(validatePluginOptions(validOptions)).toEqual(validOptions)
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
+
it('accepts optional properties keyGenerator, sessionHydrator, and sessionPersister', () => {
|
|
20
|
+
const validOptionsWithOptionals = {
|
|
21
|
+
nunjucks: {
|
|
22
|
+
baseLayoutPath: 'dxt-devtool-baselayout.html',
|
|
23
|
+
paths: ['src/server/devserver']
|
|
24
|
+
},
|
|
25
|
+
viewContext: () => {
|
|
26
|
+
return { hello: 'world' }
|
|
27
|
+
},
|
|
28
|
+
baseUrl: 'http://localhost:3009',
|
|
29
|
+
keyGenerator: () => 'test-key',
|
|
30
|
+
sessionHydrator: () => Promise.resolve({ someState: 'value' }),
|
|
31
|
+
sessionPersister: () => Promise.resolve(undefined)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
expect(validatePluginOptions(validOptionsWithOptionals)).toEqual(
|
|
35
|
+
validOptionsWithOptionals
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
|
|
19
39
|
/**
|
|
20
40
|
* tsc would usually check compliance with the type, but given a user might be using plain JS we still want a test
|
|
21
41
|
*/
|
|
@@ -204,5 +204,30 @@ describe('PageController', () => {
|
|
|
204
204
|
'Unsupported POST route handler for this page'
|
|
205
205
|
)
|
|
206
206
|
})
|
|
207
|
+
|
|
208
|
+
it('supports save and return functionality', async () => {
|
|
209
|
+
const mockRequest = {
|
|
210
|
+
...request,
|
|
211
|
+
payload: { saveAndReturn: true }
|
|
212
|
+
} as FormRequest
|
|
213
|
+
|
|
214
|
+
const mockResponse = {
|
|
215
|
+
redirect: jest.fn(),
|
|
216
|
+
view: jest.fn()
|
|
217
|
+
} as unknown as ResponseToolkit
|
|
218
|
+
|
|
219
|
+
await controller1.makeGetRouteHandler()(
|
|
220
|
+
mockRequest,
|
|
221
|
+
model.getFormContext(mockRequest, { $$__referenceNumber: 'test-ref' }),
|
|
222
|
+
mockResponse
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
expect(mockResponse.view).toHaveBeenCalledWith(
|
|
226
|
+
controller1.viewName,
|
|
227
|
+
expect.objectContaining({
|
|
228
|
+
pageTitle: 'Buy a rod fishing licence'
|
|
229
|
+
})
|
|
230
|
+
)
|
|
231
|
+
})
|
|
207
232
|
})
|
|
208
233
|
})
|
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
type FormState,
|
|
11
11
|
type FormSubmissionState
|
|
12
12
|
} from '~/src/server/plugins/engine/types.js'
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
type FormRequest,
|
|
15
|
+
type FormRequestPayload
|
|
16
|
+
} from '~/src/server/routes/types.js'
|
|
14
17
|
import { CacheService } from '~/src/server/services/cacheService.js'
|
|
15
18
|
import conditionalReveal from '~/test/form/definitions/conditional-reveal.js'
|
|
16
19
|
import definitionConditionsBasic, {
|
|
@@ -1274,3 +1277,177 @@ describe('QuestionPageController V2', () => {
|
|
|
1274
1277
|
})
|
|
1275
1278
|
})
|
|
1276
1279
|
})
|
|
1280
|
+
|
|
1281
|
+
describe('Save and Return functionality', () => {
|
|
1282
|
+
let model: FormModel
|
|
1283
|
+
let controller1: QuestionPageController
|
|
1284
|
+
let requestPage1: FormRequest
|
|
1285
|
+
|
|
1286
|
+
beforeEach(() => {
|
|
1287
|
+
const { pages } = definitionConditionsBasic
|
|
1288
|
+
|
|
1289
|
+
model = new FormModel(definitionConditionsBasic, {
|
|
1290
|
+
basePath: 'test'
|
|
1291
|
+
})
|
|
1292
|
+
|
|
1293
|
+
controller1 = new QuestionPageController(model, pages[0])
|
|
1294
|
+
|
|
1295
|
+
requestPage1 = {
|
|
1296
|
+
method: 'get',
|
|
1297
|
+
url: new URL('http://example.com/test/first-page'),
|
|
1298
|
+
path: '/test/first-page',
|
|
1299
|
+
params: {
|
|
1300
|
+
path: 'first-page',
|
|
1301
|
+
slug: 'test'
|
|
1302
|
+
},
|
|
1303
|
+
query: {},
|
|
1304
|
+
app: { model }
|
|
1305
|
+
} as FormRequest
|
|
1306
|
+
})
|
|
1307
|
+
|
|
1308
|
+
const response = {
|
|
1309
|
+
code: jest.fn().mockImplementation(() => response)
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const h: Pick<ResponseToolkit, 'redirect' | 'view'> = {
|
|
1313
|
+
redirect: jest.fn().mockReturnValue(response),
|
|
1314
|
+
view: jest.fn()
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
beforeEach(() => {
|
|
1318
|
+
jest.clearAllMocks()
|
|
1319
|
+
jest.spyOn(CacheService.prototype, 'setState')
|
|
1320
|
+
})
|
|
1321
|
+
|
|
1322
|
+
describe('shouldShowSaveAndReturn', () => {
|
|
1323
|
+
it('should return true by default', () => {
|
|
1324
|
+
expect(controller1.shouldShowSaveAndReturn()).toBe(true)
|
|
1325
|
+
})
|
|
1326
|
+
})
|
|
1327
|
+
|
|
1328
|
+
describe('handleSaveAndReturn', () => {
|
|
1329
|
+
it('should save state and redirect to exit page', async () => {
|
|
1330
|
+
const state: FormSubmissionState = {
|
|
1331
|
+
$$__referenceNumber: 'foobar',
|
|
1332
|
+
yesNoField: true
|
|
1333
|
+
}
|
|
1334
|
+
const request = {
|
|
1335
|
+
...requestPage1,
|
|
1336
|
+
method: 'post',
|
|
1337
|
+
payload: { yesNoField: true, action: 'save-and-return' }
|
|
1338
|
+
} as unknown as FormRequestPayload
|
|
1339
|
+
|
|
1340
|
+
const context = model.getFormContext(request, state)
|
|
1341
|
+
|
|
1342
|
+
jest.spyOn(controller1, 'setState').mockResolvedValue(state)
|
|
1343
|
+
|
|
1344
|
+
await controller1.handleSaveAndReturn(request, context, h)
|
|
1345
|
+
|
|
1346
|
+
expect(controller1.setState).toHaveBeenCalledWith(request, state)
|
|
1347
|
+
expect(h.redirect).toHaveBeenCalledWith('/test/exit')
|
|
1348
|
+
})
|
|
1349
|
+
|
|
1350
|
+
it('should handle save-and-return with incomplete data', async () => {
|
|
1351
|
+
const state: FormSubmissionState = {
|
|
1352
|
+
$$__referenceNumber: 'foobar',
|
|
1353
|
+
yesNoField: null
|
|
1354
|
+
}
|
|
1355
|
+
const request = {
|
|
1356
|
+
...requestPage1,
|
|
1357
|
+
method: 'post',
|
|
1358
|
+
payload: { yesNoField: '', action: 'save-and-return' }
|
|
1359
|
+
} as unknown as FormRequestPayload
|
|
1360
|
+
|
|
1361
|
+
const context = model.getFormContext(request, state)
|
|
1362
|
+
|
|
1363
|
+
jest.spyOn(controller1, 'setState').mockResolvedValue(state)
|
|
1364
|
+
|
|
1365
|
+
await controller1.handleSaveAndReturn(request, context, h)
|
|
1366
|
+
|
|
1367
|
+
expect(controller1.setState).toHaveBeenCalledWith(request, state)
|
|
1368
|
+
expect(h.redirect).toHaveBeenCalledWith('/test/exit')
|
|
1369
|
+
})
|
|
1370
|
+
|
|
1371
|
+
it('should handle save-and-return with validation errors', async () => {
|
|
1372
|
+
const state: FormSubmissionState = { $$__referenceNumber: 'foobar' }
|
|
1373
|
+
const request = {
|
|
1374
|
+
...requestPage1,
|
|
1375
|
+
method: 'post',
|
|
1376
|
+
payload: { action: 'save-and-return' }
|
|
1377
|
+
} as unknown as FormRequestPayload
|
|
1378
|
+
|
|
1379
|
+
const context = model.getFormContext(request, state)
|
|
1380
|
+
|
|
1381
|
+
jest.spyOn(controller1, 'setState').mockResolvedValue(state)
|
|
1382
|
+
|
|
1383
|
+
await controller1.handleSaveAndReturn(request, context, h)
|
|
1384
|
+
|
|
1385
|
+
expect(controller1.setState).toHaveBeenCalledWith(request, state)
|
|
1386
|
+
expect(h.redirect).toHaveBeenCalledWith('/test/exit')
|
|
1387
|
+
})
|
|
1388
|
+
})
|
|
1389
|
+
|
|
1390
|
+
describe('POST handler with save-and-return action', () => {
|
|
1391
|
+
it('should handle FormAction.SaveAndReturn', async () => {
|
|
1392
|
+
const state: FormSubmissionState = {
|
|
1393
|
+
$$__referenceNumber: 'foobar',
|
|
1394
|
+
yesNoField: true
|
|
1395
|
+
}
|
|
1396
|
+
const request = {
|
|
1397
|
+
...requestPage1,
|
|
1398
|
+
method: 'post',
|
|
1399
|
+
payload: { yesNoField: true, action: 'save-and-return' }
|
|
1400
|
+
} as unknown as FormRequestPayload
|
|
1401
|
+
|
|
1402
|
+
const context = model.getFormContext(request, state)
|
|
1403
|
+
|
|
1404
|
+
jest.spyOn(controller1, 'getState').mockResolvedValue({})
|
|
1405
|
+
jest
|
|
1406
|
+
.spyOn(controller1, 'handleSaveAndReturn')
|
|
1407
|
+
.mockResolvedValue(h.redirect('/test/exit'))
|
|
1408
|
+
|
|
1409
|
+
const postHandler = controller1.makePostRouteHandler()
|
|
1410
|
+
await postHandler(request, context, h)
|
|
1411
|
+
|
|
1412
|
+
expect(controller1.handleSaveAndReturn).toHaveBeenCalledWith(
|
|
1413
|
+
request,
|
|
1414
|
+
context,
|
|
1415
|
+
h
|
|
1416
|
+
)
|
|
1417
|
+
})
|
|
1418
|
+
|
|
1419
|
+
it('should not call handleSaveAndReturn for continue action', async () => {
|
|
1420
|
+
const state: FormSubmissionState = {
|
|
1421
|
+
$$__referenceNumber: 'foobar',
|
|
1422
|
+
yesNoField: true
|
|
1423
|
+
}
|
|
1424
|
+
const request = {
|
|
1425
|
+
...requestPage1,
|
|
1426
|
+
method: 'post',
|
|
1427
|
+
payload: { yesNoField: true, action: 'continue' }
|
|
1428
|
+
} as unknown as FormRequestPayload
|
|
1429
|
+
|
|
1430
|
+
const context = model.getFormContext(request, state)
|
|
1431
|
+
|
|
1432
|
+
jest.spyOn(controller1, 'getState').mockResolvedValue({})
|
|
1433
|
+
jest
|
|
1434
|
+
.spyOn(controller1, 'handleSaveAndReturn')
|
|
1435
|
+
.mockResolvedValue(h.redirect('/test/exit'))
|
|
1436
|
+
jest.spyOn(controller1, 'setState').mockResolvedValue(state)
|
|
1437
|
+
|
|
1438
|
+
const mockResponse = {
|
|
1439
|
+
code: jest.fn().mockReturnValue({ redirect: jest.fn() })
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
const mockH = {
|
|
1443
|
+
redirect: jest.fn().mockReturnValue(mockResponse),
|
|
1444
|
+
view: jest.fn()
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const postHandler = controller1.makePostRouteHandler()
|
|
1448
|
+
await postHandler(request, context, mockH)
|
|
1449
|
+
|
|
1450
|
+
expect(controller1.handleSaveAndReturn).not.toHaveBeenCalled()
|
|
1451
|
+
})
|
|
1452
|
+
})
|
|
1453
|
+
})
|