@defra/forms-engine-plugin 0.1.26 → 0.1.28

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 (95) hide show
  1. package/.public/assets/images/govuk-crest.svg +1 -1
  2. package/.public/assets/rebrand/images/favicon.ico +0 -0
  3. package/.public/assets/rebrand/images/favicon.svg +1 -0
  4. package/.public/assets/rebrand/images/govuk-crest.svg +1 -0
  5. package/.public/assets/rebrand/images/govuk-icon-180.png +0 -0
  6. package/.public/assets/rebrand/images/govuk-icon-192.png +0 -0
  7. package/.public/assets/rebrand/images/govuk-icon-512.png +0 -0
  8. package/.public/assets/rebrand/images/govuk-icon-mask.svg +1 -0
  9. package/.public/assets/rebrand/images/govuk-opengraph-image.png +0 -0
  10. package/.public/assets/rebrand/manifest.json +39 -0
  11. package/.public/assets-manifest.json +9 -0
  12. package/.public/javascripts/application.min.js +1 -1
  13. package/.public/javascripts/application.min.js.LICENSE.txt +5 -1
  14. package/.public/javascripts/application.min.js.map +1 -1
  15. package/.public/javascripts/shared.min.js +1 -1
  16. package/.public/javascripts/shared.min.js.LICENSE.txt +5 -1
  17. package/.public/javascripts/shared.min.js.map +1 -1
  18. package/.public/stylesheets/application.min.css +2 -2
  19. package/.public/stylesheets/application.min.css.map +1 -1
  20. package/.server/index.js +6 -2
  21. package/.server/index.js.map +1 -1
  22. package/.server/server/common/helpers/logging/request-tracing.js +1 -1
  23. package/.server/server/common/helpers/logging/request-tracing.js.map +1 -1
  24. package/.server/server/common/helpers/redis-client.js +5 -3
  25. package/.server/server/common/helpers/redis-client.js.map +1 -1
  26. package/.server/server/constants.d.ts +0 -1
  27. package/.server/server/constants.js +0 -1
  28. package/.server/server/constants.js.map +1 -1
  29. package/.server/server/index.js +3 -1
  30. package/.server/server/index.js.map +1 -1
  31. package/.server/server/plugins/engine/components/DatePartsField.d.ts +1 -6
  32. package/.server/server/plugins/engine/components/DatePartsField.js +2 -1
  33. package/.server/server/plugins/engine/components/DatePartsField.js.map +1 -1
  34. package/.server/server/plugins/engine/components/MonthYearField.d.ts +1 -5
  35. package/.server/server/plugins/engine/components/MonthYearField.js +3 -2
  36. package/.server/server/plugins/engine/components/MonthYearField.js.map +1 -1
  37. package/.server/server/plugins/engine/components/YesNoField.js +2 -1
  38. package/.server/server/plugins/engine/components/YesNoField.js.map +1 -1
  39. package/.server/server/plugins/engine/components/types.d.ts +9 -0
  40. package/.server/server/plugins/engine/components/types.js.map +1 -1
  41. package/.server/server/plugins/engine/date-helper.d.ts +12 -0
  42. package/.server/server/plugins/engine/date-helper.js +21 -0
  43. package/.server/server/plugins/engine/date-helper.js.map +1 -0
  44. package/.server/server/plugins/engine/helpers.js +4 -3
  45. package/.server/server/plugins/engine/helpers.js.map +1 -1
  46. package/.server/server/plugins/engine/index.js +3 -2
  47. package/.server/server/plugins/engine/index.js.map +1 -1
  48. package/.server/server/plugins/engine/models/FormModel.d.ts +13 -6
  49. package/.server/server/plugins/engine/models/FormModel.js +51 -18
  50. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  51. package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
  52. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +3 -2
  53. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  54. package/.server/server/plugins/engine/plugin.js +6 -5
  55. package/.server/server/plugins/engine/plugin.js.map +1 -1
  56. package/.server/server/plugins/engine/services/notifyService.js +3 -1
  57. package/.server/server/plugins/engine/services/notifyService.js.map +1 -1
  58. package/.server/server/plugins/engine/views/components/tag-env/template.njk +5 -24
  59. package/.server/server/plugins/engine/views/components/tag-env/template.test.js +19 -53
  60. package/.server/server/plugins/engine/views/components/tag-env/template.test.js.map +1 -1
  61. package/.server/server/plugins/errorPages.js +1 -1
  62. package/.server/server/plugins/errorPages.js.map +1 -1
  63. package/.server/server/plugins/nunjucks/context.js +1 -1
  64. package/.server/server/plugins/nunjucks/context.js.map +1 -1
  65. package/.server/server/plugins/nunjucks/environment.d.ts +1 -0
  66. package/.server/server/plugins/nunjucks/environment.js +4 -0
  67. package/.server/server/plugins/nunjucks/environment.js.map +1 -1
  68. package/package.json +18 -18
  69. package/src/index.ts +7 -2
  70. package/src/server/common/helpers/logging/request-tracing.js +1 -1
  71. package/src/server/common/helpers/redis-client.js +5 -3
  72. package/src/server/constants.js +0 -1
  73. package/src/server/index.ts +5 -2
  74. package/src/server/plugins/engine/components/DatePartsField.test.ts +17 -0
  75. package/src/server/plugins/engine/components/DatePartsField.ts +7 -8
  76. package/src/server/plugins/engine/components/MonthYearField.test.ts +15 -0
  77. package/src/server/plugins/engine/components/MonthYearField.ts +12 -8
  78. package/src/server/plugins/engine/components/YesNoField.ts +16 -2
  79. package/src/server/plugins/engine/components/types.ts +11 -0
  80. package/src/server/plugins/engine/date-helper.test.ts +47 -0
  81. package/src/server/plugins/engine/date-helper.ts +32 -0
  82. package/src/server/plugins/engine/helpers.ts +9 -2
  83. package/src/server/plugins/engine/index.ts +4 -2
  84. package/src/server/plugins/engine/models/FormModel.test.ts +163 -1
  85. package/src/server/plugins/engine/models/FormModel.ts +90 -23
  86. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +4 -2
  87. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +2 -1
  88. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +6 -2
  89. package/src/server/plugins/engine/plugin.ts +11 -7
  90. package/src/server/plugins/engine/services/notifyService.ts +6 -2
  91. package/src/server/plugins/engine/views/components/tag-env/template.njk +5 -24
  92. package/src/server/plugins/engine/views/components/tag-env/template.test.js +18 -56
  93. package/src/server/plugins/errorPages.ts +5 -1
  94. package/src/server/plugins/nunjucks/context.js +3 -1
  95. package/src/server/plugins/nunjucks/environment.js +6 -0
