@defra/forms-engine-plugin 4.0.6 → 4.0.8

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 (112) hide show
  1. package/.public/stylesheets/application.min.css +1 -1
  2. package/.public/stylesheets/application.min.css.map +1 -1
  3. package/.server/client/stylesheets/_location-input.scss +60 -0
  4. package/.server/client/stylesheets/application.scss +1 -0
  5. package/.server/client/stylesheets/shared.scss +1 -0
  6. package/.server/server/forms/components.json +7 -0
  7. package/.server/server/forms/register-as-a-unicorn-breeder.yaml +40 -1
  8. package/.server/server/plugins/engine/components/ComponentBase.d.ts +2 -2
  9. package/.server/server/plugins/engine/components/ComponentBase.js.map +1 -1
  10. package/.server/server/plugins/engine/components/ComponentCollection.js.map +1 -1
  11. package/.server/server/plugins/engine/components/DeclarationField.d.ts +81 -0
  12. package/.server/server/plugins/engine/components/DeclarationField.js +123 -0
  13. package/.server/server/plugins/engine/components/DeclarationField.js.map +1 -0
  14. package/.server/server/plugins/engine/components/EastingNorthingField.d.ts +121 -0
  15. package/.server/server/plugins/engine/components/EastingNorthingField.js +166 -0
  16. package/.server/server/plugins/engine/components/EastingNorthingField.js.map +1 -0
  17. package/.server/server/plugins/engine/components/LatLongField.d.ts +121 -0
  18. package/.server/server/plugins/engine/components/LatLongField.js +164 -0
  19. package/.server/server/plugins/engine/components/LatLongField.js.map +1 -0
  20. package/.server/server/plugins/engine/components/LocationFieldBase.d.ts +134 -0
  21. package/.server/server/plugins/engine/components/LocationFieldBase.js +85 -0
  22. package/.server/server/plugins/engine/components/LocationFieldBase.js.map +1 -0
  23. package/.server/server/plugins/engine/components/LocationFieldHelpers.d.ts +108 -0
  24. package/.server/server/plugins/engine/components/LocationFieldHelpers.js +96 -0
  25. package/.server/server/plugins/engine/components/LocationFieldHelpers.js.map +1 -0
  26. package/.server/server/plugins/engine/components/NationalGridFieldNumberField.d.ts +19 -0
  27. package/.server/server/plugins/engine/components/NationalGridFieldNumberField.js +40 -0
  28. package/.server/server/plugins/engine/components/NationalGridFieldNumberField.js.map +1 -0
  29. package/.server/server/plugins/engine/components/OsGridRefField.d.ts +19 -0
  30. package/.server/server/plugins/engine/components/OsGridRefField.js +56 -0
  31. package/.server/server/plugins/engine/components/OsGridRefField.js.map +1 -0
  32. package/.server/server/plugins/engine/components/helpers/components.d.ts +3 -4
  33. package/.server/server/plugins/engine/components/helpers/components.js +24 -29
  34. package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
  35. package/.server/server/plugins/engine/components/index.d.ts +5 -0
  36. package/.server/server/plugins/engine/components/index.js +5 -0
  37. package/.server/server/plugins/engine/components/index.js.map +1 -1
  38. package/.server/server/plugins/engine/components/markdownParser.d.ts +2 -0
  39. package/.server/server/plugins/engine/components/markdownParser.js +28 -0
  40. package/.server/server/plugins/engine/components/markdownParser.js.map +1 -0
  41. package/.server/server/plugins/engine/components/types.d.ts +10 -0
  42. package/.server/server/plugins/engine/components/types.js.map +1 -1
  43. package/.server/server/plugins/engine/pageControllers/helpers/pages.js +7 -0
  44. package/.server/server/plugins/engine/pageControllers/helpers/pages.js.map +1 -1
  45. package/.server/server/plugins/engine/pageControllers/validationOptions.js +1 -0
  46. package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
  47. package/.server/server/plugins/engine/types/index.d.ts +1 -1
  48. package/.server/server/plugins/engine/types/index.js.map +1 -1
  49. package/.server/server/plugins/engine/types.d.ts +2 -2
  50. package/.server/server/plugins/engine/types.js.map +1 -1
  51. package/.server/server/plugins/engine/views/components/_location-field-base.html +53 -0
  52. package/.server/server/plugins/engine/views/components/declarationfield.html +14 -0
  53. package/.server/server/plugins/engine/views/components/eastingnorthingfield.html +5 -0
  54. package/.server/server/plugins/engine/views/components/latlongfield.html +5 -0
  55. package/.server/server/plugins/engine/views/components/nationalgridfieldnumberfield.html +13 -0
  56. package/.server/server/plugins/engine/views/components/osgridreffield.html +13 -0
  57. package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
  58. package/.server/server/plugins/nunjucks/filters/index.d.ts +1 -0
  59. package/.server/server/plugins/nunjucks/filters/index.js +1 -0
  60. package/.server/server/plugins/nunjucks/filters/index.js.map +1 -1
  61. package/.server/server/plugins/nunjucks/filters/merge.d.ts +7 -0
  62. package/.server/server/plugins/nunjucks/filters/merge.js +16 -0
  63. package/.server/server/plugins/nunjucks/filters/merge.js.map +1 -0
  64. package/.server/server/plugins/nunjucks/filters/merge.test.js +19 -0
  65. package/.server/server/plugins/nunjucks/filters/merge.test.js.map +1 -0
  66. package/package.json +3 -3
  67. package/src/client/stylesheets/_location-input.scss +60 -0
  68. package/src/client/stylesheets/application.scss +1 -0
  69. package/src/client/stylesheets/shared.scss +1 -0
  70. package/src/server/forms/components.json +7 -0
  71. package/src/server/forms/page-events.yaml +1 -1
  72. package/src/server/forms/register-as-a-unicorn-breeder.yaml +40 -1
  73. package/src/server/index.test.ts +1 -0
  74. package/src/server/plugins/engine/components/ComponentBase.ts +2 -1
  75. package/src/server/plugins/engine/components/ComponentCollection.ts +1 -0
  76. package/src/server/plugins/engine/components/DeclarationField.test.ts +426 -0
  77. package/src/server/plugins/engine/components/DeclarationField.ts +167 -0
  78. package/src/server/plugins/engine/components/EastingNorthingField.test.ts +665 -0
  79. package/src/server/plugins/engine/components/EastingNorthingField.ts +224 -0
  80. package/src/server/plugins/engine/components/LatLongField.test.ts +700 -0
  81. package/src/server/plugins/engine/components/LatLongField.ts +213 -0
  82. package/src/server/plugins/engine/components/LocationFieldBase.test.ts +253 -0
  83. package/src/server/plugins/engine/components/LocationFieldBase.ts +152 -0
  84. package/src/server/plugins/engine/components/LocationFieldHelpers.test.ts +338 -0
  85. package/src/server/plugins/engine/components/LocationFieldHelpers.ts +123 -0
  86. package/src/server/plugins/engine/components/NationalGridFieldNumberField.test.ts +438 -0
  87. package/src/server/plugins/engine/components/NationalGridFieldNumberField.ts +52 -0
  88. package/src/server/plugins/engine/components/OsGridRefField.test.ts +469 -0
  89. package/src/server/plugins/engine/components/OsGridRefField.ts +71 -0
  90. package/src/server/plugins/engine/components/helpers/components.test.ts +270 -0
  91. package/src/server/plugins/engine/components/helpers/components.ts +44 -47
  92. package/src/server/plugins/engine/components/helpers/helpers.test.ts +71 -1
  93. package/src/server/plugins/engine/components/index.ts +5 -0
  94. package/src/server/plugins/engine/components/markdownParser.ts +40 -0
  95. package/src/server/plugins/engine/components/types.ts +14 -0
  96. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +76 -3
  97. package/src/server/plugins/engine/models/SummaryViewModel.ts +5 -1
  98. package/src/server/plugins/engine/outputFormatters/adapter/v1.location.test.ts +356 -0
  99. package/src/server/plugins/engine/pageControllers/helpers/helpers.test.ts +4 -0
  100. package/src/server/plugins/engine/pageControllers/helpers/pages.ts +8 -0
  101. package/src/server/plugins/engine/pageControllers/validationOptions.ts +4 -0
  102. package/src/server/plugins/engine/types/index.ts +2 -0
  103. package/src/server/plugins/engine/types.ts +4 -0
  104. package/src/server/plugins/engine/views/components/_location-field-base.html +53 -0
  105. package/src/server/plugins/engine/views/components/declarationfield.html +14 -0
  106. package/src/server/plugins/engine/views/components/eastingnorthingfield.html +5 -0
  107. package/src/server/plugins/engine/views/components/latlongfield.html +5 -0
  108. package/src/server/plugins/engine/views/components/nationalgridfieldnumberfield.html +13 -0
  109. package/src/server/plugins/engine/views/components/osgridreffield.html +13 -0
  110. package/src/server/plugins/nunjucks/filters/index.js +1 -0
  111. package/src/server/plugins/nunjucks/filters/merge.js +16 -0
  112. package/src/server/plugins/nunjucks/filters/merge.test.js +15 -0
