@defra/forms-engine-plugin 2.0.3 → 2.1.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.
Files changed (34) hide show
  1. package/.public/javascripts/application.min.js +1 -1
  2. package/.public/javascripts/application.min.js.map +1 -1
  3. package/.public/javascripts/shared.min.js +1 -1
  4. package/.public/javascripts/shared.min.js.map +1 -1
  5. package/.public/stylesheets/application.min.css +2 -2
  6. package/.public/stylesheets/application.min.css.map +1 -1
  7. package/.server/client/javascripts/file-upload.js +3 -3
  8. package/.server/client/javascripts/file-upload.js.map +1 -1
  9. package/.server/server/forms/page-events.yaml +87 -0
  10. package/.server/server/index.js +2 -1
  11. package/.server/server/index.js.map +1 -1
  12. package/.server/server/plugins/engine/routes/questions.d.ts +4 -2
  13. package/.server/server/plugins/engine/routes/questions.js +67 -43
  14. package/.server/server/plugins/engine/routes/questions.js.map +1 -1
  15. package/.server/server/plugins/engine/services/localFormsService.js +7 -9
  16. package/.server/server/plugins/engine/services/localFormsService.js.map +1 -1
  17. package/.server/server/routes/dummy-api.d.ts +38 -0
  18. package/.server/server/routes/dummy-api.js +33 -0
  19. package/.server/server/routes/dummy-api.js.map +1 -0
  20. package/.server/server/routes/index.d.ts +1 -0
  21. package/.server/server/routes/index.js +1 -0
  22. package/.server/server/routes/index.js.map +1 -1
  23. package/package.json +12 -10
  24. package/src/client/javascripts/file-upload.js +3 -3
  25. package/src/server/forms/page-events.yaml +87 -0
  26. package/src/server/index.ts +4 -2
  27. package/src/server/plugins/engine/routes/questions.test.ts +416 -0
  28. package/src/server/plugins/engine/routes/questions.ts +96 -40
  29. package/src/server/plugins/engine/services/localFormsService.js +7 -8
  30. package/src/server/routes/dummy-api.test.ts +96 -0
  31. package/src/server/routes/dummy-api.ts +62 -0
  32. package/src/server/routes/index.ts +1 -0
  33. package/.server/server/forms/register-as-a-unicorn-breeder.json +0 -393
  34. package/src/server/forms/register-as-a-unicorn-breeder.json +0 -393
