@defra/forms-engine-plugin 1.4.0 → 2.0.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.
Files changed (78) hide show
  1. package/.server/server/plugins/engine/README.md +2 -46
  2. package/.server/server/plugins/engine/configureEnginePlugin.d.ts +1 -1
  3. package/.server/server/plugins/engine/configureEnginePlugin.js +5 -2
  4. package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
  5. package/.server/server/plugins/engine/helpers.d.ts +11 -0
  6. package/.server/server/plugins/engine/helpers.js +7 -1
  7. package/.server/server/plugins/engine/helpers.js.map +1 -1
  8. package/.server/server/plugins/engine/models/FormModel.js +2 -2
  9. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  10. package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +1 -1
  11. package/.server/server/plugins/engine/models/SummaryViewModel.js +1 -1
  12. package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
  13. package/.server/server/plugins/engine/options.js +5 -3
  14. package/.server/server/plugins/engine/options.js.map +1 -1
  15. package/.server/server/plugins/engine/options.test.js +18 -9
  16. package/.server/server/plugins/engine/options.test.js.map +1 -1
  17. package/.server/server/plugins/engine/pageControllers/PageController.d.ts +3 -1
  18. package/.server/server/plugins/engine/pageControllers/PageController.js +5 -1
  19. package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
  20. package/.server/server/plugins/engine/pageControllers/QuestionPageController.d.ts +1 -1
  21. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +2 -4
  22. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  23. package/.server/server/plugins/engine/pageControllers/StartPageController.d.ts +2 -2
  24. package/.server/server/plugins/engine/pageControllers/StartPageController.js +1 -3
  25. package/.server/server/plugins/engine/pageControllers/StartPageController.js.map +1 -1
  26. package/.server/server/plugins/engine/pageControllers/StatusPageController.d.ts +1 -0
  27. package/.server/server/plugins/engine/pageControllers/StatusPageController.js +1 -0
  28. package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
  29. package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +0 -1
  30. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +1 -4
  31. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  32. package/.server/server/plugins/engine/pageControllers/TerminalPageController.d.ts +1 -0
  33. package/.server/server/plugins/engine/pageControllers/TerminalPageController.js +1 -0
  34. package/.server/server/plugins/engine/pageControllers/TerminalPageController.js.map +1 -1
  35. package/.server/server/plugins/engine/pageControllers/__stubs__/request.d.ts +4 -0
  36. package/.server/server/plugins/engine/pageControllers/__stubs__/request.js +14 -0
  37. package/.server/server/plugins/engine/pageControllers/__stubs__/request.js.map +1 -0
  38. package/.server/server/plugins/engine/pageControllers/__stubs__/server.d.ts +3 -0
  39. package/.server/server/plugins/engine/pageControllers/__stubs__/server.js +23 -0
  40. package/.server/server/plugins/engine/pageControllers/__stubs__/server.js.map +1 -0
  41. package/.server/server/plugins/engine/plugin.js +5 -6
  42. package/.server/server/plugins/engine/plugin.js.map +1 -1
  43. package/.server/server/plugins/engine/types.d.ts +7 -5
  44. package/.server/server/plugins/engine/types.js.map +1 -1
  45. package/.server/server/types.d.ts +2 -1
  46. package/.server/server/types.js.map +1 -1
  47. package/.server/typings/hapi/index.d.js.map +1 -1
  48. package/package.json +1 -1
  49. package/src/server/plugins/engine/README.md +2 -46
  50. package/src/server/plugins/engine/configureEnginePlugin.ts +4 -2
  51. package/src/server/plugins/engine/helpers.test.ts +3 -2
  52. package/src/server/plugins/engine/helpers.ts +9 -1
  53. package/src/server/plugins/engine/models/FormModel.test.ts +96 -21
  54. package/src/server/plugins/engine/models/FormModel.ts +5 -2
  55. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +7 -5
  56. package/src/server/plugins/engine/models/SummaryViewModel.ts +1 -1
  57. package/src/server/plugins/engine/options.js +5 -3
  58. package/src/server/plugins/engine/options.test.js +22 -11
  59. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +3 -3
  60. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +12 -1
  61. package/src/server/plugins/engine/pageControllers/PageController.test.ts +9 -0
  62. package/src/server/plugins/engine/pageControllers/PageController.ts +10 -1
  63. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +34 -28
  64. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +2 -5
  65. package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +19 -4
  66. package/src/server/plugins/engine/pageControllers/StartPageController.test.ts +32 -0
  67. package/src/server/plugins/engine/pageControllers/StartPageController.ts +2 -4
  68. package/src/server/plugins/engine/pageControllers/StatusPageController.test.ts +32 -0
  69. package/src/server/plugins/engine/pageControllers/StatusPageController.ts +1 -0
  70. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +1 -5
  71. package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +9 -0
  72. package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +1 -0
  73. package/src/server/plugins/engine/pageControllers/__stubs__/request.ts +21 -0
  74. package/src/server/plugins/engine/pageControllers/__stubs__/server.ts +27 -0
  75. package/src/server/plugins/engine/plugin.ts +6 -6
  76. package/src/server/plugins/engine/types.ts +14 -9
  77. package/src/server/types.ts +2 -0
  78. package/src/typings/hapi/index.d.ts +2 -0