@@ -0,0 +1,270 @@
1
+ import {
2
+ ComponentType,
3
+ type EastingNorthingFieldComponent,
4
+ type LatLongFieldComponent,
5
+ type NationalGridFieldNumberFieldComponent,
6
+ type OsGridRefFieldComponent
7
+ } from '@defra/forms-model'
8
+
9
+ import {
10
+ getAnswer,
11
+ getAnswerMarkdown
12
+ } from '~/src/server/plugins/engine/components/helpers/components.js'
13
+ import {
14
+ EastingNorthingField,
15
+ LatLongField,
16
+ NationalGridFieldNumberField,
17
+ OsGridRefField
18
+ } from '~/src/server/plugins/engine/components/index.js'
19
+ import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
20
+ import definition from '~/test/form/definitions/blank.js'
21
+
22
+ describe('Location field formatting', () => {
23
+ let model: FormModel
24
+
25
+ beforeEach(() => {
26
+ model = new FormModel(definition, {
27
+ basePath: 'test'
28
+ })
29
+ })
30
+
31
+ describe('EastingNorthingField', () => {
32
+ let field: EastingNorthingField
33
+
34
+ beforeEach(() => {
35
+ const def: EastingNorthingFieldComponent = {
36
+ type: ComponentType.EastingNorthingField,
37
+ name: 'locationEN',
38
+ title: 'Location',
39
+ options: {}
40
+ }
41
+ field = new EastingNorthingField(def, { model })
42
+ })
43
+
44
+ it('formats for email output with labels on separate lines', () => {
45
+ const state = {
46
+ locationEN__easting: 123456,
47
+ locationEN__northing: 654321
48
+ }
49
+
50
+ const answer = getAnswer(field, state, { format: 'email' })
51
+ expect(answer).toBe('Northing: 654321\nEasting: 123456\n')
52
+ })
53
+
54
+ it('formats for data output', () => {
55
+ const state = {
56
+ locationEN__easting: 123456,
57
+ locationEN__northing: 654321
58
+ }
59
+
60
+ const answer = getAnswer(field, state, { format: 'data' })
61
+ expect(answer).toBe('Northing: 654321\nEasting: 123456')
62
+ })
63
+
64
+ it('formats for summary display', () => {
65
+ const state = {
66
+ locationEN__easting: 123456,
67
+ locationEN__northing: 654321
68
+ }
69
+
70
+ const answer = getAnswer(field, state, { format: 'summary' })
71
+ // Should render as HTML from markdown
72
+ expect(answer).toContain('Northing: 654321')
73
+ expect(answer).toContain('Easting: 123456')
74
+ })
75
+
76
+ it('returns empty string when no values', () => {
77
+ const state = {}
78
+
79
+ const answer = getAnswer(field, state, { format: 'email' })
80
+ expect(answer).toBe('')
81
+ })
82
+ })
83
+
84
+ describe('LatLongField', () => {
85
+ let field: LatLongField
86
+
87
+ beforeEach(() => {
88
+ const def: LatLongFieldComponent = {
89
+ type: ComponentType.LatLongField,
90
+ name: 'locationLL',
91
+ title: 'Coordinates',
92
+ options: {}
93
+ }
94
+ field = new LatLongField(def, { model })
95
+ })
96
+
97
+ it('formats for email output with labels on separate lines', () => {
98
+ const state = {
99
+ locationLL__latitude: 51.51945,
100
+ locationLL__longitude: -0.127758
101
+ }
102
+
103
+ const answer = getAnswer(field, state, { format: 'email' })
104
+ expect(answer).toBe('Lat: 51.51945\nLong: -0.127758\n')
105
+ })
106
+
107
+ it('formats for data output', () => {
108
+ const state = {
109
+ locationLL__latitude: 51.51945,
110
+ locationLL__longitude: -0.127758
111
+ }
112
+
113
+ const answer = getAnswer(field, state, { format: 'data' })
114
+ expect(answer).toBe('Lat: 51.51945\nLong: -0.127758')
115
+ })
116
+
117
+ it('formats for summary display', () => {
118
+ const state = {
119
+ locationLL__latitude: 51.51945,
120
+ locationLL__longitude: -0.127758
121
+ }
122
+
123
+ const answer = getAnswer(field, state, { format: 'summary' })
124
+ // Should render as HTML from markdown
125
+ expect(answer).toContain('Lat: 51.51945')
126
+ expect(answer).toContain('Long: -0.127758')
127
+ })
128
+
129
+ it('returns empty string when no values', () => {
130
+ const state = {}
131
+
132
+ const answer = getAnswer(field, state, { format: 'email' })
133
+ expect(answer).toBe('')
134
+ })
135
+ })
136
+
137
+ describe('OsGridRefField', () => {
138
+ let field: OsGridRefField
139
+
140
+ beforeEach(() => {
141
+ const def: OsGridRefFieldComponent = {
142
+ type: ComponentType.OsGridRefField,
143
+ name: 'gridRef',
144
+ title: 'OS Grid Reference',
145
+ options: {}
146
+ }
147
+ field = new OsGridRefField(def, { model })
148
+ })
149
+
150
+ it('formats for email output as single value', () => {
151
+ const state = {
152
+ gridRef: 'TQ123456'
153
+ }
154
+
155
+ const answer = getAnswer(field, state, { format: 'email' })
156
+ expect(answer).toBe('TQ123456\n')
157
+ })
158
+
159
+ it('formats for data output', () => {
160
+ const state = {
161
+ gridRef: 'TQ123456'
162
+ }
163
+
164
+ const answer = getAnswer(field, state, { format: 'data' })
165
+ expect(answer).toBe('TQ123456')
166
+ })
167
+
168
+ it('formats for summary display', () => {
169
+ const state = {
170
+ gridRef: 'TQ123456'
171
+ }
172
+
173
+ const answer = getAnswer(field, state, { format: 'summary' })
174
+ expect(answer).toBe('TQ123456')
175
+ })
176
+ })
177
+
178
+ describe('NationalGridFieldNumberField', () => {
179
+ let field: NationalGridFieldNumberField
180
+
181
+ beforeEach(() => {
182
+ const def: NationalGridFieldNumberFieldComponent = {
183
+ type: ComponentType.NationalGridFieldNumberField,
184
+ name: 'ngField',
185
+ title: 'National Grid Field Number',
186
+ options: {}
187
+ }
188
+ field = new NationalGridFieldNumberField(def, { model })
189
+ })
190
+
191
+ it('formats for email output as single value', () => {
192
+ const state = {
193
+ ngField: 'NG12345678'
194
+ }
195
+
196
+ const answer = getAnswer(field, state, { format: 'email' })
197
+ expect(answer).toBe('NG12345678\n')
198
+ })
199
+
200
+ it('formats for data output', () => {
201
+ const state = {
202
+ ngField: 'NG12345678'
203
+ }
204
+
205
+ const answer = getAnswer(field, state, { format: 'data' })
206
+ expect(answer).toBe('NG12345678')
207
+ })
208
+
209
+ it('formats for summary display', () => {
210
+ const state = {
211
+ ngField: 'NG12345678'
212
+ }
213
+
214
+ const answer = getAnswer(field, state, { format: 'summary' })
215
+ expect(answer).toBe('NG12345678')
216
+ })
217
+ })
218
+
219
+ describe('getAnswerMarkdown', () => {
220
+ it('formats EastingNorthingField correctly', () => {
221
+ const def: EastingNorthingFieldComponent = {
222
+ type: ComponentType.EastingNorthingField,
223
+ name: 'locationEN',
224
+ title: 'Location',
225
+ options: {}
226
+ }
227
+ const field = new EastingNorthingField(def, { model })
228
+ const state = {
229
+ locationEN__easting: 123456,
230
+ locationEN__northing: 654321
231
+ }
232
+
233
+ const answer = getAnswerMarkdown(field, state, { format: 'email' })
234
+ expect(answer).toBe('Northing: 654321\nEasting: 123456\n')
235
+ })
236
+
237
+ it('formats LatLongField correctly', () => {
238
+ const def: LatLongFieldComponent = {
239
+ type: ComponentType.LatLongField,
240
+ name: 'locationLL',
241
+ title: 'Coordinates',
242
+ options: {}
243
+ }
244
+ const field = new LatLongField(def, { model })
245
+ const state = {
246
+ locationLL__latitude: 51.51945,
247
+ locationLL__longitude: -0.127758
248
+ }
249
+
250
+ const answer = getAnswerMarkdown(field, state, { format: 'email' })
251
+ expect(answer).toBe('Lat: 51.51945\nLong: -0.127758\n')
252
+ })
253
+
254
+ it('formats simple location fields correctly', () => {
255
+ const def: OsGridRefFieldComponent = {
256
+ type: ComponentType.OsGridRefField,
257
+ name: 'gridRef',
258
+ title: 'OS Grid Reference',
259
+ options: {}
260
+ }
261
+ const field = new OsGridRefField(def, { model })
262
+ const state = {
263
+ gridRef: 'TQ123456'
264
+ }
265
+
266
+ const answer = getAnswerMarkdown(field, state, { format: 'email' })
267
+ expect(answer).toBe('TQ123456\n')
268
+ })
269
+ })
270
+ })
@@ -1,11 +1,11 @@
1
1
  import { ComponentType, type ComponentDef } from '@defra/forms-model'