@@ -0,0 +1,33 @@
1
+ function calculateAge(day, month, year) {
2
+ const dobDate = new Date(Number(day), Number(month) - 1, Number(year));
3
+ const today = new Date();
4
+ let age = today.getFullYear() - dobDate.getFullYear();
5
+ const m = today.getMonth() - dobDate.getMonth();
6
+ if (m < 0 || m === 0 && today.getDate() < dobDate.getDate()) {
7
+ age--;
8
+ }
9
+ return age;
10
+ }
11
+ export default [{
12
+ method: 'POST',
13
+ path: '/api/example/on-load-page',
14
+ handler(request, _h) {
15
+ return {
16
+ submissionEvent: 'GET',
17
+ submissionReferenceNumber: request.payload.meta.referenceNumber
18
+ };
19
+ }
20
+ }, {
21
+ method: 'POST',
22
+ path: '/api/example/on-summary',
23
+ handler(request, _h) {
24
+ const [day, month, year] = request.payload.data.main.dateOfBirth.split('-');
25
+ const age = calculateAge(day, month, year);
26
+ return {
27
+ calculatedAge: age,
28
+ submissionEvent: 'POST',
29
+ submissionReferenceNumber: request.payload.meta.referenceNumber // example of receiving a payload from DXT
30
+ };
31
+ }
32
+ }];
33
+ //# sourceMappingURL=dummy-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dummy-api.js","names":["calculateAge","day","month","year","dobDate","Date","Number","today","age","getFullYear","m","getMonth","getDate","method","path","handler","request","_h","submissionEvent","submissionReferenceNumber","payload","meta","referenceNumber","data","main","dateOfBirth","split","calculatedAge"],"sources":["../../../src/server/routes/dummy-api.ts"],"sourcesContent":["import { type Request, type ResponseToolkit } from '@hapi/hapi'\n\nfunction calculateAge(day: string, month: string, year: string) {\n const dobDate = new Date(Number(day), Number(month) - 1, Number(year))\n\n const today = new Date()\n\n let age = today.getFullYear() - dobDate.getFullYear()\n const m = today.getMonth() - dobDate.getMonth()\n\n if (m < 0 || (m === 0 && today.getDate() < dobDate.getDate())) {\n age--\n }\n\n return age\n}\n\nexport default [\n {\n method: 'POST',\n path: '/api/example/on-load-page',\n handler(\n request: Request<{ Payload: { meta: { referenceNumber: string } } }>,\n _h: ResponseToolkit\n ) {\n return {\n submissionEvent: 'GET',\n submissionReferenceNumber: request.payload.meta.referenceNumber\n }\n }\n },\n {\n method: 'POST',\n path: '/api/example/on-summary',\n handler(\n request: Request<{\n Payload: {\n data: {\n main: {\n applicantFirstName: string\n applicantLastName: string\n dateOfBirth: string\n }\n }\n meta: { event: string; referenceNumber: string }\n }\n }>,\n _h: ResponseToolkit\n ) {\n const [day, month, year] =\n request.payload.data.main.dateOfBirth.split('-')\n\n const age = calculateAge(day, month, year)\n\n return {\n calculatedAge: age,\n submissionEvent: 'POST',\n submissionReferenceNumber: request.payload.meta.referenceNumber // example of receiving a payload from DXT\n }\n }\n }\n]\n"],"mappings":"AAEA,SAASA,YAAYA,CAACC,GAAW,EAAEC,KAAa,EAAEC,IAAY,EAAE;EAC9D,MAAMC,OAAO,GAAG,IAAIC,IAAI,CAACC,MAAM,CAACL,GAAG,CAAC,EAAEK,MAAM,CAACJ,KAAK,CAAC,GAAG,CAAC,EAAEI,MAAM,CAACH,IAAI,CAAC,CAAC;EAEtE,MAAMI,KAAK,GAAG,IAAIF,IAAI,CAAC,CAAC;EAExB,IAAIG,GAAG,GAAGD,KAAK,CAACE,WAAW,CAAC,CAAC,GAAGL,OAAO,CAACK,WAAW,CAAC,CAAC;EACrD,MAAMC,CAAC,GAAGH,KAAK,CAACI,QAAQ,CAAC,CAAC,GAAGP,OAAO,CAACO,QAAQ,CAAC,CAAC;EAE/C,IAAID,CAAC,GAAG,CAAC,IAAKA,CAAC,KAAK,CAAC,IAAIH,KAAK,CAACK,OAAO,CAAC,CAAC,GAAGR,OAAO,CAACQ,OAAO,CAAC,CAAE,EAAE;IAC7DJ,GAAG,EAAE;EACP;EAEA,OAAOA,GAAG;AACZ;AAEA,eAAe,CACb;EACEK,MAAM,EAAE,MAAM;EACdC,IAAI,EAAE,2BAA2B;EACjCC,OAAOA,CACLC,OAAoE,EACpEC,EAAmB,EACnB;IACA,OAAO;MACLC,eAAe,EAAE,KAAK;MACtBC,yBAAyB,EAAEH,OAAO,CAACI,OAAO,CAACC,IAAI,CAACC;IAClD,CAAC;EACH;AACF,CAAC,EACD;EACET,MAAM,EAAE,MAAM;EACdC,IAAI,EAAE,yBAAyB;EAC/BC,OAAOA,CACLC,OAWE,EACFC,EAAmB,EACnB;IACA,MAAM,CAAChB,GAAG,EAAEC,KAAK,EAAEC,IAAI,CAAC,GACtBa,OAAO,CAACI,OAAO,CAACG,IAAI,CAACC,IAAI,CAACC,WAAW,CAACC,KAAK,CAAC,GAAG,CAAC;IAElD,MAAMlB,GAAG,GAAGR,YAAY,CAACC,GAAG,EAAEC,KAAK,EAAEC,IAAI,CAAC;IAE1C,OAAO;MACLwB,aAAa,EAAEnB,GAAG;MAClBU,eAAe,EAAE,MAAM;MACvBC,yBAAyB,EAAEH,OAAO,CAACI,OAAO,CAACC,IAAI,CAACC,eAAe,CAAC;IAClE,CAAC;EACH;AACF,CAAC,CACF","ignoreList":[]}
@@ -1 +1,2 @@
1
1
  export { default as publicRoutes } from '~/src/server/routes/public.js';