@@ -1,13 +1,27 @@
1
+ import {
2
+ SchemaVersion,
3
+ formDefinitionV2Schema,
4
+ type FormDefinition
5
+ } from '@defra/forms-model'
6
+
7
+ import { todayAsDateOnly } from '~/src/server/plugins/engine/date-helper.js'
1
8
  import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
2
9
  import { type FormContextRequest } from '~/src/server/plugins/engine/types.js'
3
10
  import { V2 as definitionV2 } from '~/test/form/definitions/conditions-basic.js'
4
11
  import definition from '~/test/form/definitions/conditions-escaping.js'
5
12
  import conditionsListDefinition from '~/test/form/definitions/conditions-list.js'
13
+ import relativeDatesDefinition from '~/test/form/definitions/conditions-relative-dates-v2.js'
6
14
  import fieldsRequiredDefinition from '~/test/form/definitions/fields-required.js'
7
15
 
16
+ jest.mock('~/src/server/plugins/engine/date-helper.ts')
17
+
8
18
  describe('FormModel', () => {
19
+ beforeEach(() => {
20
+ jest.resetAllMocks()
21
+ })
22
+
9
23
  describe('Constructor', () => {
10
- it("doesn't throw when conditions are passed with apostrophes", () => {
24
+ it('loads a valid form definition', () => {
11
25
  expect(
12
26
  () => new FormModel(definition, { basePath: 'test' })
13
27
  ).not.toThrow()
@@ -26,6 +40,109 @@ describe('FormModel', () => {
26
40
  )
27
41
  expect(model.def.pages.at(1)?.title).toBe('Date of marriage')
28
42
  })
43
+
44
+ it('Gets a list by ID', () => {
45
+ jest.mock('@defra/forms-model')
46
+
47
+ const definitionWithLists: FormDefinition = {
48
+ ...definitionV2,
49
+ lists: [
50
+ {
51
+ id: 'c5eba145-b04d-4d41-a50c-e5e2f9b6357f',
52
+ type: 'string',
53
+ title: 'foo',
54
+ name: 'foo',
55
+ items: [
56
+ { text: 'a', value: 'a' },
57
+ { text: 'b', value: 'b' }
58
+ ]
59
+ },
60
+ {
61
+ type: 'string',
62
+ title: 'bar',
63
+ name: 'bar',
64
+ items: [
65
+ {
66
+ id: 'a85a42a8-3e08-4c2a-b263-a0dc0b8c49f6',
67
+ text: 'a',
68
+ value: 'a'
69
+ },
70
+ {
71
+ id: 'c31664ac-887b-434b-b9f4-e5bc30d24439',
72
+ text: 'b',
73
+ value: 'b'
74
+ }
75
+ ]
76
+ }
77
+ ]
78
+ }
79
+
80
+ formDefinitionV2Schema.validate = jest
81
+ .fn()
82
+ .mockReturnValue({ value: definitionWithLists })
83
+
84
+ const model = new FormModel(definitionWithLists, { basePath: 'test' })
85
+
86
+ expect(
87
+ model.getListById('c5eba145-b04d-4d41-a50c-e5e2f9b6357f')
88
+ ).toBeDefined()
89
+ expect(model.listDefIdMap.size).toBe(2) // 1 + the yes/no list. list 'bar' isn't present as there's no ID
90
+ })
91
+
92
+ it('Gets a component by ID', () => {
93
+ jest.mock('@defra/forms-model')
94
+
95
+ formDefinitionV2Schema.validate = jest
96
+ .fn()
97
+ .mockReturnValue({ value: definitionV2 })
98
+
99
+ const model = new FormModel(definitionV2, { basePath: 'test' })
100
+
101
+ expect(
102
+ model.getComponentById('717eb213-4e4b-4a2d-9cfd-2780f5e1e3e5')
103
+ ).toBeDefined()
104
+ expect(model.listDefIdMap.size).toBe(1)
105
+ })
106
+
107
+ it('gets a condition by its ID', () => {
108
+ jest.mock('@defra/forms-model')
109
+ formDefinitionV2Schema.validate = jest
110
+ .fn()
111
+ .mockReturnValue({ value: definitionV2 })
112
+ const model = new FormModel(definitionV2, { basePath: 'test' })
113
+
114
+ expect(
115
+ model.getConditionById('6c9e2f4a-1d7b-5e8c-3f6a-9e2d5b7c4f1a')
116
+ ).toBeDefined()
117
+ })
118
+
119
+ it('throws an error if schema validation fails', () => {
120
+ jest.mock('@defra/forms-model')
121
+
122
+ formDefinitionV2Schema.validate = jest.fn().mockReturnValueOnce({
123
+ error: 'Validation error'
124
+ })
125
+
126
+ expect(() => new FormModel(definitionV2, { basePath: 'test' })).toThrow(
127
+ 'Validation error'
128
+ )
129
+ })
130
+
131
+ it('assigns v1 to the schema if not defined', () => {
132
+ const definitionWithoutSchema: FormDefinition = {
133
+ ...definition,
134
+ schema: undefined
135
+ }
136
+
137
+ // Mock validation to just return the definition
138
+ formDefinitionV2Schema.validate = jest
139
+ .fn()
140
+ .mockReturnValue({ value: definitionWithoutSchema })
141
+
142
+ const model = new FormModel(definitionWithoutSchema, { basePath: 'test' })
143
+
144
+ expect(model.schemaVersion).toBe(SchemaVersion.V1)
145
+ })
29
146
  })
30
147
 
31
148
  describe('getFormContext', () => {
@@ -184,4 +301,49 @@ describe('FormModel', () => {
184
301
  })
185
302
  })
186
303
  })
304
+
305
+ describe('makeCondition', () => {
306
+ test('relative date condition', () => {
307
+ jest.mock('@defra/forms-model')
308
+ formDefinitionV2Schema.validate = jest
309
+ .fn()
310
+ .mockReturnValue({ value: relativeDatesDefinition })
311
+ const model = new FormModel(relativeDatesDefinition, { basePath: 'test' })
312
+
313
+ const allConditionsKeys = Object.keys(model.conditions)
314
+ expect(allConditionsKeys).toHaveLength(8)
315
+
316
+ // Only test releative date conditions
317
+ const relativeConditionsKeys = allConditionsKeys.slice(4)
318
+ expect(relativeConditionsKeys).toHaveLength(4)
319
+
320
+ const formState = {
321
+ ybMHIv: '2023-06-18'
322
+ }
323
+
324
+ const expectedResultsDayBefore = [true, false, false, true]
325
+
326
+ const expectedResultsDayOf = [true, true, false, false]
327
+
328
+ const expectedResultsDayAfter = [false, true, true, false]
329
+
330
+ // Only relative date conditions
331
+ for (let i = 0; i < relativeConditionsKeys.length; i++) {
332
+ const condition = model.conditions[relativeConditionsKeys[i]]
333
+ jest.mocked(todayAsDateOnly).mockReturnValue(new Date(2025, 5, 19))
334
+ const conditionExec = model.makeCondition(
335
+ // @ts-expect-error - type doesnt need to match for this test
336
+ condition
337
+ )
338
+ formState.ybMHIv = '2023-06-18'
339
+ expect(conditionExec.fn(formState)).toBe(expectedResultsDayBefore[i])
340
+
341
+ formState.ybMHIv = '2023-06-19'
342
+ expect(conditionExec.fn(formState)).toBe(expectedResultsDayOf[i])
343
+
344
+ formState.ybMHIv = '2023-06-20'
345
+ expect(conditionExec.fn(formState)).toBe(expectedResultsDayAfter[i])
346
+ }
347
+ })
348
+ })
187
349
  })
@@ -4,26 +4,35 @@ import {
4
4
  ControllerPath,
5
5
  ControllerType,
6
6
  Engine,
7
+ SchemaVersion,
8
+ convertConditionWrapperFromV2,
7
9
  formDefinitionSchema,
10
+ formDefinitionV2Schema,
8
11
  hasComponents,
9
12
  hasRepeater,
13
+ isConditionWrapperV2,
14
+ yesNoListId,
15
+ yesNoListName,
10
16
  type ComponentDef,
11
17
  type ConditionWrapper,
18
+ type ConditionWrapperV2,
12
19
  type ConditionsModelData,
13
20
  type DateUnits,
14
21
  type FormDefinition,
15
22
  type List,
16
23
  type Page
17
24
  } from '@defra/forms-model'
18
- import { add } from 'date-fns'
25
+ import { add, format } from 'date-fns'
19
26
  import { Parser, type Value } from 'expr-eval'
20
27
  import joi from 'joi'
21
28
 
22
29
  import { type ListFormComponent } from '~/src/server/plugins/engine/components/ListFormComponent.js'
30
+ import {} from '~/src/server/plugins/engine/components/YesNoField.js'
23
31
  import {
24
32
  hasListFormField,
25
33
  type Component
26
34
  } from '~/src/server/plugins/engine/components/helpers.js'
35
+ import { todayAsDateOnly } from '~/src/server/plugins/engine/date-helper.js'
27
36
  import {
28
37
  findPage,
29
38
  getError,
@@ -53,6 +62,8 @@ export class FormModel {
53
62
  /** The runtime engine that should be used */
54
63
  engine?: Engine
55
64
 
65
+ schemaVersion: SchemaVersion
66
+
56
67
  /** the entire form JSON as an object */
57
68
  def: FormDefinition
58
69
 
@@ -67,8 +78,13 @@ export class FormModel {
67
78
 
68
79
  controllers?: Record<string, typeof PageController>
69
80
  pageDefMap: Map<string, Page>
81
+
70
82
  listDefMap: Map<string, List>
83
+ listDefIdMap: Map<string, List>
84
+
71
85
  componentDefMap: Map<string, ComponentDef>
86
+ componentDefIdMap: Map<string, ComponentDef>
87
+
72
88
  pageMap: Map<string, PageControllerClass>
73
89
  componentMap: Map<string, Component>
74
90
 
@@ -78,7 +94,13 @@ export class FormModel {
78
94
  services: Services = defaultServices,
79
95
  controllers?: Record<string, typeof PageController>
80
96
  ) {
81
- const result = formDefinitionSchema.validate(def, { abortEarly: false })
97
+ let schema = formDefinitionSchema
98
+
99
+ if (def.schema === SchemaVersion.V2) {
100
+ schema = formDefinitionV2Schema
101
+ }
102
+
103
+ const result = schema.validate(def, { abortEarly: false })
82
104
 
83
105
  if (result.error) {
84
106
  throw result.error
@@ -90,15 +112,18 @@ export class FormModel {
90
112
 
91
113
  // Add default lists
92
114
  def.lists.push({
115
+ id: def.schema === SchemaVersion.V1 ? yesNoListName : yesNoListId,
93
116
  name: '__yesNo',
94
117
  title: 'Yes/No',
95
118
  type: 'boolean',
96
119
  items: [
97
120
  {
121
+ id: '02900d42-83d1-4c72-a719-c4e8228952fa',
98
122
  text: 'Yes',
99
123
  value: true
100
124
  },
101
125
  {
126
+ id: 'f39000eb-c51b-4019-8f82-bbda0423f04d',
102
127
  text: 'No',
103
128
  value: false
104
129
  }
@@ -109,6 +134,7 @@ export class FormModel {
109
134
  setPageTitles(def)
110
135
 
111
136
  this.engine = def.engine
137
+ this.schemaVersion = def.schema ?? SchemaVersion.V1
112
138
  this.def = def
113
139
  this.lists = def.lists
114
140
  this.sections = def.sections
@@ -119,8 +145,38 @@ export class FormModel {
119
145
  this.services = services
120
146
  this.controllers = controllers
121
147
 
148
+ this.pageDefMap = new Map(def.pages.map((page) => [page.path, page]))
149
+ this.listDefMap = new Map(def.lists.map((list) => [list.name, list]))
150
+ this.listDefIdMap = new Map(
151
+ def.lists
152
+ .filter((list) => list.id) // Skip lists without an ID
153
+ // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
154
+ .map((list) => [list.id as string, list])
155
+ )
156
+ this.componentDefMap = new Map(
157
+ def.pages
158
+ .filter(hasComponents)
159
+ .flatMap((page) =>
160
+ page.components.map((component) => [component.name, component])
161
+ )
162
+ )
163
+ this.componentDefIdMap = new Map(
164
+ def.pages.filter(hasComponents).flatMap((page) =>
165
+ page.components
166
+ .filter((component) => component.id) // Skip components without an ID
167
+ .map((component) => {
168
+ // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
169
+ return [component.id as string, component]
170
+ })
171
+ )
172
+ )
173
+
122
174
  def.conditions.forEach((conditionDef) => {
123
- const condition = this.makeCondition(conditionDef)
175
+ const condition = this.makeCondition(
176
+ isConditionWrapperV2(conditionDef)
177
+ ? convertConditionWrapperFromV2(conditionDef, this)
178
+ : conditionDef
179
+ )
124
180
  this.conditions[condition.name] = condition
125
181
  })
126
182
 
@@ -142,16 +198,6 @@ export class FormModel {
142
198
  )
143
199
  }
144
200
 
145
- this.pageDefMap = new Map(def.pages.map((page) => [page.path, page]))
146
- this.listDefMap = new Map(def.lists.map((list) => [list.name, list]))
147
- this.componentDefMap = new Map(
148
- def.pages
149
- .filter(hasComponents)
150
- .flatMap((page) =>
151
- page.components.map((component) => [component.name, component])
152
- )
153
- )
154
-
155
201
  this.pageMap = new Map(this.pages.map((page) => [page.path, page]))
156
202
  this.componentMap = new Map(
157
203
  this.pages.flatMap((page) =>
@@ -163,13 +209,6 @@ export class FormModel {
163
209
  )
164
210
  }
165
211
 
166
- /**
167
- * build the entire model schema from individual pages/sections
168
- */
169
- makeSchema() {
170
- return this.makeFilteredSchema(this.pages)
171
- }
172
-
173
212
  /**
174
213
  * build the entire model schema from individual pages/sections and filter out answers
175
214
  * for pages which are no longer accessible due to an answer that has been changed
@@ -199,7 +238,14 @@ export class FormModel {
199
238
 
200
239
  Object.assign(parser.functions, {
201
240
  dateForComparison(timePeriod: number, timeUnit: DateUnits) {
202
- return add(new Date(), { [timeUnit]: timePeriod }).toISOString()
241
+ // The time element must be stripped (hence using startOfDay() which has no time element),
242
+ // then formatted as YYYY-MM-DD otherwise we can hit time element and BST issues giving the
243
+ // wrong date to compare against.
244
+ // Do not use .toISOString() to format the date as that introduces BST errors.
245
+ return format(
246
+ add(todayAsDateOnly(), { [timeUnit]: timePeriod }),
247
+ 'yyyy-MM-dd'
248
+ )
203
249
  }
204
250
  })
205
251
 
@@ -246,8 +292,10 @@ export class FormModel {
246
292
  return parser.parse(conditions.toExpression())
247
293
  }
248
294
 
249
- getList(name: string): List | undefined {
250
- return this.lists.find((list) => list.name === name)
295
+ getList(nameOrId: string): List | undefined {
296
+ return this.schemaVersion === SchemaVersion.V1
297
+ ? this.lists.find((list) => list.name === nameOrId)
298
+ : this.lists.find((list) => list.id === nameOrId)
251
299
  }
252
300
 
253
301
  /**
@@ -448,6 +496,25 @@ export class FormModel {
448
496
  }
449
497
  }
450
498
  }
499
+
500
+ getComponentById(componentId: string): ComponentDef | undefined {
501
+ return this.componentDefIdMap.get(componentId)
502
+ }
503
+
504
+ getListById(listId: string): List | undefined {
505
+ return this.listDefIdMap.get(listId)
506
+ }
507
+
508
+ /**
509
+ * Returns a condition by its ID. O(n) lookup time.
510
+ * @param conditionId
511
+ * @returns
512
+ */
513
+ getConditionById(conditionId: string): ConditionWrapperV2 | undefined {
514
+ return this.def.conditions
515
+ .filter(isConditionWrapperV2)
516
+ .find((condition) => condition.id === conditionId)
517
+ }
451
518
  }
452
519
 
453
520
  /**
@@ -1,10 +1,12 @@
1
1
  import { type SubmitResponsePayload } from '@defra/forms-model'
2
2
 
3
3
  import { config } from '~/src/config/index.js'
4
- import { type DatePartsState } from '~/src/server/plugins/engine/components/DatePartsField.js'
5
- import { type MonthYearState } from '~/src/server/plugins/engine/components/MonthYearField.js'
6
4
  import { type UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js'
7
5
  import { FileUploadField } from '~/src/server/plugins/engine/components/index.js'
6
+ import {
7
+ type DatePartsState,
8
+ type MonthYearState
9
+ } from '~/src/server/plugins/engine/components/types.js'
8
10
  import { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js'
9
11
  import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
10
12
  import {
@@ -253,8 +253,9 @@ describe('FileUploadPageController', () => {
253
253
  )
254
254
 
255
255
  expect(request.logger.error).toHaveBeenCalledWith(
256
+ expect.any(Error),
256
257
  expect.stringContaining(
257
- 'Exceeded cumulative retry delay for some-id (depth: 7). Re-initiating a new upload.'
258
+ '[uploadTimeout] Exceeded cumulative retry delay for uploadId: some-id at depth: 7 - re-initiating new upload'
258
259
  )
259
260
  )
260
261
 
@@ -325,9 +325,13 @@ export class FileUploadPageController extends QuestionPageController {
325
325
  // Depth 1: 2000ms, Depth 2: 4000ms, Depth 3: 8000ms, Depth 4: 16000ms, Depth 5+: 30000ms (capped)
326
326
  // A depth of 5 (or more) implies cumulative delays roughly reaching 55 seconds.
327
327
  if (depth >= 5) {
328
- request.logger.error(
328
+ const error = new Error(
329
329
  `Exceeded cumulative retry delay for ${uploadId} (depth: ${depth}). Re-initiating a new upload.`
330
330
  )
331
+ request.logger.error(
332
+ error,
333
+ `[uploadTimeout] Exceeded cumulative retry delay for uploadId: ${uploadId} at depth: ${depth} - re-initiating new upload`
334
+ )
331
335
  await this.initiateAndStoreNewUpload(request, state)
332
336
  throw Boom.gatewayTimeout(
333
337
  `Timed out waiting for ${uploadId} after cumulative retries exceeding ${((CDP_UPLOAD_TIMEOUT_MS - 5000) / 1000).toFixed(0)} seconds`
@@ -335,7 +339,7 @@ export class FileUploadPageController extends QuestionPageController {
335
339
  }
336
340
  const delay = getExponentialBackoffDelay(depth)
337
341
  request.logger.info(
338
- `Waiting ${delay / 1000} seconds for ${uploadId} to complete (depth: ${depth})`
342
+ `[uploadRetry] Waiting ${delay / 1000} seconds for uploadId: ${uploadId} to complete (retry depth: ${depth})`
339
343
  )
340
344
  await wait(delay)
341
345
  return this.checkUploadStatus(request, state, depth + 1)
@@ -2,7 +2,11 @@ import { existsSync } from 'fs'
2
2
  import { dirname, join } from 'path'
3
3
  import { fileURLToPath } from 'url'
4
4
 
5
- import { hasFormComponents, slugSchema } from '@defra/forms-model'
5
+ import {
6
+ getErrorMessage,
7
+ hasFormComponents,
8
+ slugSchema
9
+ } from '@defra/forms-model'
6
10
  import Boom from '@hapi/boom'
7
11
  import {
8
12
  type Plugin,
@@ -773,10 +777,10 @@ export const plugin = {
773
777
  request: FormRequest,
774
778
  h: Pick<ResponseToolkit, 'response'>
775
779
  ) => {
780
+ const { uploadId } = request.params as unknown as {
781
+ uploadId: string
782
+ }
776
783
  try {
777
- const { uploadId } = request.params as unknown as {
778
- uploadId: string
779
- }
780
784
  const status = await getUploadStatus(uploadId)
781
785
 
782
786
  if (!status) {
@@ -785,10 +789,10 @@ export const plugin = {
785
789
 
786
790
  return h.response(status)
787
791
  } catch (error) {
792
+ const errMsg = getErrorMessage(error)
788
793
  request.logger.error(
789
- ['upload-status'],
790
- 'Upload status check failed',
791
- error
794
+ errMsg,
795
+ `[uploadStatusFailed] Upload status check failed for uploadId: ${uploadId} - ${errMsg}`
792
796
  )
793
797
  return h.response({ error: 'Status check error' }).code(500)
794
798
  }
@@ -1,4 +1,4 @@
1
- import { type SubmitResponsePayload } from '@defra/forms-model'
1
+ import { getErrorMessage, type SubmitResponsePayload } from '@defra/forms-model'
2
2
 
3
3
  import { config } from '~/src/config/index.js'
4
4
  import { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers.js'
@@ -56,7 +56,11 @@ export async function submit(
56
56
 
57
57
  request.logger.info(logTags, 'Email sent successfully')
58
58
  } catch (err) {
59
- request.logger.error(logTags, 'Error sending email', err)
59
+ const errMsg = getErrorMessage(err)
60
+ request.logger.error(
61
+ errMsg,
62
+ `[emailSendFailed] Error sending notification email - templateId: ${templateId} - recipient: ${emailAddress} - ${errMsg}`
63
+ )
60
64
 
61
65
  throw err
62
66
  }
@@ -1,30 +1,11 @@
1
1
  {%- from "govuk/components/tag/macro.njk" import govukTag -%}
2
2
 
3
- {%- switch params.env %}
4
- {% case "local" %}
5
- {% set text = "Local" %}
6
- {% set classes = "govuk-tag--green" %}
7
- {% case "dev" %}
8
- {% set text = "Development" %}
9
- {% set classes = "govuk-tag--grey" %}
10
- {% case "test" %}
11
- {% set text = "Test" %}
12
- {% set classes = "govuk-tag--yellow" %}
13
- {% case "ext-test" %}
14
- {% set text = "External test" %}
15
- {% set classes = "govuk-tag--yellow" %}
16
- {% case "perf-test" %}
17
- {% set text = "Performance test" %}
18
- {% set classes = "govuk-tag--yellow" %}
19
- {% case "prod" %}
20
- {% set text = "Production" %}
21
- {% set classes = "govuk-tag--red" %}
22
- {% default %}
23
- {% set text = params.env | replace("-", " ") | capitalize %}
24
- {% set classes = "govuk-tag--grey" %}
25
- {% endswitch -%}
3
+ {% set tagEnv = " app-tag--env" %}
4
+
5
+ {% set text = params.env | replace("-", " ") | capitalize %}
6
+ {% set classes = "govuk-tag--grey" %}
26
7
 
27
8
  {{ govukTag({
28
9
  text: text,
29
- classes: classes + " app-tag--env"
10
+ classes: classes + tagEnv
30
11
  }) }}
@@ -1,66 +1,28 @@
1
1
  import { renderMacro } from '~/test/helpers/component-helpers.js'
2
2
 
3
3
  describe('Tag environment component', () => {
4
- describe.each([
5
- {
6
- text: 'Local',
7
- env: 'local',
8
- colour: 'green'
9
- },
10
- {
11
- text: 'Development',
12
- env: 'dev',
13
- colour: 'grey'
14
- },
15
- {
16
- text: 'Test',
17
- env: 'test',
18
- colour: 'yellow'
19
- },
20
- {
21
- text: 'External test',
22
- env: 'ext-test',
23
- colour: 'yellow'
24
- },
25
- {
26
- text: 'Performance test',
27
- env: 'perf-test',
28
- colour: 'yellow'
29
- },
30
- {
31
- text: 'Production',
32
- env: 'prod',
33
- colour: 'red'
34
- },
35
- {
36
- text: 'Unknown environment',
37
- env: 'unknown-environment',
38
- colour: 'grey'
39
- }
40
- ])('Environment: $text', ({ text, env, colour }) => {
41
- let $component = /** @type {HTMLElement | null} */ (null)
4
+ let $component = /** @type {HTMLElement | null} */ (null)
42
5
 
43
- beforeEach(() => {
44
- const { container } = renderMacro(
45
- 'appTagEnv',
46
- 'components/tag-env/macro.njk',
47
- { params: { env } }
48
- )
6
+ beforeEach(() => {
7
+ const { container } = renderMacro(
8
+ 'appTagEnv',
9
+ 'components/tag-env/macro.njk',
10
+ { params: { env: 'Devtool' } }
11
+ )
49
12
 
50
- $component = container.getByRole('strong')
51
- })
13
+ $component = container.getByRole('strong')
14
+ })
52
15
 
53
- it('should render contents', () => {
54
- expect($component).toBeInTheDocument()
55
- expect($component).toHaveClass('govuk-tag')
56
- })
16
+ it('should render contents', () => {
17
+ expect($component).toBeInTheDocument()
18
+ expect($component).toHaveClass('govuk-tag')
19
+ })
57
20
 
58
- it('should have text content', () => {
59
- expect($component).toHaveTextContent(text)
60
- })
21
+ it('should have text content', () => {
22
+ expect($component).toHaveTextContent('Devtool')
23
+ })
61
24
 
62
- it('should use environment colour', () => {
63
- expect($component).toHaveClass(`govuk-tag--${colour}`)
64
- })
25
+ it('should use environment colour', () => {
26
+ expect($component).toHaveClass(`govuk-tag--grey`)
65
27
  })
66
28
  })
@@ -25,10 +25,14 @@ export default {
25
25
  stack: response.stack
26
26
  }
27
27
 
28
- request.log('error', error)
28
+ request.logger.error(
29
+ error,
30
+ `[httpError] HTTP ${statusCode} error occurred - ${response.message} - path: ${request.path} - method: ${request.method}`
31
+ )
29
32
 
30
33
  return h.response(error).code(statusCode)
31
34
  }
35
+
32
36
  return h.continue
33
37
  })
34
38
  }
@@ -73,7 +73,9 @@ export function devtoolContext(_request) {
73
73
  // eslint-disable-next-line -- Allow JSON type 'any'
74
74
  webpackManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))
75
75
  } catch {
76
- logger.error(`Webpack ${basename(manifestPath)} not found`)
76
+ logger.info(
77
+ `[webpackManifestMissing] Webpack ${basename(manifestPath)} not found - running without asset manifest`
78
+ )
77
79
  }
78
80
  }
79
81
 
@@ -109,6 +109,12 @@ export function evaluate(template) {
109
109
 
110
110
  environment.addGlobal('evaluate', evaluate)
111
111
 
112
+ export function govukRebrand() {
113
+ return true
114
+ }
115
+
116
+ environment.addGlobal('govukRebrand', govukRebrand())
117
+
112
118
  /**
113
119
  * @import { NunjucksContext } from '~/src/server/plugins/nunjucks/types.js'
114
120
  * @import { FormSubmissionError } from '~/src/server/plugins/engine/types.js'