2
- import { Marked, type Token } from 'marked'
3
2
 
4
3
  import { config } from '~/src/config/index.js'
5
4
  import { type ComponentBase } from '~/src/server/plugins/engine/components/ComponentBase.js'
6
5
  import { ListFormComponent } from '~/src/server/plugins/engine/components/ListFormComponent.js'
7
6
  import { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers/index.js'
8
7
  import * as Components from '~/src/server/plugins/engine/components/index.js'
8
+ import { markdown } from '~/src/server/plugins/engine/components/markdownParser.js'
9
9
  import { type FormState } from '~/src/server/plugins/engine/types.js'
10
10
 
11
11
  // All component instances
@@ -20,10 +20,15 @@ export type Field = InstanceType<
20
20
  | typeof Components.YesNoField
21
21
  | typeof Components.CheckboxesField
22
22
  | typeof Components.DatePartsField
23
+ | typeof Components.DeclarationField
24
+ | typeof Components.EastingNorthingField
23
25
  | typeof Components.EmailAddressField
26
+ | typeof Components.LatLongField
24
27
  | typeof Components.MonthYearField
25
28
  | typeof Components.MultilineTextField
29
+ | typeof Components.NationalGridFieldNumberField
26
30
  | typeof Components.NumberField
31
+ | typeof Components.OsGridRefField
27
32
  | typeof Components.SelectField
28
33
  | typeof Components.TelephoneNumberField
29
34
  | typeof Components.TextField
@@ -32,13 +37,12 @@ export type Field = InstanceType<
32
37
  >
33
38
 
34
39
  // Guidance component instances only
35
- export type Guidance = InstanceType<
36
- | typeof Components.Details
37
- | typeof Components.Html
38
- | typeof Components.Markdown
39
- | typeof Components.InsetText
40
- | typeof Components.List
41
- >
40
+ export type Guidance =
41
+ | InstanceType<typeof Components.Details>
42
+ | InstanceType<typeof Components.Html>
43
+ | InstanceType<typeof Components.Markdown>
44
+ | InstanceType<typeof Components.InsetText>
45
+ | InstanceType<typeof Components.List>
42
46
 
43
47
  // List component instances only
44
48
  export type ListField = InstanceType<
@@ -51,43 +55,8 @@ export type ListField = InstanceType<
51
55
 
52
56
  export const designerUrl = config.get('designerUrl')
53
57
 
54
- export const markdown = new Marked({
55
- breaks: true,
56
- gfm: true,
57
-
58
- /**
59
- * Render paragraphs without `<p>` wrappers
60
- * for check answers summary list `<dd>`
61
- */
62
- extensions: [
63
- {
64
- name: 'paragraph',
65
- renderer({ tokens = [] }) {
66
- const text = this.parser.parseInline(tokens)
67
- return tokens.length > 1 ? `${text}<br>` : text
68
- }
69
- }
70
- ],
71
-
72
- /**
73
- * Restrict allowed Markdown tokens
74
- */
75
- walkTokens(token) {
76
- const tokens: Token['type'][] = [
77
- 'br',
78
- 'escape',
79
- 'list',
80
- 'list_item',
81
- 'paragraph',
82
- 'space',
83
- 'text'
84
- ]
85
-
86
- if (!tokens.includes(token.type)) {
87
- token.type = 'text'
88
- }
89
- }
90
- })
58
+ // Re-export markdown from its own module to avoid circular dependencies
59
+ export { markdown } from '~/src/server/plugins/engine/components/markdownParser.js'
91
60
 
92
61
  /**
93
62
  * Filter known components with lists
@@ -95,7 +64,7 @@ export const markdown = new Marked({
95
64
  export function hasListFormField(
96
65
  field?: Partial<Component>
97
66
  ): field is ListFormComponent {
98
- return !!field && isListFieldType(field.type)
67
+ return !!field && field.type !== undefined && isListFieldType(field.type)
99
68
  }
100
69
 
101
70
  export function isListFieldType(
@@ -134,6 +103,10 @@ export function createComponent(
134
103
  component = new Components.DatePartsField(def, options)
135
104
  break
136
105
 
106
+ case ComponentType.DeclarationField:
107
+ component = new Components.DeclarationField(def, options)
108
+ break
109
+
137
110
  case ComponentType.Details:
138
111
  component = new Components.Details(def, options)
139
112
  break
@@ -197,6 +170,22 @@ export function createComponent(
197
170
  case ComponentType.FileUploadField:
198
171
  component = new Components.FileUploadField(def, options)
199
172
  break
173
+
174
+ case ComponentType.EastingNorthingField:
175
+ component = new Components.EastingNorthingField(def, options)
176
+ break
177
+
178
+ case ComponentType.OsGridRefField:
179
+ component = new Components.OsGridRefField(def, options)
180
+ break
181
+
182
+ case ComponentType.NationalGridFieldNumberField:
183
+ component = new Components.NationalGridFieldNumberField(def, options)
184
+ break
185
+
186
+ case ComponentType.LatLongField:
187
+ component = new Components.LatLongField(def, options)
188
+ break
200
189
  }
201
190
 
202
191
  if (typeof component === 'undefined') {
@@ -234,7 +223,9 @@ export function getAnswer(
234
223
  if (
235
224
  field instanceof ListFormComponent ||
236
225
  field instanceof Components.MultilineTextField ||
237
- field instanceof Components.UkAddressField
226
+ field instanceof Components.UkAddressField ||
227
+ field instanceof Components.EastingNorthingField ||
228
+ field instanceof Components.LatLongField
238
229
  ) {
239
230
  return markdown
240
231
  .parse(getAnswerMarkdown(field, state), { async: false })
@@ -325,6 +316,12 @@ export function getAnswerMarkdown(
325
316
  .map(escapeMarkdown)
326
317
  .join('\n')
327
318
  .concat('\n')
319
+ } else if (
320
+ field instanceof Components.EastingNorthingField ||
321
+ field instanceof Components.LatLongField
322
+ ) {
323
+ const contextValue = field.getContextValueFromState(state)
324
+ answerEscaped = contextValue ? `${contextValue}\n` : ''
328
325
  }
329
326
 
330
327
  return answerEscaped
@@ -1,6 +1,10 @@
1
- import { type ComponentDef } from '@defra/forms-model'
1
+ import { ComponentType, type ComponentDef } from '@defra/forms-model'
2
2
 
3
3
  import { ComponentBase } from '~/src/server/plugins/engine/components/ComponentBase.js'
4
+ import { EastingNorthingField } from '~/src/server/plugins/engine/components/EastingNorthingField.js'
5
+ import { LatLongField } from '~/src/server/plugins/engine/components/LatLongField.js'
6
+ import { NationalGridFieldNumberField } from '~/src/server/plugins/engine/components/NationalGridFieldNumberField.js'
7
+ import { OsGridRefField } from '~/src/server/plugins/engine/components/OsGridRefField.js'
4
8
  import { createComponent } from '~/src/server/plugins/engine/components/helpers/components.js'
5
9
  import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
6
10
  import definition from '~/test/form/definitions/basic.js'
@@ -22,6 +26,72 @@ describe('helpers tests', () => {
22
26
  )
23
27
  ).toThrow('Component type invalid-type does not exist')
24
28
  })
29
+
30
+ test('should create EastingNorthingField component', () => {
31
+ const component = createComponent(
32
+ {
33
+ type: ComponentType.EastingNorthingField,
34
+ name: 'testField',
35
+ title: 'Test Easting Northing',
36
+ options: {},
37
+ schema: {}
38
+ },
39
+ { model: formModel }
40
+ )
41
+
42
+ expect(component).toBeInstanceOf(EastingNorthingField)
43
+ expect(component.name).toBe('testField')
44
+ expect(component.title).toBe('Test Easting Northing')
45
+ })
46
+
47
+ test('should create LatLongField component', () => {
48
+ const component = createComponent(
49
+ {
50
+ type: ComponentType.LatLongField,
51
+ name: 'testField',
52
+ title: 'Test Lat Long',
53
+ options: {},
54
+ schema: {}
55
+ },
56
+ { model: formModel }
57
+ )
58
+
59
+ expect(component).toBeInstanceOf(LatLongField)
60
+ expect(component.name).toBe('testField')
61
+ expect(component.title).toBe('Test Lat Long')
62
+ })
63
+
64
+ test('should create OsGridRefField component', () => {
65
+ const component = createComponent(
66
+ {
67
+ type: ComponentType.OsGridRefField,
68
+ name: 'testField',
69
+ title: 'Test OS Grid Ref',
70
+ options: {}
71
+ },
72
+ { model: formModel }
73
+ )
74
+
75
+ expect(component).toBeInstanceOf(OsGridRefField)
76
+ expect(component.name).toBe('testField')
77
+ expect(component.title).toBe('Test OS Grid Ref')
78
+ })
79
+
80
+ test('should create NationalGridFieldNumberField component', () => {
81
+ const component = createComponent(
82
+ {
83
+ type: ComponentType.NationalGridFieldNumberField,
84
+ name: 'testField',
85
+ title: 'Test National Grid',
86
+ options: {}
87
+ },
88
+ { model: formModel }
89
+ )
90
+
91
+ expect(component).toBeInstanceOf(NationalGridFieldNumberField)
92
+ expect(component.name).toBe('testField')
93
+ expect(component.title).toBe('Test National Grid')
94
+ })
25
95
  })
26
96
 
27
97
  describe('ComponentBase tests', () => {
@@ -7,6 +7,7 @@
7
7
  export { AutocompleteField } from '~/src/server/plugins/engine/components/AutocompleteField.js'
8
8
  export { CheckboxesField } from '~/src/server/plugins/engine/components/CheckboxesField.js'
9
9
  export { DatePartsField } from '~/src/server/plugins/engine/components/DatePartsField.js'
10
+ export { DeclarationField } from '~/src/server/plugins/engine/components/DeclarationField.js'
10
11
  export { Details } from '~/src/server/plugins/engine/components/Details.js'
11
12
  export { EmailAddressField } from '~/src/server/plugins/engine/components/EmailAddressField.js'
12
13
  export { FileUploadField } from '~/src/server/plugins/engine/components/FileUploadField.js'
@@ -23,3 +24,7 @@ export { TelephoneNumberField } from '~/src/server/plugins/engine/components/Tel
23
24
  export { TextField } from '~/src/server/plugins/engine/components/TextField.js'
24
25
  export { UkAddressField } from '~/src/server/plugins/engine/components/UkAddressField.js'
25
26
  export { YesNoField } from '~/src/server/plugins/engine/components/YesNoField.js'
27
+ export { EastingNorthingField } from '~/src/server/plugins/engine/components/EastingNorthingField.js'
28
+ export { OsGridRefField } from '~/src/server/plugins/engine/components/OsGridRefField.js'
29
+ export { NationalGridFieldNumberField } from '~/src/server/plugins/engine/components/NationalGridFieldNumberField.js'
30
+ export { LatLongField } from '~/src/server/plugins/engine/components/LatLongField.js'
@@ -0,0 +1,40 @@
1
+ import { Marked, type Token } from 'marked'
2
+
3
+ export const markdown = new Marked({
4
+ breaks: true,
5
+ gfm: true,
6
+
7
+ /**
8
+ * Render paragraphs without `<p>` wrappers
9
+ * for check answers summary list `<dd>`
10
+ */
11
+ extensions: [
12
+ {
13
+ name: 'paragraph',
14
+ renderer({ tokens = [] }) {
15
+ const text = this.parser.parseInline(tokens)
16
+ return tokens.length > 1 ? `${text}<br>` : text
17
+ }
18
+ }
19
+ ],
20
+
21
+ /**
22
+ * Restrict allowed Markdown tokens
23
+ */
24
+ walkTokens(token) {
25
+ const tokens: Token['type'][] = [
26
+ 'br',
27
+ 'escape',
28
+ 'link',
29
+ 'list',
30
+ 'list_item',
31
+ 'paragraph',
32
+ 'space',
33
+ 'text'
34
+ ]
35
+
36
+ if (!tokens.includes(token.type)) {
37
+ token.type = 'text'
38
+ }
39
+ }
40
+ })
@@ -60,6 +60,10 @@ export interface DateInputItem {
60
60
  name?: string
61
61
  value?: Item['value']
62
62
  classes?: string
63
+ // Prefix/suffix are used by location fields (e.g., LatLong, EastingNorthing) for units like "°"
64
+ // but not by date fields. This interface is reused by both component types.
65
+ prefix?: ComponentText
66
+ suffix?: ComponentText
63
67
  condition?: undefined
64
68
  }