2
+ export { default as dummyApiRoutes } from '~/src/server/routes/dummy-api.js';
@@ -1,2 +1,3 @@
1
1
  export { default as publicRoutes } from "./public.js";
2
+ export { default as dummyApiRoutes } from "./dummy-api.js";
2
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["default","publicRoutes"],"sources":["../../../src/server/routes/index.ts"],"sourcesContent":["export { default as publicRoutes } from '~/src/server/routes/public.js'\n"],"mappings":"AAAA,SAASA,OAAO,IAAIC,YAAY","ignoreList":[]}
1
+ {"version":3,"file":"index.js","names":["default","publicRoutes","dummyApiRoutes"],"sources":["../../../src/server/routes/index.ts"],"sourcesContent":["export { default as publicRoutes } from '~/src/server/routes/public.js'\nexport { default as dummyApiRoutes } from '~/src/server/routes/dummy-api.js'\n"],"mappings":"AAAA,SAASA,OAAO,IAAIC,YAAY;AAChC,SAASD,OAAO,IAAIE,cAAc","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "2.0.3",
3
+ "version": "2.1.1",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -87,9 +87,9 @@
87
87
  "btoa": "^1.2.1",
88
88
  "convict": "^6.2.4",
89
89
  "date-fns": "^4.1.0",
90
- "dotenv": "^17.0.1",
90
+ "dotenv": "^17.2.1",
91
91
  "expr-eval": "^2.0.2",
92
- "govuk-frontend": "^5.10.2",
92
+ "govuk-frontend": "^5.11.1",
93
93
  "hapi-pino": "^12.1.0",
94
94
  "hapi-pulse": "^3.0.1",
95
95
  "highlight.js": "^11.11.1",
@@ -103,7 +103,7 @@
103
103
  "nunjucks": "^3.2.3",
104
104
  "outdent": "^0.8.0",
105
105
  "pino": "^9.7.0",
106
- "pino-pretty": "^13.0.0",
106
+ "pino-pretty": "^13.1.1",
107
107
  "proxy-agent": "^6.5.0",
108
108
  "resolve": "^1.22.10",
109
109
  "yaml": "^2.8.0"
@@ -116,23 +116,23 @@
116
116
  "@babel/preset-typescript": "^7.27.1",
117
117
  "@hapi/basic": "^7.0.2",
118
118
  "@testing-library/dom": "^10.4.0",
119
- "@testing-library/jest-dom": "^6.6.3",
119
+ "@testing-library/jest-dom": "^6.6.4",
120
120
  "@types/atob": "^2.1.4",
121
121
  "@types/btoa": "^1.2.5",
122
122
  "@types/convict": "^6.1.6",
123
123
  "@types/eslint": "^9.6.1",
124
124
  "@types/govuk-frontend": "^5.9.0",
125
125
  "@types/hapi": "^18.0.14",
126
- "@types/hapi__catbox-memory": "^4.1.8",
126
+ "@types/hapi__catbox-memory": "^6.0.2",
127
127
  "@types/hapi__cookie": "^12.0.5",
128
128
  "@types/hapi__crumb": "^7.3.7",
129
129
  "@types/hapi__yar": "^10.1.6",
130
130
  "@types/hoek": "^4.1.7",
131
131
  "@types/jest": "^30.0.0",
132
132
  "@types/jsdom": "^21.1.7",
133
- "@types/lodash": "^4.17.17",
133
+ "@types/lodash": "^4.17.20",
134
134
  "@types/mysql": "^2.15.27",
135
- "@types/node": "^24.0.2",
135
+ "@types/node": "^24.2.0",
136
136
  "@types/nunjucks": "^3.2.6",
137
137
  "@types/resolve": "^1.20.6",
138
138
  "@types/url-parse": "^1.4.11",
@@ -164,10 +164,12 @@
164
164
  "eslint-plugin-promise": "^6.6.0",
165
165
  "global-jsdom": "^26.0.0",
166
166
  "husky": "^9.1.7",
167
- "jest": "^30.0.4",
168
- "jest-extended": "^4.0.2",
167
+ "jest": "^30.0.5",
168
+ "jest-extended": "^6.0.0",
169
169
  "jsdom": "^26.1.0",
170
170
  "lint-staged": "^15.3.0",
171
+ "mockdate": "^3.0.5",
172
+ "nock": "^14.0.8",
171
173
  "postcss": "^8.5.6",
172
174
  "postcss-load-config": "^6.0.1",
173
175
  "postcss-loader": "^8.1.1",
@@ -66,10 +66,10 @@ function findOrCreateSummaryList(form, fileCountP) {
66
66
  summaryList = document.createElement('dl')
67
67
  summaryList.className = 'govuk-summary-list govuk-summary-list--long-key'
68
68
 
69
- const continueButton = form.querySelector('.govuk-button')
69
+ const buttonGroup = form.querySelector('.govuk-button-group')
70
70
 
71
- if (continueButton) {
72
- form.insertBefore(summaryList, continueButton)
71
+ if (buttonGroup) {
72
+ form.insertBefore(summaryList, buttonGroup)
73
73
  } else {
74
74
  form.insertBefore(summaryList, fileCountP.nextSibling)
75
75
  }
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: Page events
3
+ engine: V2
4
+ schema: 2
5
+ startPage: '/summary'
6
+ pages:
7
+ - title: Your name
8
+ path: '/your-name'
9
+ components:
10
+ - type: TextField
11
+ title: What is your first name?
12
+ name: applicantFirstName
13
+ shortDescription: Your first name
14
+ hint: ''
15
+ options:
16
+ required: true
17
+ schema: {}
18
+ id: 1fb8e182-c709-4792-8f83-e01d8b1fee1a
19
+ - type: TextField
20
+ title: What is your last name?
21
+ name: applicantLastName
22
+ shortDescription: Your last name
23
+ hint: ''
24
+ options:
25
+ required: true
26
+ schema: {}
27
+ id: b68df7f1-d4f4-4c17-83c8-402f584906c9
28
+ next: []
29
+ id: 622a35ec-3795-418a-81f3-a45746959045
30
+ - title: ''
31
+ path: '/date-of-birth'
32
+ events:
33
+ onLoad:
34
+ type: http
35
+ options:
36
+ method: 'POST'
37
+ url: http://localhost:3009/api/example/on-load-page
38
+ components:
39
+ - type: Html
40
+ # technically context.data.submissionReferenceNumber is redundant as the reference number is available locally as context.referenceNumber
41
+ # but this is to demonstrate that the server can send data back to the client
42
+ content: >
43
+ <p class="govuk-body">
44
+ The backend received a full copy of the form state even though you haven't submitted the form yet.
45
+ </p>
46
+ <p class="govuk-body">
47
+ Your submission was received with the reference: <strong>{{ context.data.submissionReferenceNumber }}</strong>.
48
+ </p>
49
+ id: 334b10dc-3373-4928-8fed-575578a67de8
50
+ - type: DatePartsField
51
+ title: When is {{ applicantFirstName }} {{ applicantLastName }}'s birthday?
52
+ name: dateOfBirth
53
+ shortDescription: Your birthday
54
+ hint: ''
55
+ options:
56
+ required: true
57
+ schema: {}
58
+ id: '00738799-3489-4ab2-a57b-542eecb31bfa'
59
+ next: []
60
+ id: da0fbdb4-a2de-4650-be16-9ba552af135f
61
+ - id: 449a45f6-4541-4a46-91bd-8b8931b07b50
62
+ title: Summary
63
+ path: '/summary'
64
+ controller: SummaryPageController
65
+ events:
66
+ onLoad:
67
+ type: http
68
+ options:
69
+ method: 'POST'
70
+ url: http://localhost:3009/api/example/on-summary
71
+ onSave:
72
+ type: http
73
+ options:
74
+ method: 'POST'
75
+ url: http://localhost:3009/api/example/on-summary
76
+ components:
77
+ - type: Html
78
+ content: >
79
+ <h2 class="govuk-heading-m">Your age</h1>
80
+ <p class="govuk-body">
81
+ We've calculated that you are {{ context.data.calculatedAge }} years old. Only proceed if this is correct.
82
+ </p>
83
+ id: c42ea488-a38c-4b67-b8fa-4cf543a4f82d
84
+ next: []
85
+ conditions: []
86
+ sections: []
87
+ lists: []
@@ -3,7 +3,8 @@ import { Engine as CatboxRedis } from '@hapi/catbox-redis'
3
3
  import hapi, {
4
4
  type Request,
5
5
  type ResponseToolkit,
6
- type ServerOptions
6
+ type ServerOptions,
7
+ type ServerRoute
7
8
  } from '@hapi/hapi'
8
9
  import inert from '@hapi/inert'
9
10
  import Scooter from '@hapi/scooter'
@@ -21,7 +22,7 @@ import pluginErrorPages from '~/src/server/plugins/errorPages.js'
21
22
  import { plugin as pluginViews } from '~/src/server/plugins/nunjucks/index.js'
22
23
  import pluginPulse from '~/src/server/plugins/pulse.js'
23
24
  import pluginSession from '~/src/server/plugins/session.js'
24
- import { publicRoutes } from '~/src/server/routes/index.js'
25
+ import { dummyApiRoutes, publicRoutes } from '~/src/server/routes/index.js'
25
26
  import { prepareSecureContext } from '~/src/server/secure-context.js'
26
27
  import { type RouteConfig } from '~/src/server/types.js'
27
28
 
@@ -120,6 +121,7 @@ export async function createServer(routeConfig?: RouteConfig) {
120
121
  name: 'router',
121
122
  register: (server) => {
122
123
  server.route(publicRoutes)
124
+ server.route(dummyApiRoutes as ServerRoute[])
123
125
  }
124
126
  }
125
127
  })
@@ -0,0 +1,416 @@
1
+ import Boom from '@hapi/boom'
2
+ import { type ResponseObject, type ResponseToolkit } from '@hapi/hapi'
3
+ // eslint-disable-next-line n/no-unpublished-import
4
+ import nock from 'nock'
5
+
6
+ import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
7
+ import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'
8
+ import { redirectOrMakeHandler } from '~/src/server/plugins/engine/routes/index.js'
9
+ import {
10
+ makeGetHandler,
11
+ makePostHandler
12
+ } from '~/src/server/plugins/engine/routes/questions.js'
13
+ import { type FormContext } from '~/src/server/plugins/engine/types.js'
14
+ import {
15
+ type FormRequest,
16
+ type FormRequestPayload
17
+ } from '~/src/server/routes/types.js'
18
+ jest.mock('~/src/server/plugins/engine/models/SummaryViewModel', () => ({
19
+ SummaryViewModel: class {
20
+ summary = 'mocked summary'
21
+ }
22
+ }))
23
+
24
+ jest.mock(
25
+ '~/src/server/plugins/engine/pageControllers/SummaryPageController',
26
+ () => ({
27
+ getFormSubmissionData: jest.fn().mockReturnValue([])
28
+ })
29
+ )
30
+
31
+ jest.mock('~/src/server/plugins/engine/outputFormatters/machine/v1', () => ({
32
+ format: jest.fn().mockReturnValue('mocked format')
33
+ }))
34
+
35
+ jest.mock('~/src/server/plugins/engine/routes/index')
36
+
37
+ describe('makeGetHandler', () => {
38
+ const hMock: Pick<ResponseToolkit, 'redirect' | 'view'> = {
39
+ redirect: jest.fn(),
40
+ view: jest.fn()
41
+ }
42
+
43
+ beforeEach(() => {
44
+ nock('http://test').persist().post('/load').reply(200, {
45
+ wasGetCalled: true
46
+ })
47
+ })
48
+
49
+ afterEach(() => {
50
+ jest.mocked(redirectOrMakeHandler).mockRestore()
51
+ nock.cleanAll()
52
+ })
53
+
54
+ it('calls the callback when events.onLoad.type is http', async () => {
55
+ let data = {}
56
+
57
+ const modelMock = {
58
+ basePath: 'some-base-path',
59
+ def: { name: 'Hello world' }
60
+ } as FormModel
61
+
62
+ const pageMock = createMockPageController(
63
+ modelMock,
64
+ (
65
+ _request: FormRequest,
66
+ context: FormContext,
67
+ _h: Pick<ResponseToolkit, 'redirect' | 'view'>
68
+ ) => {
69
+ data = context.data
70
+ return Promise.resolve({} as unknown as ResponseObject)
71
+ }
72
+ )
73
+
74
+ const contextMock = { data: {}, model: {} } as unknown as FormContext
75
+
76
+ const requestMock = {
77
+ params: { path: 'some-path' },
78
+ app: { model: modelMock }
79
+ } as FormRequest
80
+
81
+ jest
82
+ .mocked(redirectOrMakeHandler)
83
+ .mockImplementation(
84
+ (
85
+ _req: FormRequest | FormRequestPayload,
86
+ _h: Pick<ResponseToolkit, 'redirect' | 'view'>,
87
+ fn
88
+ ) => Promise.resolve(fn(pageMock, contextMock))
89
+ )
90
+
91
+ await makeGetHandler()(requestMock, hMock)
92
+
93
+ expect(data).toMatchObject({
94
+ wasGetCalled: true
95
+ })
96
+ })
97
+
98
+ it('does not call the callback when the events.onLoad.type is not http', async () => {
99
+ let data = {}
100
+
101
+ const modelMock = {
102
+ basePath: 'some-base-path',
103
+ def: { name: 'Hello world' }
104
+ } as FormModel
105
+
106
+ const pageMock = createMockPageController(
107
+ modelMock,
108
+ (
109
+ _request: FormRequest,
110
+ context: FormContext,
111
+ _h: Pick<ResponseToolkit, 'redirect' | 'view'>
112
+ ) => {
113
+ data = context.data
114
+ return Promise.resolve({} as unknown as ResponseObject)
115
+ }
116
+ )
117
+
118
+ pageMock.events = {}
119
+
120
+ const contextMock = { data: {}, model: {} } as unknown as FormContext
121
+
122
+ const requestMock = {
123
+ params: { path: 'some-path' },
124
+ app: { model: modelMock }
125
+ } as FormRequest
126
+
127
+ jest
128
+ .mocked(redirectOrMakeHandler)
129
+ .mockImplementation(
130
+ (
131
+ _req: FormRequest | FormRequestPayload,
132
+ _h: Pick<ResponseToolkit, 'redirect' | 'view'>,
133
+ fn
134
+ ) => Promise.resolve(fn(pageMock, contextMock))
135
+ )
136
+
137
+ await makeGetHandler()(requestMock, hMock)
138
+
139
+ expect(data).toMatchObject({})
140
+ })
141
+
142
+ it('throws when model is missing', async () => {
143
+ let error
144
+
145
+ const modelMock = {
146
+ basePath: 'some-base-path',
147
+ def: { name: 'Hello world' }
148
+ } as FormModel
149
+
150
+ const pageMock = createMockPageController(
151
+ modelMock,
152
+ (
153
+ _request: FormRequest,
154
+ _context: FormContext,
155
+ _h: Pick<ResponseToolkit, 'redirect' | 'view'>
156
+ ) => {
157
+ return Promise.resolve({} as unknown as ResponseObject)
158
+ }
159
+ )
160
+
161
+ const contextMock = { data: {}, model: {} } as unknown as FormContext
162
+
163
+ const requestMock = {
164
+ params: { path: 'some-path' },
165
+ app: {}
166
+ } as FormRequest
167
+
168
+ jest
169
+ .mocked(redirectOrMakeHandler)
170
+ .mockImplementation(
171
+ async (
172
+ _req: FormRequest | FormRequestPayload,
173
+ _h: Pick<ResponseToolkit, 'redirect' | 'view'>,
174
+ fn
175
+ ) => {
176
+ try {
177
+ await fn(pageMock, contextMock)
178
+ } catch (err) {
179
+ error = err
180
+ }
181
+
182
+ return Promise.resolve({} as unknown as ResponseObject)
183
+ }
184
+ )
185
+
186
+ await makeGetHandler()(requestMock, hMock)
187
+
188
+ expect(error).toEqual(Boom.notFound('No model found for /some-path'))
189
+ })
190
+ })
191
+
192
+ describe('makePostHandler', () => {
193
+ const hMock: Pick<ResponseToolkit, 'redirect' | 'view'> = {
194
+ redirect: jest.fn(),
195
+ view: jest.fn()
196
+ }
197
+
198
+ beforeEach(() => {
199
+ nock('http://test').post('/save').reply(200, {
200
+ wasPostCalled: true
201
+ })
202
+ })
203
+
204
+ afterEach(() => {
205
+ jest.mocked(redirectOrMakeHandler).mockRestore()
206
+ nock.cleanAll()
207
+ })
208
+
209
+ it('calls the callback when events.onSave.type is http and the page controller was successful', async () => {
210
+ const mockPostResponse: ResponseObject = {
211
+ statusCode: 200
212
+ } as ResponseObject
213
+
214
+ const modelMock = {
215
+ basePath: 'some-base-path',
216
+ def: { name: 'Hello world' }
217
+ } as FormModel
218
+
219
+ const pageMock = createMockPageController(
220
+ modelMock,
221
+ (
222
+ _request: FormRequest,
223
+ _context: FormContext,
224
+ _h: Pick<ResponseToolkit, 'redirect' | 'view'>
225
+ ) => {
226
+ // do return a valid ResponseObject wrapped in Promise.resolve
227
+ return mockPostResponse
228
+ }
229
+ )
230
+
231
+ const contextMock = { data: {}, model: {} } as unknown as FormContext
232
+
233
+ const requestMock = {
234
+ params: { path: 'some-path' },
235
+ app: { model: modelMock },
236
+ payload: { some: 'payload' }
237
+ } as unknown as FormRequestPayload
238
+
239
+ jest
240
+ .mocked(redirectOrMakeHandler)
241
+ .mockImplementation(
242
+ (
243
+ _req: FormRequest | FormRequestPayload,
244
+ _h: Pick<ResponseToolkit, 'redirect' | 'view'>,
245
+ fn
246
+ ) => Promise.resolve(fn(pageMock, contextMock))
247
+ )
248
+
249
+ const response = await makePostHandler()(requestMock, hMock)
250
+
251
+ expect(nock.pendingMocks()).toBeEmpty()
252
+ expect(response).toBe(mockPostResponse)
253
+ })
254
+
255
+ it('does not call the callback when the events.onSave.type is not http', async () => {
256
+ const modelMock = {
257
+ basePath: 'some-base-path',
258
+ def: { name: 'Hello world' }
259
+ } as FormModel
260
+
261
+ const pageMock = createMockPageController(
262
+ modelMock,
263
+ (
264
+ _request: FormRequest,
265
+ _context: FormContext,
266
+ _h: Pick<ResponseToolkit, 'redirect' | 'view'>
267
+ ) => {
268
+ return Promise.resolve({} as unknown as ResponseObject)
269
+ }
270
+ )
271
+
272
+ pageMock.events = {}
273
+
274
+ const contextMock = { data: {}, model: {} } as unknown as FormContext
275
+
276
+ const requestMock = {
277
+ params: { path: 'some-path' },
278
+ app: { model: modelMock },
279
+ payload: { some: 'payload' }
280
+ } as unknown as FormRequestPayload
281
+
282
+ jest
283
+ .mocked(redirectOrMakeHandler)
284
+ .mockImplementation(
285
+ (
286
+ _req: FormRequest | FormRequestPayload,
287
+ _h: Pick<ResponseToolkit, 'redirect' | 'view'>,
288
+ fn
289
+ ) => Promise.resolve(fn(pageMock, contextMock))
290
+ )
291
+
292
+ await makePostHandler()(requestMock, hMock)
293
+
294
+ expect(nock.pendingMocks()).not.toBeEmpty()
295
+ })
296
+
297
+ it('does not call the callback when events.onSave.type is http and the page controller was unsuccessful', async () => {
298
+ const mockPostResponse: ResponseObject = {
299
+ statusCode: 500
300
+ } as ResponseObject
301
+
302
+ const modelMock = {
303
+ basePath: 'some-base-path',
304
+ def: { name: 'Hello world' }
305
+ } as FormModel
306
+
307
+ const pageMock = createMockPageController(
308
+ modelMock,
309
+ (
310
+ _request: FormRequest,
311
+ _context: FormContext,
312
+ _h: Pick<ResponseToolkit, 'redirect' | 'view'>
313
+ ) => {
314
+ // do return a valid ResponseObject wrapped in Promise.resolve
315
+ return mockPostResponse
316
+ }
317
+ )
318
+
319
+ const contextMock = { data: {}, model: {} } as unknown as FormContext
320
+
321
+ const requestMock = {
322
+ params: { path: 'some-path' },
323
+ app: { model: modelMock },
324
+ payload: { some: 'payload' }
325
+ } as unknown as FormRequestPayload
326
+
327
+ jest
328
+ .mocked(redirectOrMakeHandler)
329
+ .mockImplementation(
330
+ (
331
+ _req: FormRequest | FormRequestPayload,
332
+ _h: Pick<ResponseToolkit, 'redirect' | 'view'>,
333
+ fn
334
+ ) => Promise.resolve(fn(pageMock, contextMock))
335
+ )
336
+
337
+ await makePostHandler()(requestMock, hMock)
338
+
339
+ expect(nock.pendingMocks()).not.toBeEmpty()
340
+ })
341
+
342
+ it('throws when model is missing', async () => {
343
+ let error
344
+
345
+ const modelMock = {
346
+ basePath: 'some-base-path',
347
+ def: { name: 'Hello world' }
348
+ } as FormModel
349
+
350
+ const pageMock = createMockPageController(
351
+ modelMock,
352
+ (
353
+ _request: FormRequest,
354
+ _context: FormContext,
355
+ _h: Pick<ResponseToolkit, 'redirect' | 'view'>
356
+ ) => {
357
+ return Promise.resolve({} as unknown as ResponseObject)
358
+ }
359
+ )
360
+
361
+ const contextMock = { data: {}, model: {} } as unknown as FormContext
362
+
363
+ const requestMock = {
364
+ params: { path: 'some-path' },
365
+ app: {},
366
+ payload: { some: 'payload' }
367
+ } as unknown as FormRequestPayload
368
+
369
+ jest
370
+ .mocked(redirectOrMakeHandler)
371
+ .mockImplementation(
372
+ async (
373
+ _req: FormRequest | FormRequestPayload,
374
+ _h: Pick<ResponseToolkit, 'redirect' | 'view'>,
375
+ fn
376
+ ) => {
377
+ try {
378
+ await fn(pageMock, contextMock)
379
+ } catch (err) {
380
+ error = err
381
+ }
382
+
383
+ return Promise.resolve({} as unknown as ResponseObject)
384
+ }
385
+ )
386
+
387
+ await makePostHandler()(requestMock, hMock)
388
+
389
+ expect(error).toEqual(Boom.notFound('No model found for /some-path'))
390
+ })
391
+ })
392
+
393
+ function createMockPageController(
394
+ model: FormModel,
395
+ routeHandler: (
396
+ request: FormRequest,
397
+ context: FormContext,
398
+ h: Pick<ResponseToolkit, 'redirect' | 'view'>
399
+ ) => ResponseObject | Promise<ResponseObject>
400
+ ): PageControllerClass {
401
+ return {
402
+ model,
403
+ events: {
404
+ onLoad: {
405
+ type: 'http',
406
+ options: { method: 'POST', url: 'http://test/load' }
407
+ },
408
+ onSave: {
409
+ type: 'http',
410
+ options: { method: 'POST', url: 'http://test/save' }
411
+ }
412
+ },
413
+ makeGetRouteHandler: () => routeHandler,
414
+ makePostRouteHandler: () => routeHandler
415
+ } as unknown as PageControllerClass
416
+ }