@@ -6,13 +6,14 @@ import {
6
6
 
7
7
  import { todayAsDateOnly } from '~/src/server/plugins/engine/date-helper.js'
8
8
  import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
9
+ import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
9
10
  import { type FormContextRequest } from '~/src/server/plugins/engine/types.js'
10
11
  import { V2 as definitionV2 } from '~/test/form/definitions/conditions-basic.js'
11
12
  import definition from '~/test/form/definitions/conditions-escaping.js'
12
13
  import conditionsListDefinition from '~/test/form/definitions/conditions-list.js'
13
14
  import relativeDatesDefinition from '~/test/form/definitions/conditions-relative-dates-v2.js'
14
15
  import fieldsRequiredDefinition from '~/test/form/definitions/fields-required.js'
15
- import joinedConditionsDefinition from '~/test/form/definitions/joined-conditions-test.js'
16
+ import joinedConditionsDefinition from '~/test/form/definitions/joined-conditions-simple-v2.js'
16
17
 
17
18
  jest.mock('~/src/server/plugins/engine/date-helper.ts')
18
19
 
@@ -151,7 +152,7 @@ describe('FormModel', () => {
151
152
  }
152
153
  const pageUrl = new URL('http://example.com/components/fields-required')
153
154
 
154
- const request: FormContextRequest = {
155
+ const request: FormContextRequest = buildFormContextRequest({
155
156
  method: 'post',
156
157
  payload: { crumb: 'dummyCrumb', action: 'validate' },
157
158
  query: {},
@@ -159,7 +160,7 @@ describe('FormModel', () => {
159
160
  params: { path: 'components', slug: 'fields-required' },
160
161
  url: pageUrl,
161
162
  app: { model: formModel }
162
- }
163
+ })
163
164
 
164
165
  const context = formModel.getFormContext(request, state)
165
166
 
@@ -180,7 +181,7 @@ describe('FormModel', () => {
180
181
  }
181
182
  const pageUrl = new URL('http://example.com/components/fields-required')
182
183
 
183
- const request: FormContextRequest = {
184
+ const request: FormContextRequest = buildFormContextRequest({
184
185
  method: 'post',
185
186
  payload: { crumb: 'dummyCrumb', action: 'validate' },
186
187
  query: {},
@@ -188,7 +189,7 @@ describe('FormModel', () => {
188
189
  params: { path: 'components', slug: 'fields-required' },
189
190
  url: pageUrl,
190
191
  app: { model: formModel }
191
- }
192
+ })
192
193
 
193
194
  expect(() => formModel.getFormContext(request, state)).toThrow(
194
195
  'Reference number not found in form state'
@@ -206,7 +207,7 @@ describe('FormModel', () => {
206
207
  }
207
208
  const pageUrl = new URL('http://example.com/components/fields-required')
208
209
 
209
- const request: FormContextRequest = {
210
+ const request: FormContextRequest = buildFormContextRequest({
210
211
  method: 'post',
211
212
  payload: { crumb: 'dummyCrumb', action: 'validate' },
212
213
  query: {},
@@ -214,7 +215,7 @@ describe('FormModel', () => {
214
215
  params: { path: 'components', slug: 'fields-required' },
215
216
  url: pageUrl,
216
217
  app: { model: formModel }
217
- }
218
+ })
218
219
 
219
220
  expect(() => formModel.getFormContext(request, state)).toThrow(
220
221
  'Reference number not found in form state'
@@ -236,14 +237,14 @@ describe('FormModel', () => {
236
237
  'http://example.com/conditional-list-items/summary'
237
238
  )
238
239
 
239
- const request: FormContextRequest = {
240
+ const request: FormContextRequest = buildFormContextRequest({
240
241
  method: 'get',
241
242
  query: {},
242
243
  path: pageUrl.pathname,
243
244
  params: { path: 'summary', slug: 'conditional-list-items' },
244
245
  url: pageUrl,
245
246
  app: { model: formModel }
246
- }
247
+ })
247
248
 
248
249
  const context = formModel.getFormContext(request, state)
249
250
 
@@ -271,14 +272,14 @@ describe('FormModel', () => {
271
272
  'http://example.com/conditional-list-items/summary'
272
273
  )
273
274
 
274
- const request: FormContextRequest = {
275
+ const request: FormContextRequest = buildFormContextRequest({
275
276
  method: 'get',
276
277
  query: {},
277
278
  path: pageUrl.pathname,
278
279
  params: { path: 'summary', slug: 'conditional-list-items' },
279
280
  url: pageUrl,
280
281
  app: { model: formModel }
281
- }
282
+ })
282
283
 
283
284
  const context = formModel.getFormContext(request, state)
284
285
 
@@ -357,15 +358,15 @@ describe('FormModel - Joined Conditions', () => {
357
358
  const joinedCondition =
358
359
  model.conditions['db43c6bc-9ce6-478b-8345-4fff5eff2ba3']
359
360
  expect(joinedCondition).toBeDefined()
360
- expect(joinedCondition?.displayName).toBe('joined condition')
361
+ expect(joinedCondition?.displayName).toBe('is Bob AND over 18')
361
362
 
362
- const stateAllTrue = { fsZNJr: 'Bob', DaBGpS: true }
363
+ const stateAllTrue = { userName: 'Bob', isOverEighteen: true }
363
364
  expect(joinedCondition?.fn(stateAllTrue)).toBe(true)
364
365
 
365
- const statePartialTrue = { fsZNJr: 'Alice', DaBGpS: true }
366
+ const statePartialTrue = { userName: 'Alice', isOverEighteen: true }
366
367
  expect(joinedCondition?.fn(statePartialTrue)).toBe(false)
367
368
 
368
- const stateFalse = { fsZNJr: 'Alice', DaBGpS: false }
369
+ const stateFalse = { userName: 'Alice', isOverEighteen: false }
369
370
  expect(joinedCondition?.fn(stateFalse)).toBe(false)
370
371
  })
371
372
 
@@ -379,18 +380,92 @@ describe('FormModel - Joined Conditions', () => {
379
380
  })
380
381
 
381
382
  const joinedConditionPage = model.pages.find(
382
- (page) => page.path === '/joined-condition-page'
383
+ (page) => page.path === '/simple-and-page'
383
384
  )
384
385
 
385
386
  expect(joinedConditionPage?.condition).toBeDefined()
386
387
 
387
- const trueState = { fsZNJr: 'Bob', DaBGpS: true }
388
+ const trueState = { userName: 'Bob', isOverEighteen: true }
388
389
  expect(joinedConditionPage?.condition?.fn(trueState)).toBe(true)
389
390
 
390
- const falseState = { fsZNJr: 'Bob', DaBGpS: false }
391
+ const falseState = { userName: 'Bob', isOverEighteen: false }
391
392
  expect(joinedConditionPage?.condition?.fn(falseState)).toBe(false)
392
393
  })
393
394
 
395
+ it('should handle V1 joined conditions without aliases', () => {
396
+ formDefinitionV2Schema.validate = jest
397
+ .fn()
398
+ .mockReturnValue({ value: definition })
399
+
400
+ const model = new FormModel(definition, {
401
+ basePath: 'test'
402
+ })
403
+
404
+ expect(model.conditions).toBeDefined()
405
+ expect(Object.keys(model.conditions)).toHaveLength(1)
406
+
407
+ const joinedCondition = model.conditions.ZCXeMz
408
+ expect(joinedCondition).toBeDefined()
409
+ expect(joinedCondition?.displayName).toBe('test')
410
+
411
+ const testState = { NIJphU: "ap'ostrophe's", iraEpG: "shouldn't've" }
412
+ expect(joinedCondition?.fn(testState)).toBe(true)
413
+
414
+ const testStateFalse = { NIJphU: 'other', iraEpG: "shouldn't've" }
415
+ expect(joinedCondition?.fn(testStateFalse)).toBe(false)
416
+
417
+ const context = model.toConditionContext(testState, model.conditions)
418
+
419
+ expect(context).not.toHaveProperty('cond_ZCXeMz')
420
+
421
+ expect(context).toHaveProperty('ZCXeMz')
422
+
423
+ expect(context).toHaveProperty('NIJphU', "ap'ostrophe's")
424
+ expect(context).toHaveProperty('iraEpG', "shouldn't've")
425
+ })
426
+
427
+ it('should use schema version to determine condition aliases', () => {
428
+ const v1Definition = { ...definition, schema: SchemaVersion.V1 }
429
+ formDefinitionV2Schema.validate = jest
430
+ .fn()
431
+ .mockReturnValue({ value: v1Definition })
432
+
433
+ const v1Model = new FormModel(v1Definition, { basePath: 'test' })
434
+ expect(v1Model.schemaVersion).toBe(SchemaVersion.V1)
435
+
436
+ const v1TestState = { NIJphU: "ap'ostrophe's", iraEpG: "shouldn't've" }
437
+ const v1Context = v1Model.toConditionContext(
438
+ v1TestState,
439
+ v1Model.conditions
440
+ )
441
+
442
+ expect(v1Context).toHaveProperty('ZCXeMz')
443
+ expect(v1Context).not.toHaveProperty('cond_ZCXeMz')
444
+
445
+ formDefinitionV2Schema.validate = jest
446
+ .fn()
447
+ .mockReturnValue({ value: joinedConditionsDefinition })
448
+
449
+ const v2Model = new FormModel(joinedConditionsDefinition, {
450
+ basePath: 'test'
451
+ })
452
+ expect(v2Model.schemaVersion).toBe(SchemaVersion.V2)
453
+
454
+ const v2TestState = { userName: 'Bob', isOverEighteen: true }
455
+ const v2Context = v2Model.toConditionContext(
456
+ v2TestState,
457
+ v2Model.conditions
458
+ )
459
+
460
+ expect(v2Context).toHaveProperty('cond_d15aff7a622440a28e5f51a5af2f7910')
461
+ expect(v2Context).toHaveProperty('cond_d1f9fcc7f09847e79d314f5ee57ba985')
462
+ expect(v2Context).toHaveProperty('cond_db43c6bc9ce6478b83454fff5eff2ba3')
463
+
464
+ expect(v2Context).not.toHaveProperty('d15aff7a-6224-40a2-8e5f-51a5af2f7910')
465
+ expect(v2Context).not.toHaveProperty('d1f9fcc7-f098-47e7-9d31-4f5ee57ba985')
466
+ expect(v2Context).not.toHaveProperty('db43c6bc-9ce6-478b-8345-4fff5eff2ba3')
467
+ })
468
+
394
469
  describe('generateConditionAlias', () => {
395
470
  it('should generate valid JavaScript identifiers from condition IDs', () => {
396
471
  formDefinitionV2Schema.validate = jest
@@ -401,7 +476,7 @@ describe('FormModel - Joined Conditions', () => {
401
476
  basePath: 'test'
402
477
  })
403
478
 
404
- const evaluationState = { fsZNJr: 'Bob', DaBGpS: true }
479
+ const evaluationState = { userName: 'Bob', isOverEighteen: true }
405
480
 
406
481
  const context = model.toConditionContext(
407
482
  evaluationState,
@@ -428,8 +503,8 @@ describe('FormModel - Joined Conditions', () => {
428
503
  model.conditions['db43c6bc-9ce6-478b-8345-4fff5eff2ba3']
429
504
  expect(joinedCondition).toBeDefined()
430
505
 
431
- const stateTrue = { fsZNJr: 'Bob', DaBGpS: true }
432
- const stateFalse = { fsZNJr: 'Alice', DaBGpS: false }
506
+ const stateTrue = { userName: 'Bob', isOverEighteen: true }
507
+ const stateFalse = { userName: 'Alice', isOverEighteen: false }
433
508
 
434
509
  expect(joinedCondition?.fn(stateTrue)).toBe(true)
435
510
  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 alias = generateConditionAlias(conditionId)
281
+ const propertyName =
282
+ this.schemaVersion === SchemaVersion.V2
283
+ ? generateConditionAlias(conditionId)
284
+ : conditionId
282
285
 
283
- Object.defineProperty(context, alias, {
286
+ Object.defineProperty(context, propertyName, {
284
287
  get() {
285
288
  return conditions[conditionId]?.fn(evaluationState)
286
289
  }
@@ -4,6 +4,8 @@ import {
4
4
  SummaryViewModel
5
5
  } from '~/src/server/plugins/engine/models/index.js'
6
6
  import { SummaryPageController } from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'
7
+ import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
8
+ import { serverWithSaveAndReturn } from '~/src/server/plugins/engine/pageControllers/__stubs__/server.js'
7
9
  import {
8
10
  createPage,
9
11
  type PageControllerClass
@@ -14,7 +16,6 @@ import {
14
16
  type FormState
15
17
  } from '~/src/server/plugins/engine/types.js'
16
18
  import definition from '~/test/form/definitions/repeat-mixed.js'
17
-
18
19
  const basePath = `${FORM_PREFIX}/test`
19
20
 
20
21
  describe('SummaryViewModel', () => {
@@ -36,7 +37,7 @@ describe('SummaryViewModel', () => {
36
37
  page = createPage(model, definition.pages[2])
37
38
  pageUrl = new URL('http://example.com/repeat/pizza-order/summary')
38
39
 
39
- request = {
40
+ request = buildFormContextRequest({
40
41
  method: 'get',
41
42
  url: pageUrl,
42
43
  path: pageUrl.pathname,
@@ -46,7 +47,7 @@ describe('SummaryViewModel', () => {
46
47
  },
47
48
  query: {},
48
49
  app: { model }
49
- }
50
+ })
50
51
  })
51
52
 
52
53
  describe.each([
@@ -272,13 +273,14 @@ describe('SummaryPageController', () => {
272
273
  slug: 'repeat'
273
274
  },
274
275
  query: {},
275
- app: { model }
276
+ app: { model },
277
+ server: serverWithSaveAndReturn
276
278
  }
277
279
  })
278
280
 
279
281
  describe('Save and Return functionality', () => {
280
282
  it('should show save and return button on summary page', () => {
281
- expect(controller.shouldShowSaveAndReturn()).toBe(true)
283
+ expect(controller.shouldShowSaveAndReturn(request.server)).toBe(true)
282
284
  })
283
285
 
284
286
  it('should handle save and return from summary page', () => {
@@ -51,7 +51,7 @@ export class SummaryViewModel {
51
51
  serviceUrl: string
52
52
  hasMissingNotificationEmail?: boolean
53
53
  components?: ComponentViewModel[]
54
- allowSaveAndReturn?: boolean
54
+ allowSaveAndReturn = false
55
55
 
56
56
  constructor(
57
57
  request: FormContextRequest,
@@ -20,9 +20,11 @@ const pluginRegistrationOptionsSchema = Joi.object({
20
20
  preparePageEventRequestOptions: Joi.function().optional(),
21
21
  onRequest: Joi.function().optional(),
22
22
  baseUrl: Joi.string().uri().required(),
23
- keyGenerator: Joi.function().optional(),
24
- sessionHydrator: Joi.function().optional(),
25
- sessionPersister: Joi.function().optional()
23
+ saveAndReturn: Joi.object({
24
+ keyGenerator: Joi.function(),
25
+ sessionHydrator: Joi.function(),
26
+ sessionPersister: Joi.function()
27
+ }).optional()
26
28
  })
27
29
 
28
30
  /**
@@ -2,6 +2,9 @@ import { validatePluginOptions } from '~/src/server/plugins/engine/options.js'
2
2
 
3
3
  describe('validatePluginOptions', () => {
4
4
  it('returns the validated value for valid options', () => {
5
+ /**
6
+ * @type {PluginOptions}
7
+ */
5
8
  const validOptions = {
6
9
  nunjucks: {
7
10
  baseLayoutPath: 'dxt-devtool-baselayout.html',
@@ -17,6 +20,9 @@ describe('validatePluginOptions', () => {
17
20
  })
18
21
 
19
22
  it('accepts optional properties keyGenerator, sessionHydrator, and sessionPersister', () => {
23
+ /**
24
+ * @type {PluginOptions}
25
+ */
20
26
  const validOptionsWithOptionals = {
21
27
  nunjucks: {
22
28
  baseLayoutPath: 'dxt-devtool-baselayout.html',
@@ -26,9 +32,11 @@ describe('validatePluginOptions', () => {
26
32
  return { hello: 'world' }
27
33
  },
28
34
  baseUrl: 'http://localhost:3009',
29
- keyGenerator: () => 'test-key',
30
- sessionHydrator: () => Promise.resolve({ someState: 'value' }),
31
- sessionPersister: () => Promise.resolve(undefined)
35
+ saveAndReturn: {
36
+ keyGenerator: () => 'test-key',
37
+ sessionHydrator: () => Promise.resolve({ someState: 'value' }),
38
+ sessionPersister: () => Promise.resolve(undefined)
39
+ }
32
40
  }
33
41
 
34
42
  expect(validatePluginOptions(validOptionsWithOptionals)).toEqual(
@@ -40,17 +48,20 @@ describe('validatePluginOptions', () => {
40
48
  * tsc would usually check compliance with the type, but given a user might be using plain JS we still want a test
41
49
  */
42
50
  it('fails if a required attribute is missing', () => {
43
- // viewContext is missing
44
- const invalidOptions = {
45
- nunjucks: {
46
- baseLayoutPath: 'dxt-devtool-baselayout.html',
47
- paths: ['src/server/devserver'] // custom layout to make it really clear this is not the same as the runner
48
- }
49
- }
51
+ const invalidOptions =
52
+ /** @type {PluginOptions} testing without viewContext */ ({
53
+ nunjucks: {
54
+ baseLayoutPath: 'dxt-devtool-baselayout.html',
55
+ paths: ['src/server/devserver'] // custom layout to make it really clear this is not the same as the runner
56
+ }
57
+ })
50
58
 
51
- // @ts-expect-error -- add a test for JS users
52
59
  expect(() => validatePluginOptions(invalidOptions)).toThrow(
53
60
  'Invalid plugin options'
54
61
  )
55
62
  })
56
63
  })
64
+
65
+ /**
66
+ * @import { PluginOptions } from '~/src/server/plugins/engine/types.js'
67
+ */
@@ -7,7 +7,7 @@ import {
7
7
  SummaryPageController,
8
8
  getFormSubmissionData
9
9
  } from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'
10
- import { type FormContextRequest } from '~/src/server/plugins/engine/types.js'
10
+ import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
11
11
  import { FormStatus } from '~/src/server/routes/types.js'
12
12
  import definition from '~/test/form/definitions/repeat-mixed.js'
13
13
 
@@ -52,7 +52,7 @@ const pageUrl = new URL('http://example.com/repeat/pizza-order/summary')
52
52
 
53
53
  const controller = new SummaryPageController(model, pageDef)
54
54
 
55
- const request = {
55
+ const request = buildFormContextRequest({
56
56
  method: 'get',
57
57
  url: pageUrl,
58
58
  path: pageUrl.pathname,
@@ -62,7 +62,7 @@ const request = {
62
62
  },
63
63
  query: {},
64
64
  app: { model }
65
- } satisfies FormContextRequest
65
+ })
66
66
 
67
67
  const context = model.getFormContext(request, state)
68
68
  const summaryViewModel = controller.getSummaryViewModel(request, context)
@@ -14,6 +14,7 @@ import {
14
14
  prepareStatus
15
15
  } from '~/src/server/plugins/engine/pageControllers/FileUploadPageController.js'
16
16
  import { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
17
+ import { serverWithSaveAndReturn } from '~/src/server/plugins/engine/pageControllers/__stubs__/server.js'
17
18
  import * as pageHelpers from '~/src/server/plugins/engine/pageControllers/helpers.js'
18
19
  import * as uploadService from '~/src/server/plugins/engine/services/uploadService.js'
19
20
  import {
@@ -31,6 +32,7 @@ import {
31
32
  type FormRequest,
32
33
  type FormRequestPayload
33
34
  } from '~/src/server/routes/types.js'
35
+ import { type CacheService } from '~/src/server/services/index.js'
34
36
  import definition from '~/test/form/definitions/file-upload-basic.js'
35
37
 
36
38
  type TestableFileUploadPageController = FileUploadPageController & {
@@ -77,12 +79,13 @@ describe('FileUploadPageController', () => {
77
79
  server: {
78
80
  plugins: {
79
81
  'forms-engine-plugin': {
82
+ baseLayoutPath: '',
80
83
  cacheService: {
81
84
  setFlash: jest.fn(),
82
85
  setState: jest
83
86
  .fn()
84
87
  .mockImplementation((req, updated) => Promise.resolve(updated))
85
- }
88
+ } as unknown as CacheService
86
89
  }
87
90
  }
88
91
  },
@@ -1115,4 +1118,12 @@ describe('FileUploadPageController', () => {
1115
1118
  )
1116
1119
  })
1117
1120
  })
1121
+
1122
+ describe('shouldShowSaveAndReturn', () => {
1123
+ it('should return true when save and return is enabled', () => {
1124
+ expect(controller.shouldShowSaveAndReturn(serverWithSaveAndReturn)).toBe(
1125
+ true
1126
+ )
1127
+ })
1128
+ })
1118
1129
  })
@@ -3,6 +3,7 @@ import { type ResponseToolkit } from '@hapi/hapi'
3
3
  import { FORM_PREFIX } from '~/src/server/constants.js'
4
4
  import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
5
5
  import { PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
6
+ import { serverWithSaveAndReturn } from '~/src/server/plugins/engine/pageControllers/__stubs__/server.js'
6
7
  import { type FormRequest } from '~/src/server/routes/types.js'
7
8
  import definition from '~/test/form/definitions/basic.js'
8
9
 
@@ -230,4 +231,12 @@ describe('PageController', () => {
230
231
  )
231
232
  })
232
233
  })
234
+
235
+ describe('shouldShowSaveAndReturn', () => {
236
+ it('should return false (PageController does not allow save and return)', () => {
237
+ expect(controller1.shouldShowSaveAndReturn(serverWithSaveAndReturn)).toBe(
238
+ false
239
+ )
240
+ })
241
+ })
233
242
  })
@@ -9,12 +9,14 @@ import Boom from '@hapi/boom'
9
9
  import {
10
10
  type Lifecycle,
11
11
  type ResponseToolkit,
12
- type RouteOptions
12
+ type RouteOptions,
13
+ type Server
13
14
  } from '@hapi/hapi'
14
15
 
15
16
  import { type ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
16
17
  import {
17
18
  encodeUrl,
19
+ getSaveAndReturnHelpers,
18
20
  getStartPath,
19
21
  normalisePath
20
22
  } from '~/src/server/plugins/engine/helpers.js'
@@ -45,6 +47,7 @@ export class PageController {
45
47
  events?: Events
46
48
  collection?: ComponentCollection
47
49
  viewName = 'index'
50
+ allowSaveAndReturn = false
48
51
 
49
52
  constructor(model: FormModel, pageDef: Page) {
50
53
  const { def } = model
@@ -183,4 +186,10 @@ export class PageController {
183
186
  ) => ReturnType<Lifecycle.Method<FormRequestPayloadRefs>> {
184
187
  throw Boom.badRequest('Unsupported POST route handler for this page')
185
188
  }
189
+
190
+ shouldShowSaveAndReturn(server: Server): boolean {
191
+ return (
192
+ getSaveAndReturnHelpers(server) !== undefined && this.allowSaveAndReturn
193
+ )
194
+ }
186
195
  }