65
69
 
@@ -126,3 +130,13 @@ export interface MonthYearState extends Record<string, number> {
126
130
  month: number
127
131
  year: number
128
132
  }
133
+
134
+ export interface EastingNorthingState extends Record<string, number> {
135
+ easting: number
136
+ northing: number
137
+ }
138
+
139
+ export interface LatLongState extends Record<string, number> {
140
+ latitude: number
141
+ longitude: number
142
+ }
@@ -15,6 +15,7 @@ import {
15
15
  type FormContextRequest,
16
16
  type FormState
17
17
  } from '~/src/server/plugins/engine/types.js'
18
+ import v2Definition from '~/test/form/definitions/conditions-relative-dates-v2.js'
18
19
  import definition from '~/test/form/definitions/repeat-mixed.js'
19
20
  const basePath = `${FORM_PREFIX}/test`
20
21
 
@@ -326,7 +327,7 @@ describe('SummaryPageController', () => {
326
327
  expect(viewModel).toHaveProperty('allowSaveAndExit', true)
327
328
  })
328
329
 
329
- it('should display correct page title', () => {
330
+ it('should display correct page title for v1 form', () => {
330
331
  const state: FormState = {
331
332
  $$__referenceNumber: 'foobar',
332
333
  orderType: 'collection',
@@ -334,9 +335,81 @@ describe('SummaryPageController', () => {
334
335
  }
335
336
 
336
337
  const context = model.getFormContext(request, state)
337
- const viewModel = controller.getViewModel(request, context)
338
+ const viewModel = controller.getSummaryViewModel(request, context)
339
+
340
+ expect(viewModel.pageTitle).toBe(
341
+ 'Check your answers before sending your form'
342
+ )
343
+ })
344
+
345
+ it('should display default page title for v2 form when title not supplied', () => {
346
+ const state: FormState = {
347
+ $$__referenceNumber: 'foobar',
348
+ orderType: 'collection',
349
+ pizza: []
350
+ }
351
+
352
+ const titleModel = new FormModel(v2Definition, {
353
+ basePath: `${FORM_PREFIX}/test`
354
+ })
355
+
356
+ controller = new SummaryPageController(titleModel, v2Definition.pages[5])
357
+
358
+ request = {
359
+ method: 'get',
360
+ url: new URL('http://example.com/repeat/pizza-order/summary'),
361
+ path: '/test/summary',
362
+ params: {
363
+ path: 'summary',
364
+ slug: 'test'
365
+ },
366
+ query: {},
367
+ app: { model: titleModel },
368
+ server: serverWithSaveAndExit
369
+ }
370
+
371
+ const context = titleModel.getFormContext(request, state)
372
+ const viewModel = controller.getSummaryViewModel(request, context)
373
+
374
+ expect(viewModel.pageTitle).toBe(
375
+ 'Check your answers before sending your form'
376
+ )
377
+ })
378
+
379
+ it('should display override page title for v2 form when title supplied', () => {
380
+ const state: FormState = {
381
+ $$__referenceNumber: 'foobar',
382
+ orderType: 'collection',
383
+ pizza: []
384
+ }
385
+
386
+ const v2DefinitionWithSummaryTitle = structuredClone(v2Definition)
387
+ const summaryPage = v2DefinitionWithSummaryTitle.pages[5]
388
+ summaryPage.title = 'Override summary title'
389
+
390
+ const titleModel = new FormModel(v2DefinitionWithSummaryTitle, {
391
+ basePath: `${FORM_PREFIX}/test`
392
+ })
393
+
394
+ controller = new SummaryPageController(titleModel, summaryPage)
395
+
396
+ request = {
397
+ method: 'get',
398
+ url: new URL('http://example.com/repeat/pizza-order/summary'),
399
+ path: '/test/summary',
400
+ params: {
401
+ path: 'summary',
402
+ slug: 'test'
403
+ },
404
+ query: {},
405
+ app: { model: titleModel },
406
+ server: serverWithSaveAndExit
407
+ }
408
+
409
+ const context = titleModel.getFormContext(request, state)
410
+ const viewModel = controller.getSummaryViewModel(request, context)
338
411
 
339
- expect(viewModel.pageTitle).toBe('Check your answers')
412
+ expect(viewModel.pageTitle).toBe('Override summary title')
340
413
  })
341
414
  })
342
415
  })